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
@@ -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):
@@ -1,24 +1,23 @@
1
1
  """Views for reading comic books."""
2
- from django.db.models import F
2
+ from django.db.models import F, IntegerField, Value
3
3
  from django.urls import reverse
4
4
  from rest_framework.exceptions import NotFound
5
5
  from rest_framework.response import Response
6
6
 
7
7
  from codex.logger.logging import get_logger
8
- from codex.models import Bookmark, Comic
9
- from codex.serializers.reader import ReaderInfoSerializer
8
+ from codex.models import AdminFlag, Bookmark, Comic
9
+ from codex.serializers.reader import ReaderComicsSerializer
10
10
  from codex.serializers.redirect import ReaderRedirectSerializer
11
11
  from codex.views.bookmark import BookmarkBaseView
12
12
  from codex.views.session import BrowserSessionViewBase
13
13
 
14
14
  LOG = get_logger(__name__)
15
- PAGE_TTL = 60 * 60 * 24
16
15
 
17
16
 
18
17
  class ReaderView(BookmarkBaseView):
19
18
  """Get info for displaying comic pages."""
20
19
 
21
- serializer_class = ReaderInfoSerializer
20
+ serializer_class = ReaderComicsSerializer
22
21
 
23
22
  SETTINGS_ATTRS = ("fit_to", "two_pages", "read_in_reverse", "vertical")
24
23
  _COMIC_FIELDS = (
@@ -30,103 +29,234 @@ class ReaderView(BookmarkBaseView):
30
29
  "volume",
31
30
  "read_ltr",
32
31
  )
32
+ _VALID_ARC_GROUPS = frozenset(("f", "s", "a"))
33
33
 
34
- def _append_with_settings(self, books, book, bookmark_filter):
35
- """Get bookmarks and filename and append to book list."""
36
- book.settings = (
37
- Bookmark.objects.filter(**bookmark_filter, comic=book)
38
- .only(*self.SETTINGS_ATTRS)
39
- .first()
40
- )
41
- book.filename = book.filename()
42
- books.append(book)
43
-
44
- def _get_comic_query_params(self, pk):
34
+ def _get_comics_list(self):
45
35
  """Get the reader naviation group filter."""
46
- session = self.request.session.get(BrowserSessionViewBase.SESSION_KEY, {})
47
- top_group = session.get("top_group")
36
+ arc_group = self.params.get("arc_group")
48
37
 
49
- select_related_fields = ["series", "volume"]
50
- if top_group == "f":
51
- rel = "parent_folder__comic"
38
+ if arc_group == "a":
39
+ # for story arcs
40
+ rel = "story_arc_numbers__story_arc"
41
+ fields = self._COMIC_FIELDS
42
+ arc_name_rel = "story_arc_numbers__story_arc__name"
43
+ arc_pk_rel = "story_arc_numbers__story_arc__pk"
44
+ arc_index = F("story_arc_numbers__number")
45
+ ordering = ("arc_index", "date", *Comic.ORDERING)
46
+ elif arc_group == self.FOLDER_GROUP:
47
+ # folder mode
48
+ rel = "parent_folder"
49
+ fields = (*self._COMIC_FIELDS, "parent_folder")
50
+ arc_pk_rel = "parent_folder__pk"
51
+ arc_name_rel = "parent_folder__name"
52
+ arc_index = Value(None, IntegerField())
52
53
  ordering = ("path", "pk")
53
- select_related_fields += ["parent_folder"]
54
54
  else:
55
- rel = "series__comic"
55
+ # browser mode.
56
+ rel = "series"
57
+ fields = self._COMIC_FIELDS
58
+ arc_pk_rel = "series__pk"
59
+ arc_name_rel = "series__name"
60
+ arc_index = Value(None, IntegerField())
56
61
  ordering = Comic.ORDERING
57
62
 
58
- return {rel: pk}, ordering, select_related_fields
63
+ group_acl_filter = self.get_group_acl_filter(Comic)
64
+ arc_pk = self.params.get("arc_pk")
65
+ if not arc_pk:
66
+ rel += "__comic"
67
+ arc_pk = self.kwargs.get("pk")
68
+ nav_filter = {rel: arc_pk}
59
69
 
