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
@@ -0,0 +1,199 @@
1
+ """OPDS v1 feed."""
2
+ from datetime import datetime, timezone
3
+
4
+ from drf_spectacular.utils import extend_schema
5
+ from rest_framework.authentication import BasicAuthentication, SessionAuthentication
6
+ from rest_framework.response import Response
7
+
8
+ from codex.logger.logging import get_logger
9
+ from codex.serializers.opds.v1 import (
10
+ OPDS1TemplateSerializer,
11
+ )
12
+ from codex.views.browser.browser import BrowserView
13
+ from codex.views.opds.const import (
14
+ BLANK_TITLE,
15
+ FALSY,
16
+ MimeType,
17
+ )
18
+ from codex.views.opds.v1.entry.data import OPDS1EntryData
19
+ from codex.views.opds.v1.entry.entry import OPDS1Entry
20
+ from codex.views.opds.v1.links import (
21
+ LinksMixin,
22
+ RootTopLinks,
23
+ TopLinks,
24
+ )
25
+ from codex.views.template import CodexXMLTemplateView
26
+
27
+ LOG = get_logger(__name__)
28
+
29
+
30
+ class OpdsNs:
31
+ """XML Namespaces."""
32
+
33
+ CATALOG = "http://opds-spec.org/2010/catalog"
34
+ ACQUISITION = "http://opds-spec.org/2010/acquisition"
35
+
36
+
37
+ class UserAgentPrefixes:
38
+ """Control whether to hack in facets with nav links."""
39
+
40
+ CLIENT_REORDERS = ("Chunky",)
41
+ FACET_SUPPORT = ("yar",) # kybooks
42
+ SIMPLE_DOWNLOAD_MIME_TYPES = ("PocketBook",)
43
+ # Other known valid prefixes:
44
+ # "Panels", "Chunky"
45
+
46
+
47
+ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
48
+ """OPDS 1 Feed."""
49
+
50
+ authentication_classes = (BasicAuthentication, SessionAuthentication)
51
+ template_name = "opds_v1/index.xml"
52
+ serializer_class = OPDS1TemplateSerializer
53
+
54
+ @property
55
+ def opds_ns(self):
56
+ """Dynamic opds namespace."""
57
+ try:
58
+ return OpdsNs.ACQUISITION if self.is_aq_feed else OpdsNs.CATALOG
59
+ except Exception as exc:
60
+ LOG.exception(exc)
61
+
62
+ @property
63
+ def is_acquisition(self):
64
+ """Is acquisition."""
65
+ return self.is_aq_feed
66
+
67
+ @property
68
+ def id_tag(self):
69
+ """Feed id is the url."""
70
+ try:
71
+ return self.request.build_absolute_uri()
72
+ except Exception as exc:
73
+ LOG.exception(exc)
74
+
75
+ @property
76
+ def title(self):
77
+ """Create the feed title."""
78
+ result = ""
79
+ try:
80
+ if browser_title := self.obj.get("browser_title"):
81
+ parent_name = browser_title.get("parent_name", "All")
82
+ if not parent_name and self.kwargs.get("pk") == 0:
83
+ parent_name = "All"
84
+ group_name = browser_title.get("group_name")
85
+ result = " ".join(filter(None, (parent_name, group_name))).strip()
86
+
87
+ if not result:
88
+ result = BLANK_TITLE
89
+ except Exception as exc:
90
+ LOG.exception(exc)
91
+ return result
92
+
93
+ @property
94
+ def updated(self):
95
+ """Hack in feed update time from cover timestamp."""
96
+ try:
97
+ if ts := self.obj.get("covers_timestamp"):
98
+ return datetime.fromtimestamp(ts, tz=timezone.utc)
99
+ except Exception as exc:
100
+ LOG.exception(exc)
101
+
102
+ @property
103
+ def items_per_page(self):
104
+ """Return opensearch:itemsPerPage."""
105
+ try:
106
+ if self.params.get("q"):
107
+ return self.MAX_OBJ_PER_PAGE
108
+ except Exception as exc:
109
+ LOG.exception(exc)
110
+
111
+ @property
112
+ def total_results(self):
113
+ """Return opensearch:totalResults."""
114
+ try:
115
+ if self.params.get("q"):
116
+ return self.obj.get("total_count", 0)
117
+ except Exception as exc:
118
+ LOG.exception(exc)
119
+
120
+ def _get_entries_section(self, key, metadata):
121
+ """Get entries by key section."""
122
+ entries = []
123
+ issue_max = self.obj.get("issue_max", 0)
124
+ data = OPDS1EntryData(
125
+ self.acquisition_groups, issue_max, metadata, self.mime_type_map
126
+ )
127
+ if objs := self.obj.get(key):
128
+ for obj in objs:
129
+ entries += [OPDS1Entry(obj, self.request.query_params, data)]
130
+ return entries
131
+
132
+ @property
133
+ def entries(self):
134
+ """Create all the entries."""
135
+ entries = []
136
+ try:
137
+ at_root = self.kwargs.get("pk") == 0
138
+ if not self.use_facets and self.kwargs.get("page") == 1:
139
+ entries += self.add_top_links(TopLinks.ALL)
140
+ if at_root:
141
+ entries += self.add_top_links(RootTopLinks.ALL)
142
+ entries += self.facets(entries=True, root=at_root)
143
+
144
+ entries += self._get_entries_section("groups", False)
145
+ metadata = (
146
+ self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
147
+ )
148
+ entries += self._get_entries_section("books", metadata)
149
+ except Exception as exc:
150
+ LOG.exception(exc)
151
+ return entries
152
+
153
+ def get_object(self):
154
+ """Get the browser page and serialize it for this subclass."""
155
+ group = self.kwargs.get("group")
156
+ if group == "a":
157
+ self.acquisition_groups = frozenset(["a"])
158
+ pk = self.kwargs.get("pk")
159
+ self.is_opds_1_acquisition = group in self.acquisition_groups and pk
160
+ else:
161
+ self.acquisition_groups = frozenset(self.valid_nav_groups[-2:])
162
+ self.is_opds_1_acquisition = group in self.acquisition_groups
163
+ self.is_opds_metadata = (
164
+ self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
165
+ )
166
+ self.obj = super().get_object()
167
+ self.is_aq_feed = self.obj.get("model_group") == "c"
168
+ return self
169
+
170
+ def _set_user_agent_variables(self):
171
+ """Set User Agent variables."""
172
+ # defaults in FacetsMixin
173
+ user_agent = self.request.headers.get("User-Agent")
174
+ if not user_agent:
175
+ return
176
+ for prefix in UserAgentPrefixes.FACET_SUPPORT:
177
+ if user_agent.startswith(prefix):
178
+ self.use_facets = True
179
+ break
180
+ for prefix in UserAgentPrefixes.CLIENT_REORDERS:
181
+ if user_agent.startswith(prefix):
182
+ self.skip_order_facets = True
183
+ break
184
+ for prefix in UserAgentPrefixes.SIMPLE_DOWNLOAD_MIME_TYPES:
185
+ if user_agent.startswith(prefix):
186
+ self.mime_type_map = MimeType.SIMPLE_FILE_TYPE_MAP
187
+ break
188
+
189
+ @extend_schema(request=BrowserView.input_serializer_class)
190
+ def get(self, *args, **kwargs):
191
+ """Get the feed."""
192
+ self.parse_params()
193
+ self.validate_settings()
194
+ self._set_user_agent_variables()
195
+ self.skip_order_facets |= self.kwargs.get("group") == "c"
196
+
197
+ obj = self.get_object()
198
+ serializer = self.get_serializer(obj)
199
+ return Response(serializer.data, content_type=self.content_type)
@@ -0,0 +1,198 @@
1
+ """OPDS v1 Links methods."""
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass
4
+ from typing import Union
5
+
6
+ from comicbox.metadata.comic_json import json
7
+ from django.urls import reverse
8
+
9
+ from codex.logger.logging import get_logger
10
+ from codex.views.opds.const import MimeType, Rel
11
+ from codex.views.opds.util import update_href_query_params
12
+ from codex.views.opds.v1.data import OPDS1Link
13
+ from codex.views.opds.v1.entry.data import OPDS1EntryData, OPDS1EntryObject
14
+ from codex.views.opds.v1.entry.entry import OPDS1Entry
15
+ from codex.views.opds.v1.facets import FacetsMixin
16
+
17
+ LOG = get_logger(__name__)
18
+
19
+
20
+ class TopRoutes:
21
+ """Routes for top groups."""
22
+
23
+ PUBLISHER = {"group": "p", "pk": 0, "page": 1}
24
+ SERIES = {"group": "s", "pk": 0, "page": 1}
25
+ FOLDER = {"group": "f", "pk": 0, "page": 1}
26
+ ROOT = {"group": "r", "pk": 0, "page": 1}
27
+ STORY_ARC = {"group": "a", "pk": 0, "page": 1}
28
+
29
+
30
+ @dataclass
31
+ class TopLink:
32
+ """A non standard root link when facets are unsupported."""
33
+
34
+ kwargs: dict
35
+ rel: str
36
+ mime_type: str
37
+ query_params: defaultdict[str, Union[str, bool, int]]
38
+ glyph: str
39
+ title: str
40
+ desc: str
41
+
42
+
43
+ class TopLinks:
44
+ """Top link definitions."""
45
+
46
+ START = TopLink(
47
+ TopRoutes.ROOT,
48
+ Rel.START,
49
+ MimeType.NAV,
50
+ defaultdict(),
51
+ "⌂",
52
+ "Start of the catalog",
53
+ "",
54
+ )
55
+ ALL = (START,)
56
+
57
+
58
+ class RootTopLinks:
59
+ """Top Links that only appear at the root."""
60
+
61
+ NEW = TopLink(
62
+ TopRoutes.SERIES,
63
+ Rel.SORT_NEW,
64
+ MimeType.ACQUISITION,
65
+ defaultdict(
66
+ None, {"orderBy": "created_at", "orderReverse": True, "limit": 100}
67
+ ),
68
+ "📥",
69
+ "Recently Added",
70
+ "",
71
+ )
72
+ FEATURED = TopLink(
73
+ TopRoutes.SERIES,
74
+ Rel.FEATURED,
75
+ MimeType.NAV,
76
+ defaultdict(
77
+ None,
78
+ {
79
+ "orderBy": "date",
80
+ "filters": json.dumps({"bookmark": "UNREAD"}),
81
+ "limit": 100,
82
+ },
83
+ ),
84
+ "📚",
85
+ "Oldest Unread",
86
+ "Unread issues, oldest published first",
87
+ )
88
+ LAST_READ = TopLink(
89
+ TopRoutes.SERIES,
90
+ Rel.POPULAR,
91
+ MimeType.NAV,
92
+ defaultdict(
93
+ None, {"orderBy": "bookmark_updated_at", "orderReverse": True, "limit": 100}
94
+ ),
95
+ "👀",
96
+ "Last Read",
97
+ "Last Read issues, recently read first.",
98
+ )
99
+ ALL = (NEW, FEATURED, LAST_READ)
100
+
101
+
102
+ class LinksMixin(FacetsMixin):
103
+ """OPDS 1 Links methods."""
104
+
105
+ # overwritten in get_object()
106
+ DEFAULT_ROUTE_NAME = "opds:v1:feed"
107
+ is_aq_feed = False
108
+
109
+ def is_top_link_displayed(self, top_link):
110
+ """Determine if this top link should be displayed."""
111
+ for key, value in top_link.kwargs.items():
112
+ if str(self.kwargs.get(key)) != str(value):
113
+ return False
114
+
115
+ for key, value in top_link.query_params.items():
116
+ if str(self.request.query_params.get(key)) != str(value):
117
+ return False
118
+
119
+ return True
120
+
121
+ def _link(self, kwargs, rel, query_params=None, mime_type=MimeType.NAV):
122
+ """Create a link."""
123
+ if query_params is None:
124
+ query_params = self.request.query_params
125
+ href = reverse("opds:v1:feed", kwargs=kwargs)
126
+ href = update_href_query_params(href, query_params)
127
+ return OPDS1Link(rel, href, mime_type)
128
+
129
+ def _top_link(self, top_link):
130
+ """Create a link from a top link."""
131
+ return self._link(
132
+ top_link.kwargs, top_link.rel, top_link.query_params, top_link.mime_type
133
+ )
134
+
135
+ def _root_links(self):
136
+ """Navigation Root Links."""
137
+ links = []
138
+ if route := self.obj.get("up_route"):
139
+ links += [self._link(route, Rel.UP)]
140
+ page = self.kwargs.get("page", 1)
141
+ if page > 1:
142
+ route = {**self.kwargs, "page": page - 1}
143
+ links += [self._link(route, Rel.PREV)]
144
+ if page < self.obj.get("num_pages", 1):
145
+ route = {**self.kwargs, "page": page + 1}
146
+ links += [self._link(route, Rel.NEXT)]
147
+ return links
148
+
149
+ @property
150
+ def links(self):
151
+ """Create all the links."""
152
+ links = []
153
+ try:
154
+ mime_type = MimeType.ACQUISITION if self.is_aq_feed else MimeType.NAV
155
+ links += [
156
+ OPDS1Link("self", self.request.get_full_path(), mime_type),
157
+ OPDS1Link(
158
+ Rel.AUTHENTICATION,
159
+ reverse("opds:authentication:v1"),
160
+ MimeType.AUTHENTICATION,
161
+ ),
162
+ OPDS1Link(
163
+ "search", reverse("opds:v1:opensearch_v1"), MimeType.OPENSEARCH
164
+ ),
165
+ ]
166
+ links += self._root_links()
167
+ if self.use_facets:
168
+ for top_link in TopLinks.ALL + RootTopLinks.ALL:
169
+ if not self.is_top_link_displayed(top_link):
170
+ links += [self._top_link(top_link)]
171
+ if facets := self.facets():
172
+ links += facets
173
+ except Exception as exc:
174
+ LOG.exception(exc)
175
+ return links
176
+
177
+ def _top_link_entry(self, top_link):
178
+ """Create a entry instead of a facet."""
179
+ name = " ".join(filter(None, (top_link.glyph, top_link.title)))
180
+ entry_obj = OPDS1EntryObject(
181
+ group=top_link.kwargs["group"],
182
+ pk=top_link.kwargs["pk"],
183
+ name=name,
184
+ summary=top_link.desc,
185
+ )
186
+ issue_max = self.obj.get("issue_max", 0)
187
+ data = OPDS1EntryData(
188
+ self.acquisition_groups, issue_max, False, self.mime_type_map
189
+ )
190
+ return OPDS1Entry(entry_obj, top_link.query_params, data)
191
+
192
+ def add_top_links(self, top_links):
193
+ """Add a list of top links as entries if they should be enabled."""
194
+ entries = []
195
+ for tl in top_links:
196
+ if not self.is_top_link_displayed(tl):
197
+ entries += [self._top_link_entry(tl)]
198
+ return entries
@@ -1,4 +1,4 @@
1
- """Serve an opensearch document."""
1
+ """Serve an opensearch v1 document."""
2
2
  from drf_spectacular.types import OpenApiTypes
