codex 1.4.0a0__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 (279) 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/coverd.py +3 -11
  5. codex/librarian/covers/create.py +15 -21
  6. codex/librarian/covers/tasks.py +2 -16
  7. codex/librarian/importer/aggregate_metadata.py +75 -41
  8. codex/librarian/importer/clean_metadata.py +30 -7
  9. codex/librarian/importer/create_fks.py +154 -55
  10. codex/librarian/importer/deleted.py +11 -2
  11. codex/librarian/importer/failed_imports.py +44 -5
  12. codex/librarian/importer/importerd.py +37 -12
  13. codex/librarian/importer/link_comics.py +54 -31
  14. codex/librarian/importer/moved.py +55 -11
  15. codex/librarian/importer/query_fks.py +210 -48
  16. codex/librarian/importer/tasks.py +7 -7
  17. codex/librarian/janitor/cleanup.py +17 -5
  18. codex/librarian/librariand.py +10 -0
  19. codex/librarian/watchdog/events.py +11 -14
  20. codex/librarian/watchdog/observers.py +5 -1
  21. codex/logger/loggerd.py +7 -3
  22. codex/logger/logging.py +1 -1
  23. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
  24. codex/migrations/0025_add_story_arc_number.py +83 -0
  25. codex/models.py +21 -11
  26. codex/search/backend.py +1 -1
  27. codex/search/indexes.py +1 -1
  28. codex/serializers/browser.py +1 -0
  29. codex/serializers/metadata.py +5 -1
  30. codex/serializers/models.py +16 -1
  31. codex/serializers/opds/v1.py +1 -0
  32. codex/serializers/opds/v2.py +5 -2
  33. codex/serializers/reader.py +55 -16
  34. codex/settings/settings.py +1 -1
  35. codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
  36. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  37. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  38. codex/static_root/assets/admin-12749881.js +41 -0
  39. codex/static_root/assets/admin-12749881.js.br +0 -0
  40. codex/static_root/assets/admin-12749881.js.gz +0 -0
  41. codex/static_root/assets/admin-beda768d.a614eee46307.css +1 -0
  42. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  43. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  44. codex/static_root/assets/admin-beda768d.css +1 -0
  45. codex/static_root/assets/admin-beda768d.css.br +0 -0
  46. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  47. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css +1 -0
  48. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
  49. codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
  50. codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
  51. codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
  52. codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
  53. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -0
  54. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  55. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  56. codex/static_root/assets/admin-drawer-panel-522f1e6c.js +1 -0
  57. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  58. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  59. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css +1 -0
  60. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  61. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  62. codex/static_root/assets/browser-7f7d7134.css +1 -0
  63. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  64. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  65. codex/static_root/assets/browser-af622672.d51aca96d64d.js +1 -0
  66. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  67. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  68. codex/static_root/assets/browser-af622672.js +1 -0
  69. codex/static_root/assets/browser-af622672.js.br +0 -0
  70. codex/static_root/assets/browser-af622672.js.gz +0 -0
  71. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js +1 -0
  72. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  73. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  74. codex/static_root/assets/http-error-5e17b794.js +1 -0
  75. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  76. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  77. codex/static_root/assets/{main-a6ac9581.2fd9e52cbcc3.css → main-0898f4bb.181e0145c642.css} +1 -1
  78. codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
  79. codex/static_root/assets/{main-a6ac9581.2fd9e52cbcc3.css.gz → main-0898f4bb.181e0145c642.css.gz} +0 -0
  80. codex/static_root/assets/{main-a6ac9581.css → main-0898f4bb.css} +1 -1
  81. codex/static_root/assets/main-0898f4bb.css.br +0 -0
  82. codex/static_root/assets/{main-a6ac9581.css.gz → main-0898f4bb.css.gz} +0 -0
  83. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +1 -0
  84. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  85. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  86. codex/static_root/assets/main-9e76a4c3.js +1 -0
  87. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  88. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  89. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +1 -0
  90. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  91. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  92. codex/static_root/assets/metadata-dialog-62c29ce0.js +1 -0
  93. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  94. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  95. codex/static_root/assets/{metadata-dialog-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -1
  96. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  97. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  98. codex/static_root/assets/{metadata-dialog-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -1
  99. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  100. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  101. codex/static_root/assets/{page-pdf-abfd509d.3870dab8eaf4.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
  102. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  103. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  104. codex/static_root/assets/{page-pdf-abfd509d.js → page-pdf-157ba97e.js} +61 -51
  105. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  106. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  107. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +1 -0
  108. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  109. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  110. codex/static_root/assets/reader-36266549.js +1 -0
  111. codex/static_root/assets/reader-36266549.js.br +0 -0
  112. codex/static_root/assets/reader-36266549.js.gz +0 -0
  113. codex/static_root/assets/reader-7f004141.506eecc6954b.css +1 -0
  114. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  115. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  116. codex/static_root/assets/reader-7f004141.css +1 -0
  117. codex/static_root/assets/reader-7f004141.css.br +0 -0
  118. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  119. codex/static_root/js/choices-admin.24cecf0a0568.json +1 -0
  120. codex/static_root/js/choices-admin.24cecf0a0568.json.br +0 -0
  121. codex/static_root/js/choices-admin.24cecf0a0568.json.gz +0 -0
  122. codex/static_root/js/choices-admin.json +1 -1
  123. codex/static_root/js/choices-admin.json.br +0 -0
  124. codex/static_root/js/choices-admin.json.gz +0 -0
  125. codex/static_root/js/choices.8c58714cf5b2.json +1 -0
  126. codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
  127. codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
  128. codex/static_root/js/choices.json +1 -1
  129. codex/static_root/js/choices.json.br +0 -0
  130. codex/static_root/js/choices.json.gz +0 -0
  131. codex/static_root/{manifest.64a989215af8.json → manifest.d2f93a519ada.json} +34 -34
  132. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  133. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  134. codex/static_root/manifest.json +34 -34
  135. codex/static_root/manifest.json.br +0 -0
  136. codex/static_root/manifest.json.gz +0 -0
  137. codex/static_root/staticfiles.json +1 -1
  138. codex/templates/headers-script-globals.html +1 -1
  139. codex/templates/{opds → opds_v1}/index.xml +3 -1
  140. codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
  141. codex/templates/search/indexes/codex/comic_text.txt +2 -2
  142. codex/urls/converters.py +1 -1
  143. codex/urls/opds/authentication.py +1 -1
  144. codex/urls/opds/root.py +8 -12
  145. codex/urls/opds/v1.py +12 -5
  146. codex/urls/opds/v2.py +2 -2
  147. codex/views/admin/tasks.py +6 -1
  148. codex/views/bookmark.py +2 -2
  149. codex/views/browser/base.py +23 -7
  150. codex/views/browser/browser.py +66 -56
  151. codex/views/browser/browser_annotations.py +159 -50
  152. codex/views/browser/browser_order_by.py +51 -105
  153. codex/views/browser/choices.py +75 -38
  154. codex/views/browser/filters/bookmark.py +6 -9
  155. codex/views/browser/filters/field.py +9 -6
  156. codex/views/browser/filters/group.py +12 -27
  157. codex/views/browser/filters/search.py +5 -10
  158. codex/views/browser/metadata.py +44 -19
  159. codex/views/download.py +1 -1
  160. codex/views/frontend.py +2 -3
  161. codex/views/mixins.py +15 -2
  162. codex/views/opds/const.py +8 -1
  163. codex/views/opds/util.py +37 -1
  164. codex/views/opds/v1/__init__.py +1 -1
  165. codex/views/opds/v1/data.py +21 -0
  166. codex/views/opds/v1/entry/__init__.py +1 -0
  167. codex/views/opds/v1/entry/data.py +23 -0
  168. codex/views/opds/v1/entry/entry.py +151 -0
  169. codex/views/opds/v1/entry/links.py +135 -0
  170. codex/views/opds/v1/facets.py +190 -0
  171. codex/views/opds/v1/feed.py +199 -0
  172. codex/views/opds/v1/links.py +198 -0
  173. codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
  174. codex/views/opds/v2/__init__.py +1 -1
  175. codex/views/opds/v2/const.py +10 -2
  176. codex/views/opds/v2/feed.py +82 -21
  177. codex/views/opds/v2/links.py +1 -1
  178. codex/views/opds/v2/publications.py +1 -1
  179. codex/views/opds/v2/top_links.py +1 -1
  180. codex/views/reader/page.py +6 -7
  181. codex/views/reader/reader.py +191 -61
  182. codex/views/session.py +2 -1
  183. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
  184. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/RECORD +187 -185
  185. codex/librarian/importer/db_ops.py +0 -248
  186. codex/pdf.py +0 -115
  187. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js +0 -48
  188. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.br +0 -0
  189. codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.gz +0 -0
  190. codex/static_root/assets/admin-73d93dc7.js +0 -48
  191. codex/static_root/assets/admin-73d93dc7.js.br +0 -0
  192. codex/static_root/assets/admin-73d93dc7.js.gz +0 -0
  193. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css +0 -1
  194. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.br +0 -0
  195. codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.gz +0 -0
  196. codex/static_root/assets/admin-79555229.css +0 -1
  197. codex/static_root/assets/admin-79555229.css.br +0 -0
  198. codex/static_root/assets/admin-79555229.css.gz +0 -0
  199. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js +0 -1
  200. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.br +0 -0
  201. codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.gz +0 -0
  202. codex/static_root/assets/admin-drawer-panel-64bcc083.js +0 -1
  203. codex/static_root/assets/admin-drawer-panel-64bcc083.js.br +0 -0
  204. codex/static_root/assets/admin-drawer-panel-64bcc083.js.gz +0 -0
  205. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
  206. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
  207. codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
  208. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
  209. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
  210. codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
  211. codex/static_root/assets/browser-7325db61.css +0 -1
  212. codex/static_root/assets/browser-7325db61.css.br +0 -0
  213. codex/static_root/assets/browser-7325db61.css.gz +0 -0
  214. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
  215. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
  216. codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
  217. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js +0 -1
  218. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.br +0 -0
  219. codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.gz +0 -0
  220. codex/static_root/assets/browser-d2caeed7.js +0 -1
  221. codex/static_root/assets/browser-d2caeed7.js.br +0 -0
  222. codex/static_root/assets/browser-d2caeed7.js.gz +0 -0
  223. codex/static_root/assets/http-error-0221c37d.480d5066da92.js +0 -1
  224. codex/static_root/assets/http-error-0221c37d.480d5066da92.js.br +0 -0
  225. codex/static_root/assets/http-error-0221c37d.480d5066da92.js.gz +0 -0
  226. codex/static_root/assets/http-error-0221c37d.js +0 -1
  227. codex/static_root/assets/http-error-0221c37d.js.br +0 -0
  228. codex/static_root/assets/http-error-0221c37d.js.gz +0 -0
  229. codex/static_root/assets/main-a6ac9581.2fd9e52cbcc3.css.br +0 -0
  230. codex/static_root/assets/main-a6ac9581.css.br +0 -0
  231. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js +0 -1
  232. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.br +0 -0
  233. codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.gz +0 -0
  234. codex/static_root/assets/main-e33dcfb0.js +0 -1
  235. codex/static_root/assets/main-e33dcfb0.js.br +0 -0
  236. codex/static_root/assets/main-e33dcfb0.js.gz +0 -0
  237. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
  238. codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
  239. codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
  240. codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
  241. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js +0 -1
  242. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.br +0 -0
  243. codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.gz +0 -0
  244. codex/static_root/assets/metadata-dialog-8b0e8aaa.js +0 -1
  245. codex/static_root/assets/metadata-dialog-8b0e8aaa.js.br +0 -0
  246. codex/static_root/assets/metadata-dialog-8b0e8aaa.js.gz +0 -0
  247. codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.br +0 -0
  248. codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.gz +0 -0
  249. codex/static_root/assets/page-pdf-abfd509d.js.br +0 -0
  250. codex/static_root/assets/page-pdf-abfd509d.js.gz +0 -0
  251. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css +0 -1
  252. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.br +0 -0
  253. codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.gz +0 -0
  254. codex/static_root/assets/reader-a8b8f766.css +0 -1
  255. codex/static_root/assets/reader-a8b8f766.css.br +0 -0
  256. codex/static_root/assets/reader-a8b8f766.css.gz +0 -0
  257. codex/static_root/assets/reader-fe9345d2.759c31f82998.js +0 -1
  258. codex/static_root/assets/reader-fe9345d2.759c31f82998.js.br +0 -0
  259. codex/static_root/assets/reader-fe9345d2.759c31f82998.js.gz +0 -0
  260. codex/static_root/assets/reader-fe9345d2.js +0 -1
  261. codex/static_root/assets/reader-fe9345d2.js.br +0 -0
  262. codex/static_root/assets/reader-fe9345d2.js.gz +0 -0
  263. codex/static_root/js/choices-admin.3d958ea7f83b.json +0 -1
  264. codex/static_root/js/choices-admin.3d958ea7f83b.json.br +0 -0
  265. codex/static_root/js/choices-admin.3d958ea7f83b.json.gz +0 -0
  266. codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
  267. codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
  268. codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
  269. codex/static_root/manifest.64a989215af8.json.br +0 -0
  270. codex/static_root/manifest.64a989215af8.json.gz +0 -0
  271. codex/urls/opds/opensearch.py +0 -18
  272. codex/views/opds/v1/browser.py +0 -346
  273. codex/views/opds/v1/entry.py +0 -278
  274. codex/views/opds/v1/start.py +0 -28
  275. codex/views/opds/v1/util.py +0 -162
  276. codex/views/opds/v2/start.py +0 -28
  277. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
  278. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
  279. {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -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
  )