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
codex/config_default.yaml CHANGED
@@ -1,12 +1,20 @@
1
1
  comicbox:
2
- comet: True
3
- comicbookinfo: True
4
- comicinfoxml: True
5
2
  delete_orig: False
6
3
  delete_tags: False
7
4
  dest_path: .
8
5
  dry_run: False
9
- filename: True
10
6
  raw: False
7
+ read_comet: True
8
+ read_comicbookinfo: True
9
+ read_comicinfoxml: True
10
+ read_filename: True
11
+ read_pdf: True
11
12
  recurse: False
12
13
  metadata: {}
14
+ close_fd: True
15
+ check_unrar: False
16
+ write_comet: False
17
+ write_comicbookinfo: False
18
+ write_comicinfoxml: False
19
+ write_filename: False
20
+ write_pdf: False
codex/db_functions.py CHANGED
@@ -3,9 +3,11 @@ from django.db.models import Aggregate
3
3
  from django.db.models.fields import CharField
4
4
 
5
5
 
6
- # TODO unused
7
6
  class GroupConcat(Aggregate):
8
- """Sqlite3 group_concat() function."""
7
+ """Sqlite3 group_concat() function.
8
+
9
+ Unused.
10
+ """
9
11
 
10
12
  function = "GROUP_CONCAT"
11
13
  template = "%(function)s(%(distinct)s %(expressions)s)"
codex/integrity.py CHANGED
@@ -31,17 +31,19 @@ MIGRATION_0010 = "0010_haystack"
31
31
  MIGRATION_0011 = "0010_library_groups_and_metadata_changes"
32
32
  MIGRATION_0018 = "0018_rename_userbookmark_bookmark"
33
33
  MIGRATION_0023 = "0023_rename_credit_creator_and_more"
34
+ MIGRATION_0025 = "0025_add_story_arc_number"
34
35
  M2M_NAMES = {
35
36
  "Character": "characters",
36
37
  "Creator": "creators",
37
38
  "Genre": "genres",
38
39
  "Location": "locations",
39
40
  "SeriesGroup": "series_groups",
40
- "StoryArc": "story_arcs",
41
+ "StoryArcNumber": "story_arc_numbers",
41
42
  "Tag": "tags",
42
43
  "Team": "teams",
43
44
  "Folder": "folders",
44
45
  }
46
+ SKIP_M2M_CHECKS = {"Creator": MIGRATION_0023, "StoryArcNumber": MIGRATION_0025}
45
47
  NULL_SET = frozenset([None])
46
48
  HAVE_LIBRARY_FKS = ("FailedImport", "Folder", "Comic")
