codex 1.6.0a7__py3-none-any.whl → 1.6.0a9__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 (517) hide show
  1. codex/integrity.py +5 -6
  2. codex/librarian/importer/{aggregate_metadata.py → aggregate.py} +54 -76
  3. codex/librarian/importer/cache.py +162 -29
  4. codex/librarian/importer/const.py +15 -6
  5. codex/librarian/importer/create_comics.py +101 -17
  6. codex/librarian/importer/create_covers.py +96 -0
  7. codex/librarian/importer/create_fks.py +73 -170
  8. codex/librarian/importer/deleted.py +59 -46
  9. codex/librarian/importer/{clean_metadata.py → extract.py} +33 -4
  10. codex/librarian/importer/failed_imports.py +42 -64
  11. codex/librarian/importer/importer.py +96 -0
  12. codex/librarian/importer/importerd.py +71 -397
  13. codex/librarian/importer/init.py +265 -0
  14. codex/librarian/importer/link_comics.py +26 -81
  15. codex/librarian/importer/link_covers.py +73 -0
  16. codex/librarian/importer/moved.py +76 -104
  17. codex/librarian/importer/query_covers.py +52 -0
  18. codex/librarian/importer/query_fks.py +128 -172
  19. codex/librarian/importer/status.py +2 -39
  20. codex/librarian/importer/tasks.py +22 -13
  21. codex/librarian/janitor/cleanup.py +2 -2
  22. codex/librarian/janitor/janitor.py +10 -0
  23. codex/librarian/librariand.py +7 -1
  24. codex/migrations/0027_import_order_and_covers.py +345 -0
  25. codex/models/comic.py +1 -2
  26. codex/models/groups.py +3 -2
  27. codex/models/paths.py +0 -1
  28. codex/search/query.py +25 -2
  29. codex/search/writing.py +1 -1
  30. codex/serializers/browser/mixins.py +0 -18
  31. codex/serializers/browser/mtime.py +21 -0
  32. codex/serializers/browser/settings.py +2 -3
  33. codex/serializers/choices.py +10 -1
  34. codex/serializers/fields.py +8 -9
  35. codex/serializers/reader.py +5 -5
  36. codex/serializers/route.py +9 -4
  37. codex/settings/settings.py +2 -2
  38. codex/static_root/assets/{VCheckbox-BOUtyxuo.c690f0cdbe48.js → VCheckbox-eNspVUje.da8282a08876.js} +1 -1
  39. codex/static_root/assets/VCheckbox-eNspVUje.da8282a08876.js.br +0 -0
  40. codex/static_root/assets/VCheckbox-eNspVUje.da8282a08876.js.gz +0 -0
  41. codex/static_root/assets/{VCheckbox-BOUtyxuo.js → VCheckbox-eNspVUje.js} +1 -1
  42. codex/static_root/assets/VCheckbox-eNspVUje.js.br +0 -0
  43. codex/static_root/assets/VCheckbox-eNspVUje.js.gz +0 -0
  44. codex/static_root/assets/{VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js → VCheckboxBtn-DLzF_WHB.9a47873b8548.js} +1 -1
  45. codex/static_root/assets/VCheckboxBtn-DLzF_WHB.9a47873b8548.js.br +0 -0
  46. codex/static_root/assets/VCheckboxBtn-DLzF_WHB.9a47873b8548.js.gz +0 -0
  47. codex/static_root/assets/{VCheckboxBtn-B1-m5pEh.js → VCheckboxBtn-DLzF_WHB.js} +1 -1
  48. codex/static_root/assets/VCheckboxBtn-DLzF_WHB.js.br +0 -0
  49. codex/static_root/assets/VCheckboxBtn-DLzF_WHB.js.gz +0 -0
  50. codex/static_root/assets/{VCombobox-DjkDXe33.22617ea193b1.js → VCombobox-BIO7lU_B.d607ada71b06.js} +1 -1
  51. codex/static_root/assets/VCombobox-BIO7lU_B.d607ada71b06.js.br +0 -0
  52. codex/static_root/assets/VCombobox-BIO7lU_B.d607ada71b06.js.gz +0 -0
  53. codex/static_root/assets/{VCombobox-DjkDXe33.js → VCombobox-BIO7lU_B.js} +1 -1
  54. codex/static_root/assets/VCombobox-BIO7lU_B.js.br +0 -0
  55. codex/static_root/assets/VCombobox-BIO7lU_B.js.gz +0 -0
  56. codex/static_root/assets/{VDialog-X0zn9AGX.0d89c749b9a6.js → VDialog-87AymJC5.4a46716358c0.js} +1 -1
  57. codex/static_root/assets/VDialog-87AymJC5.4a46716358c0.js.br +0 -0
  58. codex/static_root/assets/VDialog-87AymJC5.4a46716358c0.js.gz +0 -0
  59. codex/static_root/assets/{VDialog-X0zn9AGX.js → VDialog-87AymJC5.js} +1 -1
  60. codex/static_root/assets/VDialog-87AymJC5.js.br +0 -0
  61. codex/static_root/assets/VDialog-87AymJC5.js.gz +0 -0
  62. codex/static_root/assets/{VExpansionPanels-CRevojaF.43d65c777c58.js → VExpansionPanels-CgA3shsd.9e524c73726a.js} +1 -1
  63. codex/static_root/assets/VExpansionPanels-CgA3shsd.9e524c73726a.js.br +0 -0
  64. codex/static_root/assets/VExpansionPanels-CgA3shsd.9e524c73726a.js.gz +0 -0
  65. codex/static_root/assets/{VExpansionPanels-CRevojaF.js → VExpansionPanels-CgA3shsd.js} +1 -1
  66. codex/static_root/assets/VExpansionPanels-CgA3shsd.js.br +0 -0
  67. codex/static_root/assets/VExpansionPanels-CgA3shsd.js.gz +0 -0
  68. codex/static_root/assets/{VRadioGroup-8nrrxjDv.2b4dfb984a8c.js → VRadioGroup-B--7K_v5.4a4e9c2d6c42.js} +1 -1
  69. codex/static_root/assets/VRadioGroup-B--7K_v5.4a4e9c2d6c42.js.br +0 -0
  70. codex/static_root/assets/VRadioGroup-B--7K_v5.4a4e9c2d6c42.js.gz +0 -0
  71. codex/static_root/assets/{VRadioGroup-8nrrxjDv.js → VRadioGroup-B--7K_v5.js} +1 -1
  72. codex/static_root/assets/VRadioGroup-B--7K_v5.js.br +0 -0
  73. codex/static_root/assets/VRadioGroup-B--7K_v5.js.gz +0 -0
  74. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +1 -0
  75. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
  76. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
  77. codex/static_root/assets/VSelect-ARDhJiQK.css +1 -0
  78. codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
  79. codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
  80. codex/static_root/assets/VSelect-BoJq222l.fc714657e214.js +1 -0
  81. codex/static_root/assets/VSelect-BoJq222l.fc714657e214.js.br +0 -0
  82. codex/static_root/assets/VSelect-BoJq222l.fc714657e214.js.gz +0 -0
  83. codex/static_root/assets/VSelect-BoJq222l.js +1 -0
  84. codex/static_root/assets/VSelect-BoJq222l.js.br +0 -0
  85. codex/static_root/assets/VSelect-BoJq222l.js.gz +0 -0
  86. codex/static_root/assets/{VSelectionControl-CGu0oz0K.3030b7a270d6.js → VSelectionControl-CVQ9wkE2.c02186cceba6.js} +1 -1
  87. codex/static_root/assets/VSelectionControl-CVQ9wkE2.c02186cceba6.js.br +0 -0
  88. codex/static_root/assets/VSelectionControl-CVQ9wkE2.c02186cceba6.js.gz +0 -0
  89. codex/static_root/assets/{VSelectionControl-CGu0oz0K.js → VSelectionControl-CVQ9wkE2.js} +1 -1
  90. codex/static_root/assets/VSelectionControl-CVQ9wkE2.js.br +0 -0
  91. codex/static_root/assets/VSelectionControl-CVQ9wkE2.js.gz +0 -0
  92. codex/static_root/assets/{VSlideGroup-D_oNvCOd.71a7101cdddd.js → VSlideGroup-Ce0j2BKK.85e64443a928.js} +1 -1
  93. codex/static_root/assets/VSlideGroup-Ce0j2BKK.85e64443a928.js.br +0 -0
  94. codex/static_root/assets/VSlideGroup-Ce0j2BKK.85e64443a928.js.gz +0 -0
  95. codex/static_root/assets/{VSlideGroup-D_oNvCOd.js → VSlideGroup-Ce0j2BKK.js} +1 -1
  96. codex/static_root/assets/VSlideGroup-Ce0j2BKK.js.br +0 -0
  97. codex/static_root/assets/VSlideGroup-Ce0j2BKK.js.gz +0 -0
  98. codex/static_root/assets/{VTable-C7pKI7gU.087912f706f5.js → VTable-CrWkhkiP.6d530eea0c09.js} +1 -1
  99. codex/static_root/assets/VTable-CrWkhkiP.6d530eea0c09.js.br +0 -0
  100. codex/static_root/assets/VTable-CrWkhkiP.6d530eea0c09.js.gz +0 -0
  101. codex/static_root/assets/{VTable-C7pKI7gU.js → VTable-CrWkhkiP.js} +1 -1
  102. codex/static_root/assets/VTable-CrWkhkiP.js.br +0 -0
  103. codex/static_root/assets/VTable-CrWkhkiP.js.gz +0 -0
  104. codex/static_root/assets/{VTextField-Bs8oq9mk.js → VTextField-CkWbil3K.2cac13161dfe.js} +1 -1
  105. codex/static_root/assets/VTextField-CkWbil3K.2cac13161dfe.js.br +0 -0
  106. codex/static_root/assets/VTextField-CkWbil3K.2cac13161dfe.js.gz +0 -0
  107. codex/static_root/assets/{VTextField-Bs8oq9mk.7999fef12a03.js → VTextField-CkWbil3K.js} +1 -1
  108. codex/static_root/assets/VTextField-CkWbil3K.js.br +0 -0
  109. codex/static_root/assets/VTextField-CkWbil3K.js.gz +0 -0
  110. codex/static_root/assets/{VWindowItem-iyZ1XOkP.409f7d292253.js → VWindowItem-7z55Y4lK.98eb7487e039.js} +1 -1
  111. codex/static_root/assets/VWindowItem-7z55Y4lK.98eb7487e039.js.br +0 -0
  112. codex/static_root/assets/VWindowItem-7z55Y4lK.98eb7487e039.js.gz +0 -0
  113. codex/static_root/assets/{VWindowItem-iyZ1XOkP.js → VWindowItem-7z55Y4lK.js} +1 -1
  114. codex/static_root/assets/VWindowItem-7z55Y4lK.js.br +0 -0
  115. codex/static_root/assets/VWindowItem-7z55Y4lK.js.gz +0 -0
  116. codex/static_root/assets/{admin-B0pCBjma.2407da79bd20.js → admin-D3OK784R.f69891726ec5.js} +1 -1
  117. codex/static_root/assets/admin-D3OK784R.f69891726ec5.js.br +0 -0
  118. codex/static_root/assets/admin-D3OK784R.f69891726ec5.js.gz +0 -0
  119. codex/static_root/assets/{admin-B0pCBjma.js → admin-D3OK784R.js} +1 -1
  120. codex/static_root/assets/admin-D3OK784R.js.br +0 -0
  121. codex/static_root/assets/admin-D3OK784R.js.gz +0 -0
  122. codex/static_root/assets/{admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js → admin-drawer-panel-1pnfvY8O.5438f252970e.js} +11 -11
  123. codex/static_root/assets/admin-drawer-panel-1pnfvY8O.5438f252970e.js.br +0 -0
  124. codex/static_root/assets/admin-drawer-panel-1pnfvY8O.5438f252970e.js.gz +0 -0
  125. codex/static_root/assets/{admin-drawer-panel-BxoNUHQr.js → admin-drawer-panel-1pnfvY8O.js} +11 -11
  126. codex/static_root/assets/admin-drawer-panel-1pnfvY8O.js.br +0 -0
  127. codex/static_root/assets/admin-drawer-panel-1pnfvY8O.js.gz +0 -0
  128. codex/static_root/assets/browser-BuU2x9y7.css +1 -0
  129. codex/static_root/assets/browser-BuU2x9y7.css.br +0 -0
  130. codex/static_root/assets/browser-BuU2x9y7.css.gz +0 -0
  131. codex/static_root/assets/browser-BuU2x9y7.f67595d24664.css +1 -0
  132. codex/static_root/assets/browser-BuU2x9y7.f67595d24664.css.br +0 -0
  133. codex/static_root/assets/browser-BuU2x9y7.f67595d24664.css.gz +0 -0
  134. codex/static_root/assets/browser-CO9EYHIr.be49367c6a75.js +1 -0
  135. codex/static_root/assets/browser-CO9EYHIr.be49367c6a75.js.br +0 -0
  136. codex/static_root/assets/browser-CO9EYHIr.be49367c6a75.js.gz +0 -0
  137. codex/static_root/assets/browser-CO9EYHIr.js +1 -0
  138. codex/static_root/assets/browser-CO9EYHIr.js.br +0 -0
  139. codex/static_root/assets/browser-CO9EYHIr.js.gz +0 -0
  140. codex/static_root/assets/{change-password-dialog-DZ9dP8_F.930f4cae3cb0.js → change-password-dialog-CjZdzaIk.188c765431ab.js} +1 -1
  141. codex/static_root/assets/change-password-dialog-CjZdzaIk.188c765431ab.js.br +0 -0
  142. codex/static_root/assets/change-password-dialog-CjZdzaIk.188c765431ab.js.gz +0 -0
  143. codex/static_root/assets/{change-password-dialog-DZ9dP8_F.js → change-password-dialog-CjZdzaIk.js} +1 -1
  144. codex/static_root/assets/change-password-dialog-CjZdzaIk.js.br +0 -0
  145. codex/static_root/assets/change-password-dialog-CjZdzaIk.js.gz +0 -0
  146. codex/static_root/assets/{confirm-dialog-RP7JNStF.a752a7c1e697.js → confirm-dialog-jwL7B98r.1cfd579cbcfe.js} +1 -1
  147. codex/static_root/assets/confirm-dialog-jwL7B98r.1cfd579cbcfe.js.br +0 -0
  148. codex/static_root/assets/confirm-dialog-jwL7B98r.1cfd579cbcfe.js.gz +0 -0
  149. codex/static_root/assets/{confirm-dialog-RP7JNStF.js → confirm-dialog-jwL7B98r.js} +1 -1
  150. codex/static_root/assets/confirm-dialog-jwL7B98r.js.br +0 -0
  151. codex/static_root/assets/confirm-dialog-jwL7B98r.js.gz +0 -0
  152. codex/static_root/assets/datetime-column-9w3GcKvo.f9172256f091.js +1 -0
  153. codex/static_root/assets/datetime-column-9w3GcKvo.f9172256f091.js.br +0 -0
  154. codex/static_root/assets/datetime-column-9w3GcKvo.f9172256f091.js.gz +0 -0
  155. codex/static_root/assets/datetime-column-9w3GcKvo.js +1 -0
  156. codex/static_root/assets/datetime-column-9w3GcKvo.js.br +0 -0
  157. codex/static_root/assets/datetime-column-9w3GcKvo.js.gz +0 -0
  158. codex/static_root/assets/datetime-column-DeCthByU.1f3bf499e063.css +1 -0
  159. codex/static_root/assets/datetime-column-DeCthByU.css +1 -0
  160. codex/static_root/assets/{filter-DAdGUt-1.3261cbcd50b5.js → filter-C-pghOri.eeb7c2ecf398.js} +1 -1
  161. codex/static_root/assets/filter-C-pghOri.eeb7c2ecf398.js.br +0 -0
  162. codex/static_root/assets/filter-C-pghOri.eeb7c2ecf398.js.gz +0 -0
  163. codex/static_root/assets/{filter-DAdGUt-1.js → filter-C-pghOri.js} +1 -1
  164. codex/static_root/assets/filter-C-pghOri.js.br +0 -0
  165. codex/static_root/assets/filter-C-pghOri.js.gz +0 -0
  166. codex/static_root/assets/flag-tab-Da2DBNyz.ffbe5ca37e1c.js +1 -0
  167. codex/static_root/assets/flag-tab-Da2DBNyz.ffbe5ca37e1c.js.br +0 -0
  168. codex/static_root/assets/flag-tab-Da2DBNyz.ffbe5ca37e1c.js.gz +0 -0
  169. codex/static_root/assets/flag-tab-Da2DBNyz.js +1 -0
  170. codex/static_root/assets/flag-tab-Da2DBNyz.js.br +0 -0
  171. codex/static_root/assets/flag-tab-Da2DBNyz.js.gz +0 -0
  172. codex/static_root/assets/{group-tab-DEwh4jfB.a95097b5e320.js → group-tab-BONYWOc1.6ddf321a4a56.js} +1 -1
  173. codex/static_root/assets/group-tab-BONYWOc1.6ddf321a4a56.js.br +0 -0
  174. codex/static_root/assets/group-tab-BONYWOc1.6ddf321a4a56.js.gz +0 -0
  175. codex/static_root/assets/{group-tab-DEwh4jfB.js → group-tab-BONYWOc1.js} +1 -1
  176. codex/static_root/assets/group-tab-BONYWOc1.js.br +0 -0
  177. codex/static_root/assets/group-tab-BONYWOc1.js.gz +0 -0
  178. codex/static_root/assets/http-error-D2Jnc20C.32846bc3a43d.js +1 -0
  179. codex/static_root/assets/http-error-D2Jnc20C.32846bc3a43d.js.br +0 -0
  180. codex/static_root/assets/http-error-D2Jnc20C.32846bc3a43d.js.gz +0 -0
  181. codex/static_root/assets/http-error-D2Jnc20C.js +1 -0
  182. codex/static_root/assets/http-error-D2Jnc20C.js.br +0 -0
  183. codex/static_root/assets/http-error-D2Jnc20C.js.gz +0 -0
  184. codex/static_root/assets/{library-tab-DrXvD9B2.js → library-tab-B419agXA.d4b778172aaa.js} +1 -1
  185. codex/static_root/assets/library-tab-B419agXA.d4b778172aaa.js.br +0 -0
  186. codex/static_root/assets/library-tab-B419agXA.d4b778172aaa.js.gz +0 -0
  187. codex/static_root/assets/{library-tab-DrXvD9B2.f765f9d3bae4.js → library-tab-B419agXA.js} +1 -1
  188. codex/static_root/assets/library-tab-B419agXA.js.br +0 -0
  189. codex/static_root/assets/library-tab-B419agXA.js.gz +0 -0
  190. codex/static_root/assets/main-CMQwJ9aG.9836fe81c80a.js +32 -0
  191. codex/static_root/assets/main-CMQwJ9aG.9836fe81c80a.js.br +0 -0
  192. codex/static_root/assets/main-CMQwJ9aG.9836fe81c80a.js.gz +0 -0
  193. codex/static_root/assets/main-CMQwJ9aG.js +32 -0
  194. codex/static_root/assets/main-CMQwJ9aG.js.br +0 -0
  195. codex/static_root/assets/main-CMQwJ9aG.js.gz +0 -0
  196. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css +1 -0
  197. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.br +0 -0
  198. codex/static_root/assets/pagination-toolbar-DWdp_kmz.css.gz +0 -0
  199. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css +1 -0
  200. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.br +0 -0
  201. codex/static_root/assets/pagination-toolbar-DWdp_kmz.f98f47e87612.css.gz +0 -0
  202. codex/static_root/assets/pagination-toolbar-LTuj5U-G.a295defbcd2f.js +1 -0
  203. codex/static_root/assets/pagination-toolbar-LTuj5U-G.a295defbcd2f.js.br +0 -0
  204. codex/static_root/assets/pagination-toolbar-LTuj5U-G.a295defbcd2f.js.gz +0 -0
  205. codex/static_root/assets/pagination-toolbar-LTuj5U-G.js +1 -0
  206. codex/static_root/assets/pagination-toolbar-LTuj5U-G.js.br +0 -0
  207. codex/static_root/assets/pagination-toolbar-LTuj5U-G.js.gz +0 -0
  208. codex/static_root/assets/{pdf-doc-obrBwxa6.ec9bd13a5786.js → pdf-doc-8RVZqILx.2e7166dd26fb.js} +1 -1
  209. codex/static_root/assets/pdf-doc-8RVZqILx.2e7166dd26fb.js.br +0 -0
  210. codex/static_root/assets/pdf-doc-8RVZqILx.2e7166dd26fb.js.gz +0 -0
  211. codex/static_root/assets/{pdf-doc-obrBwxa6.js → pdf-doc-8RVZqILx.js} +1 -1
  212. codex/static_root/assets/pdf-doc-8RVZqILx.js.br +0 -0
  213. codex/static_root/assets/pdf-doc-8RVZqILx.js.gz +0 -0
  214. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css +1 -0
  215. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.br +0 -0
  216. codex/static_root/assets/reader-CCCfpM3x.1553e45bd775.css.gz +0 -0
  217. codex/static_root/assets/reader-CCCfpM3x.css +1 -0
  218. codex/static_root/assets/reader-CCCfpM3x.css.br +0 -0
  219. codex/static_root/assets/reader-CCCfpM3x.css.gz +0 -0
  220. codex/static_root/assets/reader-DNnK_Chr.114d0e321547.js +2 -0
  221. codex/static_root/assets/reader-DNnK_Chr.114d0e321547.js.br +0 -0
  222. codex/static_root/assets/reader-DNnK_Chr.114d0e321547.js.gz +0 -0
  223. codex/static_root/assets/reader-DNnK_Chr.js +2 -0
  224. codex/static_root/assets/reader-DNnK_Chr.js.br +0 -0
  225. codex/static_root/assets/reader-DNnK_Chr.js.gz +0 -0
  226. codex/static_root/assets/{relation-chips-CZJcLjc8.1d7426dab654.js → relation-chips-PYua7aXv.428ba0b5861e.js} +1 -1
  227. codex/static_root/assets/relation-chips-PYua7aXv.428ba0b5861e.js.br +0 -0
  228. codex/static_root/assets/relation-chips-PYua7aXv.428ba0b5861e.js.gz +0 -0
  229. codex/static_root/assets/{relation-chips-CZJcLjc8.js → relation-chips-PYua7aXv.js} +1 -1
  230. codex/static_root/assets/relation-chips-PYua7aXv.js.br +0 -0
  231. codex/static_root/assets/relation-chips-PYua7aXv.js.gz +0 -0
  232. codex/static_root/assets/{settings-drawer-7iTnAdjf.337eecf2fdbb.js → settings-drawer-DbN6ISRH.d14f525d8eee.js} +2 -2
  233. codex/static_root/assets/settings-drawer-DbN6ISRH.d14f525d8eee.js.br +0 -0
  234. codex/static_root/assets/settings-drawer-DbN6ISRH.d14f525d8eee.js.gz +0 -0
  235. codex/static_root/assets/{settings-drawer-7iTnAdjf.js → settings-drawer-DbN6ISRH.js} +2 -2
  236. codex/static_root/assets/settings-drawer-DbN6ISRH.js.br +0 -0
  237. codex/static_root/assets/settings-drawer-DbN6ISRH.js.gz +0 -0
  238. codex/static_root/assets/{stats-tab-CQsK1n8O.544bf50ffb22.js → stats-tab-DImNhH8O.d5026283841a.js} +1 -1
  239. codex/static_root/assets/stats-tab-DImNhH8O.d5026283841a.js.br +0 -0
  240. codex/static_root/assets/stats-tab-DImNhH8O.d5026283841a.js.gz +0 -0
  241. codex/static_root/assets/{stats-tab-CQsK1n8O.js → stats-tab-DImNhH8O.js} +1 -1
  242. codex/static_root/assets/stats-tab-DImNhH8O.js.br +0 -0
  243. codex/static_root/assets/stats-tab-DImNhH8O.js.gz +0 -0
  244. codex/static_root/assets/{task-tab-C60TLP6e.63aa929a4ed6.js → task-tab-DPD53kjv.e56a35ff1691.js} +1 -1
  245. codex/static_root/assets/task-tab-DPD53kjv.e56a35ff1691.js.br +0 -0
  246. codex/static_root/assets/task-tab-DPD53kjv.e56a35ff1691.js.gz +0 -0
  247. codex/static_root/assets/{task-tab-C60TLP6e.js → task-tab-DPD53kjv.js} +1 -1
  248. codex/static_root/assets/task-tab-DPD53kjv.js.br +0 -0
  249. codex/static_root/assets/task-tab-DPD53kjv.js.gz +0 -0
  250. codex/static_root/assets/to-case-ehC9Ccyj.a7b8e25b391f.js +1 -0
  251. codex/static_root/assets/to-case-ehC9Ccyj.js +1 -0
  252. codex/static_root/assets/{unauthorized-Bs7NSqea.c55650fac055.js → unauthorized-BOIx7Y18.ee3961abfccb.js} +1 -1
  253. codex/static_root/assets/unauthorized-BOIx7Y18.ee3961abfccb.js.br +0 -0
  254. codex/static_root/assets/unauthorized-BOIx7Y18.ee3961abfccb.js.gz +0 -0
  255. codex/static_root/assets/{unauthorized-Bs7NSqea.js → unauthorized-BOIx7Y18.js} +1 -1
  256. codex/static_root/assets/unauthorized-BOIx7Y18.js.br +0 -0
  257. codex/static_root/assets/unauthorized-BOIx7Y18.js.gz +0 -0
  258. codex/static_root/assets/{user-tab-ChuH7Atm.7e80cf1fb78e.js → user-tab-BABwfxLO.37dd0440fbdd.js} +1 -1
  259. codex/static_root/assets/user-tab-BABwfxLO.37dd0440fbdd.js.br +0 -0
  260. codex/static_root/assets/user-tab-BABwfxLO.37dd0440fbdd.js.gz +0 -0
  261. codex/static_root/assets/{user-tab-ChuH7Atm.js → user-tab-BABwfxLO.js} +1 -1
  262. codex/static_root/assets/user-tab-BABwfxLO.js.br +0 -0
  263. codex/static_root/assets/user-tab-BABwfxLO.js.gz +0 -0
  264. codex/static_root/js/choices-admin.d790df01c20a.json +1 -0
  265. codex/static_root/js/choices-admin.d790df01c20a.json.br +0 -0
  266. codex/static_root/js/choices-admin.d790df01c20a.json.gz +0 -0
  267. codex/static_root/js/choices-admin.json +1 -1
  268. codex/static_root/js/choices-admin.json.br +0 -0
  269. codex/static_root/js/choices-admin.json.gz +0 -0
  270. codex/static_root/js/{choices.079a01e0be1a.json → choices.a0aaaa6c7ef7.json} +1 -1
  271. codex/static_root/js/choices.a0aaaa6c7ef7.json.br +0 -0
  272. codex/static_root/js/choices.a0aaaa6c7ef7.json.gz +0 -0
  273. codex/static_root/js/choices.json +1 -1
  274. codex/static_root/js/choices.json.br +0 -0
  275. codex/static_root/js/choices.json.gz +0 -0
  276. codex/static_root/{manifest.dad79c8475ae.json → manifest.cbea5663003a.json} +239 -239
  277. codex/static_root/manifest.cbea5663003a.json.br +0 -0
  278. codex/static_root/manifest.cbea5663003a.json.gz +0 -0
  279. codex/static_root/manifest.json +239 -239
  280. codex/static_root/manifest.json.br +0 -0
  281. codex/static_root/manifest.json.gz +0 -0
  282. codex/static_root/staticfiles.json +1 -1
  283. codex/urls/api/browser.py +11 -2
  284. codex/urls/api/reader.py +1 -7
  285. codex/urls/api/v3.py +1 -1
  286. codex/urls/opds/binary.py +1 -1
  287. codex/views/admin/auth.py +0 -2
  288. codex/views/admin/tasks.py +10 -1
  289. codex/views/auth.py +2 -4
  290. codex/views/bookmark.py +44 -40
  291. codex/views/browser/annotations.py +38 -174
  292. codex/views/browser/base.py +6 -22
  293. codex/views/browser/breadcrumbs.py +16 -14
  294. codex/views/browser/browser.py +11 -20
  295. codex/views/browser/choices.py +15 -39
  296. codex/views/browser/cover.py +177 -0
  297. codex/views/browser/filters/annotations.py +63 -18
  298. codex/views/browser/filters/bookmark.py +21 -5
  299. codex/views/browser/filters/field.py +1 -2
  300. codex/views/browser/filters/group.py +19 -11
  301. codex/views/browser/filters/search.py +19 -10
  302. codex/views/browser/metadata.py +20 -25
  303. codex/views/browser/mtime.py +72 -0
  304. codex/views/const.py +20 -0
  305. codex/views/opds/binary.py +1 -1
  306. codex/views/opds/urls.py +2 -1
  307. codex/views/opds/util.py +2 -4
  308. codex/views/opds/v1/entry/links.py +18 -33
  309. codex/views/opds/v1/facets.py +2 -1
  310. codex/views/opds/v1/feed.py +42 -23
  311. codex/views/opds/v1/links.py +2 -1
  312. codex/views/opds/v2/feed.py +11 -9
  313. codex/views/opds/v2/links.py +2 -1
  314. codex/views/opds/v2/publications.py +13 -34
  315. codex/views/public.py +0 -2
  316. codex/views/reader/books.py +7 -9
  317. codex/views/reader/reader.py +25 -22
  318. codex/views/session.py +5 -3
  319. codex/views/util.py +32 -0
  320. {codex-1.6.0a7.dist-info → codex-1.6.0a9.dist-info}/METADATA +8 -7
  321. {codex-1.6.0a7.dist-info → codex-1.6.0a9.dist-info}/RECORD +324 -325
  322. codex/librarian/importer/update_comics.py +0 -85
  323. codex/migrations/0027_sort_name.py +0 -139
  324. codex/migrations/0028_custom_covers.py +0 -166
  325. codex/migrations/0029_choices_adminflag_and_timestamp.py +0 -44
  326. codex/serializers/mtime.py +0 -25
  327. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.br +0 -0
  328. codex/static_root/assets/VCheckbox-BOUtyxuo.c690f0cdbe48.js.gz +0 -0
  329. codex/static_root/assets/VCheckbox-BOUtyxuo.js.br +0 -0
  330. codex/static_root/assets/VCheckbox-BOUtyxuo.js.gz +0 -0
  331. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.br +0 -0
  332. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.e2b8cbdb92c9.js.gz +0 -0
  333. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.br +0 -0
  334. codex/static_root/assets/VCheckboxBtn-B1-m5pEh.js.gz +0 -0
  335. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.br +0 -0
  336. codex/static_root/assets/VCombobox-DjkDXe33.22617ea193b1.js.gz +0 -0
  337. codex/static_root/assets/VCombobox-DjkDXe33.js.br +0 -0
  338. codex/static_root/assets/VCombobox-DjkDXe33.js.gz +0 -0
  339. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.br +0 -0
  340. codex/static_root/assets/VDialog-X0zn9AGX.0d89c749b9a6.js.gz +0 -0
  341. codex/static_root/assets/VDialog-X0zn9AGX.js.br +0 -0
  342. codex/static_root/assets/VDialog-X0zn9AGX.js.gz +0 -0
  343. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.br +0 -0
  344. codex/static_root/assets/VExpansionPanels-CRevojaF.43d65c777c58.js.gz +0 -0
  345. codex/static_root/assets/VExpansionPanels-CRevojaF.js.br +0 -0
  346. codex/static_root/assets/VExpansionPanels-CRevojaF.js.gz +0 -0
  347. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.br +0 -0
  348. codex/static_root/assets/VRadioGroup-8nrrxjDv.2b4dfb984a8c.js.gz +0 -0
  349. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.br +0 -0
  350. codex/static_root/assets/VRadioGroup-8nrrxjDv.js.gz +0 -0
  351. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js +0 -1
  352. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.br +0 -0
  353. codex/static_root/assets/VSelect-DgfIKRnC.fa7a73a9c415.js.gz +0 -0
  354. codex/static_root/assets/VSelect-DgfIKRnC.js +0 -1
  355. codex/static_root/assets/VSelect-DgfIKRnC.js.br +0 -0
  356. codex/static_root/assets/VSelect-DgfIKRnC.js.gz +0 -0
  357. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css +0 -1
  358. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.br +0 -0
  359. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.gz +0 -0
  360. codex/static_root/assets/VSelect-MGVSeLgr.css +0 -1
  361. codex/static_root/assets/VSelect-MGVSeLgr.css.br +0 -0
  362. codex/static_root/assets/VSelect-MGVSeLgr.css.gz +0 -0
  363. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.br +0 -0
  364. codex/static_root/assets/VSelectionControl-CGu0oz0K.3030b7a270d6.js.gz +0 -0
  365. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.br +0 -0
  366. codex/static_root/assets/VSelectionControl-CGu0oz0K.js.gz +0 -0
  367. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.br +0 -0
  368. codex/static_root/assets/VSlideGroup-D_oNvCOd.71a7101cdddd.js.gz +0 -0
  369. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.br +0 -0
  370. codex/static_root/assets/VSlideGroup-D_oNvCOd.js.gz +0 -0
  371. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.br +0 -0
  372. codex/static_root/assets/VTable-C7pKI7gU.087912f706f5.js.gz +0 -0
  373. codex/static_root/assets/VTable-C7pKI7gU.js.br +0 -0
  374. codex/static_root/assets/VTable-C7pKI7gU.js.gz +0 -0
  375. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.br +0 -0
  376. codex/static_root/assets/VTextField-Bs8oq9mk.7999fef12a03.js.gz +0 -0
  377. codex/static_root/assets/VTextField-Bs8oq9mk.js.br +0 -0
  378. codex/static_root/assets/VTextField-Bs8oq9mk.js.gz +0 -0
  379. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.br +0 -0
  380. codex/static_root/assets/VWindowItem-iyZ1XOkP.409f7d292253.js.gz +0 -0
  381. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.br +0 -0
  382. codex/static_root/assets/VWindowItem-iyZ1XOkP.js.gz +0 -0
  383. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.br +0 -0
  384. codex/static_root/assets/admin-B0pCBjma.2407da79bd20.js.gz +0 -0
  385. codex/static_root/assets/admin-B0pCBjma.js.br +0 -0
  386. codex/static_root/assets/admin-B0pCBjma.js.gz +0 -0
  387. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.br +0 -0
  388. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.5e2e1a4a255d.js.gz +0 -0
  389. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.br +0 -0
  390. codex/static_root/assets/admin-drawer-panel-BxoNUHQr.js.gz +0 -0
  391. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js +0 -1
  392. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.br +0 -0
  393. codex/static_root/assets/browser-DbyAjCNh.577a228a966b.js.gz +0 -0
  394. codex/static_root/assets/browser-DbyAjCNh.js +0 -1
  395. codex/static_root/assets/browser-DbyAjCNh.js.br +0 -0
  396. codex/static_root/assets/browser-DbyAjCNh.js.gz +0 -0
  397. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css +0 -1
  398. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.br +0 -0
  399. codex/static_root/assets/browser-VnzNj1t3.cff861ad2ca3.css.gz +0 -0
  400. codex/static_root/assets/browser-VnzNj1t3.css +0 -1
  401. codex/static_root/assets/browser-VnzNj1t3.css.br +0 -0
  402. codex/static_root/assets/browser-VnzNj1t3.css.gz +0 -0
  403. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.br +0 -0
  404. codex/static_root/assets/change-password-dialog-DZ9dP8_F.930f4cae3cb0.js.gz +0 -0
  405. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.br +0 -0
  406. codex/static_root/assets/change-password-dialog-DZ9dP8_F.js.gz +0 -0
  407. codex/static_root/assets/confirm-dialog-RP7JNStF.a752a7c1e697.js.br +0 -0
  408. codex/static_root/assets/confirm-dialog-RP7JNStF.a752a7c1e697.js.gz +0 -0
  409. codex/static_root/assets/confirm-dialog-RP7JNStF.js.br +0 -0
  410. codex/static_root/assets/confirm-dialog-RP7JNStF.js.gz +0 -0
  411. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js +0 -1
  412. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.br +0 -0
  413. codex/static_root/assets/datetime-column-Cu3WYyPj.54b4c3817b74.js.gz +0 -0
  414. codex/static_root/assets/datetime-column-Cu3WYyPj.js +0 -1
  415. codex/static_root/assets/datetime-column-Cu3WYyPj.js.br +0 -0
  416. codex/static_root/assets/datetime-column-Cu3WYyPj.js.gz +0 -0
  417. codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css +0 -1
  418. codex/static_root/assets/datetime-column-Dg_RxTVM.1fa7c5cbead7.css.br +0 -0
  419. codex/static_root/assets/datetime-column-Dg_RxTVM.css +0 -1
  420. codex/static_root/assets/datetime-column-Dg_RxTVM.css.br +0 -0
  421. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.br +0 -0
  422. codex/static_root/assets/filter-DAdGUt-1.3261cbcd50b5.js.gz +0 -0
  423. codex/static_root/assets/filter-DAdGUt-1.js.br +0 -0
  424. codex/static_root/assets/filter-DAdGUt-1.js.gz +0 -0
  425. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js +0 -1
  426. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.br +0 -0
  427. codex/static_root/assets/flag-tab-BP7Zs7iQ.cb16ef6363d4.js.gz +0 -0
  428. codex/static_root/assets/flag-tab-BP7Zs7iQ.js +0 -1
  429. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.br +0 -0
  430. codex/static_root/assets/flag-tab-BP7Zs7iQ.js.gz +0 -0
  431. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.br +0 -0
  432. codex/static_root/assets/group-tab-DEwh4jfB.a95097b5e320.js.gz +0 -0
  433. codex/static_root/assets/group-tab-DEwh4jfB.js.br +0 -0
  434. codex/static_root/assets/group-tab-DEwh4jfB.js.gz +0 -0
  435. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js +0 -1
  436. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.br +0 -0
  437. codex/static_root/assets/http-error-xs3mIL8U.e143c472a96c.js.gz +0 -0
  438. codex/static_root/assets/http-error-xs3mIL8U.js +0 -1
  439. codex/static_root/assets/http-error-xs3mIL8U.js.br +0 -0
  440. codex/static_root/assets/http-error-xs3mIL8U.js.gz +0 -0
  441. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.br +0 -0
  442. codex/static_root/assets/library-tab-DrXvD9B2.f765f9d3bae4.js.gz +0 -0
  443. codex/static_root/assets/library-tab-DrXvD9B2.js.br +0 -0
  444. codex/static_root/assets/library-tab-DrXvD9B2.js.gz +0 -0
  445. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js +0 -32
  446. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.br +0 -0
  447. codex/static_root/assets/main-Dy9Uqpmh.2f99b1914b3e.js.gz +0 -0
  448. codex/static_root/assets/main-Dy9Uqpmh.js +0 -32
  449. codex/static_root/assets/main-Dy9Uqpmh.js.br +0 -0
  450. codex/static_root/assets/main-Dy9Uqpmh.js.gz +0 -0
  451. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js +0 -1
  452. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.br +0 -0
  453. codex/static_root/assets/pagination-toolbar-DbS_HOT1.81222ab51ef4.js.gz +0 -0
  454. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js +0 -1
  455. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.br +0 -0
  456. codex/static_root/assets/pagination-toolbar-DbS_HOT1.js.gz +0 -0
  457. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css +0 -1
  458. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.br +0 -0
  459. codex/static_root/assets/pagination-toolbar-r6nbtWxg.4088ab042c0c.css.gz +0 -0
  460. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css +0 -1
  461. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.br +0 -0
  462. codex/static_root/assets/pagination-toolbar-r6nbtWxg.css.gz +0 -0
  463. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.br +0 -0
  464. codex/static_root/assets/pdf-doc-obrBwxa6.ec9bd13a5786.js.gz +0 -0
  465. codex/static_root/assets/pdf-doc-obrBwxa6.js.br +0 -0
  466. codex/static_root/assets/pdf-doc-obrBwxa6.js.gz +0 -0
  467. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js +0 -2
  468. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.br +0 -0
  469. codex/static_root/assets/reader-BXKUaUeC.953d30b969ef.js.gz +0 -0
  470. codex/static_root/assets/reader-BXKUaUeC.js +0 -2
  471. codex/static_root/assets/reader-BXKUaUeC.js.br +0 -0
  472. codex/static_root/assets/reader-BXKUaUeC.js.gz +0 -0
  473. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css +0 -1
  474. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.br +0 -0
  475. codex/static_root/assets/reader-OXJ1KVaW.a463b19d0508.css.gz +0 -0
  476. codex/static_root/assets/reader-OXJ1KVaW.css +0 -1
  477. codex/static_root/assets/reader-OXJ1KVaW.css.br +0 -0
  478. codex/static_root/assets/reader-OXJ1KVaW.css.gz +0 -0
  479. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.br +0 -0
  480. codex/static_root/assets/relation-chips-CZJcLjc8.1d7426dab654.js.gz +0 -0
  481. codex/static_root/assets/relation-chips-CZJcLjc8.js.br +0 -0
  482. codex/static_root/assets/relation-chips-CZJcLjc8.js.gz +0 -0
  483. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.br +0 -0
  484. codex/static_root/assets/settings-drawer-7iTnAdjf.337eecf2fdbb.js.gz +0 -0
  485. codex/static_root/assets/settings-drawer-7iTnAdjf.js.br +0 -0
  486. codex/static_root/assets/settings-drawer-7iTnAdjf.js.gz +0 -0
  487. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.br +0 -0
  488. codex/static_root/assets/stats-tab-CQsK1n8O.544bf50ffb22.js.gz +0 -0
  489. codex/static_root/assets/stats-tab-CQsK1n8O.js.br +0 -0
  490. codex/static_root/assets/stats-tab-CQsK1n8O.js.gz +0 -0
  491. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.br +0 -0
  492. codex/static_root/assets/task-tab-C60TLP6e.63aa929a4ed6.js.gz +0 -0
  493. codex/static_root/assets/task-tab-C60TLP6e.js.br +0 -0
  494. codex/static_root/assets/task-tab-C60TLP6e.js.gz +0 -0
  495. codex/static_root/assets/to-case-CR9beRR0.21bb805fdab4.js +0 -1
  496. codex/static_root/assets/to-case-CR9beRR0.js +0 -1
  497. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.br +0 -0
  498. codex/static_root/assets/unauthorized-Bs7NSqea.c55650fac055.js.gz +0 -0
  499. codex/static_root/assets/unauthorized-Bs7NSqea.js.br +0 -0
  500. codex/static_root/assets/unauthorized-Bs7NSqea.js.gz +0 -0
  501. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.br +0 -0
  502. codex/static_root/assets/user-tab-ChuH7Atm.7e80cf1fb78e.js.gz +0 -0
  503. codex/static_root/assets/user-tab-ChuH7Atm.js.br +0 -0
  504. codex/static_root/assets/user-tab-ChuH7Atm.js.gz +0 -0
  505. codex/static_root/js/choices-admin.ef1ff3a8b9da.json +0 -1
  506. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
  507. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
  508. codex/static_root/js/choices.079a01e0be1a.json.br +0 -0
  509. codex/static_root/js/choices.079a01e0be1a.json.gz +0 -0
  510. codex/static_root/manifest.dad79c8475ae.json.br +0 -0
  511. codex/static_root/manifest.dad79c8475ae.json.gz +0 -0
  512. codex/views/cover.py +0 -76
  513. codex/views/mtime.py +0 -70
  514. codex/views/utils.py +0 -26
  515. {codex-1.6.0a7.dist-info → codex-1.6.0a9.dist-info}/LICENSE +0 -0
  516. {codex-1.6.0a7.dist-info → codex-1.6.0a9.dist-info}/WHEEL +0 -0
  517. {codex-1.6.0a7.dist-info → codex-1.6.0a9.dist-info}/entry_points.txt +0 -0