3
3
  from drf_spectacular.utils import extend_schema
4
4
  from rest_framework.authentication import BasicAuthentication, SessionAuthentication
@@ -8,10 +8,10 @@ from codex.views.template import CodexXMLTemplateView
8
8
 
9
9
 
10
10
  @extend_schema(responses={("200", "application/xml"): OpenApiTypes.BYTE})
11
- class OpenSearchView(CodexXMLTemplateView):
11
+ class OpenSearch1View(CodexXMLTemplateView):
12
12
  """OpenSearchView."""
13
13
 
14
14
  authentication_classes = (BasicAuthentication, SessionAuthentication)
15
15
  permission_classes = [IsAuthenticatedOrEnabledNonUsers]
16
- template_name = "opds/opensearch.xml"
16
+ template_name = "opds_v1/opensearch_v1.xml"
17
17
  content_type = "application/xml"
@@ -1 +1 @@
1
- """OPDS 2 Views."""
1
+ """OPDS v2 Views."""
@@ -1,4 +1,4 @@
1
- """OPDS Common consts."""
1
+ """OPDS v2 consts."""
2
2
  from dataclasses import dataclass
3
3
  from typing import Optional, Union
4
4
 
@@ -41,6 +41,7 @@ FACETS = (
41
41
  Facet("p", "Publishers View"),
42
42
  Facet("s", "Series View"),
43
43
  Facet("f", "Folder View"),
44
+ Facet("a", "Story Arc View"),
44
45
  ),
