codex 1.6.0a3__py3-none-any.whl → 1.6.0a5__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 (594) hide show
  1. codex/exceptions.py +17 -6
  2. codex/integrity.py +67 -3
  3. codex/librarian/covers/coverd.py +2 -2
  4. codex/librarian/covers/create.py +29 -13
  5. codex/librarian/covers/path.py +8 -6
  6. codex/librarian/covers/purge.py +27 -16
  7. codex/librarian/covers/tasks.py +3 -2
  8. codex/librarian/importer/aggregate_metadata.py +2 -2
  9. codex/librarian/importer/const.py +23 -7
  10. codex/librarian/importer/create_fks.py +88 -28
  11. codex/librarian/importer/deleted.py +26 -2
  12. codex/librarian/importer/importerd.py +124 -27
  13. codex/librarian/importer/link_comics.py +55 -0
  14. codex/librarian/importer/moved.py +78 -3
  15. codex/librarian/importer/query_fks.py +68 -4
  16. codex/librarian/importer/status.py +6 -0
  17. codex/librarian/importer/tasks.py +11 -3
  18. codex/librarian/importer/update_comics.py +1 -3
  19. codex/librarian/janitor/cleanup.py +24 -0
  20. codex/librarian/janitor/failed_imports.py +1 -1
  21. codex/librarian/janitor/janitor.py +5 -0
  22. codex/librarian/janitor/janitord.py +1 -1
  23. codex/librarian/janitor/status.py +1 -0
  24. codex/librarian/janitor/tasks.py +5 -0
  25. codex/librarian/librariand.py +3 -3
  26. codex/librarian/search/update.py +1 -1
  27. codex/librarian/watchdog/db_snapshot.py +9 -6
  28. codex/librarian/watchdog/emitter.py +3 -0
  29. codex/librarian/watchdog/event_batcherd.py +19 -3
  30. codex/librarian/watchdog/events.py +176 -36
  31. codex/librarian/watchdog/observers.py +13 -3
  32. codex/migrations/0027_sort_name.py +2 -12
  33. codex/migrations/0028_custom_covers.py +166 -0
  34. codex/models/admin.py +0 -1
  35. codex/models/base.py +3 -0
  36. codex/models/comic.py +15 -9
  37. codex/models/groups.py +30 -14
  38. codex/models/library.py +11 -3
  39. codex/models/paths.py +62 -14
  40. codex/models/{const.py → util.py} +15 -2
  41. codex/serializers/admin.py +11 -2
  42. codex/serializers/browser/mixins.py +1 -0
  43. codex/serializers/browser/settings.py +3 -0
  44. codex/settings/settings.py +11 -5
  45. codex/startup.py +77 -3
  46. codex/static_root/assets/{VCheckbox-CRu3BJ5D.48cbd6b412f4.js → VCheckbox-BF_99tgB.7fe59e56d9d4.js} +1 -1
  47. codex/static_root/assets/VCheckbox-BF_99tgB.7fe59e56d9d4.js.br +0 -0
  48. codex/static_root/assets/VCheckbox-BF_99tgB.7fe59e56d9d4.js.gz +0 -0
  49. codex/static_root/assets/{VCheckbox-CRu3BJ5D.js → VCheckbox-BF_99tgB.js} +1 -1
  50. codex/static_root/assets/VCheckbox-BF_99tgB.js.br +0 -0
  51. codex/static_root/assets/VCheckbox-BF_99tgB.js.gz +0 -0
  52. codex/static_root/assets/{VCheckboxBtn-CAUrQoP4.b162429d9a68.js → VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js} +1 -1
  53. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js.br +0 -0
  54. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.31e1e6aa44c9.js.gz +0 -0
  55. codex/static_root/assets/{VCheckboxBtn-CAUrQoP4.js → VCheckboxBtn-Coc7g8m7.js} +1 -1
  56. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.js.br +0 -0
  57. codex/static_root/assets/VCheckboxBtn-Coc7g8m7.js.gz +0 -0
  58. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js +1 -0
  59. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js.br +0 -0
  60. codex/static_root/assets/VCombobox-CTRWnOuM.0f00903c2ebc.js.gz +0 -0
  61. codex/static_root/assets/VCombobox-CTRWnOuM.js +1 -0
  62. codex/static_root/assets/VCombobox-CTRWnOuM.js.br +0 -0
  63. codex/static_root/assets/VCombobox-CTRWnOuM.js.gz +0 -0
  64. codex/static_root/assets/{VDialog-BcnzrzZO.7fa7ec337394.js → VDialog-pzwzInEh.51acf566a069.js} +1 -1
  65. codex/static_root/assets/VDialog-pzwzInEh.51acf566a069.js.br +0 -0
  66. codex/static_root/assets/VDialog-pzwzInEh.51acf566a069.js.gz +0 -0
  67. codex/static_root/assets/{VDialog-BcnzrzZO.js → VDialog-pzwzInEh.js} +1 -1
  68. codex/static_root/assets/VDialog-pzwzInEh.js.br +0 -0
  69. codex/static_root/assets/VDialog-pzwzInEh.js.gz +0 -0
  70. codex/static_root/assets/{VExpansionPanels-DP3HSBJj.a5600e541dc2.js → VExpansionPanels-CKSq1GpG.0ecf702c5b67.js} +1 -1
  71. codex/static_root/assets/VExpansionPanels-CKSq1GpG.0ecf702c5b67.js.br +0 -0
  72. codex/static_root/assets/VExpansionPanels-CKSq1GpG.0ecf702c5b67.js.gz +0 -0
  73. codex/static_root/assets/{VExpansionPanels-DP3HSBJj.js → VExpansionPanels-CKSq1GpG.js} +1 -1
  74. codex/static_root/assets/VExpansionPanels-CKSq1GpG.js.br +0 -0
  75. codex/static_root/assets/VExpansionPanels-CKSq1GpG.js.gz +0 -0
  76. codex/static_root/assets/{VRadioGroup-B9OZxVQw.28f10e381149.js → VRadioGroup-CW5GobGU.b72925bca3de.js} +1 -1
  77. codex/static_root/assets/VRadioGroup-CW5GobGU.b72925bca3de.js.br +0 -0
  78. codex/static_root/assets/VRadioGroup-CW5GobGU.b72925bca3de.js.gz +0 -0
  79. codex/static_root/assets/{VRadioGroup-B9OZxVQw.js → VRadioGroup-CW5GobGU.js} +1 -1
  80. codex/static_root/assets/VRadioGroup-CW5GobGU.js.br +0 -0
  81. codex/static_root/assets/VRadioGroup-CW5GobGU.js.gz +0 -0
  82. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js +1 -0
  83. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js.br +0 -0
  84. codex/static_root/assets/VSelect-ChOlLRyH.00b11751b25e.js.gz +0 -0
  85. codex/static_root/assets/VSelect-ChOlLRyH.js +1 -0
  86. codex/static_root/assets/VSelect-ChOlLRyH.js.br +0 -0
  87. codex/static_root/assets/VSelect-ChOlLRyH.js.gz +0 -0
  88. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css +1 -0
  89. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.br +0 -0
  90. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.gz +0 -0
  91. codex/static_root/assets/VSelect-MGVSeLgr.css +1 -0
  92. codex/static_root/assets/VSelect-MGVSeLgr.css.br +0 -0
  93. codex/static_root/assets/VSelect-MGVSeLgr.css.gz +0 -0
  94. codex/static_root/assets/{VSelectionControl-gDbXN6J_.js → VSelectionControl-DKOhc2zB.061dca094303.js} +1 -1
  95. codex/static_root/assets/VSelectionControl-DKOhc2zB.061dca094303.js.br +0 -0
  96. codex/static_root/assets/VSelectionControl-DKOhc2zB.061dca094303.js.gz +0 -0
  97. codex/static_root/assets/{VSelectionControl-gDbXN6J_.137a5bce3954.js → VSelectionControl-DKOhc2zB.js} +1 -1
  98. codex/static_root/assets/VSelectionControl-DKOhc2zB.js.br +0 -0
  99. codex/static_root/assets/VSelectionControl-DKOhc2zB.js.gz +0 -0
  100. codex/static_root/assets/{VSlideGroup-D8FKczaP.3c2d0e591ae3.js → VSlideGroup-zFz5_byd.e0e246b7acbb.js} +1 -1
  101. codex/static_root/assets/VSlideGroup-zFz5_byd.e0e246b7acbb.js.br +0 -0
  102. codex/static_root/assets/VSlideGroup-zFz5_byd.e0e246b7acbb.js.gz +0 -0
  103. codex/static_root/assets/{VSlideGroup-D8FKczaP.js → VSlideGroup-zFz5_byd.js} +1 -1
  104. codex/static_root/assets/VSlideGroup-zFz5_byd.js.br +0 -0
  105. codex/static_root/assets/VSlideGroup-zFz5_byd.js.gz +0 -0
  106. codex/static_root/assets/{VTable-D86x2F7y.60c6356a0b73.js → VTable-FKMTcdkF.6c1eed84d25e.js} +1 -1
  107. codex/static_root/assets/VTable-FKMTcdkF.6c1eed84d25e.js.br +0 -0
  108. codex/static_root/assets/VTable-FKMTcdkF.6c1eed84d25e.js.gz +0 -0
  109. codex/static_root/assets/{VTable-D86x2F7y.js → VTable-FKMTcdkF.js} +1 -1
  110. codex/static_root/assets/VTable-FKMTcdkF.js.br +0 -0
  111. codex/static_root/assets/VTable-FKMTcdkF.js.gz +0 -0
  112. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css +1 -0
  113. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css.br +0 -0
  114. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css.gz +0 -0
  115. codex/static_root/assets/VTextField-BssVoDhB.css +1 -0
  116. codex/static_root/assets/VTextField-BssVoDhB.css.br +0 -0
  117. codex/static_root/assets/VTextField-BssVoDhB.css.gz +0 -0
  118. codex/static_root/assets/VTextField-Dftcik85.cb2f1dda87e1.js +1 -0
  119. codex/static_root/assets/VTextField-Dftcik85.cb2f1dda87e1.js.br +0 -0
  120. codex/static_root/assets/VTextField-Dftcik85.cb2f1dda87e1.js.gz +0 -0
  121. codex/static_root/assets/VTextField-Dftcik85.js +1 -0
  122. codex/static_root/assets/VTextField-Dftcik85.js.br +0 -0
  123. codex/static_root/assets/VTextField-Dftcik85.js.gz +0 -0
  124. codex/static_root/assets/{VWindowItem-aBb7Akq5.f0d7de98e392.js → VWindowItem-CYCwnt3s.28025dc36c6a.js} +1 -1
  125. codex/static_root/assets/VWindowItem-CYCwnt3s.28025dc36c6a.js.br +0 -0
  126. codex/static_root/assets/VWindowItem-CYCwnt3s.28025dc36c6a.js.gz +0 -0
  127. codex/static_root/assets/{VWindowItem-aBb7Akq5.js → VWindowItem-CYCwnt3s.js} +1 -1
  128. codex/static_root/assets/VWindowItem-CYCwnt3s.js.br +0 -0
  129. codex/static_root/assets/VWindowItem-CYCwnt3s.js.gz +0 -0
  130. codex/static_root/assets/{admin-BbrOU9Y9.29936748af9e.css → admin-B8Bp2Uih.82c98714bcf1.css} +1 -1
  131. codex/static_root/assets/admin-B8Bp2Uih.82c98714bcf1.css.br +0 -0
  132. codex/static_root/assets/admin-B8Bp2Uih.82c98714bcf1.css.gz +0 -0
  133. codex/static_root/assets/{admin-BbrOU9Y9.css → admin-B8Bp2Uih.css} +1 -1
  134. codex/static_root/assets/admin-B8Bp2Uih.css.br +0 -0
  135. codex/static_root/assets/admin-B8Bp2Uih.css.gz +0 -0
  136. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js +1 -0
  137. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js.br +0 -0
  138. codex/static_root/assets/admin-DH_DrB0F.dc45fb726e07.js.gz +0 -0
  139. codex/static_root/assets/admin-DH_DrB0F.js +1 -0
  140. codex/static_root/assets/admin-DH_DrB0F.js.br +0 -0
  141. codex/static_root/assets/admin-DH_DrB0F.js.gz +0 -0
  142. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js +30 -0
  143. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js.br +0 -0
  144. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.a9e72fbfdadc.js.gz +0 -0
  145. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js +30 -0
  146. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js.br +0 -0
  147. codex/static_root/assets/admin-drawer-panel-B2rmpAVA.js.gz +0 -0
  148. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css +1 -0
  149. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.br +0 -0
  150. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.gz +0 -0
  151. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css +1 -0
  152. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.br +0 -0
  153. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.gz +0 -0
  154. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css +1 -0
  155. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css.br +0 -0
  156. codex/static_root/assets/browser-CpppBvim.a1b5d4fe97b1.css.gz +0 -0
  157. codex/static_root/assets/browser-CpppBvim.css +1 -0
  158. codex/static_root/assets/browser-CpppBvim.css.br +0 -0
  159. codex/static_root/assets/browser-CpppBvim.css.gz +0 -0
  160. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js +1 -0
  161. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js.br +0 -0
  162. codex/static_root/assets/browser-lKU7rqLd.cdb86e625ed9.js.gz +0 -0
  163. codex/static_root/assets/browser-lKU7rqLd.js +1 -0
  164. codex/static_root/assets/browser-lKU7rqLd.js.br +0 -0
  165. codex/static_root/assets/browser-lKU7rqLd.js.gz +0 -0
  166. codex/static_root/assets/{change-password-dialog-DywVK04u.eabe6b4c3e27.js → change-password-dialog-BIBG6Bk8.3583021c124a.js} +1 -1
  167. codex/static_root/assets/change-password-dialog-BIBG6Bk8.3583021c124a.js.br +0 -0
  168. codex/static_root/assets/change-password-dialog-BIBG6Bk8.3583021c124a.js.gz +0 -0
  169. codex/static_root/assets/{change-password-dialog-DywVK04u.js → change-password-dialog-BIBG6Bk8.js} +1 -1
  170. codex/static_root/assets/change-password-dialog-BIBG6Bk8.js.br +0 -0
  171. codex/static_root/assets/change-password-dialog-BIBG6Bk8.js.gz +0 -0
  172. codex/static_root/assets/{confirm-dialog-UnMzYsqN.d732afc0dc59.js → confirm-dialog-DARLmXRf.53fc6938fb8c.js} +1 -1
  173. codex/static_root/assets/confirm-dialog-DARLmXRf.53fc6938fb8c.js.br +0 -0
  174. codex/static_root/assets/confirm-dialog-DARLmXRf.53fc6938fb8c.js.gz +0 -0
  175. codex/static_root/assets/{confirm-dialog-UnMzYsqN.js → confirm-dialog-DARLmXRf.js} +1 -1
  176. codex/static_root/assets/confirm-dialog-DARLmXRf.js.br +0 -0
  177. codex/static_root/assets/confirm-dialog-DARLmXRf.js.gz +0 -0
  178. codex/static_root/assets/{datetime-column-IeiZdinf.e2807c847ddc.js → datetime-column-LpLgWpkf.16ce3324970f.js} +1 -1
  179. codex/static_root/assets/datetime-column-LpLgWpkf.16ce3324970f.js.br +0 -0
  180. codex/static_root/assets/datetime-column-LpLgWpkf.16ce3324970f.js.gz +0 -0
  181. codex/static_root/assets/{datetime-column-IeiZdinf.js → datetime-column-LpLgWpkf.js} +1 -1
  182. codex/static_root/assets/datetime-column-LpLgWpkf.js.br +0 -0
  183. codex/static_root/assets/datetime-column-LpLgWpkf.js.gz +0 -0
  184. codex/static_root/assets/{filter-BT3OCu_E.39135c83c357.js → filter-jRB1Ob6r.6a559a53e805.js} +1 -1
  185. codex/static_root/assets/filter-jRB1Ob6r.6a559a53e805.js.br +0 -0
  186. codex/static_root/assets/filter-jRB1Ob6r.6a559a53e805.js.gz +0 -0
  187. codex/static_root/assets/{filter-BT3OCu_E.js → filter-jRB1Ob6r.js} +1 -1
  188. codex/static_root/assets/filter-jRB1Ob6r.js.br +0 -0
  189. codex/static_root/assets/filter-jRB1Ob6r.js.gz +0 -0
  190. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css +1 -0
  191. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css.br +0 -0
  192. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css.gz +0 -0
  193. codex/static_root/assets/flag-tab-BNwbLfrN.css +1 -0
  194. codex/static_root/assets/flag-tab-BNwbLfrN.css.br +0 -0
  195. codex/static_root/assets/flag-tab-BNwbLfrN.css.gz +0 -0
  196. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js +1 -0
  197. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js.br +0 -0
  198. codex/static_root/assets/flag-tab-mYS2wg5e.17829c46cae9.js.gz +0 -0
  199. codex/static_root/assets/flag-tab-mYS2wg5e.js +1 -0
  200. codex/static_root/assets/flag-tab-mYS2wg5e.js.br +0 -0
  201. codex/static_root/assets/flag-tab-mYS2wg5e.js.gz +0 -0
  202. codex/static_root/assets/{group-tab-Bt_AURnL.59aa6b13b14b.js → group-tab-MK1lcjUU.98406d5babdc.js} +1 -1
  203. codex/static_root/assets/group-tab-MK1lcjUU.98406d5babdc.js.br +0 -0
  204. codex/static_root/assets/group-tab-MK1lcjUU.98406d5babdc.js.gz +0 -0
  205. codex/static_root/assets/{group-tab-Bt_AURnL.js → group-tab-MK1lcjUU.js} +1 -1
  206. codex/static_root/assets/group-tab-MK1lcjUU.js.br +0 -0
  207. codex/static_root/assets/group-tab-MK1lcjUU.js.gz +0 -0
  208. codex/static_root/assets/http-error-DKulXuTl.95e42988d5d5.css +1 -0
  209. codex/static_root/assets/{http-error-_wlDqEcp.5b78d3e332c9.css.br → http-error-DKulXuTl.95e42988d5d5.css.br} +0 -0
  210. codex/static_root/assets/http-error-DKulXuTl.95e42988d5d5.css.gz +0 -0
  211. codex/static_root/assets/http-error-DKulXuTl.css +1 -0
  212. codex/static_root/assets/{http-error-_wlDqEcp.css.br → http-error-DKulXuTl.css.br} +0 -0
  213. codex/static_root/assets/http-error-DKulXuTl.css.gz +0 -0
  214. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js +1 -0
  215. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.br +0 -0
  216. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.gz +0 -0
  217. codex/static_root/assets/http-error-HFeYnF0n.js +1 -0
  218. codex/static_root/assets/http-error-HFeYnF0n.js.br +0 -0
  219. codex/static_root/assets/http-error-HFeYnF0n.js.gz +0 -0
  220. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css +1 -0
  221. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.br +0 -0
  222. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.gz +0 -0
  223. codex/static_root/assets/library-tab-BN0SfPcH.css +1 -0
  224. codex/static_root/assets/library-tab-BN0SfPcH.css.br +0 -0
  225. codex/static_root/assets/library-tab-BN0SfPcH.css.gz +0 -0
  226. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js +1 -0
  227. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.br +0 -0
  228. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.gz +0 -0
  229. codex/static_root/assets/library-tab-DYeEBYlT.js +1 -0
  230. codex/static_root/assets/library-tab-DYeEBYlT.js.br +0 -0
  231. codex/static_root/assets/library-tab-DYeEBYlT.js.gz +0 -0
  232. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js +32 -0
  233. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.br +0 -0
  234. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.gz +0 -0
  235. codex/static_root/assets/main-gSW7TFDY.js +32 -0
  236. codex/static_root/assets/main-gSW7TFDY.js.br +0 -0
  237. codex/static_root/assets/main-gSW7TFDY.js.gz +0 -0
  238. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.7695fafdefce.css → pagination-toolbar-ClZui9Zf.697d7ae11c21.css} +1 -1
  239. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.br +0 -0
  240. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.gz +0 -0
  241. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.css → pagination-toolbar-ClZui9Zf.css} +1 -1
  242. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.br +0 -0
  243. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.gz +0 -0
  244. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js +1 -0
  245. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.br +0 -0
  246. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.gz +0 -0
  247. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js +1 -0
  248. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.br +0 -0
  249. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.gz +0 -0
  250. codex/static_root/assets/{pdf-doc-DNwjWxO_.70b904e8e4ff.js → pdf-doc-BTdS9hUz.7c966524280a.js} +1 -1
  251. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.br +0 -0
  252. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.gz +0 -0
  253. codex/static_root/assets/{pdf-doc-DNwjWxO_.js → pdf-doc-BTdS9hUz.js} +1 -1
  254. codex/static_root/assets/pdf-doc-BTdS9hUz.js.br +0 -0
  255. codex/static_root/assets/pdf-doc-BTdS9hUz.js.gz +0 -0
  256. codex/static_root/assets/{reader-CScIyWY8.0f2b4c8ea2bf.js → reader-BtSTIETq.f940f66c0e58.js} +2 -2
  257. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.br +0 -0
  258. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.gz +0 -0
  259. codex/static_root/assets/{reader-CScIyWY8.js → reader-BtSTIETq.js} +2 -2
  260. codex/static_root/assets/reader-BtSTIETq.js.br +0 -0
  261. codex/static_root/assets/reader-BtSTIETq.js.gz +0 -0
  262. codex/static_root/assets/{reader-pqgXBr7J.92a23ab06864.css → reader-CF3xt6JG.19fad7f62450.css} +1 -1
  263. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.br +0 -0
  264. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.gz +0 -0
  265. codex/static_root/assets/{reader-pqgXBr7J.css → reader-CF3xt6JG.css} +1 -1
  266. codex/static_root/assets/reader-CF3xt6JG.css.br +0 -0
  267. codex/static_root/assets/reader-CF3xt6JG.css.gz +0 -0
  268. codex/static_root/assets/{relation-chips-D6T6FX7G.css → relation-chips-Bsc-gUxE.98e65384c3c9.css} +1 -1
  269. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.br +0 -0
  270. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.gz +0 -0
  271. codex/static_root/assets/{relation-chips-D6T6FX7G.db095b2497e4.css → relation-chips-Bsc-gUxE.css} +1 -1
  272. codex/static_root/assets/relation-chips-Bsc-gUxE.css.br +0 -0
  273. codex/static_root/assets/relation-chips-Bsc-gUxE.css.gz +0 -0
  274. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js +1 -0
  275. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.br +0 -0
  276. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.gz +0 -0
  277. codex/static_root/assets/relation-chips-C4Ob7ySi.js +1 -0
  278. codex/static_root/assets/relation-chips-C4Ob7ySi.js.br +0 -0
  279. codex/static_root/assets/relation-chips-C4Ob7ySi.js.gz +0 -0
  280. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css +1 -0
  281. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.br +0 -0
  282. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.gz +0 -0
  283. codex/static_root/assets/settings-drawer-BJOfMtqA.css +1 -0
  284. codex/static_root/assets/settings-drawer-BJOfMtqA.css.br +0 -0
  285. codex/static_root/assets/settings-drawer-BJOfMtqA.css.gz +0 -0
  286. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js +2 -0
  287. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.br +0 -0
  288. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.gz +0 -0
  289. codex/static_root/assets/settings-drawer-xSctoxZn.js +2 -0
  290. codex/static_root/assets/settings-drawer-xSctoxZn.js.br +0 -0
  291. codex/static_root/assets/settings-drawer-xSctoxZn.js.gz +0 -0
  292. codex/static_root/assets/{stats-tab-Bn9lHtZb.b2969fd36ae9.js → stats-tab-C_uC_U7o.e6b014b6e8c6.js} +1 -1
  293. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.br +0 -0
  294. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.gz +0 -0
  295. codex/static_root/assets/{stats-tab-Bn9lHtZb.js → stats-tab-C_uC_U7o.js} +1 -1
  296. codex/static_root/assets/stats-tab-C_uC_U7o.js.br +0 -0
  297. codex/static_root/assets/stats-tab-C_uC_U7o.js.gz +0 -0
  298. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js +1 -0
  299. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.br +0 -0
  300. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.gz +0 -0
  301. codex/static_root/assets/task-tab-VIpy1Ula.js +1 -0
  302. codex/static_root/assets/task-tab-VIpy1Ula.js.br +0 -0
  303. codex/static_root/assets/task-tab-VIpy1Ula.js.gz +0 -0
  304. codex/static_root/assets/{unauthorized-DWgytUow.ca2f13dbdbf5.css → unauthorized-DHV6Q1n-.b7847b2872a1.css} +1 -1
  305. codex/static_root/assets/unauthorized-DHV6Q1n-.b7847b2872a1.css.br +0 -0
  306. codex/static_root/assets/unauthorized-DHV6Q1n-.b7847b2872a1.css.gz +0 -0
  307. codex/static_root/assets/{unauthorized-DWgytUow.css → unauthorized-DHV6Q1n-.css} +1 -1
  308. codex/static_root/assets/unauthorized-DHV6Q1n-.css.br +0 -0
  309. codex/static_root/assets/unauthorized-DHV6Q1n-.css.gz +0 -0
  310. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js +1 -0
  311. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.br +0 -0
  312. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.gz +0 -0
  313. codex/static_root/assets/unauthorized-kEuFL5br.js +1 -0
  314. codex/static_root/assets/unauthorized-kEuFL5br.js.br +0 -0
  315. codex/static_root/assets/unauthorized-kEuFL5br.js.gz +0 -0
  316. codex/static_root/assets/{user-tab-DDViQ8Q6.c258185d0ebe.js → user-tab-CZNFTTEI.f3940a75daae.js} +1 -1
  317. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.br +0 -0
  318. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.gz +0 -0
  319. codex/static_root/assets/{user-tab-DDViQ8Q6.js → user-tab-CZNFTTEI.js} +1 -1
  320. codex/static_root/assets/user-tab-CZNFTTEI.js.br +0 -0
  321. codex/static_root/assets/user-tab-CZNFTTEI.js.gz +0 -0
  322. codex/static_root/js/choices-admin.ef1ff3a8b9da.json +1 -0
  323. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
  324. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
  325. codex/static_root/js/choices-admin.json +1 -1
  326. codex/static_root/js/choices-admin.json.br +0 -0
  327. codex/static_root/js/choices-admin.json.gz +0 -0
  328. codex/static_root/js/{choices.b60817ba0a93.json → choices.23fedac2b9ba.json} +1 -1
  329. codex/static_root/js/choices.23fedac2b9ba.json.br +0 -0
  330. codex/static_root/js/choices.23fedac2b9ba.json.gz +0 -0
  331. codex/static_root/js/choices.json +1 -1
  332. codex/static_root/js/choices.json.br +0 -0
  333. codex/static_root/js/choices.json.gz +0 -0
  334. codex/static_root/manifest.6ef112683dae.json +642 -0
  335. codex/static_root/manifest.6ef112683dae.json.br +0 -0
  336. codex/static_root/manifest.6ef112683dae.json.gz +0 -0
  337. codex/static_root/manifest.json +259 -258
  338. codex/static_root/manifest.json.br +0 -0
  339. codex/static_root/manifest.json.gz +0 -0
  340. codex/static_root/staticfiles.json +1 -1
  341. codex/status_controller.py +11 -2
  342. codex/views/admin/library.py +47 -27
  343. codex/views/admin/stats.py +4 -1
  344. codex/views/admin/tasks.py +6 -1
  345. codex/views/browser/base.py +2 -2
  346. codex/views/browser/browser.py +1 -2
  347. codex/views/browser/browser_annotations.py +87 -17
  348. codex/views/browser/browser_breadcrumbs.py +2 -2
  349. codex/views/browser/browser_order_by.py +31 -20
  350. codex/views/browser/filters/search.py +1 -1
  351. codex/views/browser/metadata.py +0 -1
  352. codex/views/cover.py +3 -2
  353. codex/views/session.py +1 -0
  354. {codex-1.6.0a3.dist-info → codex-1.6.0a5.dist-info}/METADATA +3 -2
  355. {codex-1.6.0a3.dist-info → codex-1.6.0a5.dist-info}/RECORD +358 -357
  356. codex/static_root/assets/VCheckbox-CRu3BJ5D.48cbd6b412f4.js.br +0 -0
  357. codex/static_root/assets/VCheckbox-CRu3BJ5D.48cbd6b412f4.js.gz +0 -0
  358. codex/static_root/assets/VCheckbox-CRu3BJ5D.js.br +0 -0
  359. codex/static_root/assets/VCheckbox-CRu3BJ5D.js.gz +0 -0
  360. codex/static_root/assets/VCheckboxBtn-CAUrQoP4.b162429d9a68.js.br +0 -0
  361. codex/static_root/assets/VCheckboxBtn-CAUrQoP4.b162429d9a68.js.gz +0 -0
  362. codex/static_root/assets/VCheckboxBtn-CAUrQoP4.js.br +0 -0
  363. codex/static_root/assets/VCheckboxBtn-CAUrQoP4.js.gz +0 -0
  364. codex/static_root/assets/VCombobox-DJFO04Gk.b335ed051992.js +0 -1
  365. codex/static_root/assets/VCombobox-DJFO04Gk.b335ed051992.js.br +0 -0
  366. codex/static_root/assets/VCombobox-DJFO04Gk.b335ed051992.js.gz +0 -0
  367. codex/static_root/assets/VCombobox-DJFO04Gk.js +0 -1
  368. codex/static_root/assets/VCombobox-DJFO04Gk.js.br +0 -0
  369. codex/static_root/assets/VCombobox-DJFO04Gk.js.gz +0 -0
  370. codex/static_root/assets/VDialog-BcnzrzZO.7fa7ec337394.js.br +0 -0
  371. codex/static_root/assets/VDialog-BcnzrzZO.7fa7ec337394.js.gz +0 -0
  372. codex/static_root/assets/VDialog-BcnzrzZO.js.br +0 -0
  373. codex/static_root/assets/VDialog-BcnzrzZO.js.gz +0 -0
  374. codex/static_root/assets/VExpansionPanels-DP3HSBJj.a5600e541dc2.js.br +0 -0
  375. codex/static_root/assets/VExpansionPanels-DP3HSBJj.a5600e541dc2.js.gz +0 -0
  376. codex/static_root/assets/VExpansionPanels-DP3HSBJj.js.br +0 -0
  377. codex/static_root/assets/VExpansionPanels-DP3HSBJj.js.gz +0 -0
  378. codex/static_root/assets/VRadioGroup-B9OZxVQw.28f10e381149.js.br +0 -5
  379. codex/static_root/assets/VRadioGroup-B9OZxVQw.28f10e381149.js.gz +0 -0
  380. codex/static_root/assets/VRadioGroup-B9OZxVQw.js.br +0 -5
  381. codex/static_root/assets/VRadioGroup-B9OZxVQw.js.gz +0 -0
  382. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +0 -1
  383. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
  384. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
  385. codex/static_root/assets/VSelect-ARDhJiQK.css +0 -1
  386. codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
  387. codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
  388. codex/static_root/assets/VSelect-BIXGglsb.018666f134d5.js +0 -1
  389. codex/static_root/assets/VSelect-BIXGglsb.018666f134d5.js.br +0 -0
  390. codex/static_root/assets/VSelect-BIXGglsb.018666f134d5.js.gz +0 -0
  391. codex/static_root/assets/VSelect-BIXGglsb.js +0 -1
  392. codex/static_root/assets/VSelect-BIXGglsb.js.br +0 -0
  393. codex/static_root/assets/VSelect-BIXGglsb.js.gz +0 -0
  394. codex/static_root/assets/VSelectionControl-gDbXN6J_.137a5bce3954.js.br +0 -0
  395. codex/static_root/assets/VSelectionControl-gDbXN6J_.137a5bce3954.js.gz +0 -0
  396. codex/static_root/assets/VSelectionControl-gDbXN6J_.js.br +0 -0
  397. codex/static_root/assets/VSelectionControl-gDbXN6J_.js.gz +0 -0
  398. codex/static_root/assets/VSlideGroup-D8FKczaP.3c2d0e591ae3.js.br +0 -0
  399. codex/static_root/assets/VSlideGroup-D8FKczaP.3c2d0e591ae3.js.gz +0 -0
  400. codex/static_root/assets/VSlideGroup-D8FKczaP.js.br +0 -0
  401. codex/static_root/assets/VSlideGroup-D8FKczaP.js.gz +0 -0
  402. codex/static_root/assets/VTable-D86x2F7y.60c6356a0b73.js.br +0 -0
  403. codex/static_root/assets/VTable-D86x2F7y.60c6356a0b73.js.gz +0 -0
  404. codex/static_root/assets/VTable-D86x2F7y.js.br +0 -0
  405. codex/static_root/assets/VTable-D86x2F7y.js.gz +0 -0
  406. codex/static_root/assets/VTextField-BEgIi5E-.6a5cf9192004.js +0 -1
  407. codex/static_root/assets/VTextField-BEgIi5E-.6a5cf9192004.js.br +0 -0
  408. codex/static_root/assets/VTextField-BEgIi5E-.6a5cf9192004.js.gz +0 -0
  409. codex/static_root/assets/VTextField-BEgIi5E-.js +0 -1
  410. codex/static_root/assets/VTextField-BEgIi5E-.js.br +0 -0
  411. codex/static_root/assets/VTextField-BEgIi5E-.js.gz +0 -0
  412. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css +0 -1
  413. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.br +0 -0
  414. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.gz +0 -0
  415. codex/static_root/assets/VTextField-DuIIp-VY.css +0 -1
  416. codex/static_root/assets/VTextField-DuIIp-VY.css.br +0 -0
  417. codex/static_root/assets/VTextField-DuIIp-VY.css.gz +0 -0
  418. codex/static_root/assets/VWindowItem-aBb7Akq5.f0d7de98e392.js.br +0 -0
  419. codex/static_root/assets/VWindowItem-aBb7Akq5.f0d7de98e392.js.gz +0 -0
  420. codex/static_root/assets/VWindowItem-aBb7Akq5.js.br +0 -0
  421. codex/static_root/assets/VWindowItem-aBb7Akq5.js.gz +0 -0
  422. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.br +0 -0
  423. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.gz +0 -0
  424. codex/static_root/assets/admin-BbrOU9Y9.css.br +0 -0
  425. codex/static_root/assets/admin-BbrOU9Y9.css.gz +0 -0
  426. codex/static_root/assets/admin-CUlXuIkb.2c5ca314c698.js +0 -1
  427. codex/static_root/assets/admin-CUlXuIkb.2c5ca314c698.js.br +0 -0
  428. codex/static_root/assets/admin-CUlXuIkb.2c5ca314c698.js.gz +0 -0
  429. codex/static_root/assets/admin-CUlXuIkb.js +0 -1
  430. codex/static_root/assets/admin-CUlXuIkb.js.br +0 -0
  431. codex/static_root/assets/admin-CUlXuIkb.js.gz +0 -0
  432. codex/static_root/assets/admin-drawer-panel-B98ueC5c.3fb257a9e9e8.js +0 -30
  433. codex/static_root/assets/admin-drawer-panel-B98ueC5c.3fb257a9e9e8.js.br +0 -0
  434. codex/static_root/assets/admin-drawer-panel-B98ueC5c.3fb257a9e9e8.js.gz +0 -0
  435. codex/static_root/assets/admin-drawer-panel-B98ueC5c.js +0 -30
  436. codex/static_root/assets/admin-drawer-panel-B98ueC5c.js.br +0 -0
  437. codex/static_root/assets/admin-drawer-panel-B98ueC5c.js.gz +0 -0
  438. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css +0 -1
  439. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.br +0 -0
  440. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.gz +0 -0
  441. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css +0 -1
  442. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.br +0 -0
  443. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.gz +0 -0
  444. codex/static_root/assets/browser-C3YcP-vH.31dac9c003c4.js +0 -1
  445. codex/static_root/assets/browser-C3YcP-vH.31dac9c003c4.js.br +0 -0
  446. codex/static_root/assets/browser-C3YcP-vH.31dac9c003c4.js.gz +0 -0
  447. codex/static_root/assets/browser-C3YcP-vH.js +0 -1
  448. codex/static_root/assets/browser-C3YcP-vH.js.br +0 -0
  449. codex/static_root/assets/browser-C3YcP-vH.js.gz +0 -0
  450. codex/static_root/assets/browser-CwzOGMJs.css +0 -1
  451. codex/static_root/assets/browser-CwzOGMJs.css.br +0 -0
  452. codex/static_root/assets/browser-CwzOGMJs.css.gz +0 -0
  453. codex/static_root/assets/browser-CwzOGMJs.e53bf262a521.css +0 -1
  454. codex/static_root/assets/browser-CwzOGMJs.e53bf262a521.css.br +0 -0
  455. codex/static_root/assets/browser-CwzOGMJs.e53bf262a521.css.gz +0 -0
  456. codex/static_root/assets/change-password-dialog-DywVK04u.eabe6b4c3e27.js.br +0 -0
  457. codex/static_root/assets/change-password-dialog-DywVK04u.eabe6b4c3e27.js.gz +0 -0
  458. codex/static_root/assets/change-password-dialog-DywVK04u.js.br +0 -0
  459. codex/static_root/assets/change-password-dialog-DywVK04u.js.gz +0 -0
  460. codex/static_root/assets/confirm-dialog-UnMzYsqN.d732afc0dc59.js.br +0 -0
  461. codex/static_root/assets/confirm-dialog-UnMzYsqN.d732afc0dc59.js.gz +0 -0
  462. codex/static_root/assets/confirm-dialog-UnMzYsqN.js.br +0 -0
  463. codex/static_root/assets/confirm-dialog-UnMzYsqN.js.gz +0 -0
  464. codex/static_root/assets/datetime-column-IeiZdinf.e2807c847ddc.js.br +0 -0
  465. codex/static_root/assets/datetime-column-IeiZdinf.e2807c847ddc.js.gz +0 -0
  466. codex/static_root/assets/datetime-column-IeiZdinf.js.br +0 -0
  467. codex/static_root/assets/datetime-column-IeiZdinf.js.gz +0 -0
  468. codex/static_root/assets/filter-BT3OCu_E.39135c83c357.js.br +0 -0
  469. codex/static_root/assets/filter-BT3OCu_E.39135c83c357.js.gz +0 -0
  470. codex/static_root/assets/filter-BT3OCu_E.js.br +0 -0
  471. codex/static_root/assets/filter-BT3OCu_E.js.gz +0 -0
  472. codex/static_root/assets/flag-tab-BQmB4JNc.29dd38ce57d1.js +0 -1
  473. codex/static_root/assets/flag-tab-BQmB4JNc.29dd38ce57d1.js.br +0 -0
  474. codex/static_root/assets/flag-tab-BQmB4JNc.29dd38ce57d1.js.gz +0 -0
  475. codex/static_root/assets/flag-tab-BQmB4JNc.js +0 -1
  476. codex/static_root/assets/flag-tab-BQmB4JNc.js.br +0 -0
  477. codex/static_root/assets/flag-tab-BQmB4JNc.js.gz +0 -0
  478. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css +0 -1
  479. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.br +0 -0
  480. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.gz +0 -0
  481. codex/static_root/assets/flag-tab-BacZLlVi.css +0 -1
  482. codex/static_root/assets/flag-tab-BacZLlVi.css.br +0 -0
  483. codex/static_root/assets/flag-tab-BacZLlVi.css.gz +0 -0
  484. codex/static_root/assets/group-tab-Bt_AURnL.59aa6b13b14b.js.br +0 -0
  485. codex/static_root/assets/group-tab-Bt_AURnL.59aa6b13b14b.js.gz +0 -0
  486. codex/static_root/assets/group-tab-Bt_AURnL.js.br +0 -0
  487. codex/static_root/assets/group-tab-Bt_AURnL.js.gz +0 -0
  488. codex/static_root/assets/http-error-Cy29flzW.26884b2f42da.js +0 -1
  489. codex/static_root/assets/http-error-Cy29flzW.26884b2f42da.js.br +0 -4
  490. codex/static_root/assets/http-error-Cy29flzW.26884b2f42da.js.gz +0 -0
  491. codex/static_root/assets/http-error-Cy29flzW.js +0 -1
  492. codex/static_root/assets/http-error-Cy29flzW.js.br +0 -4
  493. codex/static_root/assets/http-error-Cy29flzW.js.gz +0 -0
  494. codex/static_root/assets/http-error-_wlDqEcp.5b78d3e332c9.css +0 -1
  495. codex/static_root/assets/http-error-_wlDqEcp.5b78d3e332c9.css.gz +0 -0
  496. codex/static_root/assets/http-error-_wlDqEcp.css +0 -1
  497. codex/static_root/assets/http-error-_wlDqEcp.css.gz +0 -0
  498. codex/static_root/assets/library-tab-0LLO3xlP.3636f75510b6.js +0 -1
  499. codex/static_root/assets/library-tab-0LLO3xlP.3636f75510b6.js.br +0 -0
  500. codex/static_root/assets/library-tab-0LLO3xlP.3636f75510b6.js.gz +0 -0
  501. codex/static_root/assets/library-tab-0LLO3xlP.js +0 -1
  502. codex/static_root/assets/library-tab-0LLO3xlP.js.br +0 -0
  503. codex/static_root/assets/library-tab-0LLO3xlP.js.gz +0 -0
  504. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css +0 -1
  505. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.br +0 -2
  506. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.gz +0 -0
  507. codex/static_root/assets/library-tab-CXbkxNZt.css +0 -1
  508. codex/static_root/assets/library-tab-CXbkxNZt.css.br +0 -2
  509. codex/static_root/assets/library-tab-CXbkxNZt.css.gz +0 -0
  510. codex/static_root/assets/main-BCYKxiL-.67d660e491ad.js +0 -32
  511. codex/static_root/assets/main-BCYKxiL-.67d660e491ad.js.br +0 -0
  512. codex/static_root/assets/main-BCYKxiL-.67d660e491ad.js.gz +0 -0
  513. codex/static_root/assets/main-BCYKxiL-.js +0 -32
  514. codex/static_root/assets/main-BCYKxiL-.js.br +0 -0
  515. codex/static_root/assets/main-BCYKxiL-.js.gz +0 -0
  516. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.br +0 -0
  517. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.gz +0 -0
  518. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.br +0 -0
  519. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.gz +0 -0
  520. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.845b2f091ae7.js +0 -1
  521. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.845b2f091ae7.js.br +0 -0
  522. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.845b2f091ae7.js.gz +0 -0
  523. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.js +0 -1
  524. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.js.br +0 -0
  525. codex/static_root/assets/pagination-toolbar-DQ8B0BSx.js.gz +0 -0
  526. codex/static_root/assets/pdf-doc-DNwjWxO_.70b904e8e4ff.js.br +0 -0
  527. codex/static_root/assets/pdf-doc-DNwjWxO_.70b904e8e4ff.js.gz +0 -0
  528. codex/static_root/assets/pdf-doc-DNwjWxO_.js.br +0 -0
  529. codex/static_root/assets/pdf-doc-DNwjWxO_.js.gz +0 -0
  530. codex/static_root/assets/reader-CScIyWY8.0f2b4c8ea2bf.js.br +0 -0
  531. codex/static_root/assets/reader-CScIyWY8.0f2b4c8ea2bf.js.gz +0 -0
  532. codex/static_root/assets/reader-CScIyWY8.js.br +0 -0
  533. codex/static_root/assets/reader-CScIyWY8.js.gz +0 -0
  534. codex/static_root/assets/reader-pqgXBr7J.92a23ab06864.css.br +0 -0
  535. codex/static_root/assets/reader-pqgXBr7J.92a23ab06864.css.gz +0 -0
  536. codex/static_root/assets/reader-pqgXBr7J.css.br +0 -0
  537. codex/static_root/assets/reader-pqgXBr7J.css.gz +0 -0
  538. codex/static_root/assets/relation-chips-D6T6FX7G.css.br +0 -0
  539. codex/static_root/assets/relation-chips-D6T6FX7G.css.gz +0 -0
  540. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.br +0 -0
  541. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.gz +0 -0
  542. codex/static_root/assets/relation-chips-LT1N5iCN.37fceb98c9dd.js +0 -1
  543. codex/static_root/assets/relation-chips-LT1N5iCN.37fceb98c9dd.js.br +0 -0
  544. codex/static_root/assets/relation-chips-LT1N5iCN.37fceb98c9dd.js.gz +0 -0
  545. codex/static_root/assets/relation-chips-LT1N5iCN.js +0 -1
  546. codex/static_root/assets/relation-chips-LT1N5iCN.js.br +0 -0
  547. codex/static_root/assets/relation-chips-LT1N5iCN.js.gz +0 -0
  548. codex/static_root/assets/settings-drawer-BgUazs7O.7bbeb1fd98f1.css +0 -1
  549. codex/static_root/assets/settings-drawer-BgUazs7O.7bbeb1fd98f1.css.br +0 -0
  550. codex/static_root/assets/settings-drawer-BgUazs7O.7bbeb1fd98f1.css.gz +0 -0
  551. codex/static_root/assets/settings-drawer-BgUazs7O.css +0 -1
  552. codex/static_root/assets/settings-drawer-BgUazs7O.css.br +0 -0
  553. codex/static_root/assets/settings-drawer-BgUazs7O.css.gz +0 -0
  554. codex/static_root/assets/settings-drawer-DwqbWCCT.c6f1febbec8d.js +0 -2
  555. codex/static_root/assets/settings-drawer-DwqbWCCT.c6f1febbec8d.js.br +0 -0
  556. codex/static_root/assets/settings-drawer-DwqbWCCT.c6f1febbec8d.js.gz +0 -0
  557. codex/static_root/assets/settings-drawer-DwqbWCCT.js +0 -2
  558. codex/static_root/assets/settings-drawer-DwqbWCCT.js.br +0 -0
  559. codex/static_root/assets/settings-drawer-DwqbWCCT.js.gz +0 -0
  560. codex/static_root/assets/stats-tab-Bn9lHtZb.b2969fd36ae9.js.br +0 -0
  561. codex/static_root/assets/stats-tab-Bn9lHtZb.b2969fd36ae9.js.gz +0 -0
  562. codex/static_root/assets/stats-tab-Bn9lHtZb.js.br +0 -0
  563. codex/static_root/assets/stats-tab-Bn9lHtZb.js.gz +0 -0
  564. codex/static_root/assets/task-tab-BBfwR3Q4.d0de374d7ec0.js +0 -1
  565. codex/static_root/assets/task-tab-BBfwR3Q4.d0de374d7ec0.js.br +0 -0
  566. codex/static_root/assets/task-tab-BBfwR3Q4.d0de374d7ec0.js.gz +0 -0
  567. codex/static_root/assets/task-tab-BBfwR3Q4.js +0 -1
  568. codex/static_root/assets/task-tab-BBfwR3Q4.js.br +0 -0
  569. codex/static_root/assets/task-tab-BBfwR3Q4.js.gz +0 -0
  570. codex/static_root/assets/unauthorized-BWF4C258.3da18d521795.js +0 -1
  571. codex/static_root/assets/unauthorized-BWF4C258.3da18d521795.js.br +0 -0
  572. codex/static_root/assets/unauthorized-BWF4C258.3da18d521795.js.gz +0 -0
  573. codex/static_root/assets/unauthorized-BWF4C258.js +0 -1
  574. codex/static_root/assets/unauthorized-BWF4C258.js.br +0 -0
  575. codex/static_root/assets/unauthorized-BWF4C258.js.gz +0 -0
  576. codex/static_root/assets/unauthorized-DWgytUow.ca2f13dbdbf5.css.br +0 -0
  577. codex/static_root/assets/unauthorized-DWgytUow.ca2f13dbdbf5.css.gz +0 -0
  578. codex/static_root/assets/unauthorized-DWgytUow.css.br +0 -0
  579. codex/static_root/assets/unauthorized-DWgytUow.css.gz +0 -0
  580. codex/static_root/assets/user-tab-DDViQ8Q6.c258185d0ebe.js.br +0 -0
  581. codex/static_root/assets/user-tab-DDViQ8Q6.c258185d0ebe.js.gz +0 -0
  582. codex/static_root/assets/user-tab-DDViQ8Q6.js.br +0 -0
  583. codex/static_root/assets/user-tab-DDViQ8Q6.js.gz +0 -0
  584. codex/static_root/js/choices-admin.248384ece3f6.json +0 -1
  585. codex/static_root/js/choices-admin.248384ece3f6.json.br +0 -0
  586. codex/static_root/js/choices-admin.248384ece3f6.json.gz +0 -0
  587. codex/static_root/js/choices.b60817ba0a93.json.br +0 -0
  588. codex/static_root/js/choices.b60817ba0a93.json.gz +0 -0
  589. codex/static_root/manifest.776c8d59c3e4.json +0 -641
  590. codex/static_root/manifest.776c8d59c3e4.json.br +0 -0
  591. codex/static_root/manifest.776c8d59c3e4.json.gz +0 -0
  592. {codex-1.6.0a3.dist-info → codex-1.6.0a5.dist-info}/LICENSE +0 -0
  593. {codex-1.6.0a3.dist-info → codex-1.6.0a5.dist-info}/WHEEL +0 -0
  594. {codex-1.6.0a3.dist-info → codex-1.6.0a5.dist-info}/entry_points.txt +0 -0
