1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. 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
20 from search.views import Search
25 from stats.utils import piwik_track
27 log = logging.getLogger('opds')
32 u"link": u"opds_user",
34 u"title": u"Moje półki",
35 u"description": u"Półki użytkownika dostępne po zalogowaniu"
38 u"category": u"author",
39 u"link": u"opds_by_category",
40 u"link_args": [u"author"],
42 u"description": u"Utwory wg autorów"
46 u"link": u"opds_by_category",
47 u"link_args": [u"kind"],
49 u"description": u"Utwory wg rodzajów"
52 u"category": u"genre",
53 u"link": u"opds_by_category",
54 u"link_args": [u"genre"],
56 u"description": u"Utwory wg gatunków"
59 u"category": u"epoch",
60 u"link": u"opds_by_category",
61 u"link_args": [u"epoch"],
63 u"description": u"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 = u"subsection"
77 link_type = u"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(u"link", None,
94 {u"href": reverse("opds_authors"),
96 u"type": u"application/atom+xml"})
97 handler.addQuickElement(u"link", None,
98 {u"href": full_url(os.path.join(settings.STATIC_URL, "opensearch.xml")),
100 u"type": u"application/opensearchdescription+xml"})
102 def add_item_elements(self, handler, item):
103 """ modified from Atom1Feed.add_item_elements """
104 handler.addQuickElement(u"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 u"link", u"", {u"href": item['link'], u"rel": u"subsection", u"type": u"application/atom+xml"})
110 # add a "green book" icon
111 handler.addQuickElement(
114 u"rel": u"http://opds-spec.org/thumbnail",
115 u"href": self._book_parent_img,
116 u"length": self._book_parent_img_size,
117 u"type": u"image/png",
119 if item['pubdate'] is not None:
120 # FIXME: rfc3339_date is undefined, is this ever run?
121 handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8'))
123 # Author information.
124 if item['author_name'] is not None:
125 handler.startElement(u"author", {})
126 handler.addQuickElement(u"name", item['author_name'])
127 if item['author_email'] is not None:
128 handler.addQuickElement(u"email", item['author_email'])
129 if item['author_link'] is not None:
130 handler.addQuickElement(u"uri", item['author_link'])
131 handler.endElement(u"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(u"id", unique_id)
142 # OPDS needs type=text
143 if item['description'] is not None:
144 handler.addQuickElement(u"summary", item['description'], {u"type": u"text"})
146 # Enclosure as OPDS Acquisition Link
147 for enc in item.get('enclosures', []):
148 handler.addQuickElement(
151 u"rel": u"http://opds-spec.org/acquisition",
153 u"length": enc.length,
154 u"type": enc.mime_type,
156 # add a "red book" icon
157 handler.addQuickElement(
160 u"rel": u"http://opds-spec.org/thumbnail",
161 u"href": self._book_img,
162 u"length": self._book_img_size,
163 u"type": u"image/png",
167 for cat in item['categories']:
168 handler.addQuickElement(u"category", u"", {u"term": cat})
171 if item['item_copyright'] is not None:
172 handler.addQuickElement(u"rights", item['item_copyright'])
175 class AcquisitionFeed(Feed):
177 link = u'http://www.wolnelektury.pl/'
178 item_enclosure_mime_type = "application/epub+zip"
179 author_name = u"Wolne Lektury"
180 author_link = u"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 = u'Wolne Lektury'
214 link = u'http://wolnelektury.pl/'
215 description = u"Spis utworów na stronie http://WolneLektury.pl"
216 author_name = u"Wolne Lektury"
217 author_link = u"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 = u'http://wolnelektury.pl/'
236 description = u"Spis utworów na stronie http://WolneLektury.pl"
237 author_name = u"Wolne Lektury"
238 author_link = u"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 u"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 = u'http://www.wolnelektury.pl/'
288 description = u"Półki użytkownika na stronie http://WolneLektury.pl"
289 author_name = u"Wolne Lektury"
290 author_link = u"http://wolnelektury.pl/"
292 def get_object(self, request):
295 def title(self, user):
296 return u"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 u"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 = u"Wyniki wyszukiwania na stronie WolneLektury.pl"
333 title = u"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),
355 'translator': 'translators',
357 'categories': 'tag_name_pl',
358 'description': 'text',
362 ATOM_PLACEHOLDER = re.compile(r"^{(atom|opds):\w+}$")
364 def get_object(self, request):
366 For OPDS 1.1 We should handle a query for search terms
367 and criteria provided either as opensearch or 'inline' query.
368 OpenSearch defines fields: atom:author, atom:contributor (treated as translator),
369 atom:title. Inline query provides author, title, categories (treated as book tags),
370 description (treated as content search terms).
372 if search terms are provided, we shall search for books
373 according to Hint information (from author & contributror & title).
375 but if search terms are empty, we should do a different search
376 (perhaps for is_book=True)
380 query = request.GET.get('q', '')
382 inline_criteria = re.findall(self.INLINE_QUERY_RE, query)
384 remains = re.sub(self.INLINE_QUERY_RE, '', query)
385 remains = re.sub(r'[ \t]+', ' ', remains)
387 def get_criteria(criteria, name):
389 for p in self.MATCHES[name]:
392 return c[p].replace('+', ' ')
397 lambda cn: (cn, get_criteria(inline_criteria, cn)),
398 ['author', 'translator', 'title', 'categories',
399 'description', 'text']))
401 # empty query and text set case?
402 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
404 def remove_dump_data(val):
405 """Some clients don't get opds placeholders and just send them."""
406 if self.ATOM_PLACEHOLDER.match(val):
411 (cn, remove_dump_data(request.GET.get(cn, '')))
412 for cn in self.MATCHES.keys())
413 # query is set above.
414 log.debug("Inline query = [%s], criteria: %s" % (query, criteria))
418 book_hit_filter = srch.index.Q(book_id__any=True)
419 filters = [book_hit_filter] + [srch.index.Q(
420 **{self.PARAMS_TO_FIELDS.get(cn, cn): criteria[cn]}
421 ) for cn in self.MATCHES.keys() if cn in criteria
425 q = srch.index.query(
428 [srch.index.Q(**{self.PARAMS_TO_FIELDS.get(cn, cn): query}) for cn in self.MATCHES.keys()],
431 q = srch.index.query(srch.index.Q())
433 q = srch.apply_filters(q, filters).field_limit(score=True, fields=['book_id'])
434 results = q.execute()
436 book_scores = dict([(r['book_id'], r['score']) for r in results])
437 books = Book.objects.filter(id__in=set([r['book_id'] for r in results]))
439 books.sort(reverse=True, key=lambda book: book_scores[book.id])
442 def get_link(self, query):
443 return "%s?q=%s" % (reverse('search'), query)
445 def items(self, books):