45
46
  )
46
47
  # Could add Filters as well.
@@ -81,6 +82,12 @@ GROUPS = (
81
82
  "s",
82
83
  {"orderBy": "date", "orderReverse": False},
83
84
  ),
85
+ NavigationLink(
86
+ Rel.POPULAR,
87
+ "Last Read",
88
+ "s",
89
+ {"orderBy": "bookmark_updated_at", "orderReverse": True},
90
+ ),
84
91
  ),
85
92
  ),
86
93
  NavigationGroup(
@@ -89,7 +96,8 @@ GROUPS = (
89
96
  NavigationLink(Rel.SUB, "Root", "r", None),
90
97
  NavigationLink(Rel.SUB, "Publishers", "p", None),
91
98
  NavigationLink(Rel.SUB, "Series", "s", None),
92
- NavigationLink(Rel.SUB, "Folders", "f", None),
99
+ NavigationLink(Rel.SUB, "Folders", "f", {"topGroup": "f"}),
100
+ NavigationLink(Rel.SUB, "Story Arcs", "a", {"topGroup": "a"}),
93
101
  ),
94
102
  ),
95
103
  )
@@ -1,4 +1,4 @@
1
- """OPDS 2.0 Feed."""
1
+ """OPDS v2.0 Feed."""
2
2
  from datetime import datetime, timezone