47
49
  GROUP_HOSTS = {
@@ -68,6 +70,7 @@ DELETE_BAD_COMIC_FOLDER_RELATIONS_SQL = (
68
70
  'IN (SELECT "codex_folder"."id" FROM "codex_folder")))'
69
71
  )
70
72
  MIGRATION_DIR = CODEX_PATH / "migrations"
73
+
71
74
  LOG = get_logger(__name__)
72
75
 
73
76
 
@@ -289,16 +292,24 @@ def _delete_errors():
289
292
  _delete_old_comic_folder_fks()
290
293
 
291
294
 
295
+ def _check_field_for_migration(model_name):
296
+ """Skip some models before migrations."""
297
+ for check_model_name, migration in SKIP_M2M_CHECKS.items():
298
+ if model_name == check_model_name and not has_applied_migration(migration):
299
+ LOG.debug(
300
+ f"Skipping {check_model_name} integrity check until"
301
+ f"migration {migration} applied."
302
+ )
303
+ return True
304
+ return False
305
+
306
+
292
307
  def _repair_integrity():
293
308
  """REPAIR the objects that are left."""
294
309
  comic_model = apps.get_model("codex", "Comic")
295
310
  bad_comic_ids = comic_model.objects.none()
296
311
  for m2m_model_name, field_name in M2M_NAMES.items():
297
- if m2m_model_name == "Creator" and not has_applied_migration(MIGRATION_0023):
298
- LOG.debug(
299
- "Skipping Creator integrity check until"
300
- f"migration {MIGRATION_0023} applied."
301
- )
312
+ if _check_field_for_migration(m2m_model_name):
302
313
  continue
303
314
  try:
304
315
  bad_comic_ids |= _fix_comic_m2m_integrity_errors(
@@ -2,8 +2,7 @@
2
2
  from codex.librarian.covers.create import CoverCreateMixin
3
3
  from codex.librarian.covers.purge import CoverPurgeMixin
4
4
  from codex.librarian.covers.tasks import (
5
- CoverBulkCreateTask,
6
- CoverCreateTask,
5
+ CoverCreateAllTask,
7
6
  CoverRemoveAllTask,
8
7
  CoverRemoveOrphansTask,
9
8
  CoverRemoveTask,
@@ -24,14 +23,7 @@ class CoverCreatorThread(CoverCreateMixin, CoverPurgeMixin):
24
23
  self.purge_comic_covers(task.comic_pks)
25
24
  elif isinstance(task, CoverRemoveOrphansTask):
26
25
  self.cleanup_orphan_covers()
27
-
28
- #####################
29
- # UNUSED BELOW HERE #
30
- #####################
31
-
32
- elif isinstance(task, CoverCreateTask):
33
- self.create_cover(task.pk)
34
- elif isinstance(task, CoverBulkCreateTask):
35
- self.bulk_create_comic_covers(task.comic_pks)
26
+ elif isinstance(task, CoverCreateAllTask):
27
+ self.create_all_covers()
36
28
  else:
37
29
  self.log.error(f"Bad task sent to {self.__class__.__name__}: {task}")
@@ -10,7 +10,6 @@ from codex.librarian.covers.path import CoverPathMixin
10
10
  from codex.librarian.covers.status import CoverStatusTypes
11
11
  from codex.librarian.covers.tasks import CoverSaveToCache
12
12
  from codex.models import Comic
13
- from codex.pdf import PDF
14
13
  from codex.status import Status
15
14
  from codex.version import COMICBOX_CONFIG
16
15
 
@@ -39,13 +38,12 @@ class CoverCreateMixin(CoverPathMixin):
39
38
  return cover_thumb_buffer.getvalue()
40
39
 
41
40
  @classmethod
42
- def _get_comic_cover_image(cls, comic):
41
+ def _get_comic_cover_image(cls, comic_path):
43
42
  """Create comic cover if none exists.
44
43
 
45
44
  Return image thumb data or path to missing file thumb.
46
45
  """
47
- car_class = PDF if comic.file_type == Comic.FileType.PDF.value else ComicArchive
48
- with car_class(comic.path, config=COMICBOX_CONFIG) as car:
46
+ with ComicArchive(comic_path, config=COMICBOX_CONFIG) as car:
49
47
  image_data = car.get_cover_image()
50
48
  if not image_data:
51
49
  reason = "Read empty cover"
@@ -58,14 +56,14 @@ class CoverCreateMixin(CoverPathMixin):
58
56
 
59
57
  Called from views/cover.
60
58
  """
61
- comic = None
59
+ comic_path = None
62
60
  try:
63
- comic = Comic.objects.only("path", "file_type").get(pk=pk)
64
- cover_image = cls._get_comic_cover_image(comic)
61
+ comic_path = Comic.objects.only("path").get(pk=pk).path
62
+ cover_image = cls._get_comic_cover_image(comic_path)
65
63
  data = cls._create_cover_thumbnail(cover_image)
66
64
  except Exception as exc:
67
65
  data = b""
68
- comic_str = comic.path if comic else f"{pk=}"
66
+ comic_str = comic_path if comic_path else f"{pk=}"
69
67
  log.warning(f"Could not create cover thumbnail for {comic_str}: {exc}")
70
68
 
71
69
  task = CoverSaveToCache(cover_path, data)
@@ -81,19 +79,8 @@ class CoverCreateMixin(CoverPathMixin):
81
79
  else:
82
80
  cover_path.symlink_to(self.MISSING_COVER_PATH)
83
81
 
84
- #####################
85
- # UNUSED BELOW HERE #
86
- #####################
87
-
88
- def create_cover(self, pk):
89
- """Create a cover from a comic id."""
90
- # XXX Unused.
91
- cover_path = self.get_cover_path(pk)
92
- self.create_cover_from_path(pk, cover_path, self.log, self.librarian_queue)
93
-
94
- def bulk_create_comic_covers(self, comic_pks):
82
+ def _bulk_create_comic_covers(self, comic_pks):
95
83
  """Create bulk comic covers."""
96
- # XXX Unused
97
84
  num_comics = len(comic_pks)
98
85
  if not num_comics:
99
86
  return None
@@ -111,7 +98,9 @@ class CoverCreateMixin(CoverPathMixin):
111
98
  status.decrement_total()
112
99
  else:
113
100
  # bulk creator creates covers inline
114
- self.create_cover(pk)
101
+ self.create_cover_from_path(
102
+ pk, cover_path, self.log, self.librarian_queue
103
+ )
115
104
  status.increment_complete()
116
105
  self.status_controller.update(status)
117
106
 
@@ -120,3 +109,8 @@ class CoverCreateMixin(CoverPathMixin):
120
109
  finally:
121
110
  self.status_controller.finish(status)
122
111
  return status.complete
112
+
113
+ def create_all_covers(self):
114
+ """Create all covers for all libraries."""
115
+ pks = Comic.objects.values_list("pk", flat=True)
116
+ self._bulk_create_comic_covers(pks)
@@ -39,20 +39,6 @@ class CoverSaveToCache(CoverTask):
39
39
  data: bytes
40
40
 
41
41
 
42
- #####################
43
- # UNUSED BELOW HERE #
44
- #####################
45
-
46
-
47
- @dataclass
48
- class CoverCreateTask(CoverTask):
49
- """Create one comic cover."""
50
-
51
- pk: int
52
-
53
-
54
42
  @dataclass
55
- class CoverBulkCreateTask(CoverTask):
56
- """A list of comic src and dest paths."""
57
-
58
- comic_pks: frozenset
43
+ class CoverCreateAllTask(CoverTask):
44
+ """A create all comic covers."""
@@ -4,13 +4,13 @@ from zipfile import BadZipFile
4
4
 
5
5
  from comicbox.comic_archive import ComicArchive
6
6
  from comicbox.exceptions import UnsupportedArchiveTypeError
7
+ from confuse import AttrDict
7
8
  from rarfile import BadRarFile
8
9
 
9
10
  from codex.comic_field_names import COMIC_M2M_FIELD_NAMES
10
11
  from codex.librarian.importer.clean_metadata import CleanMetadataMixin
11
12
  from codex.librarian.importer.status import ImportStatusTypes, status_notify
12
13
  from codex.models import Comic, Imprint, Publisher, Series, Volume
13
- from codex.pdf import PDF
14
14
  from codex.status import Status
15
15
  from codex.version import COMICBOX_CONFIG
16
16
 
@@ -20,6 +20,10 @@ class AggregateMetadataMixin(CleanMetadataMixin):
20
20
 
21
21
  _BROWSER_GROUPS = (Publisher, Imprint, Series, Volume)
22
22
  _BROWSER_GROUP_TREE_COUNT_FIELDS = frozenset(["volume_count", "issue_count"])
23
+ _GROUP_TREES_INIT = {
24
+ "group_trees": {Publisher: {}, Imprint: {}, Series: {}, Volume: {}},
25
+ }
26
+ _AGGREGATE_COMICBOX_CONFIG = AttrDict({**COMICBOX_CONFIG, "close_fd": False})
23
27
 
24
28
  @staticmethod
25
29
  def _get_file_type(path):
@@ -39,8 +43,7 @@ class AggregateMetadataMixin(CleanMetadataMixin):
39
43
  group_tree_md = {}
40
44
  failed_import = {}
41
45
  try:
42
- car_class = PDF if PDF.is_pdf(path) else ComicArchive
43
- with car_class(path, config=COMICBOX_CONFIG, closefd=False) as car:
46
+ with ComicArchive(path, config=self._AGGREGATE_COMICBOX_CONFIG) as car:
44
47
  md = car.get_metadata()
45
48
  md["file_type"] = car.get_file_type()
46
49
 
@@ -79,27 +82,53 @@ class AggregateMetadataMixin(CleanMetadataMixin):
79
82
  return md, m2m_md, group_tree_md, failed_import
80
83
 
81
84
  @staticmethod
82
- def _aggregate_m2m_metadata(all_m2m_mds, m2m_md, all_fks, path):
85
+ def _aggregate_m2m_metadata_creators(creator_dict_list, all_fks):
86
+ """Aggregate creators metadata."""
87
+ if not creator_dict_list:
88
+ return
89
+
90
+ if "creators" not in all_fks:
91
+ all_fks["creators"] = set()
92
+
93
+ for creator_dict in creator_dict_list:
94
+ # add the fk relations to fks to query.
95
+ for creator_field, name in creator_dict.items():
96
+ # These fields are ambiguous because they're fks to creator
97
+ # but aren't ever in Comic so query_fks.py can
98
+ # disambiguate with special code
99
+ if creator_field not in all_fks:
100
+ all_fks[creator_field] = set()
101
+ all_fks[creator_field].add(name)
102
+
103
+ # Add creators to the all_fks list as well.
104
+ creator_tuple = tuple(sorted(creator_dict.items()))
105
+ all_fks["creators"].add(creator_tuple)
106
+
107
+ @staticmethod
108
+ def _aggregate_m2m_metadata_story_arc_numbers(story_arc_numbers_dict, all_fks):
109
+ """Aggregate story arc numbers."""
110
+ if not story_arc_numbers_dict:
111
+ return
112
+
113
+ if "story_arc" not in all_fks:
114
+ all_fks["story_arc"] = set()
115
+ all_fks["story_arc"] |= frozenset(story_arc_numbers_dict.keys())
116
+
117
+ if "story_arc_numbers" not in all_fks:
118
+ all_fks["story_arc_numbers"] = set()
119
+ all_fks["story_arc_numbers"] |= frozenset(story_arc_numbers_dict.items())
120
+
121
+ @classmethod
122
+ def _aggregate_m2m_metadata(cls, all_m2m_mds, m2m_md, all_fks, path):
83
123
  """Aggregate many to many metadata by ."""
84
124
  # m2m fields and fks
85
125
  all_m2m_mds[path] = m2m_md
86
126
  # aggregate fks
87
127
  for field, names in m2m_md.items():
88
128
  if field == "creators":
89
- if names and "creators" not in all_fks:
90
- all_fks["creators"] = set()
91
-
92
- # for creators add the creator fks to fks
93
- for creator_dict in names:
94
- for creator_field, name in creator_dict.items():
95
- # These fields are ambiguous because they're fks to creator
96
- # but aren't ever in Comic so query_fks.py can
97
- # disambiguate with special code
98
- if creator_field not in all_fks:
99
- all_fks[creator_field] = set()
100
- all_fks[creator_field].add(name)
101
- creator_tuple = tuple(sorted(creator_dict.items()))
102
- all_fks["creators"].add(creator_tuple)
129
+ cls._aggregate_m2m_metadata_creators(names, all_fks)
130
+ elif field == "story_arc_numbers":
131
+ cls._aggregate_m2m_metadata_story_arc_numbers(names, all_fks)
103
132
  elif field != "folders":
104
133
  if field not in all_fks:
105
134
  all_fks[field] = set()
@@ -138,6 +167,28 @@ class AggregateMetadataMixin(CleanMetadataMixin):
138
167
  cls._set_max_group_count(common_args, Series, 3, "volume_count")
139
168
  cls._set_max_group_count(common_args, Volume, 4, "issue_count")
140
169
 
170
+ def _aggregate_path(self, data, path):
171
+ """Aggregate metadata for one path."""
172
+ path_str = str(path)
173
+ md, m2m_md, group_tree_md, failed_import = self._get_path_metadata(path_str)
174
+
175
+ all_failed_imports, all_mds, all_m2m_mds, all_fks, status = data
176
+ if failed_import:
177
+ all_failed_imports.update(failed_import)
178
+ else:
179
+ if md:
180
+ all_mds[path_str] = md
181
+
182
+ if m2m_md:
183
+ self._aggregate_m2m_metadata(all_m2m_mds, m2m_md, all_fks, path_str)
184
+
185
+ if group_tree_md:
186
+ self._aggregate_group_tree_metadata(all_fks, group_tree_md)
187
+
188
+ if status:
189
+ status.complete += 1
190
+ self.status_controller.update(status)
191
+
141
192
  @status_notify(status_type=ImportStatusTypes.AGGREGATE_TAGS)
142
193
  def get_aggregate_metadata(
143
194
  self,
@@ -155,30 +206,13 @@ class AggregateMetadataMixin(CleanMetadataMixin):
155
206
  all_m2m_mds = metadata["m2m_mds"]
156
207
  all_fks = metadata["fks"]
157
208
  all_failed_imports = metadata["fis"]
158
- all_fks.update(
159
- {
160
- "group_trees": {Publisher: {}, Imprint: {}, Series: {}, Volume: {}},
161
- }
162
- )
163
- for path in all_paths:
164
- path_str = str(path)
165
- md, m2m_md, group_tree_md, failed_import = self._get_path_metadata(path_str)
209
+ all_fks.update(self._GROUP_TREES_INIT)
166
210
 
167
- if failed_import:
168
- all_failed_imports.update(failed_import)
169
- else:
170
- if md:
171
- all_mds[path_str] = md
172
-
173
- if m2m_md:
174
- self._aggregate_m2m_metadata(all_m2m_mds, m2m_md, all_fks, path_str)
175
-
176
- if group_tree_md:
177
- self._aggregate_group_tree_metadata(all_fks, group_tree_md)
178
-
179
- if status:
180
- status.complete += 1
181
- self.status_controller.update(status)
211
+ if status and status.complete is None:
212
+ status.complete = 0
213
+ data = (all_failed_imports, all_mds, all_m2m_mds, all_fks, status)
214
+ for path in all_paths:
215
+ self._aggregate_path(data, path)
182
216
 
183
217
  all_fks["comic_paths"] = frozenset(all_mds.keys())
184
218
  fi_status = Status(ImportStatusTypes.FAILED_IMPORTS, 0, len(all_failed_imports))
@@ -21,10 +21,10 @@ _MD_INVALID_KEYS = frozenset(
21
21
  "updated_at",
22
22
  )
23
23
  )
24
- _MD_TRANFORMED_KEYS = frozenset(("credits",))
24
+ _MD_TRANSFORMED_KEYS = frozenset(("credits", "story_arcs"))
25
25
  _MD_VALID_KEYS = (
26
26
  frozenset([field.name for field in Comic._meta.get_fields()]) - _MD_INVALID_KEYS
27
- | _MD_TRANFORMED_KEYS
27
+ | _MD_TRANSFORMED_KEYS
28
28
  )
29
29
  _MD_DECIMAL_KEYS = frozenset(("community_rating", "critical_rating"))
30
30
  _MD_PSI_KEYS = frozenset(
@@ -33,6 +33,7 @@ _MD_PSI_KEYS = frozenset(
33
33
  "month",
34
34
  "day",
35
35
  "page_count",
36
+ "story_arc_number",
36
37
  ) # computed by presave: ("decade", "max_page", "size")
37
38
  )
38
39
  _MD_CHAR_KEYS = frozenset(
@@ -42,6 +43,7 @@ _MD_CHAR_KEYS = frozenset(
42
43
  "cover_pk",
43
44
  "file_type",
44
45
  "format",
46
+ "gtin",
45
47
  "issue_suffix",
46
48
  "language",
47
49
  "name",
@@ -59,7 +61,6 @@ _M2M_NAMED_KEYS = frozenset(
59
61
  "genres",
60
62
  "locations",
61
63
  "series_groups",
62
- "story_arcs",
63
64
  "tags",
64
65
  "teams",
65
66
  )
@@ -134,6 +135,16 @@ class CleanMetadataMixin(QueuedThread):
134
135
  summary += description
135
136
  md["summary"] = summary
136
137
 
138
+ @staticmethod
139
+ def _append_review(md) -> None:
140
+ """Append the CIX review to the CIX summary."""
141
+ if review := md.get("review", ""):
142
+ summary: str = md.get("summary", "")
143
+ if summary:
144
+ summary += "\n"
145
+ summary += review
146
+ md["summary"] = summary
147
+
137
148
  @staticmethod
138
149
  def _title_to_name(md):
139
150
  """Convert title to name for comics."""
@@ -174,13 +185,23 @@ class CleanMetadataMixin(QueuedThread):
174
185
  good_creators = []
175
186
  field: CharField = NamedModel._meta.get_field("name") # type:ignore
176
187
  for creator in creators:
177
- person = cls._clean_charfield(creator.get("person"), field)
178
- if person:
188
+ if person := cls._clean_charfield(creator.get("person"), field):
179
189
  role = cls._clean_charfield(creator.get("role"), field)
180
190
  good_creator = {"role": role, "person": person}
181
191
  good_creators.append(good_creator)
182
192
  md["creators"] = good_creators
183
193
 
194
+ @classmethod
195
+ def _clean_comic_story_arcs(cls, md):
196
+ """Replace story_arcs with good story_arc_numbers."""
197
+ if story_arc_numbers := md.pop("story_arcs", None):
198
+ good_story_arc_numbers = {}
199
+ field: CharField = NamedModel._meta.get_field("name") # type:ignore
200
+ for story_arc_name, number in story_arc_numbers.items():
201
+ if good_story_arc_name := cls._clean_charfield(story_arc_name, field):
202
+ good_story_arc_numbers[good_story_arc_name] = number
203
+ md["story_arc_numbers"] = good_story_arc_numbers
204
+
184
205
  @staticmethod
185
206
  def _clean_comic_web(md):
186
207
  """URL field is a special charfield."""
@@ -194,14 +215,14 @@ class CleanMetadataMixin(QueuedThread):
194
215
  @classmethod
195
216
  def _clean_comic_m2m_named(cls, md: dict[str, Any], md_keys: frozenset[str]):
196
217
  """Clean the named models in the m2m fields."""
218
+ named_field: CharField = NamedModel._meta.get_field("name") # type:ignore
197
219
  for key in _M2M_NAMED_KEYS.intersection(md_keys):
198
220
  names = md.get(key)
199
221
  if not names:
200
222
  continue
201
223
  cleaned_names = []
202
- field: CharField = NamedModel._meta.get_field("name") # type:ignore
203
224
  for name in names:
204
- cleaned_name = cls._clean_charfield(name, field)
225
+ cleaned_name = cls._clean_charfield(name, named_field)
205
226
  if cleaned_name:
206
227
  cleaned_names.append(cleaned_name)
207
228
  md[key] = cleaned_names
@@ -228,6 +249,8 @@ class CleanMetadataMixin(QueuedThread):
228
249
  self._clean_comic_charfields(md, md_keys)
229
250
  self._clean_comic_web(md)
230
251
  self._append_description(md)
252
+ self._append_review(md)
231
253
  self._clean_comic_creators(md)
254
+ self._clean_comic_story_arcs(md)
232
255
  self._clean_comic_m2m_named(md, md_keys)
233
256
  return md