@@ -3,13 +3,17 @@
3
3
  So we may safely create the comics next.
4
4
  """
5
5
 
6
- from itertools import chain
7
6
  from pathlib import Path
8
7
 
8
+ from django.core.exceptions import ObjectDoesNotExist
9
+ from django.db.models.functions.datetime import Now
10
+
9
11
  from codex.librarian.importer.const import (
10
12
  BULK_UPDATE_FOLDER_FIELDS,
13
+ CLASS_CUSTOM_COVER_GROUP_MAP,
11
14
  COUNT_FIELDS,
12
15
  CREATE_DICT_UPDATE_FIELDS,
16
+ CUSTOM_COVER_UPDATE_FIELDS,
13
17
  GROUP_BASE_FIELDS,
14
18
  GROUP_UPDATE_FIELDS,
15
19
  IMPRINT,
@@ -24,6 +28,7 @@ from codex.models import (
24
28
  Contributor,
25
29
  ContributorPerson,
26
30
  ContributorRole,
31
+ CustomCover,
27
32
  Folder,
28
33
  Imprint,
29
34
  Publisher,
@@ -41,7 +46,20 @@ class CreateForeignKeysMixin(QueuedThread):
41
46
  """Methods for creating foreign keys."""
42
47
 
43
48
  @staticmethod
44
- def _create_group_obj(group_class, group_param_tuple, group_count):
49
+ def _add_custom_cover_to_group(group_class, obj):
50
+ """If a custom cover exists for this group, add it."""
51
+ group = CLASS_CUSTOM_COVER_GROUP_MAP.get(group_class)
52
+ if not group:
53
+ # Normal, volume doesn't link to covers
54
+ return
55
+ try:
56
+ cover = CustomCover.objects.filter(group=group, sort_name=obj.sort_name)[0]
57
+ obj.custom_cover = cover
58
+ except (IndexError, ObjectDoesNotExist):
59
+ pass
60
+
61
+ @classmethod
62
+ def _create_group_obj(cls, group_class, group_param_tuple, group_count):
45
63
  """Create a set of browser group objects."""
46
64
  defaults = {"name": group_param_tuple[-1]}
47
65
  if group_class in (Imprint, Series, Volume):
@@ -64,6 +82,7 @@ class CreateForeignKeysMixin(QueuedThread):
64
82
 
65
83
  obj = group_class(**defaults)
66
84
  obj.presave()
85
+ cls._add_custom_cover_to_group(group_class, obj)
67
86
  return obj
68
87
 
69
88
  @staticmethod
@@ -99,6 +118,8 @@ class CreateForeignKeysMixin(QueuedThread):
99
118
  obj = self._create_group_obj(group_class, group_param_tuple, group_count)
100
119
  create_groups.append(obj)
101
120
  update_fields = GROUP_UPDATE_FIELDS[group_class]
121
+ if group_class in CLASS_CUSTOM_COVER_GROUP_MAP:
122
+ update_fields += ("custom_cover",)
102
123
  group_class.objects.bulk_create(
103
124
  create_groups,
104
125
  update_conflicts=True,
@@ -157,6 +178,7 @@ class CreateForeignKeysMixin(QueuedThread):
157
178
  parent_folder=parent,
158
179
  )
159
180
  folder.presave()
181
+ self._add_custom_cover_to_group(Folder, folder)
160
182
  create_folders.append(folder)
161
183
 
162
184
  def _bulk_folders_create_depth_level(self, library, paths, status):
@@ -212,6 +234,7 @@ class CreateForeignKeysMixin(QueuedThread):
212
234
  named_obj = named_class(name=name)
213
235
  if is_story_arc:
214
236
  named_obj.presave()
237
+ self._add_custom_cover_to_group(StoryArc, named_obj)
215
238
  create_named_objs.append(named_obj)
216
239
 
217
240
  update_fields = NAMED_MODEL_UPDATE_FIELDS
@@ -282,8 +305,8 @@ class CreateForeignKeysMixin(QueuedThread):
282
305
  self.status_controller.update(status)
283
306
  return count
284
307
 
285
- @staticmethod
286
- def _get_create_fks_totals(create_data):
308
+ def create_all_fks(self, library, create_data) -> int:
309
+ """Bulk create all foreign keys."""
287
310
  (
288
311
  create_groups,
289
312
  update_groups,
@@ -292,35 +315,12 @@ class CreateForeignKeysMixin(QueuedThread):
292
315
  create_contributors,
293
316
  create_story_arc_numbers,
294
317
  create_identifiers,
318
+ total_fks,
295
319
  ) = create_data
296
- total_fks = 0
297
- for data_group in chain(
298
- create_groups.values(), update_groups.values(), create_fks.values()
299
- ):
300
- total_fks += len(data_group)
301
- total_fks += (
302
- len(create_folder_paths)
303
- + len(create_contributors)
304
- + len(create_story_arc_numbers)
305
- + len(create_identifiers)
306
- )
307
- return total_fks
308
320
 
309
- def create_all_fks(self, library, create_data) -> int:
310
- """Bulk create all foreign keys."""
311
- total_fks = self._get_create_fks_totals(create_data)
312
321
  status = Status(ImportStatusTypes.CREATE_FKS, 0, total_fks)
313
322
  try:
314
323
  self.status_controller.start(status)
315
- (
316
- create_groups,
317
- update_groups,
318
- create_folder_paths,
319
- create_fks,
320
- create_contributors,
321
- create_story_arc_numbers,
322
- create_identifiers,
323
- ) = create_data
324
324
 
325
325
  for group_class, group_tree_counts in create_groups.items():
326
326
  count = self._bulk_group_create(
@@ -375,3 +375,63 @@ class CreateForeignKeysMixin(QueuedThread):
375
375
  finally:
376
376
  self.status_controller.finish(status)
377
377
  return status.complete if status.complete else 0
378
+
379
+ @status_notify(ImportStatusTypes.COVERS_MODIFIED, updates=False)
380
+ def update_custom_covers(
381
+ self, update_covers_qs, link_cover_pks, status=None
382
+ ) -> int:
383
+ """Update Custom Covers."""
384
+ count = 0
385
+ update_covers_count = update_covers_qs.count()
386
+ if not update_covers_count:
387
+ return count
388
+ if status:
389
+ status.total = update_covers_count
390
+ now = Now()
391
+
392
+ update_covers = []
393
+ for cover in update_covers_qs.only(*CUSTOM_COVER_UPDATE_FIELDS):
394
+ cover.updated_at = now
395
+ cover.presave()
396
+ update_covers.append(cover)
397
+
398
+ if update_covers:
399
+ CustomCover.objects.bulk_update(update_covers, CUSTOM_COVER_UPDATE_FIELDS)
400
+ update_cover_pks = update_covers_qs.values_list("pk", flat=True)
401
+ link_cover_pks.update(update_cover_pks)
402
+ self._remove_covers(update_cover_pks, custom=True) # type: ignore
403
+ count = len(update_covers)
404
+ if status:
405
+ status.add_complete(count)
406
+ return count
407
+
408
+ @status_notify(ImportStatusTypes.COVERS_CREATED, updates=False)
409
+ def create_custom_covers(
410
+ self, create_cover_paths, library, link_cover_pks, status=None
411
+ ) -> int:
412
+ """Create Custom Covers."""
413
+ count = 0
414
+ if not create_cover_paths:
415
+ return count
416
+ if status:
417
+ status.total = len(create_cover_paths)
418
+
419
+ create_covers = []
420
+ for path in create_cover_paths:
421
+ cover = CustomCover(library=library, path=path)
422
+ cover.presave()
423
+ create_covers.append(cover)
424
+
425
+ if create_covers:
426
+ objs = CustomCover.objects.bulk_create(
427
+ create_covers,
428
+ update_conflicts=True,
429
+ update_fields=("path", "stat"),
430
+ unique_fields=CustomCover._meta.unique_together[0],
431
+ )
432
+ created_pks = frozenset(obj.pk for obj in objs)
433
+ link_cover_pks.update(created_pks)
434
+ count = len(created_pks)
435
+ if status:
436
+ status.add_complete(count)
437
+ return count
@@ -3,14 +3,15 @@
3
3
  from codex.librarian.covers.tasks import CoverRemoveTask
4
4
  from codex.librarian.importer.status import ImportStatusTypes, status_notify
5
5
  from codex.models import Comic, Folder
6
+ from codex.models.paths import CustomCover
6
7
  from codex.threads import QueuedThread
7
8
 
8
9
 
9
10
  class DeletedMixin(QueuedThread):
10
11
  """Clean up database methods."""
11
12
 
12
- def _remove_covers(self, delete_comic_pks):
13
- task = CoverRemoveTask(delete_comic_pks)
13
+ def _remove_covers(self, delete_pks, custom=False):
14
+ task = CoverRemoveTask(delete_pks, custom)
14
15
  self.librarian_queue.put(task)
15
16
 
16
17
  @status_notify(status_type=ImportStatusTypes.DIRS_DELETED, updates=False)
@@ -53,6 +54,25 @@ class DeletedMixin(QueuedThread):
53
54
 
54
55
  return count
55
56
 
57
+ @status_notify(status_type=ImportStatusTypes.COVERS_DELETED, updates=False)
58
+ def _bulk_covers_deleted(self, delete_cover_paths, library, **kwargs):
59
+ """Bulk delete comics found missing from the filesystem."""
60
+ if not delete_cover_paths:
61
+ return 0
62
+ covers = CustomCover.objects.filter(
63
+ library=library, path__in=delete_cover_paths
64
+ )
65
+ delete_cover_pks = frozenset(covers.values_list("pk", flat=True))
66
+ covers.delete()
67
+
68
+ self._remove_covers(delete_cover_pks, custom=True)
69
+
70
+ count = len(delete_cover_paths)
71
+ if count:
72
+ self.log.info(f"Deleted {count} custom covers from {library.path}")
73
+
74
+ return count
75
+
56
76
  def delete(self, library, task):
57
77
  """Delete files and folders."""
58
78
  count = self._bulk_folders_deleted(task.dirs_deleted, library)
@@ -60,4 +80,8 @@ class DeletedMixin(QueuedThread):
60
80
 
61
81
  count += self._bulk_comics_deleted(task.files_deleted, library)
62
82
  task.files_deleted = None
83
+
84
+ count += self._bulk_covers_deleted(task.covers_deleted, library)
85
+ task.covers_deleted = None
86
+
63
87
  return count
@@ -117,6 +117,29 @@ class ComicImporterThread(
117
117
  log += ", ".join(dirs_log)
118
118
  self.log.debug(" " + log)
119
119
 
120
+ @staticmethod
121
+ def _init_librarian_status_moved(task, status_list):
122
+ """Initialize moved statuses."""
123
+ search_index_updates = 0
124
+ if task.dirs_moved:
125
+ status_list += [
126
+ Status(ImportStatusTypes.DIRS_MOVED, None, len(task.dirs_moved))
127
+ ]
128
+ if task.files_moved:
129
+ status_list += [
130
+ Status(ImportStatusTypes.FILES_MOVED, None, len(task.files_moved))
131
+ ]
132
+ search_index_updates += len(task.files_moved)
133
+ if task.covers_moved:
134
+ status_list += [
135
+ Status(ImportStatusTypes.COVERS_MOVED, None, len(task.covers_moved))
136
+ ]
137
+ if task.dirs_modified:
138
+ status_list += [
139
+ Status(ImportStatusTypes.DIRS_MODIFIED, None, len(task.dirs_modified))
140
+ ]
141
+ return search_index_updates
142
+
120
143
  @staticmethod
121
144
  def _init_if_modified_or_created(task, path, status_list):
122
145
  """Initialize librarian statuses for modified or created ops."""
@@ -124,6 +147,7 @@ class ComicImporterThread(
124
147
  status_list += [
125
148
  Status(ImportStatusTypes.AGGREGATE_TAGS, 0, total_paths, subtitle=path),
126
149
  Status(ImportStatusTypes.QUERY_MISSING_FKS),
150
+ Status(ImportStatusTypes.QUERY_MISSING_COVERS),
127
151
  Status(ImportStatusTypes.CREATE_FKS),
128
152
  ]
129
153
  if task.files_modified:
@@ -134,40 +158,56 @@ class ComicImporterThread(
134
158
  len(task.files_modified),
135
159
  )
136
160
  ]
137
- if task.files_created:
161
+ if task.covers_modified:
138
162
  status_list += [
139
- Status(ImportStatusTypes.FILES_CREATED, None, len(task.files_created))
163
+ Status(
164
+ ImportStatusTypes.COVERS_MODIFIED,
165
+ None,
166
+ len(task.covers_modified),
167
+ )
140
168
  ]
141
- if task.files_modified or task.files_created:
142
- status_list += [Status(ImportStatusTypes.LINK_M2M_FIELDS)]
143
- return total_paths
144
169
 
145
- def _init_librarian_status(self, task, path):
146
- """Update the librarian status tasks."""
147
- status_list = []
148
- search_index_updates = 0
149
- if task.dirs_moved:
170
+ if task.files_created or task.covers_created:
150
171
  status_list += [
151
- Status(ImportStatusTypes.DIRS_MOVED, None, len(task.dirs_moved))
172
+ Status(ImportStatusTypes.FILES_CREATED, None, len(task.files_created))
152
173
  ]
153
- if task.files_moved:
174
+ if task.covers_created:
154
175
  status_list += [
155
- Status(ImportStatusTypes.FILES_MOVED, None, len(task.files_moved))
176
+ Status(ImportStatusTypes.COVERS_CREATED, None, len(task.covers_created))
156
177
  ]
157
- search_index_updates += len(task.files_moved)
158
- if task.files_modified:
178
+
179
+ if task.files_modified or task.files_created:
180
+ status_list += [Status(ImportStatusTypes.LINK_M2M_FIELDS)]
181
+
182
+ num_covers_linked = (
183
+ len(task.covers_moved)
184
+ + len(task.covers_modified)
185
+ + len(task.covers_created)
186
+ )
187
+ if num_covers_linked:
159
188
  status_list += [
160
- Status(ImportStatusTypes.DIRS_MODIFIED, None, len(task.dirs_modified))
189
+ Status(ImportStatusTypes.COVERS_LINK, None, num_covers_linked)
161
190
  ]
162
- if task.files_modified or task.files_created:
163
- search_index_updates += self._init_if_modified_or_created(
164
- task, path, status_list
165
- )
191
+ return total_paths
192
+
193
+ @staticmethod
194
+ def _init_librarian_status_deleted(task, status_list):
195
+ """Init deleted statuses."""
196
+ search_index_updates = 0
166
197
  if task.files_deleted:
167
198
  status_list += [
168
199
  Status(ImportStatusTypes.FILES_DELETED, None, len(task.files_deleted))
169
200
  ]
170
201
  search_index_updates += len(task.files_deleted)
202
+ if task.covers_deleted:
203
+ status_list += [
204
+ Status(ImportStatusTypes.COVERS_DELETED, None, len(task.covers_deleted))
205
+ ]
206
+ return search_index_updates
207
+
208
+ @staticmethod
209
+ def _init_librarian_status_search_index(search_index_updates, status_list):
210
+ """Init search index statuses."""
171
211
  status_list += [
172
212
  Status(
173
213
  SearchIndexStatusTypes.SEARCH_INDEX_UPDATE,
@@ -176,6 +216,23 @@ class ComicImporterThread(
176
216
  ),
177
217
  Status(SearchIndexStatusTypes.SEARCH_INDEX_REMOVE),
178
218
  ]
219
+
220
+ def _init_librarian_status(self, task, path):
221
+ """Update the librarian status tasks."""
222
+ status_list = []
223
+ search_index_updates = 0
224
+ search_index_updates += self._init_librarian_status_moved(task, status_list)
225
+ if (
226
+ task.files_modified
227
+ or task.files_created
228
+ or task.covers_modified
229
+ or task.covers_created
230
+ ):
231
+ search_index_updates += self._init_if_modified_or_created(
232
+ task, path, status_list
233
+ )
234
+ search_index_updates += self._init_librarian_status_deleted(task, status_list)
235
+ self._init_librarian_status_search_index(search_index_updates, status_list)
179
236
  self.status_controller.start_many(status_list)
180
237
 
181
238
  def _init_apply(self, library, task):
@@ -193,12 +250,38 @@ class ComicImporterThread(
193
250
  self._log_task(library.path, task)
194
251
  self._init_librarian_status(task, library.path)
195
252
 
196
- def _create_comic_relations(self, library, fks) -> int:
253
+ def _create_comic_and_cover_relations(
254
+ self, library, fks, cover_paths: frozenset[int], link_cover_pks: set[int]
255
+ ) -> int:
197
256
  """Query all foreign keys to determine what needs creating, then create them."""
198
- if not fks:
199
- return 0
257
+ count = 0
258
+ if not fks and not cover_paths:
259
+ return count
200
260
  create_data = self.query_all_missing_fks(library.path, fks)
201
- return self.create_all_fks(library, create_data)
261
+ query_cover_data = []
262
+ count += self.query_missing_custom_covers(
263
+ cover_paths,
264
+ library,
265
+ query_cover_data,
266
+ )
267
+ count += self.create_all_fks(library, create_data)
268
+ if query_cover_data:
269
+ update_covers_qs, create_cover_paths = query_cover_data
270
+ count += self.update_custom_covers(update_covers_qs, link_cover_pks)
271
+ link_covers_status = Status(
272
+ ImportStatusTypes.COVERS_LINK, 0, len(link_cover_pks)
273
+ )
274
+ self.status_controller.update(link_covers_status, notify=False)
275
+ count += self.create_custom_covers(
276
+ create_cover_paths,
277
+ library,
278
+ link_cover_pks,
279
+ )
280
+ link_covers_status = Status(
281
+ ImportStatusTypes.COVERS_LINK, 0, len(link_cover_pks)
282
+ )
283
+ self.status_controller.update(link_covers_status, notify=False)
284
+ return count
202
285
 
203
286
  def _finish_apply_status(self, library):
204
287
  """Finish all librarian statuses."""
@@ -241,11 +324,13 @@ class ComicImporterThread(
241
324
  self._init_apply(library, task)
242
325
 
243
326
  changed: int = 0
244
- changed += self.move_and_modify_dirs(library, task)
327
+ link_cover_pks: set[int] = set()
328
+ changed += self.move_and_modify_dirs(library, task, link_cover_pks)
245
329
 
246
330
  modified_paths = task.files_modified
247
331
  created_paths = task.files_created
248
332
  task.files_modified = task.files_created = None
333
+
249
334
  mds = {}
250
335
  m2m_mds = {}
251
336
  fks = {}
@@ -266,8 +351,13 @@ class ComicImporterThread(
266
351
  modified_paths -= fis.keys()
267
352
  created_paths -= fis.keys()
268
353
 
269
- changed += self._create_comic_relations(library, fks)
270
- fks = None
354
+ cover_paths = frozenset(task.covers_modified | task.covers_created)
355
+ task.covers_modified = task.covers_created = None
356
+
357
+ changed += self._create_comic_and_cover_relations(
358
+ library, fks, cover_paths, link_cover_pks
359
+ )
360
+ fks = cover_paths = None
271
361
 
272
362
  imported_count = self.bulk_update_comics(
273
363
  modified_paths,
@@ -282,6 +372,9 @@ class ComicImporterThread(
282
372
  m2m_mds = None
283
373
  changed += imported_count
284
374
 
375
+ self.link_custom_covers(link_cover_pks)
376
+ link_cover_pks = set()
377
+
285
378
  new_failed_imports = self.fail_imports(
286
379
  library, fis, bool(task.files_deleted)
287
380
  )
@@ -310,11 +403,15 @@ class ComicImporterThread(
310
403
  library_id=library_id,
311
404
  dirs_moved={},
312
405
  files_moved={},
406
+ covers_moved={},
313
407
  dirs_modified=frozenset(),
314
408
  files_modified=frozenset(paths),
409
+ covers_modified=frozenset(),
315
410
  files_created=frozenset(),
411
+ covers_created=frozenset(),
316
412
  dirs_deleted=frozenset(),
317
413
  files_deleted=frozenset(),
414
+ covers_deleted=frozenset(),
318
415
  force_import_metadata=True,
319
416
  )
320
417
  self._apply(task)
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
  from django.db.models import Q
6
6
 
7
7
  from codex.librarian.importer.const import (
8
+ CLASS_CUSTOM_COVER_GROUP_MAP,
8
9
  COMIC_FK_FIELD_NAMES,
9
10
  DICT_MODEL_FIELD_NAME_CLASS_MAP,
10
11
  DICT_MODEL_REL_LINK_MAP,
@@ -20,6 +21,7 @@ from codex.librarian.importer.const import (
20
21
  from codex.librarian.importer.status import ImportStatusTypes, status_notify
21
22
  from codex.models import (
22
23
  Comic,
24
+ CustomCover,
23
25
  Folder,
24
26
  Imprint,
25
27
  Publisher,
@@ -257,3 +259,56 @@ class LinkComicsMixin(QueuedThread):
257
259
  if del_total:
258
260
  self.log.info(f"Deleted {del_total} stale relations for altered comics.")
259
261
  return created_total + del_total
262
+
263
+ def _link_custom_cover_prepare(self, cover, model_map):
264
+ """Prepare one cover in the model map for bulk update."""
265
+ if cover.library and cover.library.covers_only:
266
+ model = CLASS_CUSTOM_COVER_GROUP_MAP.inverse.get(cover.group)
267
+ if not model:
268
+ self.log.warning(f"Custom Cover model not found for {cover.path}")
269
+ return
270
+ group_filter = {"sort_name": cover.sort_name}
271
+ else:
272
+ model = Folder
273
+ path = str(Path(cover.path).parent)
274
+ group_filter = {"path": path}
275
+ qs = model.objects.filter(**group_filter).exclude(custom_cover=cover)
276
+ if not qs.exists():
277
+ return
278
+
279
+ if model not in model_map:
280
+ model_map[model] = []
281
+
282
+ for obj in qs.iterator():
283
+ obj.custom_cover = cover
284
+ model_map[model].append(obj)
285
+
286
+ def _link_custom_cover_group(self, model, objs, status):
287
+ """Bulk link a group to it's custom covers."""
288
+ count = 0
289
+ if not objs:
290
+ return count
291
+ model.objects.bulk_update(objs, ["custom_cover"])
292
+ count += len(objs)
293
+ self.log.info(f"Linked {count} custom covers to {model.__name__}s")
294
+ if status:
295
+ status.complete = status.complete or 0
296
+ status.complete += count
297
+ return count
298
+
299
+ @status_notify(status_type=ImportStatusTypes.COVERS_LINK)
300
+ def link_custom_covers(self, link_cover_pks, status=None):
301
+ """Link Custom Covers to Groups."""
302
+ # Aggregate objs to update for each group model.
303
+ model_map = {}
304
+ covers = CustomCover.objects.filter(pk__in=link_cover_pks).only(
305
+ "library", "path"
306
+ )
307
+ for cover in covers:
308
+ self._link_custom_cover_prepare(cover, model_map)
309
+
310
+ # Bulk update each model type
311
+ total_count = 0
312
+ for model, objs in model_map.items():
313
+ total_count += self._link_custom_cover_group(model, objs, status)
314
+ return total_count
@@ -6,6 +6,8 @@ from django.db.models.functions import Now
6
6
 
