1 # This file is part of Wolne Lektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Wolne Lektury. See NOTICE for more information.
4 from functools import reduce
6 from urllib.parse import urljoin
8 from django.contrib.syndication.views import Feed
9 from django.shortcuts import get_object_or_404
10 from django.urls import reverse
11 from django.utils.feedgenerator import Atom1Feed
12 from django.conf import settings
13 from django.http import Http404
14 from django.contrib.sites.models import Site
15 from django.utils.functional import lazy
17 from basicauth import logged_in_or_basicauth, factory_decorator
18 from catalogue.models import Book, Tag
19 from search.utils import UnaccentSearchQuery, UnaccentSearchVector
25 from stats.utils import piwik_track
27 log = logging.getLogger('opds')
34 "title": "Moje półki",
35 "description": "Półki użytkownika dostępne po zalogowaniu"
39 "link": "opds_by_category",
40 "link_args": ["author"],
42 "description": "Utwory wg autorów"
46 "link": "opds_by_category",
47 "link_args": ["kind"],
49 "description": "Utwory wg rodzajów"
53 "link": "opds_by_category",
54 "link_args": ["genre"],
56 "description": "Utwory wg gatunków"
60 "link": "opds_by_category",
61 "link_args": ["epoch"],
63 "description": "Utwory wg epok"
68 current_domain = lazy(lambda: Site.objects.get_current().domain, str)()
72 return urljoin("http://%s" % current_domain, url)
75 class OPDSFeed(Atom1Feed):
76 link_rel = "subsection"
77 link_type = "application/atom+xml"
79 _book_parent_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book-parent.png")), str)()
81 _book_parent_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book-parent.png")))
83 _book_parent_img_size = ''
85 _book_img = lazy(lambda: full_url(os.path.join(settings.STATIC_URL, "img/book.png")), str)()
87 _book_img_size = str(os.path.getsize(os.path.join(settings.STATIC_ROOT, "img/book.png")))
91 def add_root_elements(self, handler):
92 super(OPDSFeed, self).add_root_elements(handler)
93 handler.addQuickElement("link", None,
94 {"href": reverse("opds_authors"),
96 "type": "application/atom+xml"})
97 handler.addQuickElement("link", None,
98 {"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
100 "type": "application/opensearchdescription+xml"})
102 def add_item_elements(self, handler, item):
103 """ modified from Atom1Feed.add_item_elements """
104 handler.addQuickElement("title", item['title'])
106 # add a OPDS Navigation link if there's no enclosure
107 if not item.get('enclosures') is None:
108 handler.addQuickElement(
109 "link", "", {"href": item['link'], "rel": "subsection", "type": "application/atom+xml"})
110 # add a "green book" icon
111 handler.addQuickElement(
114 "rel": "http://opds-spec.org/thumbnail",
115 "href": self._book_parent_img,
116 "length": self._book_parent_img_size,
119 if item['pubdate'] is not None:
120 # FIXME: rfc3339_date is undefined, is this ever run?
121 handler.addQuickElement("updated", rfc3339_date(item['pubdate']).decode('utf-8'))
123 # Author information.
124 if item['author_name'] is not None:
125 handler.startElement("author", {})
126 handler.addQuickElement("name", item['author_name'])
127 if item['author_email'] is not None:
128 handler.addQuickElement("email", item['author_email'])
129 if item['author_link'] is not None:
130 handler.addQuickElement("uri", item['author_link'])
131 handler.endElement("author")
134 if item['unique_id'] is not None:
135 unique_id = item['unique_id']
137 # FIXME: get_tag_uri is undefined, is this ever run?
138 unique_id = get_tag_uri(item['link'], item['pubdate'])
139 handler.addQuickElement("id", unique_id)
142 # OPDS needs type=text
143 if item['description'] is not None:
144 handler.addQuickElement("summary", item['description'], {"type": "text"})
146 # Enclosure as OPDS Acquisition Link
147 for enc in item.get('enclosures', []):
148 handler.addQuickElement(
151 "rel": "http://opds-spec.org/acquisition",
153 "length": enc.length,
154 "type": enc.mime_type,
156 # add a "red book" icon
157 handler.addQuickElement(
160 "rel": "http://opds-spec.org/thumbnail",
161 "href": self._book_img,
162 "length": self._book_img_size,
167 for cat in item['categories']:
168 handler.addQuickElement("category", "", {"term": cat})
171 if item['item_copyright'] is not None:
172 handler.addQuickElement("rights", item['item_copyright'])
175 class AcquisitionFeed(Feed):
177 link = 'http://www.wolnelektury.pl/'
178 item_enclosure_mime_type = "application/epub+zip"
179 author_name = "Wolne Lektury"
180 author_link = "http://www.wolnelektury.pl/"
182 def item_title(self, book):
185 def item_description(self):
188 def item_link(self, book):
189 return book.get_absolute_url()
191 def item_author_name(self, book):
193 return book.authors().first().name
194 except AttributeError:
197 def item_author_link(self, book):
199 return book.authors().first().get_absolute_url()
200 except AttributeError:
203 def item_enclosure_url(self, book):
204 return full_url(book.epub_url()) if book.epub_file else None
206 def item_enclosure_length(self, book):
207 return book.epub_file.size if book.epub_file else None
211 class RootFeed(Feed):
213 title = 'Wolne Lektury'
214 link = 'http://wolnelektury.pl/'
215 description = "Spis utworów na stronie http://WolneLektury.pl"
216 author_name = "Wolne Lektury"
217 author_link = "http://wolnelektury.pl/"
222 def item_title(self, item):
225 def item_link(self, item):
226 return reverse(item['link'], args=item['link_args'])
228 def item_description(self, item):
229 return item['description']
233 class ByCategoryFeed(Feed):
235 link = 'http://wolnelektury.pl/'
236 description = "Spis utworów na stronie http://WolneLektury.pl"
237 author_name = "Wolne Lektury"
238 author_link = "http://wolnelektury.pl/"
240 def get_object(self, request, category):
241 feed = [feed for feed in _root_feeds if feed['category'] == category]
249 def title(self, feed):
252 def items(self, feed):
253 return Tag.objects.filter(category=feed['category']).exclude(items=None)
255 def item_title(self, item):
258 def item_link(self, item):
259 return reverse("opds_by_tag", args=[item.category, item.slug])
261 def item_description(self):
266 class ByTagFeed(AcquisitionFeed):
268 return tag.get_absolute_url()
270 def title(self, tag):
273 def description(self, tag):
274 return "Spis utworów na stronie http://WolneLektury.pl"
276 def get_object(self, request, category, slug):
277 return get_object_or_404(Tag, category=category, slug=slug)
279 def items(self, tag):
280 return Book.tagged_top_level([tag])
283 @factory_decorator(logged_in_or_basicauth())
285 class UserFeed(Feed):
287 link = 'http://www.wolnelektury.pl/'
288 description = "Półki użytkownika na stronie http://WolneLektury.pl"
289 author_name = "Wolne Lektury"
290 author_link = "http://wolnelektury.pl/"
292 def get_object(self, request):
295 def title(self, user):
296 return "Półki użytkownika %s" % user.username
298 def items(self, user):
299 return Tag.objects.filter(category='set', user=user).exclude(items=None)
301 def item_title(self, item):
304 def item_link(self, item):
305 return reverse("opds_user_set", args=[item.slug])
307 def item_description(self):
311 @factory_decorator(logged_in_or_basicauth())
313 class UserSetFeed(AcquisitionFeed):
315 return tag.get_absolute_url()
317 def title(self, tag):
320 def description(self, tag):
321 return "Spis utworów na stronie http://WolneLektury.pl"
323 def get_object(self, request, slug):
324 return get_object_or_404(Tag, category='set', slug=slug, user=request.user)
326 def items(self, tag):
327 return Book.tagged.with_any([tag])
331 class SearchFeed(AcquisitionFeed):
332 description = "Wyniki wyszukiwania na stronie WolneLektury.pl"
333 title = "Wyniki wyszukiwania"
335 QUOTE_OR_NOT = r'(?:(?=["])"([^"]+)"|([^ ]+))'
336 INLINE_QUERY_RE = re.compile(
337 r"author:" + QUOTE_OR_NOT +
338 "|translator:" + QUOTE_OR_NOT +
339 "|title:" + QUOTE_OR_NOT +
340 "|categories:" + QUOTE_OR_NOT +
341 "|description:" + QUOTE_OR_NOT +
342 "|text:" + QUOTE_OR_NOT
346 'translator': (2, 3),
348 'categories': (6, 7),
349 'description': (8, 9),
353 ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
355 def get_object(self, request):
357 For OPDS 1.1 We should handle a query for search terms
358 and criteria provided either as opensearch or 'inline' query.
359 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
360 atom:title. Inline query provides author, title, categories (treated as book tags),
361 description (treated as content search terms).
363 if search terms are provided, we shall search for books
364 according to Hint information (from author & contributror & title).
366 but if search terms are empty, we should do a different search
367 (perhaps for is_book=True)
371 query = request.GET.get('q', '')
373 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
375 remains = re.sub(self.INLINE_QUERY_RE, '', query)
376 remains = re.sub(r'[ \t]+', ' ', remains)
378 def get_criteria(criteria, name):
380 for p in self.MATCHES[name]:
383 return c[p].replace('+', ' ')
388 lambda cn: (cn, get_criteria(inline_criteria, cn)),
389 ['author', 'translator', 'title', 'categories',
390 'description', 'text']))
392 # empty query and text set case?
393 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
395 def remove_dump_data(val):
396 """Some clients don't get opds placeholders and just send them."""
397 if self.ATOM_PLACEHOLDER.match(val):
402 (cn, remove_dump_data(request.GET.get(cn, '')))
403 for cn in self.MATCHES.keys())
404 # query is set above.
405 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
407 books = Book.objects.filter(findable=True).annotate(
408 search_vector=UnaccentSearchVector('title')
411 squery = UnaccentSearchQuery(query, config=settings.SEARCH_CONFIG)
412 books = books.filter(search_vector=squery)
413 if criteria['author']:
414 authors = Tag.objects.filter(category='author').annotate(
415 search_vector=UnaccentSearchVector('name_pl')
416 ).filter(search_vector=UnaccentSearchQuery(criteria['author'], config=settings.SEARCH_CONFIG))
417 books = books.filter(tag_relations__tag__in=authors)
418 if criteria['categories']:
419 tags = Tag.objects.filter(category__in=('genre', 'kind', 'epoch')).annotate(
420 search_vector=UnaccentSearchVector('name_pl')
421 ).filter(search_vector=UnaccentSearchQuery(criteria['categories'], config=settings.SEARCH_CONFIG))
422 books = books.filter(tag_relations__tag__in=tags)
423 if criteria['translator']:
426 if criteria['title']:
427 books = books.filter(
428 search_vector=UnaccentSearchQuery(criteria['title'], config=settings.SEARCH_CONFIG)
431 books = books.exclude(ancestor__in=books)
433 books = books.order_by('popularity__count')
436 def get_link(self, query):
437 return "%s?q=%s" % (reverse('search'), query)
439 def items(self, books):