60
- def _get_group_comics(self):
61
- """Get comics for the series or folder."""
62
- pk = self.kwargs.get("pk")
63
- group_acl_filter = self.get_group_acl_filter(True)
64
- (
65
- group_nav_filter,
66
- ordering,
67
- select_related_fields,
68
- ) = self._get_comic_query_params(pk)
69
-
70
- return (
70
+ qs = (
71
71
  Comic.objects.filter(group_acl_filter)
72
- .filter(**group_nav_filter)
73
- .select_related(*select_related_fields)
74
- .only(*self._COMIC_FIELDS)
72
+ .filter(**nav_filter)
73
+ .prefetch_related("story_arc_numbers__story_arc")
74
+ .only(*fields)
75
75
  .annotate(
76
76
  series_name=F("series__name"),
77
77
  volume_name=F("volume__name"),
78
78
  issue_count=F("volume__issue_count"),
79
79
  )
80
- .order_by(*ordering)
80
+ .annotate(
81
+ arc_pk=F(arc_pk_rel),
82
+ arc_name=F(arc_name_rel),
83
+ arc_index=arc_index,
84
+ )
81
85
  )
82
86
 
83
- def _raise_not_found(self):
84
- """Raise not found exception."""
85
- pk = self.kwargs.get("pk")
86
- detail = {
87
- "route": reverse("app:start"),
88
- "reason": f"comic {pk} not found",
89
- "serializer": ReaderRedirectSerializer,
90
- }
91
- raise NotFound(detail=detail)
87
+ return qs.order_by(*ordering)
92
88
 
93
- def get_object(self):
94
- """Get the previous and next comics in a series.
89
+ def _append_with_settings(self, book, bookmark_filter):
90
+ """Get bookmarks and filename and append to book list."""
91
+ book.settings = (
92
+ Bookmark.objects.filter(**bookmark_filter, comic=book)
93
+ .only(*self.SETTINGS_ATTRS)
94
+ .first()
95
+ )
96
+ return book
97
+
98
+ def _get_book_collection(self):
99
+ """Get the -1, +1 window around the current issue.
95
100
 
96
101
  Uses iteration in python. There are some complicated ways of
97
102
  doing this with __gt[0] & __lt[0] in the db, but I think they
98
103
  might be even more expensive.
104
+
105
+ Yields 1 to 3 books
99
106
  """
100
- comics = self._get_group_comics()
107
+ comics = self._get_comics_list()
101
108
  bookmark_filter = self.get_bookmark_filter()
102
-
103
- # Select the -1, +1 window around the current issue
104
- # Yields 1 to 3 books
105
- books = []
109
+ books = {}
106
110
  prev_book = None
107
111
  pk = self.kwargs.get("pk")
108
112
  for index, book in enumerate(comics):
109
- book.series_index = index + 1 # type: ignore
110
113
  if books:
111
114
  # after match set next comic and break
112
- self._append_with_settings(books, book, bookmark_filter)
115
+ books["next"] = self._append_with_settings(book, bookmark_filter)
113
116
  break
114
117
  if book.pk == pk:
115
118
  # first match. set previous and current comic
116
119
  if prev_book:
117
- self._append_with_settings(books, prev_book, bookmark_filter)
118
- self._append_with_settings(books, book, bookmark_filter)
120
+ books["prev"] = self._append_with_settings(
121
+ prev_book, bookmark_filter
122
+ )
123
+ # create extra current book attrs:
124
+ if book.arc_index is None: # type: ignore
125
+ book.arc_index = index + 1 # type: ignore
126
+ book.filename = book.filename() # type: ignore
127
+
128
+ books["current"] = self._append_with_settings(book, bookmark_filter)
119
129
  else:
120
130
  # Haven't matched yet, so set the previous comic
121
131
  prev_book = book
132
+ return books, comics.count()
133
+
134
+ def _get_folder_arc(self, book):
135
+ """Create the folder arc."""
136
+ efv_flag = (
137
+ AdminFlag.objects.only("on")
138
+ .get(key=AdminFlag.FlagChoices.FOLDER_VIEW.value)
139
+ .on
140
+ )
122
141
 
123
- if not books:
142
+ if efv_flag:
143
+ folder_arc = {
144
+ "group": self.FOLDER_GROUP,
145
+ "pk": book.parent_folder.pk,
146
+ "name": book.parent_folder.name,
147
+ }
148
+ else:
149
+ folder_arc = None
150
+ return folder_arc
151
+
152
+ def _get_arcs(self, book):
153
+ """Get all series/folder/story arcs."""
154
+ # create top arcs
155
+ folder_arc = self._get_folder_arc(book)
156
+ series_arc = {"group": "s", "pk": book.series.pk, "name": book.series.name}
157
+
158
+ # order top arcs
159
+ top_group = self.params.get("top_group")
160
+ if top_group == self.FOLDER_GROUP and folder_arc:
161
+ arc = folder_arc
162
+ other_arc = series_arc
163
+ else:
164
+ arc = series_arc
165
+ other_arc = folder_arc
166
+
167
+ arcs = []
168
+ arcs.append(arc)
169
+ if other_arc:
170
+ arcs.append(other_arc)
171
+
172
+ # story arcs
173
+ sas = []
174
+ for san in book.story_arc_numbers.all():
175
+ sa = san.story_arc
176
+ arc = {
177
+ "group": "a",
178
+ "pk": sa.pk,
179
+ "name": sa.name,
180
+ }
181
+ sas.append(arc)
182
+ sas = sorted(sas, key=lambda x: x["name"])
183
+ arcs += sas
184
+ return arcs
185
+
186
+ def _raise_not_found(self):
187
+ """Raise not found exception."""
188
+ pk = self.kwargs.get("pk")
189
+ detail = {
190
+ "route": reverse("app:start"),
191
+ "reason": f"comic {pk} not found",
192
+ "serializer": ReaderRedirectSerializer,
193
+ }
194
+ raise NotFound(detail=detail)
195
+
196
+ def get_object(self):
197
+ """Get the previous and next comics in a group or story arc."""
198
+ # Books
199
+ books, arc_count = self._get_book_collection()
200
+
201
+ current = books.get("current")
202
+ if not current:
124
203
  self._raise_not_found()
125
204
 
126
- return {"books": books, "series_count": comics.count()}
205
+ prev_book = books.get("prev")
206
+ next_book = books.get("next")
207
+ books = {
208
+ "current": current,
209
+ "prev_book": prev_book,
210
+ "next_book": next_book,
211
+ }
212
+
213
+ # Arcs
214
+ arcs = self._get_arcs(current)
215
+
216
+ arc_group = self.params.get("arc_group")
217
+ arc = {
218
+ "group": arc_group,
219
+ "pk": current.arc_pk, # type: ignore
220
+ "index": current.arc_index, # type: ignore
221
+ "count": arc_count,
222
+ }
223
+
224
+ return {
225
+ "books": books,
226
+ "arcs": arcs,
227
+ "arc": arc,
228
+ }
229
+
230
+ def _parse_params(self):
231
+ data = self.request.GET
232
+
233
+ # PARAMS
234
+ session = self.request.session.get(BrowserSessionViewBase.SESSION_KEY, {})
235
+ top_group = session.get("top_group", "s")
236
+ arc_group = data.get("arcGroup")
237
+ if not arc_group:
238
+ arc_group = top_group
239
+
240
+ if arc_group not in self._VALID_ARC_GROUPS:
241
+ arc_group = "s"
242
+
243
+ arc_pk = data.get("arcPk")
244
+ if arc_pk is not None:
245
+ arc_pk = int(arc_pk)
246
+ elif top_group == "a":
247
+ last_route = session.get("route", {})
248
+ arc_pk = last_route.get("pk")
249
+
250
+ params = {
251
+ "arc_group": arc_group,
252
+ "arc_pk": arc_pk,
253
+ "top_group": top_group,
254
+ }
255
+ self.params = params
127
256
 
128
257
  def get(self, *args, **kwargs):
129
258
  """Get the book info."""
259
+ self._parse_params()
130
260
  obj = self.get_object()
131
261
  serializer = self.get_serializer(obj)
132
262
  return Response(serializer.data)
codex/views/session.py CHANGED
@@ -63,6 +63,7 @@ class BrowserSessionViewBase(SessionViewBaseBase):
63
63
 
64
64
  SESSION_KEY = "browser" # type: ignore
65
65
  CREATOR_PERSON_UI_FIELD = "creators"
66
+ STORY_ARC_UI_FIELD = "story_arcs"
66
67
  _DYNAMIC_FILTER_DEFAULTS = {
67
68
  "age_rating": [],
68
69
  "characters": [],
@@ -79,7 +80,7 @@ class BrowserSessionViewBase(SessionViewBaseBase):
79
80
  "q": "",
80
81
  "read_ltr": [],
81
82
  "series_groups": [],
82
- "story_arcs": [],
83
+ STORY_ARC_UI_FIELD: [],
83
84
  "tags": [],
84
85
  "teams": [],
85
86
  "year": [],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codex
3
- Version: 1.4.0a0
3
+ Version: 1.4.1
4
4
  Summary: A comic archive web server.
5
5
  Home-page: https://github.com/ajslater/codex
6
6
  License: GPL-3.0-only
@@ -28,27 +28,24 @@ Requires-Dist: aioprocessing (>=2.0.1,<3.0.0)
28
28
  Requires-Dist: ansicolors (>=1.1,<2.0)
29
29
  Requires-Dist: case-converter (>=1.1.0,<2.0.0)
30
30
  Requires-Dist: channels (>=4.0.0,<5.0.0)
31
- Requires-Dist: comicbox (>=0.6.7,<0.7.0)
31
+ Requires-Dist: comicbox[pdf] (>=0.10.1,<0.11.0)
32
32
  Requires-Dist: django (>=4.2,<5.0)
33
- Requires-Dist: django-cors-headers (>=3.2,<4.0)
33
+ Requires-Dist: django-cors-headers (>=4.0,<5.0)
34
34
  Requires-Dist: django-haystack (>=3.2.1,<4.0.0)
35
35
  Requires-Dist: django-rest-registration (>=0.8.0,<0.9.0)
36
- Requires-Dist: django-vite (>=2.0.2,<3.0.0)
36
+ Requires-Dist: django-vite (==2.1.1)
37
37
  Requires-Dist: djangorestframework (>=3.11,<4.0)
38
38
  Requires-Dist: djangorestframework-camel-case (>=1.3.0,<2.0.0)
39
39
  Requires-Dist: drf-spectacular (>=0.26.0,<0.27.0)
40
- Requires-Dist: filelock (>=3.4.2,<4.0.0)
41
- Requires-Dist: filetype (>=1.0.12,<2.0.0)
42
40
  Requires-Dist: fnvhash (>=0.1,<0.2)
43
41
  Requires-Dist: humanfriendly (>=10.0,<11.0)
44
42
  Requires-Dist: humanize (>=4.0.0,<5.0.0)
45
43
  Requires-Dist: hypercorn[h3] (>=0.14.1,<0.15.0)
46
44
  Requires-Dist: psutil (>=5.9.4,<6.0.0)
47
45
  Requires-Dist: pycountry (>=22.1,<23.0)
48
- Requires-Dist: pymupdf (>=1.21.1,<2.0.0)
49
46
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
50
47
  Requires-Dist: requests (>=2.24,<3.0)
51
- Requires-Dist: tzlocal (>=4.1,<5.0)
48
+ Requires-Dist: tzlocal (>=5.0,<6.0)
52
49
  Requires-Dist: watchdog (>=3.0,<4.0)
53
50
  Requires-Dist: websocket_client (>=1.2,<2.0)
54
51
  Requires-Dist: whitenoise[brotli] (>=6.0,<7.0)
@@ -163,45 +160,17 @@ packaged for Linux, but here are some instructions:
163
160
 
164
161
  Unrar as packaged for Alpine Linux v3.14 seems to work on Alpine v3.15
165
162
 
166
- Codex will also prefer to use the `unrar-cffi` package it finds it installed,
167
- this is not required.
168
-
169
163
  #### Windows
170
164
 
171
165
  Windows users should use Docker to run Codex until this documentation section is
172
166
  complete.
173
167
 
174
- Codex can _probably_ run using Cygwin or the Windows Linux Subsystem but I
175
- haven't done it yet. Contributions to this documentation accepted on
176
- [the outstanding issue](https://github.com/ajslater/codex/issues/76) or discord.
177
-
178
- ##### Windows Linux Subsystem
179
-
180
- Untested. Try following the instructions for [Debian](#debian) above.
181
-
182
- ##### Cygwin
168
+ Codex can _probably_ Windows Linux Subsystem but I haven't done personally
169
+ tested it yet. Try following the instructions for [Debian](#debian) above. There
170
+ may be outstanding platform related bugs.
183
171
 
184
- Untested partial instructions for the brave.
185
-
186
- 1. Install [Cygwin](https://www.cygwin.com/).
187
- 2. Install wget with cygwin.
188
- 3. Install:
189
- - python3.9+
190
- - gcc
191
- - gcc-g++
192
- - libffi-devel
193
- - libjpeg-devel
194
- - libssl-devel
195
- - mpfr
196
- - mpc
197
- - python3-devel
198
- - python39-cffi
199
- - python3.9-openssl with cygwin.
200
- 4. Using a terminal:
201
-
202
- ```sh
203
- pip install wheel
204
- ```
172
+ Contributions to this documentation accepted on
173
+ [the outstanding issue](https://github.com/ajslater/codex/issues/76) or discord.
205
174
 
206
175
  #### Install Codex with pip
207
176