7
7
  from codex.librarian.importer.const import (
8
8
  BULK_UPDATE_FOLDER_MODIFIED_FIELDS,
9
+ CLASS_CUSTOM_COVER_GROUP_MAP,
10
+ CUSTOM_COVER_UPDATE_FIELDS,
9
11
  FOLDERS_FIELD,
10
12
  MOVED_BULK_COMIC_UPDATE_FIELDS,
11
13
  MOVED_BULK_FOLDER_UPDATE_FIELDS,
@@ -17,7 +19,7 @@ from codex.librarian.importer.create_fks import (
17
19
  )
18
20
  from codex.librarian.importer.query_fks import QueryForeignKeysMixin
19
21
  from codex.librarian.importer.status import ImportStatusTypes, status_notify
20
- from codex.models import Comic, Folder, Library
22
+ from codex.models import Comic, CustomCover, Folder, Library
21
23
 
22
24
 
23
25
  class MovedMixin(CreateComicsMixin, CreateForeignKeysMixin, QueryForeignKeysMixin):
@@ -73,6 +75,76 @@ class MovedMixin(CreateComicsMixin, CreateForeignKeysMixin, QueryForeignKeysMixi
73
75
 
74
76
  return count
75
77
 
78
+ def _bulk_covers_moved_prepare(self, library, moved_paths, status):
79
+ """Create an update map for bulk update."""
80
+ covers = CustomCover.objects.filter(
81
+ library=library, path__in=moved_paths.keys()
82
+ ).only("pk", "path")
83
+
84
+ if status:
85
+ status.total = covers.count()
86
+
87
+ now = Now()
88
+ moved_covers = []
89
+ unlink_pks = set()
90
+ for cover in covers.iterator():
91
+ try:
92
+ new_path = moved_paths[cover.path]
93
+ cover.path = new_path
94
+ new_path = Path(new_path)
95
+ cover.updated_at = now
96
+ cover.presave()
97
+ moved_covers.append(cover)
98
+ unlink_pks.add(cover.pk)
99
+ except Exception:
100
+ self.log.exception(f"moving {cover.path}")
101
+ return moved_covers, unlink_pks
102
+
103
+ def _bulk_covers_moved_unlink(self, unlink_pks):
104
+ """Unlink moved covers because they could have moved between group dirs."""
105
+ if not unlink_pks:
106
+ return
107
+ self.log.debug(f"Unlinking {len(unlink_pks)} moved custom covers.")
108
+ for model in CLASS_CUSTOM_COVER_GROUP_MAP:
109
+ groups = model.objects.filter(custom_cover__in=unlink_pks)
110
+ unlink_groups = []
111
+ for group in groups:
112
+ group.custom_cover = None
113
+ unlink_groups.append(group)
114
+ if unlink_groups:
115
+ model.objects.bulk_update(unlink_groups, ["custom_cover"])
116
+ self.log.debug(
117
+ f"Unlinked {len(unlink_groups)} {model.__name__} moved custom covers."
118
+ )
119
+
120
+ self._remove_covers(unlink_pks, custom=True) # type: ignore
121
+
122
+ @status_notify(status_type=ImportStatusTypes.FILES_MOVED, updates=False)
123
+ def _bulk_covers_moved(self, moved_paths, library, link_cover_pks, status=None):
124
+ """Move covers."""
125
+ count = 0
126
+ if not moved_paths:
127
+ return count
128
+ if status:
129
+ status.total = len(moved_paths)
130
+
131
+ moved_covers, unlink_pks = self._bulk_covers_moved_prepare(
132
+ library, moved_paths, status
133
+ )
134
+ link_cover_pks.update(unlink_pks)
135
+ if moved_covers:
136
+ CustomCover.objects.bulk_update(moved_covers, CUSTOM_COVER_UPDATE_FIELDS)
137
+ count = len(moved_covers)
138
+
139
+ self._bulk_covers_moved_unlink(unlink_pks)
140
+
141
+ if count:
142
+ self.log.info(f"Moved {count} custom covers.")
143
+ if status:
144
+ status.add_complete(count)
145
+
146
+ return count
147
+
76
148
  def _get_parent_folders(self, library, dest_folder_paths, status):
77
149
  """Get destination parent folders."""
78
150
  # Determine parent folder paths.
@@ -163,7 +235,7 @@ class MovedMixin(CreateComicsMixin, CreateForeignKeysMixin, QueryForeignKeysMixi
163
235
 
164
236
  def adopt_orphan_folders(self):
165
237
  """Find orphan folders and move them into their correct place."""
166
- libraries = Library.objects.only("pk", "path")
238
+ libraries = Library.objects.filter(covers_only=False).only("pk", "path")
167
239
  for library in libraries.iterator():
168
240
  orphan_folder_paths = (
169
241
  Folder.objects.filter(library=library, parent_folder=None)
@@ -177,7 +249,7 @@ class MovedMixin(CreateComicsMixin, CreateForeignKeysMixin, QueryForeignKeysMixi
177
249
 
178
250
  self._bulk_folders_moved(folders_moved, library)
179
251
 
180
- def move_and_modify_dirs(self, library, task):
252
+ def move_and_modify_dirs(self, library, task, link_cover_pks):
181
253
  """Move files and dirs and modify dirs."""
182
254
  changed = 0
183
255
  changed += self._bulk_folders_moved(task.dirs_moved, library)
@@ -186,6 +258,9 @@ class MovedMixin(CreateComicsMixin, CreateForeignKeysMixin, QueryForeignKeysMixi
186
258
  changed += self._bulk_comics_moved(task.files_moved, library)
187
259
  task.files_moved = None
188
260
 
261
+ changed += self._bulk_covers_moved(task.covers_moved, library, link_cover_pks)
262
+ task.covers_moved = None
263
+
189
264
  changed += self.bulk_folders_modified(task.dirs_modified, library)
190
265
  task.dirs_modified = None
191
266