3
3
 
4
4
  from drf_spectacular.utils import extend_schema
@@ -6,6 +6,7 @@ from rest_framework.authentication import BasicAuthentication, SessionAuthentica
6
6
  from rest_framework.response import Response
7
7
 
8
8
  from codex.logger.logging import get_logger
9
+ from codex.models import AdminFlag
9
10
  from codex.serializers.opds.v2 import OPDS2FeedSerializer
10
11
  from codex.views.browser.browser import BrowserView
11
12
  from codex.views.opds.const import BLANK_TITLE, FALSY
@@ -27,6 +28,8 @@ LOG = get_logger(__name__)
27
28
  class OPDS2FeedView(PublicationMixin, TopLinksMixin):
28
29
  """OPDS 2.0 Feed."""
29
30
 
31
+ DEFAULT_ROUTE_NAME = "opds:v2:feed"
32
+
30
33
  authentication_classes = (BasicAuthentication, SessionAuthentication)
31
34
  serializer_class = OPDS2FeedSerializer
32
35
 
@@ -45,41 +48,92 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
45
48
  return result
46
49
 
47
50
  def _detect_user_agent(self):
48
- # Hacks for individual clients
51
+ """Hacks for individual clients."""
49
52
  user_agent = self.request.headers.get("User-Agent")
