codex 1.4.0a1__py3-none-any.whl → 1.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of codex might be problematic. Click here for more details.

Files changed (259) hide show
  1. codex/config_default.yaml +12 -4
  2. codex/db_functions.py +4 -2
  3. codex/integrity.py +17 -6
  4. codex/librarian/covers/create.py +6 -8
  5. codex/librarian/importer/aggregate_metadata.py +75 -41
  6. codex/librarian/importer/clean_metadata.py +30 -7
  7. codex/librarian/importer/create_fks.py +154 -55
  8. codex/librarian/importer/deleted.py +11 -2
  9. codex/librarian/importer/failed_imports.py +41 -5
  10. codex/librarian/importer/importerd.py +34 -11
  11. codex/librarian/importer/link_comics.py +54 -31
  12. codex/librarian/importer/moved.py +55 -11
  13. codex/librarian/importer/query_fks.py +210 -48
  14. codex/librarian/importer/tasks.py +7 -7
  15. codex/librarian/janitor/cleanup.py +17 -5
  16. codex/librarian/librariand.py +10 -0
  17. codex/librarian/watchdog/events.py +11 -14
  18. codex/librarian/watchdog/observers.py +5 -1
  19. codex/logger/loggerd.py +7 -3
  20. codex/logger/logging.py +1 -1
  21. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
  22. codex/migrations/0025_add_story_arc_number.py +83 -0
  23. codex/models.py +21 -11
  24. codex/search/backend.py +1 -1
  25. codex/search/indexes.py +1 -1
  26. codex/serializers/browser.py +1 -0
  27. codex/serializers/metadata.py +5 -1
  28. codex/serializers/models.py +16 -1
  29. codex/serializers/opds/v1.py +1 -0
  30. codex/serializers/opds/v2.py +5 -2
  31. codex/serializers/reader.py +55 -16
  32. codex/settings/settings.py +1 -1
  33. codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
  34. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  35. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  36. codex/static_root/assets/admin-12749881.js +41 -0
  37. codex/static_root/assets/admin-12749881.js.br +0 -0
  38. codex/static_root/assets/admin-12749881.js.gz +0 -0
  39. codex/static_root/assets/admin-beda768d.a614eee46307.css +1 -0
  40. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  41. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  42. codex/static_root/assets/admin-beda768d.css +1 -0
  43. codex/static_root/assets/admin-beda768d.css.br +0 -0
  44. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  45. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css +1 -0
  46. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
  47. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
  48. codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
  49. codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
  50. codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
  51. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -0
  52. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  53. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  54. codex/static_root/assets/admin-drawer-panel-522f1e6c.js +1 -0
  55. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  56. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  57. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css +1 -0
  58. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  59. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  60. codex/static_root/assets/browser-7f7d7134.css +1 -0
  61. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  62. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  63. codex/static_root/assets/browser-af622672.d51aca96d64d.js +1 -0
  64. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  65. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  66. codex/static_root/assets/browser-af622672.js +1 -0
  67. codex/static_root/assets/browser-af622672.js.br +0 -0
  68. codex/static_root/assets/browser-af622672.js.gz +0 -0
  69. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js +1 -0
  70. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  71. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  72. codex/static_root/assets/http-error-5e17b794.js +1 -0
  73. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  74. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  75. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +1 -0
  76. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  77. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  78. codex/static_root/assets/main-9e76a4c3.js +1 -0
  79. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  80. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  81. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +1 -0
  82. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  83. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  84. codex/static_root/assets/metadata-dialog-62c29ce0.js +1 -0
  85. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  86. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  87. codex/static_root/assets/{metadata-dialog-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -1
  88. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  89. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  90. codex/static_root/assets/{metadata-dialog-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -1
  91. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  92. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  93. codex/static_root/assets/{page-pdf-c603e996.ab2d147c9ae1.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
  94. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  95. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  96. codex/static_root/assets/{page-pdf-c603e996.js → page-pdf-157ba97e.js} +61 -51
  97. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  98. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  99. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +1 -0
  100. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  101. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  102. codex/static_root/assets/reader-36266549.js +1 -0
  103. codex/static_root/assets/reader-36266549.js.br +0 -0
  104. codex/static_root/assets/reader-36266549.js.gz +0 -0
  105. codex/static_root/assets/reader-7f004141.506eecc6954b.css +1 -0
  106. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  107. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  108. codex/static_root/assets/reader-7f004141.css +1 -0
  109. codex/static_root/assets/reader-7f004141.css.br +0 -0
  110. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  111. codex/static_root/js/choices.8c58714cf5b2.json +1 -0
  112. codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
  113. codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
  114. codex/static_root/js/choices.json +1 -1
  115. codex/static_root/js/choices.json.br +0 -0
  116. codex/static_root/js/choices.json.gz +0 -0
  117. codex/static_root/{manifest.c0e270b2e6b6.json → manifest.d2f93a519ada.json} +32 -32
  118. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  119. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  120. codex/static_root/manifest.json +32 -32
  121. codex/static_root/manifest.json.br +0 -0
  122. codex/static_root/manifest.json.gz +0 -0
  123. codex/static_root/staticfiles.json +1 -1
  124. codex/templates/headers-script-globals.html +1 -1
  125. codex/templates/{opds → opds_v1}/index.xml +3 -1
  126. codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
  127. codex/templates/search/indexes/codex/comic_text.txt +2 -2
  128. codex/urls/converters.py +1 -1
  129. codex/urls/opds/authentication.py +1 -1
  130. codex/urls/opds/root.py +8 -12
  131. codex/urls/opds/v1.py +12 -5
  132. codex/urls/opds/v2.py +2 -2
  133. codex/views/bookmark.py +2 -2
  134. codex/views/browser/base.py +23 -7
  135. codex/views/browser/browser.py +51 -41
  136. codex/views/browser/browser_annotations.py +159 -50
  137. codex/views/browser/browser_order_by.py +50 -106
  138. codex/views/browser/choices.py +75 -38
  139. codex/views/browser/filters/bookmark.py +6 -9
  140. codex/views/browser/filters/field.py +9 -6
  141. codex/views/browser/filters/group.py +12 -27
  142. codex/views/browser/filters/search.py +5 -10
  143. codex/views/browser/metadata.py +44 -19
  144. codex/views/download.py +1 -1
  145. codex/views/frontend.py +2 -3
  146. codex/views/mixins.py +15 -2
  147. codex/views/opds/const.py +8 -1
  148. codex/views/opds/util.py +37 -1
  149. codex/views/opds/v1/__init__.py +1 -1
  150. codex/views/opds/v1/data.py +21 -0
  151. codex/views/opds/v1/entry/__init__.py +1 -0
  152. codex/views/opds/v1/entry/data.py +23 -0
  153. codex/views/opds/v1/entry/entry.py +151 -0
  154. codex/views/opds/v1/entry/links.py +135 -0
  155. codex/views/opds/v1/facets.py +190 -0
  156. codex/views/opds/v1/feed.py +199 -0
  157. codex/views/opds/v1/links.py +198 -0
  158. codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
  159. codex/views/opds/v2/__init__.py +1 -1
  160. codex/views/opds/v2/const.py +10 -2
  161. codex/views/opds/v2/feed.py +82 -21
  162. codex/views/opds/v2/links.py +1 -1
  163. codex/views/opds/v2/publications.py +1 -1
  164. codex/views/opds/v2/top_links.py +1 -1
  165. codex/views/reader/page.py +6 -7
  166. codex/views/reader/reader.py +191 -61
  167. codex/views/session.py +2 -1
  168. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
  169. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/RECORD +172 -170
  170. codex/librarian/importer/db_ops.py +0 -251
  171. codex/pdf.py +0 -115
  172. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js +0 -48
  173. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.br +0 -0
  174. codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.gz +0 -0
  175. codex/static_root/assets/admin-75c007ce.js +0 -48
  176. codex/static_root/assets/admin-75c007ce.js.br +0 -0
  177. codex/static_root/assets/admin-75c007ce.js.gz +0 -0
  178. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css +0 -1
  179. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.br +0 -0
  180. codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.gz +0 -0
  181. codex/static_root/assets/admin-848d48b1.css +0 -1
  182. codex/static_root/assets/admin-848d48b1.css.br +0 -0
  183. codex/static_root/assets/admin-848d48b1.css.gz +0 -0
  184. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js +0 -1
  185. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.br +0 -0
  186. codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.gz +0 -0
  187. codex/static_root/assets/admin-drawer-panel-a110c068.js +0 -1
  188. codex/static_root/assets/admin-drawer-panel-a110c068.js.br +0 -0
  189. codex/static_root/assets/admin-drawer-panel-a110c068.js.gz +0 -0
  190. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
  191. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
  192. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
  193. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
  194. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
  195. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
  196. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js +0 -1
  197. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.br +0 -0
  198. codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.gz +0 -0
  199. codex/static_root/assets/browser-2c2380fd.js +0 -1
  200. codex/static_root/assets/browser-2c2380fd.js.br +0 -0
  201. codex/static_root/assets/browser-2c2380fd.js.gz +0 -0
  202. codex/static_root/assets/browser-7325db61.css +0 -1
  203. codex/static_root/assets/browser-7325db61.css.br +0 -0
  204. codex/static_root/assets/browser-7325db61.css.gz +0 -0
  205. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
  206. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
  207. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
  208. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js +0 -1
  209. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.br +0 -0
  210. codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.gz +0 -0
  211. codex/static_root/assets/http-error-402decbe.js +0 -1
  212. codex/static_root/assets/http-error-402decbe.js.br +0 -0
  213. codex/static_root/assets/http-error-402decbe.js.gz +0 -0
  214. codex/static_root/assets/main-a7f327e9.6641fe833335.js +0 -1
  215. codex/static_root/assets/main-a7f327e9.6641fe833335.js.br +0 -0
  216. codex/static_root/assets/main-a7f327e9.6641fe833335.js.gz +0 -0
  217. codex/static_root/assets/main-a7f327e9.js +0 -1
  218. codex/static_root/assets/main-a7f327e9.js.br +0 -0
  219. codex/static_root/assets/main-a7f327e9.js.gz +0 -0
  220. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
  221. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
  222. codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
  223. codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
  224. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js +0 -1
  225. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.br +0 -0
  226. codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.gz +0 -0
  227. codex/static_root/assets/metadata-dialog-8a0bd8e1.js +0 -1
  228. codex/static_root/assets/metadata-dialog-8a0bd8e1.js.br +0 -0
  229. codex/static_root/assets/metadata-dialog-8a0bd8e1.js.gz +0 -0
  230. codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.br +0 -0
  231. codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.gz +0 -0
  232. codex/static_root/assets/page-pdf-c603e996.js.br +0 -0
  233. codex/static_root/assets/page-pdf-c603e996.js.gz +0 -0
  234. codex/static_root/assets/reader-c2965a5f.b011260169f7.js +0 -1
  235. codex/static_root/assets/reader-c2965a5f.b011260169f7.js.br +0 -0
  236. codex/static_root/assets/reader-c2965a5f.b011260169f7.js.gz +0 -0
  237. codex/static_root/assets/reader-c2965a5f.js +0 -1
  238. codex/static_root/assets/reader-c2965a5f.js.br +0 -0
  239. codex/static_root/assets/reader-c2965a5f.js.gz +0 -0
  240. codex/static_root/assets/reader-d8534888.2821de925986.css +0 -1
  241. codex/static_root/assets/reader-d8534888.2821de925986.css.br +0 -0
  242. codex/static_root/assets/reader-d8534888.2821de925986.css.gz +0 -0
  243. codex/static_root/assets/reader-d8534888.css +0 -1
  244. codex/static_root/assets/reader-d8534888.css.br +0 -0
  245. codex/static_root/assets/reader-d8534888.css.gz +0 -0
  246. codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
  247. codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
  248. codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
  249. codex/static_root/manifest.c0e270b2e6b6.json.br +0 -0
  250. codex/static_root/manifest.c0e270b2e6b6.json.gz +0 -0
  251. codex/urls/opds/opensearch.py +0 -18
  252. codex/views/opds/v1/browser.py +0 -346
  253. codex/views/opds/v1/entry.py +0 -278
  254. codex/views/opds/v1/start.py +0 -28
  255. codex/views/opds/v1/util.py +0 -162
  256. codex/views/opds/v2/start.py +0 -28
  257. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
  258. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
  259. {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
codex/views/opds/util.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """OPDS Utility classes."""
2
2
  from django.db.models import F
3
+ from django.http.response import HttpResponseRedirect
4
+ from django.urls import reverse
3
5
  from django.utils.http import urlencode
4
6
 
5
7
  from codex.models import (
@@ -13,6 +15,7 @@ from codex.models import (
13
15
  Tag,
14
16
  Team,
15
17
  )
18
+ from codex.serializers.choices import DEFAULTS
16
19
 
17
20
  OPDS_M2M_MODELS = (Character, Genre, Location, SeriesGroup, StoryArc, Tag, Team)
18
21
 
@@ -61,6 +64,39 @@ def get_m2m_objects(pk) -> dict:
61
64
  cats = {}
62
65
  for model in OPDS_M2M_MODELS:
63
66
  table = model.__name__.lower()
64
- qs = model.objects.filter(comic=pk).order_by("name").only("name")
67
+ rel = "comic"
68
+ if model == StoryArc:
69
+ rel = "storyarcnumber__" + rel
70
+ comic_filter = {rel: pk}
71
+ qs = model.objects.filter(**comic_filter).order_by("name").only("name")
65
72
  cats[table] = qs
73
+
66
74
  return cats
75
+
76
+
77
+ def full_redirect_view(url_name):
78
+ """Redirect to view, for a url name."""
79
+
80
+ def func(request):
81
+ """Redirect to view, forwarding query strings and auth."""
82
+ kwargs = DEFAULTS["route"]
83
+ url = reverse(url_name, kwargs=kwargs)
84
+
85
+ # Forward the query string.
86
+ path = request.get_full_path()
87
+ if path:
88
+ parts = path.split("?")
89
+ if len(parts) >= 2: # noqa PLR2004
90
+ parts[0] = url
91
+ url = "?".join(parts)
92
+
93
+ response = HttpResponseRedirect(url)
94
+
95
+ # Forward authorization.
96
+ auth_header = request.META.get("HTTP_AUTHORIZATION")
97
+ if auth_header:
98
+ response["HTTP_AUTHORIZATION"] = auth_header
99
+
100
+ return response
101
+
102
+ return func
@@ -1 +1 @@
1
- """OPDS 1 Views."""
1
+ """OPDS v1 Views."""
@@ -0,0 +1,21 @@
1
+ """OPDS v1 Data classes."""
2
+ from dataclasses import dataclass
3
+ from datetime import datetime
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass
8
+ class OPDS1Link:
9
+ """An OPDS Link."""
10
+
11
+ rel: str
12
+ href: str
13
+ mime_type: str
14
+ title: str = ""
15
+ length: int = 0
16
+ facet_group: str = ""
17
+ facet_active: bool = False
18
+ thr_count: int = 0
19
+ pse_count: int = 0
20
+ pse_last_read: int = 0
21
+ pse_last_read_date: Optional[datetime] = None
@@ -0,0 +1 @@
1
+ """OPDS v1 Entries."""
@@ -0,0 +1,23 @@
1
+ """OPDS v1 Entry Data classes."""
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class OPDS1EntryObject:
7
+ """Fake entry db object for top link & facet entries."""
8
+
9
+ group: str = ""
10
+ pk: int = 0
11
+ name: str = ""
12
+ summary: str = ""
13
+ fake: bool = True
14
+
15
+
16
+ @dataclass
17
+ class OPDS1EntryData:
18
+ """Entry Data class to avoid to many args."""
19
+
20
+ acquisition_groups: frozenset
21
+ issue_max: int
22
+ metadata: bool
23
+ mime_type_map: dict
@@ -0,0 +1,151 @@
1
+ """OPDS v1 Entry."""
2
+ import json
3
+ from datetime import datetime, timezone
4
+ from urllib.parse import urlencode
5
+
6
+ from django.urls import reverse
7
+
8
+ from codex.logger.logging import get_logger
9
+ from codex.models import Comic
10
+ from codex.views.opds.const import (
11
+ AUTHOR_ROLES,
12
+ BLANK_TITLE,
13
+ )
14
+ from codex.views.opds.util import (
15
+ get_creator_people,
16
+ get_m2m_objects,
17
+ )
18
+ from codex.views.opds.v1.entry.links import OPDS1EntryLinksMixin
19
+
20
+ LOG = get_logger(__name__)
21
+
22
+
23
+ class OPDS1Entry(OPDS1EntryLinksMixin):
24
+ """An OPDS entry object."""
25
+
26
+ _DATE_FORMAT_BASE = "%Y-%m-%dT%H:%M:%S"
27
+ _DATE_FORMAT_MS = _DATE_FORMAT_BASE + ".%f%z"
28
+ _DATE_FORMAT = _DATE_FORMAT_BASE + "%z"
29
+ _DATE_FORMATS = (_DATE_FORMAT_MS, _DATE_FORMAT)
30
+
31
+ @property
32
+ def id_tag(self):
33
+ """GUID is a nav url."""
34
+ # Id top links by query params but not regular entries.
35
+ return self._nav_href(metadata=self.metadata)
36
+
37
+ @property
38
+ def title(self):
39
+ """Compute the item title."""
40
+ result = ""
41
+ try:
42
+ parts = []
43
+ if not self.fake:
44
+ group = self.obj.group
45
+ if group == "i":
46
+ parts.append(self.obj.publisher_name)
47
+ elif group == "v":
48
+ parts.append(self.obj.series_name)
49
+ elif group == "c":
50
+ title = Comic.get_title(self.obj, issue_max=self.issue_max)
51
+ parts.append(title)
52
+
53
+ if name := self.obj.name:
54
+ parts.append(name)
55
+
56
+ result = " ".join(filter(None, parts))
57
+ except Exception as exc:
58
+ LOG.exception(exc)
59
+
60
+ if not result:
61
+ result = BLANK_TITLE
62
+ return result
63
+
64
+ @property
65
+ def issued(self):
66
+ """Return the published date."""
67
+ if self.obj.group == "c":
68
+ return self.obj.date
69
+ return None
70
+
71
+ @property
72
+ def publisher(self):
73
+ """Return the publisher."""
74
+ return self.obj.publisher_name
75
+
76
+ def _get_datefield(self, key):
77
+ result = None
78
+ if not self.fake and (value := getattr(self.obj, key, None)):
79
+ for date_format in self._DATE_FORMATS:
80
+ try:
81
+ if isinstance(value, str):
82
+ result = datetime.strptime(value, date_format).astimezone(
83
+ timezone.utc
84
+ )
85
+ if isinstance(value, datetime):
86
+ result = value.astimezone(timezone.utc).strftime(date_format)
87
+ break
88
+ except ValueError:
89
+ pass
90
+ return result
91
+
92
+ @property
93
+ def updated(self):
94
+ """When the entry was last updated."""
95
+ return self._get_datefield("updated_at")
96
+
97
+ @property
98
+ def published(self):
99
+ """When the entry was created."""
100
+ return self._get_datefield("created_at")
101
+
102
+ @property
103
+ def language(self):
104
+ """Return the entry language."""
105
+ return self.obj.language
106
+
107
+ @property
108
+ def summary(self):
109
+ """Return a child count or comic summary."""
110
+ if self.obj.group == "c":
111
+ desc = self.obj.summary
112
+ else:
113
+ children = self.obj.child_count
114
+ desc = f"{children} issues"
115
+ return desc
116
+
117
+ @staticmethod
118
+ def _add_url_to_obj(objs, filter_key):
119
+ """Add filter urls to objects."""
120
+ kwargs = {"group": "s", "pk": 0, "page": 1}
121
+ url_base = reverse("opds:v1:feed", kwargs=kwargs)
122
+ result = []
123
+ for obj in objs:
124
+ qp = {"filters": json.dumps({filter_key: [obj.pk]})}
125
+ qp = urlencode(qp)
126
+ obj.url = url_base + "?" + qp
127
+ result.append(obj)
128
+ return result
129
+
130
+ @property
131
+ def authors(self):
132
+ """Get Author names."""
133
+ if not self.metadata:
134
+ return []
135
+ people = get_creator_people(self.obj.pk, AUTHOR_ROLES)
136
+ return self._add_url_to_obj(people, "creators")
137
+
138
+ @property
139
+ def contributors(self):
140
+ """Get Contributor names."""
141
+ if not self.metadata:
142
+ return []
143
+ people = get_creator_people(self.obj.pk, AUTHOR_ROLES, exclude=True)
144
+ return self._add_url_to_obj(people, "creators")
145
+
146
+ @property
147
+ def category_groups(self):
148
+ """Get Category labels."""
149
+ if not self.metadata:
150
+ return {}
151
+ return get_m2m_objects(self.obj.pk)
@@ -0,0 +1,135 @@
1
+ """OPDS v1 Entry Links Methods."""
2
+ from urllib.parse import quote_plus
3
+
4
+ from django.contrib.staticfiles.storage import staticfiles_storage
5
+ from django.urls import reverse
6
+
7
+ from codex.models import Comic
8
+ from codex.views.opds.const import MimeType, Rel
9
+ from codex.views.opds.util import update_href_query_params
10
+ from codex.views.opds.v1.data import OPDS1Link
11
+ from codex.views.opds.v1.entry.data import OPDS1EntryData, OPDS1EntryObject
12
+
13
+
14
+ class OPDS1EntryLinksMixin:
15
+ """OPDS v1 Entry Links Methods."""
16
+
17
+ def __init__(self, obj, query_params, data: OPDS1EntryData):
18
+ """Initialize params."""
19
+ self.obj = obj
20
+ self.fake = isinstance(self.obj, OPDS1EntryObject)
21
+ self.query_params = query_params
22
+ self.acquision_groups = data.acquisition_groups
23
+ self.issue_max = data.issue_max
24
+ self.metadata = data.metadata
25
+ self.mime_type_map = data.mime_type_map
26
+
27
+ def _thumb_link(self):
28
+ if self.fake:
29
+ return None
30
+ cover_pk = self.obj.cover_pk
31
+ if cover_pk:
32
+ kwargs = {"pk": cover_pk}
33
+ href = reverse("opds:bin:cover", kwargs=kwargs)
34
+ elif cover_pk == 0:
35
+ href = staticfiles_storage.url("img/missing_cover.webp")
36
+ else:
37
+ return None
38
+ return OPDS1Link(Rel.THUMBNAIL, href, "image/webp")
39
+
40
+ def _image_link(self):
41
+ if self.fake:
42
+ return None
43
+ cover_pk = self.obj.cover_pk
44
+ if cover_pk:
45
+ kwargs = {"pk": cover_pk, "page": 0}
46
+ href = reverse("opds:bin:page", kwargs=kwargs)
47
+ mime_type = "image/jpeg"
48
+ elif cover_pk == 0:
49
+ href = staticfiles_storage.url("img/missing_cover.webp")
50
+ mime_type = "image/webp"
51
+ else:
52
+ return None
53
+ return OPDS1Link(Rel.IMAGE, href, mime_type)
54
+
55
+ def _nav_href(self, metadata=False):
56
+ kwargs = {"group": self.obj.group, "pk": self.obj.pk, "page": 1}
57
+ href = reverse("opds:v1:feed", kwargs=kwargs)
58
+ qps = {}
59
+ if (
60
+ self.obj.group == "a"
61
+ and self.obj.pk
62
+ and not self.query_params.get("orderBy")
63
+ ):
64
+ # story arcs get ordered by story_arc_number by default
65
+ qps.update({"orderBy": "story_arc_number"})
66
+ if metadata:
67
+ qps.update({"opdsMetadata": 1})
68
+ return update_href_query_params(href, self.query_params, qps)
69
+
70
+ def _nav_link(self, metadata=False):
71
+ group = self.obj.group
72
+
73
+ if group in self.acquision_groups:
74
+ mime_type = MimeType.ENTRY_CATALOG if metadata else MimeType.ACQUISITION
75
+ else:
76
+ mime_type = MimeType.NAV
77
+
78
+ href = self._nav_href(metadata)
79
+ thr_count = 0 if self.fake else self.obj.child_count
80
+ rel = Rel.ALTERNATE if metadata else "subsection"
81
+
82
+ return OPDS1Link(rel, href, mime_type, thr_count=thr_count)
83
+
84
+ def _download_link(self):
85
+ pk = self.obj.pk
86
+ if not pk:
87
+ return None
88
+ fn = Comic.get_filename(self.obj)
89
+ fn = quote_plus(fn)
90
+ kwargs = {"pk": pk, "filename": fn}
91
+ href = reverse("opds:bin:download", kwargs=kwargs)
92
+ mime_type = self.mime_type_map.get(self.obj.file_type, MimeType.OCTET)
93
+ return OPDS1Link(Rel.ACQUISITION, href, mime_type, length=self.obj.size)
94
+
95
+ def _stream_link(self):
96
+ pk = self.obj.pk
97
+ if not pk:
98
+ return None
99
+ kwargs = {"pk": pk, "page": 0}
100
+ qps = {"bookmark": 1}
101
+ href = reverse("opds:bin:page", kwargs=kwargs)
102
+ href = update_href_query_params(href, {}, qps)
103
+ href = href.replace("0/page.jpg", "{pageNumber}/page.jpg")
104
+ page = self.obj.page
105
+ count = self.obj.page_count
106
+ bookmark_updated_at = self.obj.bookmark_updated_at
107
+ return OPDS1Link(
108
+ Rel.STREAM,
109
+ href,
110
+ MimeType.STREAM,
111
+ pse_count=count,
112
+ pse_last_read=page,
113
+ pse_last_read_date=bookmark_updated_at,
114
+ )
115
+
116
+ @property
117
+ def links(self):
118
+ """Create all entry links."""
119
+ result = []
120
+ if thumb := self._thumb_link():
121
+ result += [thumb]
122
+ if image := self._image_link():
123
+ result += [image]
124
+
125
+ if self.obj.group == "c" and not self.fake:
126
+ if download := self._download_link():
127
+ result += [download]
128
+ if stream := self._stream_link():
129
+ result += [stream]
130
+ if not self.metadata and (metadata := self._nav_link(metadata=True)):
131
+ result += [metadata]
132
+ elif nav := self._nav_link():
133
+ result += [nav]
134
+
135
+ return result
@@ -0,0 +1,190 @@
1
+ """OPDS v1 Facets methods."""
2
+ from dataclasses import dataclass
3
+
4
+ from django.urls import reverse
5
+
6
+ from codex.models import AdminFlag
7
+ from codex.views.browser.browser import BrowserView
8
+ from codex.views.opds.const import MimeType, Rel
9
+ from codex.views.opds.util import update_href_query_params
10
+ from codex.views.opds.v1.data import OPDS1Link
11
+ from codex.views.opds.v1.entry.data import OPDS1EntryData, OPDS1EntryObject
12
+ from codex.views.opds.v1.entry.entry import OPDS1Entry
13
+
14
+
15
+ @dataclass
16
+ class FacetGroup:
17
+ """An opds:facetGroup."""
18
+
19
+ title_prefix: str
20
+ query_param: str
21
+ glyph: str
22
+ facets: tuple
23
+
24
+
25
+ @dataclass
26
+ class Facet:
27
+ """An OPDS facet."""
28
+
29
+ value: str
30
+ title: str
31
+
32
+
33
+ class FacetGroups:
34
+ """Facet Group definitions."""
35
+
36
+ ORDER_BY = FacetGroup(
37
+ "Order By",
38
+ "orderBy",
39
+ "➠",
40
+ (
41
+ Facet("date", "Date"),
42
+ Facet("sort_name", "Name"),
43
+ ),
44
+ )
45
+ ORDER_REVERSE = FacetGroup(
46
+ "Order",
47
+ "orderReverse",
48
+ "⇕",
49
+ (Facet("false", "Ascending"), Facet("true", "Descending")),
50
+ )
51
+ ALL = (ORDER_BY, ORDER_REVERSE)
52
+
53
+
54
+ class RootFacetGroups:
55
+ """Facet Groups that only appear at the root."""
56
+
57
+ TOP_GROUP = FacetGroup(
58
+ "",
59
+ "topGroup",
60
+ "⊙",
61
+ (
62
+ Facet("p", "Publishers View"),
63
+ Facet("s", "Series View"),
64
+ Facet("f", "Folder View"),
65
+ Facet("a", "Story Arc View"),
66
+ ),
67
+ )
68
+ ALL = (TOP_GROUP,)
69
+
70
+
71
+ DEFAULT_FACETS = {
72
+ "topGroup": "p",
73
+ "orderBy": "sort_name",
74
+ "orderReverse": "false",
75
+ }
76
+
77
+
78
+ class FacetsMixin(BrowserView):
79
+ """OPDS 1 Facets methods."""
80
+
81
+ # Overwritten by get_object()
82
+ use_facets = False
83
+ skip_order_facets = False
84
+ acquisition_groups = frozenset()
85
+ mime_type_map = MimeType.FILE_TYPE_MAP
86
+ obj = {}
87
+
88
+ def _facet(self, kwargs, facet_group, facet_title, new_query_params):
89
+ href = reverse("opds:v1:feed", kwargs=kwargs)
90
+ facet_active = False
91
+ for key, val in new_query_params.items():
92
+ if self.request.query_params.get(key) == val:
93
+ facet_active = True
94
+ break
95
+ href = update_href_query_params(
96
+ href, self.request.query_params, new_query_params
97
+ )
98
+
99
+ title = " ".join(filter(None, (facet_group.title_prefix, facet_title))).strip()
100
+ return OPDS1Link(
101
+ Rel.FACET,
102
+ href,
103
+ MimeType.NAV,
104
+ title=title,
105
+ facet_group=facet_group.query_param,
106
+ facet_active=facet_active,
107
+ )
108
+
109
+ def _facet_entry(self, item, facet_group, facet, query_params):
110
+ name = " ".join(
111
+ filter(None, (facet_group.glyph, facet_group.title_prefix, facet.title))
112
+ ).strip()
113
+ entry_obj = OPDS1EntryObject(
114
+ group=item.get("group"),
115
+ pk=item.get("pk"),
116
+ name=name,
117
+ )
118
+ qps = {**self.request.query_params}
119
+ qps.update(query_params)
120
+ issue_max = self.obj.get("issue_max", 0)
121
+ data = OPDS1EntryData(
122
+ self.acquisition_groups, issue_max, False, self.mime_type_map
123
+ )
124
+ return OPDS1Entry(entry_obj, qps, data)
125
+
126
+ def _is_facet_active(self, facet_group, facet):
127
+ compare = [facet.value]
128
+ default_val = DEFAULT_FACETS.get(facet_group.query_param)
129
+ if facet.value == default_val:
130
+ compare += [None]
131
+ return self.request.query_params.get(facet_group.query_param) in compare
132
+
133
+ @staticmethod
134
+ def _did_special_group_change(group, facet_group):
135
+ """Test if one of the special groups changed."""
136
+ for test_group in ("f", "a"):
137
+ if (
138
+ group == test_group
139
+ and facet_group != test_group
140
+ or group != test_group
141
+ and facet_group == test_group
142
+ ):
143
+ return True
144
+ return False
145
+
146
+ def _facet_or_facet_entry(self, facet_group, facet, entries):
147
+ # This logic preempts facet:activeFacet but no one uses it.
148
+ # don't add default facets if in default mode.
149
+ if self._is_facet_active(facet_group, facet):
150
+ return None
151
+
152
+ group = self.kwargs.get("group")
153
+ if facet_group.query_param == "topGroup" and self._did_special_group_change(
154
+ group, facet.value
155
+ ):
156
+ kwargs = {"group": facet.value, "pk": 0, "page": 1}
157
+ else:
158
+ kwargs = self.kwargs
159
+
160
+ qps = {facet_group.query_param: facet.value}
161
+ if entries and self.kwargs.get("page") == 1:
162
+ facet = self._facet_entry(kwargs, facet_group, facet, qps)
163
+ else:
164
+ facet = self._facet(kwargs, facet_group, facet.title, qps)
165
+ return facet
166
+
167
+ def _facet_group(self, facet_group, entries):
168
+ facets = []
169
+ for facet in facet_group.facets:
170
+ if facet.value == "f":
171
+ efv_flag = (
172
+ AdminFlag.objects.only("on")
173
+ .get(key=AdminFlag.FlagChoices.FOLDER_VIEW.value)
174
+ .on
175
+ )
176
+ if not efv_flag:
177
+ continue
178
+ if facet_obj := self._facet_or_facet_entry(facet_group, facet, entries):
179
+ facets += [facet_obj]
180
+ return facets
181
+
182
+ def facets(self, entries=False, root=True):
183
+ """Return facets."""
184
+ facets = []
185
+ if not self.skip_order_facets:
186
+ facets += self._facet_group(FacetGroups.ORDER_BY, entries)
187
+ facets += self._facet_group(FacetGroups.ORDER_REVERSE, entries)
188
+ if root:
189
+ facets += self._facet_group(RootFacetGroups.TOP_GROUP, entries)
190
+ return facets