codex/integrity.py CHANGED
@@ -36,7 +36,7 @@ MIGRATION_0011 = "0010_library_groups_and_metadata_changes"
36
36
  MIGRATION_0018 = "0018_rename_userbookmark_bookmark"
37
37
  MIGRATION_0025 = "0025_add_story_arc_number"
38
38
  MIGRATION_0026 = "0026_comicbox_1"
39
- MIGRATION_0028 = "0028_custom_covers" # TODO merge with 27
39
+ MIGRATION_0027 = "0027_import_order_and_covers"
40
40
  M2M_NAMES = MappingProxyType(
41
41
  {
42
42
  "Character": "characters",
@@ -308,7 +308,7 @@ def _repair_library_groups(apps):
308
308
 
309
309
 
310
310
  def _delete_extra_custom_cover_libraries(apps):
311
- if not has_applied_migration(MIGRATION_0028):
311
+ if not has_applied_migration(MIGRATION_0027):
312
312
  return
313
313
  library_model = apps.get_model("codex", "library")
314
314
  custom_cover_libraries = library_model.objects.filter(covers_only=True)
@@ -339,10 +339,9 @@ def _delete_extra_custom_cover_libraries(apps):
339
339
 
340
340
 
341
341
  def _repair_groups_with_custom_covers(apps):
342
- if not has_applied_migration(MIGRATION_0028):
342
+ if not has_applied_migration(MIGRATION_0027):
343
343
  return
344
344
  custom_cover_model = apps.get_model("codex", "customcover")
345
- now = Now()
346
345
  for model_name, group_letter in CUSTOM_COVER_MODEL_NAMES.items():
347
346
  valid_covers = custom_cover_model.objects.filter(group=group_letter)
348
347
  group_model = apps.get_model("codex", model_name)
@@ -352,7 +351,7 @@ def _repair_groups_with_custom_covers(apps):
352
351
  update_groups = []
353
352
  for group in objs:
354
353
  group.custom_cover = None
355
- group.updated_at = now
354
+ group.updated_at = Now()
356
355
  update_groups.append(group)
357
356
  group_model.objects.bulk_update(update_groups, ["custom_cover"])
358
357
  count = len(update_groups)
@@ -366,7 +365,7 @@ def _delete_errors():
366
365
 
367
366
  for host_model_name in HAVE_LIBRARY_FKS:
368
367
  if host_model_name == "CustomCover" and not has_applied_migration(
369
- MIGRATION_0028
368
+ MIGRATION_0027
370
369
  ):
371
370
  continue
372
371
  _delete_fk_integrity_errors(apps, host_model_name, "Library", "library")
@@ -2,13 +2,7 @@
2
2
 
3
3
  from collections.abc import Mapping
4
4
  from pathlib import Path
5
- from zipfile import BadZipFile
6
5
 
7
- from comicbox.box import Comicbox
8
- from comicbox.exceptions import UnsupportedArchiveTypeError
9
- from rarfile import BadRarFile
10
-
11
- from codex.librarian.importer.clean_metadata import CleanMetadataMixin
12
6
  from codex.librarian.importer.const import (
13
7
  COMIC_FK_FIELD_NAMES,
14
8
  COMIC_M2M_FIELD_NAMES,
@@ -24,14 +18,15 @@ from codex.librarian.importer.const import (
24
18
  MDS,
25
19
  VOLUME_COUNT,
26
20
  )
27
- from codex.librarian.importer.status import ImportStatusTypes, status_notify
21
+ from codex.librarian.importer.extract import ExtractMetadataImporter
22
+ from codex.librarian.importer.status import ImportStatusTypes
28
23
  from codex.models import Imprint, Publisher, Series, Volume
29
24
  from codex.models.admin import AdminFlag
30
25
  from codex.status import Status
31
26
  from codex.util import max_none
32
27
 
33
28
 
34
- class AggregateMetadataMixin(CleanMetadataMixin):
29
+ class AggregateMetadataImporter(ExtractMetadataImporter):
35
30
  """Aggregate metadata from comics to prepare for importing."""
36
31
 
37
32
  _BROWSER_GROUPS = (Publisher, Imprint, Series, Volume)
@@ -87,37 +82,13 @@ class AggregateMetadataMixin(CleanMetadataMixin):
87
82
  m2m_md[FOLDERS_FIELD] = Path(path).parents
88
83
  return m2m_md
89
84
 
90
- def _get_path_metadata(self, path, import_metadata):
85
+ def _get_path_metadata(self, md, path):
91
86
  """Get the metadata from comicbox and munge it a little."""
92
- md = {}
93
- fk_md = {}
94
- group_tree_md = {}
95
- m2m_md = {}
96
- failed_import = {}
97
- try:
98
- if import_metadata:
99
- with Comicbox(path) as cb:
100
- md = cb.to_dict()
101
- md = md.get("comicbox", {})
102
- if "file_type" not in md:
103
- md["file_type"] = cb.get_file_type()
104
- if "page_count" not in md:
105
- md["page_count"] = cb.get_page_count()
106
-
107
- md["path"] = path
108
- md = self.clean_md(md)
109
-
110
- group_tree_md = self._get_group_tree(md)
111
- fk_md = self._get_fk_metadata(md)
112
- m2m_md = self._get_m2m_metadata(md, path)
113
-
114
- except (UnsupportedArchiveTypeError, BadRarFile, BadZipFile, OSError) as exc:
115
- self.log.warning(f"Failed to import {path}: {exc}")
116
- failed_import = {path: exc}
117
- except Exception as exc:
118
- self.log.exception(f"Failed to import: {path}")
119
- failed_import = {path: exc}
120
- return md, m2m_md, fk_md, group_tree_md, failed_import
87
+ group_tree_md = self._get_group_tree(md)
88
+ fk_md = self._get_fk_metadata(md)
89
+ m2m_md = self._get_m2m_metadata(md, path)
90
+
91
+ return md, m2m_md, fk_md, group_tree_md
121
92
 
122
93
  @staticmethod
123
94
  def _aggregate_m2m_metadata_dict_value(names, key, values, all_fks):
@@ -207,72 +178,79 @@ class AggregateMetadataMixin(CleanMetadataMixin):
207
178
  cls._set_max_group_count(common_args, Series, 3, VOLUME_COUNT)
208
179
  cls._set_max_group_count(common_args, Volume, 4, ISSUE_COUNT)
209
180
 
210
- def _aggregate_path(self, data, path, import_metadata):
181
+ def _aggregate_path(self, md, path, status):
211
182
  """Aggregate metadata for one path."""
212
- path_str = str(path)
213
- md, m2m_md, fk_md, group_tree_md, failed_import = self._get_path_metadata(
214
- path_str, import_metadata
215
- )
183
+ md, m2m_md, fk_md, group_tree_md = self._get_path_metadata(md, path)
216
184
 
217
- all_failed_imports, all_mds, all_m2m_mds, all_fks, status = data
218
- if failed_import:
219
- all_failed_imports.update(failed_import)
220
- else:
221
- if md:
222
- all_mds[path_str] = md
185
+ path_str = str(path)
186
+ if md:
187
+ all_mds = self.metadata[MDS]
188
+ all_mds[path_str] = md
223
189
 
224
- if m2m_md:
225
- self._aggregate_m2m_metadata(all_m2m_mds, m2m_md, all_fks, path_str)
190
+ all_fks = self.metadata[FKS]
191
+ if m2m_md:
192
+ all_m2m_mds = self.metadata[M2M_MDS]
193
+ self._aggregate_m2m_metadata(all_m2m_mds, m2m_md, all_fks, path_str)
226
194
 
227
- if fk_md:
228
- self._aggregate_fk_metadata(all_fks, fk_md)
195
+ if fk_md:
196
+ self._aggregate_fk_metadata(all_fks, fk_md)
229
197
 
230
- if group_tree_md:
231
- self._aggregate_group_tree_metadata(all_fks, group_tree_md)
198
+ if group_tree_md:
199
+ self._aggregate_group_tree_metadata(all_fks, group_tree_md)
232
200
 
233
201
  if status:
234
202
  status.complete += 1
235
203
  self.status_controller.update(status)
236
204
 
237
- @status_notify(status_type=ImportStatusTypes.AGGREGATE_TAGS)
238
- def get_aggregate_metadata( # noqa: PLR0913
205
+ def get_aggregate_metadata(
239
206
  self,
240
- all_paths,
241
- library_path,
242
- metadata,
243
- force_import_metadata,
244
207
  status=None,
245
208
  ):
246
209
  """Get aggregated metadata for the paths given."""
210
+ all_paths = self.task.files_modified | self.task.files_created
247
211
  total_paths = len(all_paths)
212
+
248
213
  if not total_paths:
249
214
  return 0
250
- self.log.info(f"Reading tags from {total_paths} comics in {library_path}...")
251
- all_mds = metadata[MDS]
252
- all_m2m_mds = metadata[M2M_MDS]
253
- all_fks = metadata[FKS]
254
- all_fks[GROUP_TREES] = {cls: {} for cls in self._BROWSER_GROUPS}
255
- all_failed_imports = metadata[FIS]
256
-
257
- if status and status.complete is None:
258
- status.complete = 0
259
- key = AdminFlag.FlagChoices.IMPORT_METADATA.value # type: ignore
260
- if force_import_metadata:
215
+ self.log.info(
216
+ f"Reading tags from {total_paths} comics in {self.library.path}..."
217
+ )
218
+ status = Status(ImportStatusTypes.AGGREGATE_TAGS, 0, total_paths)
219
+ self.status_controller.start(status, notify=False)
220
+
221
+ # Set import_metadata flag
222
+ if self.task.force_import_metadata:
261
223
  import_metadata = True
262
224
  else:
225
+ key = AdminFlag.FlagChoices.IMPORT_METADATA.value # type: ignore
263
226
  import_metadata = AdminFlag.objects.get(key=key).on
264
227
  if not import_metadata:
265
228
  self.log.warn("Admin flag set to NOT import metadata.")
266
- data = (all_failed_imports, all_mds, all_m2m_mds, all_fks, status)
229
+
230
+ # Init metadata, extract and aggregate
231
+ self.metadata[MDS] = {}
232
+ self.metadata[M2M_MDS] = {}
233
+ self.metadata[FKS] = {GROUP_TREES: {cls: {} for cls in self._BROWSER_GROUPS}}
234
+ self.metadata[FIS] = {}
267
235
  for path in all_paths:
268
- self._aggregate_path(data, path, import_metadata)
236
+ md = self.extract_and_clean(path, import_metadata)
237
+ if md:
238
+ self._aggregate_path(md, path, status)
269
239
 
270
- all_fks[COMIC_PATHS] = frozenset(all_mds.keys())
271
- fi_status = Status(ImportStatusTypes.FAILED_IMPORTS, 0, len(all_failed_imports))
240
+ # Aggregate further
241
+ self.metadata[FKS][COMIC_PATHS] = frozenset(self.metadata[MDS].keys())
242
+ fis = self.metadata[FIS]
243
+ self.task.files_modified -= fis.keys()
244
+ self.task.files_created -= fis.keys()
245
+
246
+ # Set statii
247
+ fi_status = Status(ImportStatusTypes.FAILED_IMPORTS, 0, len(fis))
272
248
  self.status_controller.update(
273
249
  fi_status,
274
250
  notify=False,
275
251
  )
276
252
  count = status.complete if status else 0
277
253
  self.log.info(f"Aggregated tags from {count} comics.")
254
+
255
+ self.status_controller.finish(status)
278
256
  return count
@@ -1,47 +1,180 @@
1
1
  """Update Groups timestamp for cover cache busting."""
2
2
 
3
- from django.db.models.functions import Now
3
+ import os
4
+
5
+ from django.db.models.aggregates import Count
6
+ from django.db.models.functions.datetime import Now
4
7
  from django.db.models.query import Q
5
8
 
6
- from codex.models import StoryArc
9
+ from codex.librarian.importer.init import InitImporter
10
+ from codex.librarian.importer.status import ImportStatusTypes
11
+ from codex.models import Comic, Folder, Imprint, Publisher, Series, StoryArc, Volume
12
+ from codex.status import Status
13
+
14
+ _FIRST_COVER_MODEL_UPDATE_ORDER = (Volume, Series, Imprint, Publisher, Folder, StoryArc)
15
+ _UPDATE_FIELDS = ("first_comic", "updated_at")
7
16
 
8
17
 
9
- class CacheMixin:
18
+ class CacheUpdateImporter(InitImporter):
10
19
  """Update Groups timestamp for cover cache busting."""
11
20
 
12
- def update_groups_with_changed_comics(self, start_time, deleted_comic_groups):
13
- """Update timestamps for each group for cover cache busting."""
14
- total_count = 0
15
- now = Now()
16
-
17
- log_list = []
18
- for model, pks in deleted_comic_groups.items():
19
- rel = "story_arc_number__" if model == StoryArc else ""
20
- updated_at_rel = rel + "comic__updated_at__gt"
21
- updated_filter = {updated_at_rel: start_time}
22
- model_qs = (
23
- model.objects.filter(Q(**updated_filter) | Q(pk__in=pks))
24
- .distinct()
25
- .only("updated_at")
26
- )
21
+ @staticmethod
22
+ def _get_folder_batches(model_qs):
23
+ """Create batches of folders by folder depth level."""
24
+ # Collect all related folders
25
+ folder_pks = model_qs.values_list("pk", flat=True)
26
+ all_folders = (
27
+ Folder.objects.filter(Q(folder__pk__in=folder_pks) | Q(pk__in=folder_pks))
28
+ .select_related("first_comic")
29
+ .only("path", *_UPDATE_FIELDS)
30
+ )
31
+
32
+ # Create a map of each folder by path depth level
33
+ batch_map = {}
34
+ for folder in all_folders:
35
+ depth = folder.path.count(os.sep)
36
+ if depth not in batch_map:
37
+ batch_map[depth] = set()
38
+ batch_map[depth].add(folder)
39
+
40
+ # Order batches by reverse depth level to update bottom up.
41
+ batches = []
42
+ for _, folders in sorted(batch_map.items(), reverse=True):
43
+ batches.append(folders)
44
+
45
+ return tuple(batches)
46
+
47
+ @staticmethod
48
+ def _update_first_cover_volume(obj):
49
+ qs = Comic.objects.filter(volume=obj)
50
+ qs = qs.order_by("issue_number", "issue_suffix", "sort_name").only("pk")
51
+ obj.first_comic = qs.first()
52
+
53
+ @staticmethod
54
+ def _update_first_cover_group(obj, model, field):
55
+ qs = model.objects.filter(**{field: obj})
56
+ order_by = "name" if model == Volume else "sort_name"
57
+ qs = qs.select_related("first_comic").order_by(order_by).only("first_comic")
58
+ obj.first_comic = qs.first().first_comic
59
+
60
+ @staticmethod
61
+ def _update_first_cover_story_arc(obj):
62
+ qs = Comic.objects.filter(story_arc_numbers__story_arc=obj)
63
+ qs = qs.order_by("story_arc_numbers__number", "date").only("pk")
64
+ obj.first_comic = qs.first()
65
+
66
+ @staticmethod
67
+ def _update_first_cover_folder(obj):
68
+ first_comic = (
69
+ Comic.objects.filter(parent_folder=obj)
70
+ .order_by("path")
71
+ .only("path")
72
+ .first()
73
+ )
74
+ first_folder = (
75
+ Folder.objects.filter(parent_folder=obj)
76
+ .select_related("first_comic")
77
+ .order_by("path")
78
+ .only("first_comic", "path")
79
+ .first()
80
+ )
81
+ if first_comic and not first_folder:
82
+ obj.first_comic = first_comic
83
+ elif first_folder and not first_comic:
84
+ obj.first_comic = first_folder.first_comic
85
+ elif first_folder and first_comic:
86
+ if first_comic and first_comic.path < first_folder.path:
87
+ obj.first_comic = first_comic
88
+ else:
89
+ obj.first_comic = first_folder.first_comic
27
90
 
91
+ @classmethod
92
+ def _update_first_cover(cls, model, obj):
93
+ if model == Volume:
94
+ cls._update_first_cover_volume(obj)
95
+ elif model == Series:
96
+ cls._update_first_cover_group(obj, Volume, "series")
97
+ elif model == Imprint:
98
+ cls._update_first_cover_group(obj, Series, "imprint")
99
+ elif model == Publisher:
100
+ cls._update_first_cover_group(obj, Imprint, "publisher")
101
+ elif model == StoryArc:
102
+ cls._update_first_cover_story_arc(obj)
103
+ elif model == Folder:
104
+ cls._update_first_cover_folder(obj)
105
+
106
+ @classmethod
107
+ def _update_group_first_comic(
108
+ cls, force_update_group_map, model, start_time, log_list
109
+ ):
110
+ rel = "storyarcnumber__" if model == StoryArc else ""
111
+ updated_at_rel = rel + "comic__updated_at__gt"
112
+ updated_filter = {updated_at_rel: start_time}
113
+ filter_query = (
114
+ Q(**updated_filter)
115
+ | Q(first_comic__isnull=True)
116
+ | Q(updated_at__gt=start_time)
117
+ )
118
+ if model != Volume:
119
+ filter_query |= Q(custom_cover__updated_at__gt=start_time)
120
+ pks = force_update_group_map.get(model)
121
+ if pks:
122
+ filter_query |= Q(pk__in=pks)
123
+ model_qs = model.objects.filter(filter_query)
124
+
125
+ # only update those with comics
126
+ rel_prefix = "storyarcnumber__" if model == StoryArc else ""
127
+ rel_prefix += "comic"
128
+ model_qs = model_qs.alias(child_count=Count(f"{rel_prefix}__pk", distinct=True))
129
+ model_qs = model_qs.filter(child_count__gt=0)
130
+ model_qs = model_qs.distinct()
131
+ if model == Folder:
132
+ batches = cls._get_folder_batches(model_qs)
133
+ else:
134
+ model_qs.select_related("first_comic").only(*_UPDATE_FIELDS)
135
+ batches = (model_qs,)
136
+
137
+ total_count = 0
138
+ for model_qs in batches:
28
139
  updated = []
29
140
  for obj in model_qs:
30
- obj.updated_at = now
141
+ cls._update_first_cover(model, obj)
142
+ obj.updated_at = Now()
31
143
  updated.append(obj)
32
144
 
33
145
  count = len(updated)
34
146
  if count:
35
- model.objects.bulk_update(updated, ["updated_at"])
36
- log_list.append(f"{count} {model.__name__}s.")
147
+ model.objects.bulk_update(updated, _UPDATE_FIELDS)
37
148
  total_count += count
38
149
  if total_count:
39
- groups_log = ", ".join(log_list)
40
- self.log.debug( # type: ignore
41
- f"Updated {groups_log} timestamps for browser cache busting."
42
- )
43
- self.log.info( # type: ignore
44
- f"Updated {total_count} group timestamps for browser cache busting."
45
- )
46
-
150
+ log_list.append(f"{total_count} {model.__name__}s")
47
151
  return total_count
152
+
153
+ def update_all_groups_first_comics(self, force_update_group_map, start_time):
154
+ """Update timestamps for each group for cover cache busting."""
155
+ total_count = 0
156
+ status = Status(ImportStatusTypes.GROUP_UPDATE)
157
+ self.status_controller.start(status)
158
+ try:
159
+ log_list = []
160
+ for model in _FIRST_COVER_MODEL_UPDATE_ORDER:
161
+ # self.log.debug(f"Updating first covers for {model.__name__}s...")
162
+ count = self._update_group_first_comic(
163
+ force_update_group_map, model, start_time, log_list
164
+ )
165
+ if count:
166
+ self.log.debug(
167
+ f"Updated {count} first covers for {model.__name__}s"
168
+ )
169
+ status.add_complete(count)
170
+ self.status_controller.update(status, notify=False)
171
+ total_count += count
172
+
173
+ if total_count:
174
+ groups_log = ", ".join(log_list)
175
+ self.log.info( # type: ignore
176
+ f"Updated first covers for {groups_log}."
177
+ )
178
+ self.changed += total_count
179
+ finally:
180
+ self.status_controller.finish(status)
@@ -115,12 +115,7 @@ DICT_MODEL_REL_LINK_MAP = MappingProxyType(
115
115
  # BULK UPDATE #
116
116
  ###############
117
117
 
118
- _EXCLUDEBULK_UPDATE_COMIC_FIELDS = {
119
- "created_at",
120
- "searchresult",
121
- "id",
122
- "bookmark",
123
- }
118
+ _EXCLUDEBULK_UPDATE_COMIC_FIELDS = {"bookmark", "created_at", "id", "library"}
124
119
  GROUP_BASE_FIELDS = ("name", "sort_name")
125
120
  BULK_UPDATE_COMIC_FIELDS = tuple(
126
121
  sorted(
@@ -130,6 +125,7 @@ BULK_UPDATE_COMIC_FIELDS = tuple(
130
125
  and (field.name not in _EXCLUDEBULK_UPDATE_COMIC_FIELDS)
131
126
  )
132
127
  )
128
+ BULK_CREATE_COMIC_FIELDS = (*BULK_UPDATE_COMIC_FIELDS, "library")
133
129
  BULK_UPDATE_FOLDER_FIELDS = (
134
130
  *GROUP_BASE_FIELDS,
135
131
  "stat",
@@ -189,6 +185,10 @@ MDS = "mds"
189
185
  M2M_MDS = "m2m_mds"
190
186
  FKS = "fks"
191
187
  FIS = "fis"
188
+ FK_CREATE = "fk_create"
189
+ COVERS_UPDATE = "covers_update"
190
+ COVERS_CREATE = "covers_create"
191
+ LINK_COVER_PKS = "link_cover_pks"
192
192
  GROUP_COMPARE_FIELDS = MappingProxyType(
193
193
  {
194
194
  Series: ("publisher__name", "imprint__name", "name"),
@@ -227,3 +227,12 @@ COMIC_GROUP_FIELD_NAMES = (
227
227
  "story_arc_numbers",
228
228
  "folders",
229
229
  )
230
+
231
+ FKC_CONTRIBUTORS = "create_contributors"
232
+ FKC_STORY_ARC_NUMBERS = "create_story_arc_numbers"
233
+ FKC_IDENTIFIERS = "create_identifiers"
234
+ FKC_CREATE_GROUPS = "create_groups"
235
+ FKC_UPDATE_GROUPS = "update_groups"
236
+ FKC_CREATE_FKS = "create_fks"
237
+ FKC_FOLDER_PATHS = "create_folder_paths"
238
+ FKC_TOTAL_FKS = "total_fks"
@@ -1,39 +1,121 @@
1
1
  """Bulk update and create comic objects and bulk update m2m fields."""
2
2
 
3
- from codex.librarian.importer.const import BULK_UPDATE_COMIC_FIELDS
4
- from codex.librarian.importer.link_comics import LinkComicsMixin
5
- from codex.librarian.importer.status import ImportStatusTypes, status_notify
3
+ from django.db.models import NOT_PROVIDED
4
+ from django.db.models.functions import Now
5
+
6
+ from codex.librarian.importer.const import (
7
+ BULK_CREATE_COMIC_FIELDS,
8
+ BULK_UPDATE_COMIC_FIELDS,
9
+ BULK_UPDATE_COMIC_FIELDS_WITH_VALUES,
10
+ MDS,
11
+ )
12
+ from codex.librarian.importer.link_comics import LinkComicsImporter
13
+ from codex.librarian.importer.status import ImportStatusTypes
6
14
  from codex.models import (
7
15
  Comic,
8
16
  )
17
+ from codex.status import Status
9
18
 
10
19
 
11
- class CreateComicsMixin(LinkComicsMixin):
20
+ class CreateComicsImporter(LinkComicsImporter):
12
21
  """Create comics methods."""
13
22
 
14
- @status_notify(status_type=ImportStatusTypes.FILES_CREATED, updates=False)
15
- def bulk_create_comics(self, comic_paths, library, mds, **kwargs):
16
- """Bulk create comics."""
17
- if not comic_paths:
18
- return 0
19
- num_comics = len(comic_paths)
23
+ def _bulk_update_comics_add_comic(self, comic, results):
24
+ """Add one comic and stats to the bulk update list."""
25
+ try:
26
+ md = self.metadata[MDS].pop(comic.path)
27
+ self.get_comic_fk_links(md, comic.path)
28
+ for field_name in BULK_UPDATE_COMIC_FIELDS_WITH_VALUES:
29
+ value = md.get(field_name)
30
+ if value is None:
31
+ default_value = Comic._meta.get_field(field_name).default
32
+ if default_value != NOT_PROVIDED:
33
+ value = default_value
34
+ setattr(comic, field_name, value)
35
+ comic.presave()
36
+ comic.updated_at = Now()
37
+ update_comics, comic_pks, comic_update_paths = results
38
+ update_comics.append(comic)
39
+ comic_pks.append(comic.pk)
40
+ comic_update_paths.add(comic.path)
41
+ except Exception:
42
+ self.log.exception(f"Error preparing {comic} for update.")
20
43
 
44
+ def bulk_update_comics(self):
45
+ """Bulk update comics, and move nonextant comics into create job.."""
46
+ num_comics = len(self.task.files_modified)
47
+ if not num_comics:
48
+ return num_comics
49
+
50
+ self.log.debug(
51
+ f"Preparing {num_comics} comics for update in library {self.library.path}."
52
+ )
53
+ status = Status(ImportStatusTypes.FILES_MODIFIED, 0, num_comics)
54
+ self.status_controller.start(status, notify=False)
55
+ # Get existing comics to update
56
+ comics = Comic.objects.filter(
57
+ library=self.library, path__in=self.task.files_modified
58
+ ).only(*BULK_UPDATE_COMIC_FIELDS)
59
+
60
+ # set attributes for each comic
61
+ update_comics = []
62
+ comic_pks = []
63
+ comic_update_paths = set()
64
+ results = update_comics, comic_pks, comic_update_paths
65
+ for comic in comics.iterator():
66
+ self._bulk_update_comics_add_comic(comic, results)
67
+
68
+ converted_create_paths = frozenset(
69
+ set(self.task.files_modified) - comic_update_paths
70
+ )
71
+ self.task.files_created |= converted_create_paths
72
+ count = len(converted_create_paths)
73
+ if count:
74
+ self.log.info(f"Converted {count} update paths to create paths.")
75
+
76
+ self.log.debug(f"Bulk updating {len(update_comics)} comics.")
77
+ try:
78
+ Comic.objects.bulk_update(update_comics, BULK_UPDATE_COMIC_FIELDS)
79
+ count = len(update_comics)
80
+
81
+ self._remove_covers(comic_pks, False) # type: ignore
82
+ self.log.debug(f"Purging covers for {len(comic_pks)} updated comics.")
83
+ if count:
84
+ self.log.info(f"Updated {count} comics.")
85
+ except Exception:
86
+ self.log.exception(f"While updating {comic_update_paths}")
87
+
88
+ self.task.files_modified = frozenset()
89
+
90
+ self.status_controller.finish(status)
91
+ return count
92
+
93
+ def bulk_create_comics(self):
94
+ """Bulk create comics."""
95
+ num_comics = len(self.task.files_created)
96
+ if not num_comics:
97
+ return num_comics
21
98
  # prepare create comics
22
99
  self.log.debug(
23
- f"Preparing {num_comics} comics for creation in library {library.path}."
100
+ f"Preparing {num_comics} comics for creation in library {self.library.path}."
24
101
  )
102
+ status = Status(ImportStatusTypes.FILES_CREATED, 0, num_comics)
103
+ self.status_controller.start(status, notify=False)
104
+
25
105
  create_comics = []
26
- for path in comic_paths:
106
+ for path in sorted(self.task.files_created):
27
107
  try:
28
- md = mds.pop(path, {})
29
- self.get_comic_fk_links(md, library, path)
30
- comic = Comic(**md)
108
+ md = self.metadata[MDS].pop(path, {})
109
+ self.get_comic_fk_links(md, path)
110
+ comic = Comic(**md, library=self.library)
31
111
  comic.presave()
32
112
  create_comics.append(comic)
33
113
  except KeyError:
34
114
  self.log.warning(f"No comic metadata for {path}")
35
115
  except Exception:
36
116
  self.log.exception(f"Error preparing {path} for create.")
117
+ self.task.files_created = frozenset()
118
+ self.metadata.pop(MDS)
37
119
 
38
120
  num_comics = len(create_comics)
39
121
  count = 0
@@ -43,13 +125,15 @@ class CreateComicsMixin(LinkComicsMixin):
43
125
  Comic.objects.bulk_create(
44
126
  create_comics,
45
127
  update_conflicts=True,
46
- update_fields=BULK_UPDATE_COMIC_FIELDS,
128
+ update_fields=BULK_CREATE_COMIC_FIELDS,
47
129
  unique_fields=Comic._meta.unique_together[0], # type: ignore
48
130
  )
49
131
  count = len(create_comics)
50
132
  if count:
51
133
  self.log.info(f"Created {count} comics.")
52
134
  except Exception:
53
- self.log.exception(f"While creating {comic_paths}")
135
+ self.log.exception(f"While creating {num_comics} comics")
54
136
 
137
+ self.changed += count
138
+ self.status_controller.finish(status)
55
139
  return count