50
53
  if not user_agent:
51
54
  return
52
55
 
56
+ @staticmethod
57
+ def _is_allowed(link_spec):
58
+ """Return if the link allowed."""
59
+ if (
60
+ getattr(link_spec, "group", None) == "f"
61
+ or getattr(link_spec, "query_param_value", None) == "f"
62
+ ):
63
+ # Folder perms
64
+ efv_flag = (
65
+ AdminFlag.objects.only("on")
66
+ .get(key=AdminFlag.FlagChoices.FOLDER_VIEW.value)
67
+ .on
68
+ )
69
+ if not efv_flag:
70
+ return False
71
+ return True
72
+
73
+ @staticmethod
74
+ def _create_link_kwargs(data, link_spec):
75
+ """Create link kwargs."""
76
+ if data.group_kwarg:
77
+ # Nav Groups
78
+ pk = getattr(link_spec, "pk", 0)
79
+ kwargs = {"group": link_spec.group, "pk": pk, "page": 1}
80
+ elif link_spec.query_param_value in ("f", "a"):
81
+ # Special Facets
82
+ kwargs = {
83
+ "group": link_spec.query_param_value,
84
+ "pk": 0,
85
+ "page": 1,
86
+ }
87
+ else:
88
+ # Regular Facets
89
+ kwargs = None
90
+ return kwargs
91
+
92
+ @staticmethod
93
+ def _create_link_query_params(group_spec, link_spec, kwargs):
94
+ """Create link query params."""
95
+ if qp_key := getattr(group_spec, "query_param_key", None):
96
+ # Facet shorthand with group
97
+ qps = {qp_key: link_spec.query_param_value}
98
+ elif ls_qps := getattr(link_spec, "query_params", None):
99
+ # Nav Group
100
+ qps = ls_qps
101
+ else:
102
+ # Regular Group
103
+ qps = None
104
+
105
+ # Special order by for story_arcs
106
+ if (
107
+ kwargs
108
+ and kwargs.get("group") == "a"
109
+ and kwargs.get("pk")
110
+ and (not qps or not qps.get("orderBy"))
111
+ ):
112
+ if not qps:
113
+ qps = {}
114
+ qps["orderBy"] = "story_arc_number"
115
+ return qps
116
+
53
117
  def _create_links_section(self, group_specs, data):
54
118
  """Create links sections for groups and facets."""
55
119
  groups = []
56
120
  for group_spec in group_specs:
57
121
  link_dict = {}
58
122
  for link_spec in group_spec.links:
59
- if data.group_kwarg:
60
- # Nav Groups
61
- pk = getattr(link_spec, "pk", 0)
62
- kwargs = {"group": link_spec.group, "pk": pk, "page": 1}
63
- else:
64
- # Facets & Regular Groups
65
- kwargs = None
66
- rel = data.rel if data.rel else link_spec.rel
123
+ if not self._is_allowed(link_spec):
124
+ continue
67
125
 
68
- if qp_key := getattr(group_spec, "query_param_key", None):
69
- # Facet shorthand with group
70
- qps = {qp_key: link_spec.query_param_value}
71
- elif ls_qps := getattr(link_spec, "query_params", None):
72
- # Nav Group
73
- qps = ls_qps
74
- else:
75
- # Regular Group
76
- qps = None
126
+ kwargs = self._create_link_kwargs(data, link_spec)
127
+
128
+ qps = self._create_link_query_params(group_spec, link_spec, kwargs)
77
129
 
78
130
  title = getattr(link_spec, "title", "")
79
131
  if not title:
80
132
  title = getattr(link_spec, "name", "")
81
133
 
82
134
  href_data = HrefData(kwargs, qps)
135
+
136
+ rel = data.rel if data.rel else link_spec.rel
83
137
  link_data = LinkData(rel, href_data, title=title)
84
138
  link = self.link(link_data)
85
139
  self.link_aggregate(link_dict, link)
@@ -117,8 +171,15 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
117
171
  def get_object(self):
118
172
  """Get the browser page and serialize it for this subclass."""
119
173
  group = self.kwargs.get("group")
120
- self.acquisition_groups = frozenset(self.valid_nav_groups[-2:])
121
- self.is_opds_2_acquisition = group in self.acquisition_groups
174
+ if group in ("f", "a"):
175
+ self.acquisition_groups = frozenset()
176
+ else:
177
+ self.acquisition_groups = frozenset(self.valid_nav_groups[-2:])
178
+ if group == "a":
179
+ pk = self.kwargs["pk"]
180
+ self.is_opds_2_acquisition = bool(pk)
181
+ else:
182
+ self.is_opds_2_acquisition = group in self.acquisition_groups
122
183
  self.is_opds_metadata = (
123
184
  self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
124
185
  )
@@ -1,4 +1,4 @@
1
- """Links methods for OPDS 2.0 Feed."""
1
+ """Links methods for OPDS v2.0 Feed."""
2
2
  from copy import deepcopy
3
3
  from dataclasses import dataclass
4
4
  from typing import Optional
@@ -1,4 +1,4 @@
1
- """Publication Methods for OPDS 2.0 feed."""
1
+ """Publication Methods for OPDS v2.0 feed."""
2
2
  from urllib.parse import quote_plus
3
3
 
4
4
  from django.contrib.staticfiles.storage import staticfiles_storage
@@ -1,4 +1,4 @@
1
- """OPDS 2.0 top links section methods."""
1
+ """OPDS v2.0 top links section methods."""
2
2
  from codex.views.opds.const import MimeType, Rel
3
3
  from codex.views.opds.v2.links import HrefData, LinkData, LinksMixin
4
4
 
@@ -8,11 +8,11 @@ from rest_framework.negotiation import BaseContentNegotiation
8
8
 
9
9
  from codex.logger.logging import get_logger
10
10
  from codex.models import Comic
11
- from codex.pdf import PDF
12
11
  from codex.version import COMICBOX_CONFIG
13
12
  from codex.views.bookmark import BookmarkBaseView
14
13
 
15
14
  LOG = get_logger(__name__)
15
+ PDF_MIME_TYPE = "application/pdf"
16
16
 
17
17
 
18
18
  class IgnoreClientContentNegotiation(BaseContentNegotiation):
@@ -50,23 +50,22 @@ class ReaderPageView(BookmarkBaseView):
50
50
 
51
51
  def _get_page_image(self):
52
52
  """Get the image data and content type."""
53
- group_acl_filter = self.get_group_acl_filter(True)
53
+ group_acl_filter = self.get_group_acl_filter(Comic)
54
54
  pk = self.kwargs.get("pk")
55
55
  comic = Comic.objects.filter(group_acl_filter).only("path").get(pk=pk)
56
56
  page = self.kwargs.get("page")
57
57
  if comic.file_type == Comic.FileType.PDF.value:
58
- car = PDF(comic.path)
59
- content_type = PDF.MIME_TYPE
58
+ content_type = PDF_MIME_TYPE
60
59
  else:
61
- car = ComicArchive(comic.path, config=COMICBOX_CONFIG)
62
60
  content_type = self.content_type
63
- page_image = car.get_page_by_index(page)
61
+ with ComicArchive(comic.path, config=COMICBOX_CONFIG) as car:
62
+ page_image = car.get_page_by_index(page)
64
63
  return page_image, content_type
65
64
 
66
65
  @extend_schema(
67
66
  responses={
68
67
  (200, content_type): OpenApiTypes.BINARY,
69
- (200, PDF.MIME_TYPE): OpenApiTypes.BINARY,
68
+ (200, PDF_MIME_TYPE): OpenApiTypes.BINARY,
70
69
  }
71
70
  )
72
71
  def get(self, *args, **kwargs):