codex 1.6.0a4__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 (554) 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 -3
  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 +65 -1
  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-CN6Sn_k7.9bf27129091c.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-CN6Sn_k7.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-CN1RPZPQ.b763c6a7abaf.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-CN1RPZPQ.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-DC3KGsMT.71e2a460e44b.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-DC3KGsMT.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-DJQYXCn2.f0d471a1b80a.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-DJQYXCn2.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-CgodTThy.b20290d57146.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-CgodTThy.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-zPp4DXWq.635b7f5aa882.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-zPp4DXWq.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-pK9Xompk.67014b86fc74.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-pK9Xompk.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-CMi6sDtP.8445f91d9023.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-CMi6sDtP.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-DNsRQx9l.c42f636c386e.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-DNsRQx9l.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-BHTkpC6Z.2d5d5dd57ad3.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-BHTkpC6Z.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-DmXGDg4z.284812bd376d.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-DmXGDg4z.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-DKaH5S3z.5c6042016200.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-DKaH5S3z.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-DUVATQW3.e1e7aa6d0b18.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-DUVATQW3.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-D3CyU-Il.fbf9ce5ef593.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-D3CyU-Il.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-0ef6-Ajc.e2879bcd242f.js → http-error-HFeYnF0n.a3c578a0265e.js} +1 -1
  209. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.br +0 -0
  210. codex/static_root/assets/http-error-HFeYnF0n.a3c578a0265e.js.gz +0 -0
  211. codex/static_root/assets/{http-error-0ef6-Ajc.js → http-error-HFeYnF0n.js} +1 -1
  212. codex/static_root/assets/http-error-HFeYnF0n.js.br +0 -0
  213. codex/static_root/assets/http-error-HFeYnF0n.js.gz +0 -0
  214. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css +1 -0
  215. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.br +0 -0
  216. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.gz +0 -0
  217. codex/static_root/assets/library-tab-BN0SfPcH.css +1 -0
  218. codex/static_root/assets/library-tab-BN0SfPcH.css.br +0 -0
  219. codex/static_root/assets/library-tab-BN0SfPcH.css.gz +0 -0
  220. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js +1 -0
  221. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.br +0 -0
  222. codex/static_root/assets/library-tab-DYeEBYlT.a2a2ef088d1c.js.gz +0 -0
  223. codex/static_root/assets/library-tab-DYeEBYlT.js +1 -0
  224. codex/static_root/assets/library-tab-DYeEBYlT.js.br +0 -0
  225. codex/static_root/assets/library-tab-DYeEBYlT.js.gz +0 -0
  226. codex/static_root/assets/{main-CuzJev9T.js → main-gSW7TFDY.329223b148d4.js} +6 -6
  227. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.br +0 -0
  228. codex/static_root/assets/main-gSW7TFDY.329223b148d4.js.gz +0 -0
  229. codex/static_root/assets/{main-CuzJev9T.f58771cc37e2.js → main-gSW7TFDY.js} +6 -6
  230. codex/static_root/assets/main-gSW7TFDY.js.br +0 -0
  231. codex/static_root/assets/main-gSW7TFDY.js.gz +0 -0
  232. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.7695fafdefce.css → pagination-toolbar-ClZui9Zf.697d7ae11c21.css} +1 -1
  233. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.br +0 -0
  234. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.gz +0 -0
  235. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.css → pagination-toolbar-ClZui9Zf.css} +1 -1
  236. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.br +0 -0
  237. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.gz +0 -0
  238. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js +1 -0
  239. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.br +0 -0
  240. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.8f23f8bd9a05.js.gz +0 -0
  241. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js +1 -0
  242. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.br +0 -0
  243. codex/static_root/assets/pagination-toolbar-Dlm70Bhs.js.gz +0 -0
  244. codex/static_root/assets/{pdf-doc-BlU5mm5n.0431bcdf48d1.js → pdf-doc-BTdS9hUz.7c966524280a.js} +1 -1
  245. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.br +0 -0
  246. codex/static_root/assets/pdf-doc-BTdS9hUz.7c966524280a.js.gz +0 -0
  247. codex/static_root/assets/{pdf-doc-BlU5mm5n.js → pdf-doc-BTdS9hUz.js} +1 -1
  248. codex/static_root/assets/pdf-doc-BTdS9hUz.js.br +0 -0
  249. codex/static_root/assets/pdf-doc-BTdS9hUz.js.gz +0 -0
  250. codex/static_root/assets/{reader-s7jttB_F.f7b43b04961a.js → reader-BtSTIETq.f940f66c0e58.js} +2 -2
  251. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.br +0 -0
  252. codex/static_root/assets/reader-BtSTIETq.f940f66c0e58.js.gz +0 -0
  253. codex/static_root/assets/{reader-s7jttB_F.js → reader-BtSTIETq.js} +2 -2
  254. codex/static_root/assets/reader-BtSTIETq.js.br +0 -0
  255. codex/static_root/assets/reader-BtSTIETq.js.gz +0 -0
  256. codex/static_root/assets/{reader-BiKxuxj7.css → reader-CF3xt6JG.19fad7f62450.css} +1 -1
  257. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.br +0 -0
  258. codex/static_root/assets/reader-CF3xt6JG.19fad7f62450.css.gz +0 -0
  259. codex/static_root/assets/{reader-BiKxuxj7.dabe592f46f1.css → reader-CF3xt6JG.css} +1 -1
  260. codex/static_root/assets/reader-CF3xt6JG.css.br +0 -0
  261. codex/static_root/assets/reader-CF3xt6JG.css.gz +0 -0
  262. codex/static_root/assets/{relation-chips-D6T6FX7G.css → relation-chips-Bsc-gUxE.98e65384c3c9.css} +1 -1
  263. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.br +0 -0
  264. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.gz +0 -0
  265. codex/static_root/assets/{relation-chips-D6T6FX7G.db095b2497e4.css → relation-chips-Bsc-gUxE.css} +1 -1
  266. codex/static_root/assets/relation-chips-Bsc-gUxE.css.br +0 -0
  267. codex/static_root/assets/relation-chips-Bsc-gUxE.css.gz +0 -0
  268. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js +1 -0
  269. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.br +0 -0
  270. codex/static_root/assets/relation-chips-C4Ob7ySi.95855a010f4a.js.gz +0 -0
  271. codex/static_root/assets/relation-chips-C4Ob7ySi.js +1 -0
  272. codex/static_root/assets/relation-chips-C4Ob7ySi.js.br +0 -0
  273. codex/static_root/assets/relation-chips-C4Ob7ySi.js.gz +0 -0
  274. codex/static_root/assets/{settings-drawer-DbJuixzh.352aaaab5925.js → settings-drawer-xSctoxZn.4ce8afcb879a.js} +2 -2
  275. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.br +0 -0
  276. codex/static_root/assets/settings-drawer-xSctoxZn.4ce8afcb879a.js.gz +0 -0
  277. codex/static_root/assets/{settings-drawer-DbJuixzh.js → settings-drawer-xSctoxZn.js} +2 -2
  278. codex/static_root/assets/settings-drawer-xSctoxZn.js.br +0 -0
  279. codex/static_root/assets/settings-drawer-xSctoxZn.js.gz +0 -0
  280. codex/static_root/assets/{stats-tab-BhRLh7XB.7be95b5021c3.js → stats-tab-C_uC_U7o.e6b014b6e8c6.js} +1 -1
  281. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.br +0 -0
  282. codex/static_root/assets/stats-tab-C_uC_U7o.e6b014b6e8c6.js.gz +0 -0
  283. codex/static_root/assets/{stats-tab-BhRLh7XB.js → stats-tab-C_uC_U7o.js} +1 -1
  284. codex/static_root/assets/stats-tab-C_uC_U7o.js.br +0 -0
  285. codex/static_root/assets/stats-tab-C_uC_U7o.js.gz +0 -0
  286. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js +1 -0
  287. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.br +0 -0
  288. codex/static_root/assets/task-tab-VIpy1Ula.c07877250c88.js.gz +0 -0
  289. codex/static_root/assets/task-tab-VIpy1Ula.js +1 -0
  290. codex/static_root/assets/task-tab-VIpy1Ula.js.br +0 -0
  291. codex/static_root/assets/task-tab-VIpy1Ula.js.gz +0 -0
  292. codex/static_root/assets/{unauthorized-BgXQnNpn.f63b36d526b5.js → unauthorized-kEuFL5br.475729603aeb.js} +1 -1
  293. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.br +0 -0
  294. codex/static_root/assets/unauthorized-kEuFL5br.475729603aeb.js.gz +0 -0
  295. codex/static_root/assets/{unauthorized-BgXQnNpn.js → unauthorized-kEuFL5br.js} +1 -1
  296. codex/static_root/assets/unauthorized-kEuFL5br.js.br +0 -0
  297. codex/static_root/assets/unauthorized-kEuFL5br.js.gz +0 -0
  298. codex/static_root/assets/{user-tab-cZoJDlb-.2a497e8b2b24.js → user-tab-CZNFTTEI.f3940a75daae.js} +1 -1
  299. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.br +0 -0
  300. codex/static_root/assets/user-tab-CZNFTTEI.f3940a75daae.js.gz +0 -0
  301. codex/static_root/assets/{user-tab-cZoJDlb-.js → user-tab-CZNFTTEI.js} +1 -1
  302. codex/static_root/assets/user-tab-CZNFTTEI.js.br +0 -0
  303. codex/static_root/assets/user-tab-CZNFTTEI.js.gz +0 -0
  304. codex/static_root/js/choices-admin.ef1ff3a8b9da.json +1 -0
  305. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
  306. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
  307. codex/static_root/js/choices-admin.json +1 -1
  308. codex/static_root/js/choices-admin.json.br +0 -0
  309. codex/static_root/js/choices-admin.json.gz +0 -0
  310. codex/static_root/js/{choices.b60817ba0a93.json → choices.23fedac2b9ba.json} +1 -1
  311. codex/static_root/js/choices.23fedac2b9ba.json.br +0 -0
  312. codex/static_root/js/choices.23fedac2b9ba.json.gz +0 -0
  313. codex/static_root/js/choices.json +1 -1
  314. codex/static_root/js/choices.json.br +0 -0
  315. codex/static_root/js/choices.json.gz +0 -0
  316. codex/static_root/manifest.6ef112683dae.json +642 -0
  317. codex/static_root/manifest.6ef112683dae.json.br +0 -0
  318. codex/static_root/manifest.6ef112683dae.json.gz +0 -0
  319. codex/static_root/manifest.json +254 -254
  320. codex/static_root/manifest.json.br +0 -0
  321. codex/static_root/manifest.json.gz +0 -0
  322. codex/static_root/staticfiles.json +1 -1
  323. codex/status_controller.py +11 -2
  324. codex/views/admin/library.py +47 -27
  325. codex/views/admin/stats.py +4 -1
  326. codex/views/admin/tasks.py +6 -1
  327. codex/views/browser/base.py +2 -2
  328. codex/views/browser/browser.py +1 -2
  329. codex/views/browser/browser_annotations.py +87 -17
  330. codex/views/browser/browser_breadcrumbs.py +2 -2
  331. codex/views/browser/browser_order_by.py +31 -20
  332. codex/views/browser/filters/search.py +1 -1
  333. codex/views/browser/metadata.py +0 -1
  334. codex/views/cover.py +3 -2
  335. codex/views/session.py +1 -0
  336. {codex-1.6.0a4.dist-info → codex-1.6.0a5.dist-info}/METADATA +3 -2
  337. {codex-1.6.0a4.dist-info → codex-1.6.0a5.dist-info}/RECORD +340 -339
  338. codex/static_root/assets/VCheckbox-CN6Sn_k7.9bf27129091c.js.br +0 -0
  339. codex/static_root/assets/VCheckbox-CN6Sn_k7.9bf27129091c.js.gz +0 -0
  340. codex/static_root/assets/VCheckbox-CN6Sn_k7.js.br +0 -0
  341. codex/static_root/assets/VCheckbox-CN6Sn_k7.js.gz +0 -0
  342. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.b763c6a7abaf.js.br +0 -0
  343. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.b763c6a7abaf.js.gz +0 -0
  344. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.js.br +0 -0
  345. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.js.gz +0 -0
  346. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js +0 -1
  347. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js.br +0 -0
  348. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js.gz +0 -0
  349. codex/static_root/assets/VCombobox-CtLUGYtD.js +0 -1
  350. codex/static_root/assets/VCombobox-CtLUGYtD.js.br +0 -0
  351. codex/static_root/assets/VCombobox-CtLUGYtD.js.gz +0 -0
  352. codex/static_root/assets/VDialog-DC3KGsMT.71e2a460e44b.js.br +0 -0
  353. codex/static_root/assets/VDialog-DC3KGsMT.71e2a460e44b.js.gz +0 -0
  354. codex/static_root/assets/VDialog-DC3KGsMT.js.br +0 -0
  355. codex/static_root/assets/VDialog-DC3KGsMT.js.gz +0 -0
  356. codex/static_root/assets/VExpansionPanels-DJQYXCn2.f0d471a1b80a.js.br +0 -0
  357. codex/static_root/assets/VExpansionPanels-DJQYXCn2.f0d471a1b80a.js.gz +0 -0
  358. codex/static_root/assets/VExpansionPanels-DJQYXCn2.js.br +0 -0
  359. codex/static_root/assets/VExpansionPanels-DJQYXCn2.js.gz +0 -0
  360. codex/static_root/assets/VRadioGroup-CgodTThy.b20290d57146.js.br +0 -0
  361. codex/static_root/assets/VRadioGroup-CgodTThy.b20290d57146.js.gz +0 -0
  362. codex/static_root/assets/VRadioGroup-CgodTThy.js.br +0 -0
  363. codex/static_root/assets/VRadioGroup-CgodTThy.js.gz +0 -0
  364. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +0 -1
  365. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
  366. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
  367. codex/static_root/assets/VSelect-ARDhJiQK.css +0 -1
  368. codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
  369. codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
  370. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js +0 -1
  371. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js.br +0 -0
  372. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js.gz +0 -0
  373. codex/static_root/assets/VSelect-B6s-x36h.js +0 -1
  374. codex/static_root/assets/VSelect-B6s-x36h.js.br +0 -0
  375. codex/static_root/assets/VSelect-B6s-x36h.js.gz +0 -0
  376. codex/static_root/assets/VSelectionControl-zPp4DXWq.635b7f5aa882.js.br +0 -0
  377. codex/static_root/assets/VSelectionControl-zPp4DXWq.635b7f5aa882.js.gz +0 -0
  378. codex/static_root/assets/VSelectionControl-zPp4DXWq.js.br +0 -0
  379. codex/static_root/assets/VSelectionControl-zPp4DXWq.js.gz +0 -0
  380. codex/static_root/assets/VSlideGroup-pK9Xompk.67014b86fc74.js.br +0 -0
  381. codex/static_root/assets/VSlideGroup-pK9Xompk.67014b86fc74.js.gz +0 -0
  382. codex/static_root/assets/VSlideGroup-pK9Xompk.js.br +0 -0
  383. codex/static_root/assets/VSlideGroup-pK9Xompk.js.gz +0 -0
  384. codex/static_root/assets/VTable-CMi6sDtP.8445f91d9023.js.br +0 -0
  385. codex/static_root/assets/VTable-CMi6sDtP.8445f91d9023.js.gz +0 -0
  386. codex/static_root/assets/VTable-CMi6sDtP.js.br +0 -0
  387. codex/static_root/assets/VTable-CMi6sDtP.js.gz +0 -0
  388. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css +0 -1
  389. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.br +0 -0
  390. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.gz +0 -0
  391. codex/static_root/assets/VTextField-DuIIp-VY.css +0 -1
  392. codex/static_root/assets/VTextField-DuIIp-VY.css.br +0 -0
  393. codex/static_root/assets/VTextField-DuIIp-VY.css.gz +0 -0
  394. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js +0 -1
  395. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js.br +0 -0
  396. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js.gz +0 -0
  397. codex/static_root/assets/VTextField-dMTizQEN.js +0 -1
  398. codex/static_root/assets/VTextField-dMTizQEN.js.br +0 -0
  399. codex/static_root/assets/VTextField-dMTizQEN.js.gz +0 -0
  400. codex/static_root/assets/VWindowItem-DNsRQx9l.c42f636c386e.js.br +0 -0
  401. codex/static_root/assets/VWindowItem-DNsRQx9l.c42f636c386e.js.gz +0 -0
  402. codex/static_root/assets/VWindowItem-DNsRQx9l.js.br +0 -0
  403. codex/static_root/assets/VWindowItem-DNsRQx9l.js.gz +0 -0
  404. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.br +0 -0
  405. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.gz +0 -0
  406. codex/static_root/assets/admin-BbrOU9Y9.css.br +0 -0
  407. codex/static_root/assets/admin-BbrOU9Y9.css.gz +0 -0
  408. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js +0 -1
  409. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js.br +0 -0
  410. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js.gz +0 -0
  411. codex/static_root/assets/admin-CApphpty.js +0 -1
  412. codex/static_root/assets/admin-CApphpty.js.br +0 -0
  413. codex/static_root/assets/admin-CApphpty.js.gz +0 -0
  414. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css +0 -1
  415. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.br +0 -0
  416. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.gz +0 -0
  417. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css +0 -1
  418. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.br +0 -0
  419. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.gz +0 -0
  420. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js +0 -30
  421. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js.br +0 -0
  422. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js.gz +0 -0
  423. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js +0 -30
  424. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js.br +0 -0
  425. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js.gz +0 -0
  426. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js +0 -1
  427. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js.br +0 -0
  428. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js.gz +0 -0
  429. codex/static_root/assets/browser-C_PHQbyN.js +0 -1
  430. codex/static_root/assets/browser-C_PHQbyN.js.br +0 -0
  431. codex/static_root/assets/browser-C_PHQbyN.js.gz +0 -0
  432. codex/static_root/assets/browser-DVI4LUNv.css +0 -1
  433. codex/static_root/assets/browser-DVI4LUNv.css.br +0 -0
  434. codex/static_root/assets/browser-DVI4LUNv.css.gz +0 -0
  435. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css +0 -1
  436. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css.br +0 -0
  437. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css.gz +0 -0
  438. codex/static_root/assets/change-password-dialog-BHTkpC6Z.2d5d5dd57ad3.js.br +0 -0
  439. codex/static_root/assets/change-password-dialog-BHTkpC6Z.2d5d5dd57ad3.js.gz +0 -0
  440. codex/static_root/assets/change-password-dialog-BHTkpC6Z.js.br +0 -0
  441. codex/static_root/assets/change-password-dialog-BHTkpC6Z.js.gz +0 -0
  442. codex/static_root/assets/confirm-dialog-DmXGDg4z.284812bd376d.js.br +0 -0
  443. codex/static_root/assets/confirm-dialog-DmXGDg4z.284812bd376d.js.gz +0 -0
  444. codex/static_root/assets/confirm-dialog-DmXGDg4z.js.br +0 -0
  445. codex/static_root/assets/confirm-dialog-DmXGDg4z.js.gz +0 -0
  446. codex/static_root/assets/datetime-column-DKaH5S3z.5c6042016200.js.br +0 -0
  447. codex/static_root/assets/datetime-column-DKaH5S3z.5c6042016200.js.gz +0 -0
  448. codex/static_root/assets/datetime-column-DKaH5S3z.js.br +0 -0
  449. codex/static_root/assets/datetime-column-DKaH5S3z.js.gz +0 -0
  450. codex/static_root/assets/filter-DUVATQW3.e1e7aa6d0b18.js.br +0 -0
  451. codex/static_root/assets/filter-DUVATQW3.e1e7aa6d0b18.js.gz +0 -0
  452. codex/static_root/assets/filter-DUVATQW3.js.br +0 -0
  453. codex/static_root/assets/filter-DUVATQW3.js.gz +0 -0
  454. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css +0 -1
  455. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.br +0 -0
  456. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.gz +0 -0
  457. codex/static_root/assets/flag-tab-BacZLlVi.css +0 -1
  458. codex/static_root/assets/flag-tab-BacZLlVi.css.br +0 -0
  459. codex/static_root/assets/flag-tab-BacZLlVi.css.gz +0 -0
  460. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js +0 -1
  461. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js.br +0 -0
  462. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js.gz +0 -0
  463. codex/static_root/assets/flag-tab-CSKMJEn-.js +0 -1
  464. codex/static_root/assets/flag-tab-CSKMJEn-.js.br +0 -0
  465. codex/static_root/assets/flag-tab-CSKMJEn-.js.gz +0 -0
  466. codex/static_root/assets/group-tab-D3CyU-Il.fbf9ce5ef593.js.br +0 -0
  467. codex/static_root/assets/group-tab-D3CyU-Il.fbf9ce5ef593.js.gz +0 -0
  468. codex/static_root/assets/group-tab-D3CyU-Il.js.br +0 -0
  469. codex/static_root/assets/group-tab-D3CyU-Il.js.gz +0 -0
  470. codex/static_root/assets/http-error-0ef6-Ajc.e2879bcd242f.js.br +0 -0
  471. codex/static_root/assets/http-error-0ef6-Ajc.e2879bcd242f.js.gz +0 -0
  472. codex/static_root/assets/http-error-0ef6-Ajc.js.br +0 -0
  473. codex/static_root/assets/http-error-0ef6-Ajc.js.gz +0 -0
  474. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css +0 -1
  475. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.br +0 -2
  476. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.gz +0 -0
  477. codex/static_root/assets/library-tab-CXbkxNZt.css +0 -1
  478. codex/static_root/assets/library-tab-CXbkxNZt.css.br +0 -2
  479. codex/static_root/assets/library-tab-CXbkxNZt.css.gz +0 -0
  480. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js +0 -1
  481. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js.br +0 -0
  482. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js.gz +0 -0
  483. codex/static_root/assets/library-tab-IdDfLn4k.js +0 -1
  484. codex/static_root/assets/library-tab-IdDfLn4k.js.br +0 -0
  485. codex/static_root/assets/library-tab-IdDfLn4k.js.gz +0 -0
  486. codex/static_root/assets/main-CuzJev9T.f58771cc37e2.js.br +0 -0
  487. codex/static_root/assets/main-CuzJev9T.f58771cc37e2.js.gz +0 -0
  488. codex/static_root/assets/main-CuzJev9T.js.br +0 -0
  489. codex/static_root/assets/main-CuzJev9T.js.gz +0 -0
  490. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.br +0 -0
  491. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.gz +0 -0
  492. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.br +0 -0
  493. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.gz +0 -0
  494. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js +0 -1
  495. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js.br +0 -0
  496. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js.gz +0 -0
  497. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js +0 -1
  498. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js.br +0 -0
  499. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js.gz +0 -0
  500. codex/static_root/assets/pdf-doc-BlU5mm5n.0431bcdf48d1.js.br +0 -0
  501. codex/static_root/assets/pdf-doc-BlU5mm5n.0431bcdf48d1.js.gz +0 -0
  502. codex/static_root/assets/pdf-doc-BlU5mm5n.js.br +0 -0
  503. codex/static_root/assets/pdf-doc-BlU5mm5n.js.gz +0 -0
  504. codex/static_root/assets/reader-BiKxuxj7.css.br +0 -0
  505. codex/static_root/assets/reader-BiKxuxj7.css.gz +0 -0
  506. codex/static_root/assets/reader-BiKxuxj7.dabe592f46f1.css.br +0 -0
  507. codex/static_root/assets/reader-BiKxuxj7.dabe592f46f1.css.gz +0 -0
  508. codex/static_root/assets/reader-s7jttB_F.f7b43b04961a.js.br +0 -0
  509. codex/static_root/assets/reader-s7jttB_F.f7b43b04961a.js.gz +0 -0
  510. codex/static_root/assets/reader-s7jttB_F.js.br +0 -0
  511. codex/static_root/assets/reader-s7jttB_F.js.gz +0 -0
  512. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js +0 -1
  513. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js.br +0 -0
  514. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js.gz +0 -0
  515. codex/static_root/assets/relation-chips-BiVsYCd9.js +0 -1
  516. codex/static_root/assets/relation-chips-BiVsYCd9.js.br +0 -0
  517. codex/static_root/assets/relation-chips-BiVsYCd9.js.gz +0 -0
  518. codex/static_root/assets/relation-chips-D6T6FX7G.css.br +0 -0
  519. codex/static_root/assets/relation-chips-D6T6FX7G.css.gz +0 -0
  520. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.br +0 -0
  521. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.gz +0 -0
  522. codex/static_root/assets/settings-drawer-DbJuixzh.352aaaab5925.js.br +0 -0
  523. codex/static_root/assets/settings-drawer-DbJuixzh.352aaaab5925.js.gz +0 -0
  524. codex/static_root/assets/settings-drawer-DbJuixzh.js.br +0 -0
  525. codex/static_root/assets/settings-drawer-DbJuixzh.js.gz +0 -0
  526. codex/static_root/assets/stats-tab-BhRLh7XB.7be95b5021c3.js.br +0 -0
  527. codex/static_root/assets/stats-tab-BhRLh7XB.7be95b5021c3.js.gz +0 -0
  528. codex/static_root/assets/stats-tab-BhRLh7XB.js.br +0 -0
  529. codex/static_root/assets/stats-tab-BhRLh7XB.js.gz +0 -0
  530. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js +0 -1
  531. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js.br +0 -0
  532. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js.gz +0 -0
  533. codex/static_root/assets/task-tab-DLrd54z2.js +0 -1
  534. codex/static_root/assets/task-tab-DLrd54z2.js.br +0 -0
  535. codex/static_root/assets/task-tab-DLrd54z2.js.gz +0 -0
  536. codex/static_root/assets/unauthorized-BgXQnNpn.f63b36d526b5.js.br +0 -0
  537. codex/static_root/assets/unauthorized-BgXQnNpn.f63b36d526b5.js.gz +0 -0
  538. codex/static_root/assets/unauthorized-BgXQnNpn.js.br +0 -0
  539. codex/static_root/assets/unauthorized-BgXQnNpn.js.gz +0 -0
  540. codex/static_root/assets/user-tab-cZoJDlb-.2a497e8b2b24.js.br +0 -0
  541. codex/static_root/assets/user-tab-cZoJDlb-.2a497e8b2b24.js.gz +0 -0
  542. codex/static_root/assets/user-tab-cZoJDlb-.js.br +0 -0
  543. codex/static_root/assets/user-tab-cZoJDlb-.js.gz +0 -0
  544. codex/static_root/js/choices-admin.248384ece3f6.json +0 -1
  545. codex/static_root/js/choices-admin.248384ece3f6.json.br +0 -0
  546. codex/static_root/js/choices-admin.248384ece3f6.json.gz +0 -0
  547. codex/static_root/js/choices.b60817ba0a93.json.br +0 -0
  548. codex/static_root/js/choices.b60817ba0a93.json.gz +0 -0
  549. codex/static_root/manifest.3c2f342d5f5a.json +0 -642
  550. codex/static_root/manifest.3c2f342d5f5a.json.br +0 -0
  551. codex/static_root/manifest.3c2f342d5f5a.json.gz +0 -0
  552. {codex-1.6.0a4.dist-info → codex-1.6.0a5.dist-info}/LICENSE +0 -0
  553. {codex-1.6.0a4.dist-info → codex-1.6.0a5.dist-info}/WHEEL +0 -0
  554. {codex-1.6.0a4.dist-info → codex-1.6.0a5.dist-info}/entry_points.txt +0 -0
codex/exceptions.py CHANGED
@@ -1,6 +1,8 @@
1
1
  """Special Redirect Error."""
2
2
 
3
3
  from collections.abc import Mapping
4
+ from copy import deepcopy
5
+ from pprint import pformat
4
6
 
5
7
  from django.shortcuts import redirect
6
8
  from django.urls import reverse
@@ -10,6 +12,7 @@ from rest_framework.status import HTTP_303_SEE_OTHER
10
12
 
11
13
  from codex.logger.logging import get_logger
12
14
  from codex.serializers.choices import DEFAULTS
15
+ from codex.serializers.route import RouteSerializer
13
16
 
14
17
  LOG = get_logger(__name__)
15
18
 
@@ -23,10 +26,20 @@ class SeeOtherRedirectError(APIException):
23
26
 
24
27
  def __init__(self, detail: Mapping[str, str | dict]):
25
28
  """Create a response to pass to the exception handler."""
29
+ detail = dict(detail)
30
+ route: dict = detail.get("route", {}) # type: ignore
31
+ params: dict = route.get("params", DEFAULTS["breadcrumbs"][0])
32
+ # Save route in server format for redirect reverse
33
+ self.route_kwargs = params
34
+ # Serialize route for detail
35
+ serializer = RouteSerializer(params)
36
+ route = deepcopy(route)
37
+ route["params"] = serializer.data
38
+ detail["route"] = route
39
+ # Pop uneccissary breadcrumbs.
40
+ detail.get("settings", {}).pop("breadcrumbs", None) # type: ignore
26
41
  self.detail: Mapping[str, str | Mapping] = detail # type: ignore
27
- LOG.debug(
28
- f"redirect {self.detail.get('reason')} to {self.detail.get('route')} {self.detail.get('settings')}"
29
- )
42
+ LOG.debug(f"redirect {pformat(detail)}")
30
43
  # super().__init__ converts every type into strings! do not use it.
31
44
 
32
45
  def _add_query_params(self, url):
@@ -44,9 +57,7 @@ class SeeOtherRedirectError(APIException):
44
57
  def get_response(self, url_name):
45
58
  """Return a Django Redirect Response."""
46
59
  # only used in codex_exception_handler for opds stuff
47
- route: Mapping = self.detail.get("route", {}) # type: ignore
48
- params: dict = route.get("params", DEFAULTS["breadcrumbs"][0])
49
- url = reverse(url_name, kwargs=params)
60
+ url = reverse(url_name, kwargs=self.route_kwargs)
50
61
  url = self._add_query_params(url)
51
62
 
52
63
  return redirect(url, permanent=False)
codex/integrity.py CHANGED
@@ -12,12 +12,14 @@ from django.db import DEFAULT_DB_ALIAS, connection, connections
12
12
  from django.db.migrations.executor import MigrationExecutor
13
13
  from django.db.migrations.recorder import MigrationRecorder
14
14
  from django.db.models.functions import Now
15
+ from django.db.models.query import Q
15
16
  from django.db.utils import OperationalError
16
17
 
17
18
  from codex.logger.logging import get_logger
18
19
  from codex.settings.settings import (
19
20
  CODEX_PATH,
20
21
  CONFIG_PATH,
22
+ CUSTOM_COVERS_DIR,
21
23
  DB_PATH,
22
24
  SKIP_INTEGRITY_CHECK,
23
25
  )
@@ -34,6 +36,7 @@ MIGRATION_0011 = "0010_library_groups_and_metadata_changes"
34
36
  MIGRATION_0018 = "0018_rename_userbookmark_bookmark"
35
37
  MIGRATION_0025 = "0025_add_story_arc_number"
36
38
  MIGRATION_0026 = "0026_comicbox_1"
39
+ MIGRATION_0028 = "0028_custom_covers" # TODO merge with 27
37
40
  M2M_NAMES = MappingProxyType(
38
41
  {
39
42
  "Character": "characters",
@@ -51,7 +54,7 @@ SKIP_M2M_CHECKS = MappingProxyType(
51
54
  {"Contributor": MIGRATION_0026, "StoryArcNumber": MIGRATION_0025}
52
55
  )
53
56
  NULL_SET = frozenset({None})
54
- HAVE_LIBRARY_FKS = ("FailedImport", "Folder", "Comic")
57
+ HAVE_LIBRARY_FKS = ("FailedImport", "Folder", "Comic", "CustomCover")
55
58
  GROUP_HOSTS = MappingProxyType(
56
59
  {
57
60
  "Imprint": ("Publisher",),
@@ -60,7 +63,6 @@ GROUP_HOSTS = MappingProxyType(
60
63
  "Comic": ("Publisher", "Imprint", "Series", "Volume"),
61
64
  }
62
65
  )
63
-
64
66
  COMIC_FK_FIELDS = MappingProxyType(
65
67
  {
66
68
  "AgeRating": "age_rating",
@@ -71,7 +73,6 @@ COMIC_FK_FIELDS = MappingProxyType(
71
73
  "Tagger": "tagger",
72
74
  }
73
75
  )
74
-
75
76
  WATCHED_PATHS = ("Comic", "Folder")
76
77
  CONTRIBUTOR_FIELDS = MappingProxyType(
77
78
  {"ContributorRole": "role", "ContributorPerson": "person"}
@@ -92,6 +93,9 @@ DELETE_BAD_COMIC_FOLDER_RELATIONS_SQL = (
92
93
  'IN (SELECT "codex_folder"."id" FROM "codex_folder")))'
93
94
  )
94
95
  MIGRATION_DIR = CODEX_PATH / "migrations"
96
+ CUSTOM_COVER_MODEL_NAMES = MappingProxyType(
97
+ {"publisher": "p", "imprint": "i", "series": "s", "storyarc": "a", "folder": "f"}
98
+ )
95
99
 
96
100
  LOG = get_logger(__name__)
97
101
 
@@ -303,9 +307,68 @@ def _repair_library_groups(apps):
303
307
  bad_relations.delete()
304
308
 
305
309
 
310
+ def _delete_extra_custom_cover_libraries(apps):
311
+ if not has_applied_migration(MIGRATION_0028):
312
+ return
313
+ library_model = apps.get_model("codex", "library")
314
+ custom_cover_libraries = library_model.objects.filter(covers_only=True)
315
+ if custom_cover_libraries.count() <= 1:
316
+ return
317
+
318
+ # Attempt to remove the bad ones, probably futile
319
+ bad_lib_pks = []
320
+ for library in custom_cover_libraries:
321
+ if library.path != CUSTOM_COVERS_DIR:
322
+ bad_lib_pks.append(library.pk)
323
+ delete_libs = library_model.object.filter(pk__in=bad_lib_pks)
324
+ count = delete_libs.count()
325
+ if count:
326
+ delete_libs.delete()
327
+ LOG.warn(
328
+ f"Removed {count} duplicate custom cover libraries pointing to unused custom cover dirs."
329
+ )
330
+
331
+ custom_cover_libraries = library_model.object.filter(covers_only=True)
332
+ count = custom_cover_libraries.count()
333
+ if count <= 1:
334
+ return
335
+ custom_cover_libraries.delete()
336
+ LOG.warn(
337
+ f"Removed all ({count}) custom cover libraries, Unable to determine valid one. Will recreate upon startup."
338
+ )
339
+
340
+
341
+ def _repair_groups_with_custom_covers(apps):
342
+ if not has_applied_migration(MIGRATION_0028):
343
+ return
344
+ custom_cover_model = apps.get_model("codex", "customcover")
345
+ now = Now()
346
+ for model_name, group_letter in CUSTOM_COVER_MODEL_NAMES.items():
347
+ valid_covers = custom_cover_model.objects.filter(group=group_letter)
348
+ group_model = apps.get_model("codex", model_name)
349
+ objs = group_model.objects.exclude(
350
+ Q(custom_cover__isnull=True) | Q(custom_cover__in=valid_covers)
351
+ )
352
+ update_groups = []
353
+ for group in objs:
354
+ group.custom_cover = None
355
+ group.updated_at = now
356
+ update_groups.append(group)
357
+ group_model.objects.bulk_update(update_groups, ["custom_cover"])
358
+ count = len(update_groups)
359
+ if count:
360
+ LOG.warn(f"Removed illegal custom cover links from {count} {model_name}s")
361
+
362
+
306
363
  def _delete_errors():
307
364
  """DELETE things we can't fix."""
365
+ _delete_extra_custom_cover_libraries(apps)
366
+
308
367
  for host_model_name in HAVE_LIBRARY_FKS:
368
+ if host_model_name == "CustomCover" and not has_applied_migration(
369
+ MIGRATION_0028
370
+ ):
371
+ continue
309
372
  _delete_fk_integrity_errors(apps, host_model_name, "Library", "library")
310
373
 
311
374
  for host_model_name in WATCHED_PATHS:
@@ -378,6 +441,7 @@ def _repair_integrity():
378
441
  _mark_comics_with_bad_rels_for_update(comic_model, bad_comic_ids)
379
442
 
380
443
  _repair_library_groups(apps)
444
+ _repair_groups_with_custom_covers(apps)
381
445
 
382
446
 
383
447
  def repair_db():
@@ -11,7 +11,7 @@ from codex.librarian.covers.tasks import (
11
11
  )
12
12
 
13
13
 
14
- class CoverContributorThread(CoverCreateMixin, CoverPurgeMixin):
14
+ class CoverThread(CoverCreateMixin, CoverPurgeMixin):
15
15
  """Create comic covers in it's own thread."""
16
16
 
17
17
  def process_item(self, item):
@@ -22,7 +22,7 @@ class CoverContributorThread(CoverCreateMixin, CoverPurgeMixin):
22
22
  elif isinstance(task, CoverRemoveAllTask):
23
23
  self.purge_all_comic_covers(self.librarian_queue)
24
24
  elif isinstance(task, CoverRemoveTask):
25
- self.purge_comic_covers(task.comic_pks)
25
+ self.purge_comic_covers(task.pks, task.custom)
26
26
  elif isinstance(task, CoverRemoveOrphansTask):
27
27
  self.cleanup_orphan_covers()
28
28
  elif isinstance(task, CoverCreateAllTask):
@@ -1,6 +1,7 @@
1
1
  """Create comic cover paths."""
2
2
 
3
3
  from io import BytesIO
4
+ from pathlib import Path
4
5
  from time import time
5
6
 
6
7
  from comicbox.box import Comicbox
@@ -10,7 +11,7 @@ from PIL import Image
10
11
  from codex.librarian.covers.path import CoverPathMixin
11
12
  from codex.librarian.covers.status import CoverStatusTypes
12
13
  from codex.librarian.covers.tasks import CoverSaveToCache
13
- from codex.models import Comic
14
+ from codex.models import Comic, CustomCover
14
15
  from codex.status import Status
15
16
 
16
17
 
@@ -51,20 +52,30 @@ class CoverCreateMixin(CoverPathMixin):
51
52
  return image_data
52
53
 
53
54
  @classmethod
54
- def create_cover_from_path(cls, pk, cover_path, log, librarian_queue):
55
+ def _get_custom_cover_image(cls, cover_path):
56
+ """Get cover image from image file."""
57
+ with Path(cover_path).open("rb") as f:
58
+ return f.read()
59
+
60
+ @classmethod
61
+ def create_cover_from_path(cls, pk, cover_path, log, librarian_queue, custom=False): # noqa: PLR0913
55
62
  """Create cover for path.
56
63
 
57
64
  Called from views/cover.
58
65
  """
59
- comic_path = None
66
+ db_path = None
60
67
  try:
61
- comic_path = Comic.objects.only("path").get(pk=pk).path
62
- cover_image = cls._get_comic_cover_image(comic_path)
68
+ model = CustomCover if custom else Comic
69
+ db_path = model.objects.only("path").get(pk=pk).path
70
+ if custom:
71
+ cover_image = cls._get_custom_cover_image(db_path)
72
+ else:
73
+ cover_image = cls._get_comic_cover_image(db_path)
63
74
  data = cls._create_cover_thumbnail(cover_image)
64
75
  except Exception as exc:
65
76
  data = b""
66
- comic_str = comic_path if comic_path else f"{pk=}"
67
- log.warning(f"Could not create cover thumbnail for {comic_str}: {exc}")
77
+ cover_str = db_path if db_path else f"{pk=}"
78
+ log.warning(f"Could not create cover thumbnail for {cover_str}: {exc}")
68
79
 
69
80
  task = CoverSaveToCache(cover_path, data)
70
81
  librarian_queue.put(task)
@@ -80,9 +91,9 @@ class CoverCreateMixin(CoverPathMixin):
80
91
  # zero length file is code for missing.
81
92
  cover_path.touch()
82
93
 
83
- def _bulk_create_comic_covers(self, comic_pks):
94
+ def _bulk_create_comic_covers(self, pks, custom=False):
84
95
  """Create bulk comic covers."""
85
- num_comics = len(comic_pks)
96
+ num_comics = len(pks)
86
97
  if not num_comics:
87
98
  return None
88
99
  status = Status(CoverStatusTypes.CREATE_COVERS, 0, num_comics)
@@ -92,9 +103,9 @@ class CoverCreateMixin(CoverPathMixin):
92
103
  self.status_controller.start(status)
93
104
 
94
105
  # Get comic objects
95
- for pk in comic_pks:
106
+ for pk in pks:
96
107
  # Create all covers.
97
- cover_path = self.get_cover_path(pk)
108
+ cover_path = self.get_cover_path(pk, custom)
98
109
  if cover_path.exists():
99
110
  status.decrement_total()
100
111
  else:
@@ -106,12 +117,17 @@ class CoverCreateMixin(CoverPathMixin):
106
117
  self.status_controller.update(status)
107
118
 
108
119
  total_elapsed = naturaldelta(time() - start_time)
109
- self.log.info(f"Created {status.complete} comic covers in {total_elapsed}.")
120
+ desc = "custom" if custom else "comic"
121
+ self.log.info(
122
+ f"Created {status.complete} {desc} covers in {total_elapsed}."
123
+ )
110
124
  finally:
111
125
  self.status_controller.finish(status)
112
126
  return status.complete
113
127
 
114
128
  def create_all_covers(self):
115
129
  """Create all covers for all libraries."""
130
+ pks = CustomCover.objects.values_list("pk", flat=True)
131
+ self._bulk_create_comic_covers(pks, True)
116
132
  pks = Comic.objects.values_list("pk", flat=True)
117
- self._bulk_create_comic_covers(pks)
133
+ self._bulk_create_comic_covers(pks, False)
@@ -11,7 +11,8 @@ from codex.threads import QueuedThread
11
11
  class CoverPathMixin(QueuedThread):
12
12
  """Path methods for covers."""
13
13
 
14
- COVER_ROOT = ROOT_CACHE_PATH / "covers"
14
+ COVERS_ROOT = ROOT_CACHE_PATH / "covers"
15
+ CUSTOM_COVERS_ROOT = ROOT_CACHE_PATH / "custom-covers"
15
16
  _HEX_FILL = 8
16
17
  _PATH_STEP = 2
17
18
  _ZFILL = 12
@@ -27,16 +28,17 @@ class CoverPathMixin(QueuedThread):
27
28
  return Path("/".join(parts))
28
29
 
29
30
  @classmethod
30
- def get_cover_path(cls, pk):
31
+ def get_cover_path(cls, pk, custom=False):
31
32
  """Get cover path for comic pk."""
32
33
  cover_path = cls._hex_path(pk)
33
- return cls.COVER_ROOT / cover_path.with_suffix(".webp")
34
+ root = cls.CUSTOM_COVERS_ROOT if custom else cls.COVERS_ROOT
35
+ return root / cover_path.with_suffix(".webp")
34
36
 
35
37
  @classmethod
36
- def get_cover_paths(cls, comic_pks):
38
+ def get_cover_paths(cls, pks, custom=False):
37
39
  """Get cover paths for many comic pks."""
38
40
  cover_paths = set()
39
- for comic_pk in comic_pks:
40
- cover_path = cls.get_cover_path(comic_pk)
41
+ for pk in pks:
42
+ cover_path = cls.get_cover_path(pk, custom)
41
43
  cover_paths.add(cover_path)
42
44
  return cover_paths
@@ -8,6 +8,7 @@ from codex.librarian.covers.path import CoverPathMixin
8
8
  from codex.librarian.covers.status import CoverStatusTypes
9
9
  from codex.librarian.notifier.tasks import LIBRARY_CHANGED_TASK
10
10
  from codex.models import Comic, Timestamp
11
+ from codex.models.paths import CustomCover
11
12
  from codex.status import Status
12
13
 
13
14
 
@@ -20,17 +21,17 @@ class CoverPurgeMixin(CoverPathMixin):
20
21
  )
21
22
 
22
23
  @classmethod
23
- def _cleanup_cover_dirs(cls, path):
24
+ def _cleanup_cover_dirs(cls, path, cover_root):
24
25
  """Recursively remove empty cover directories."""
25
- if not path or cls.COVER_ROOT not in path.parents:
26
+ if not path or cover_root not in path.parents:
26
27
  return
27
28
  try:
28
29
  path.rmdir()
29
- cls._cleanup_cover_dirs(path.parent)
30
+ cls._cleanup_cover_dirs(path.parent, cover_root)
30
31
  except OSError:
31
32
  pass
32
33
 
33
- def purge_cover_paths(self, cover_paths):
34
+ def purge_cover_paths(self, cover_paths, cover_root):
34
35
  """Purge a set a cover paths."""
35
36
  self.log.debug(f"Removing {len(cover_paths)} possible cover thumbnails...")
36
37
  status = Status(CoverStatusTypes.PURGE_COVERS, 0, len(cover_paths))
@@ -46,38 +47,41 @@ class CoverPurgeMixin(CoverPathMixin):
46
47
  cover_dirs.add(cover_path.parent)
47
48
  self.status_controller.update(status)
48
49
  for cover_dir in cover_dirs:
49
- self._cleanup_cover_dirs(cover_dir)
50
+ self._cleanup_cover_dirs(cover_dir, cover_root)
50
51
  self.log.info(f"Removed {status.complete} cover thumbnails.")
51
52
  finally:
52
53
  self.status_controller.finish(status)
53
54
  return status.complete
54
55
 
55
- def purge_comic_covers(self, comic_pks):
56
+ def purge_comic_covers(self, pks: frozenset[int], custom: bool):
56
57
  """Purge a set a cover paths."""
57
- cover_paths = self.get_cover_paths(comic_pks)
58
- return self.purge_cover_paths(cover_paths)
58
+ cover_paths = self.get_cover_paths(pks, custom)
59
+ cover_root = self.CUSTOM_COVERS_ROOT if custom else self.COVERS_ROOT
60
+ return self.purge_cover_paths(cover_paths, cover_root)
59
61
 
60
62
  def purge_all_comic_covers(self, librarian_queue):
61
63
  """Purge every comic cover."""
62
64
  self.log.debug("Removing entire comic cover cache.")
63
65
  try:
64
- shutil.rmtree(self.COVER_ROOT)
66
+ shutil.rmtree(self.COVERS_ROOT)
65
67
  self.log.info("Removed entire comic cover cache.")
68
+ shutil.rmtree(self.CUSTOM_COVERS_ROOT)
69
+ self.log.info("Removed entire custom cover cache.")
66
70
  except Exception as exc:
67
71
  self.log.warning(exc)
68
72
  Timestamp.touch(Timestamp.TimestampChoices.COVERS)
69
73
  librarian_queue.put(LIBRARY_CHANGED_TASK)
70
74
 
71
- def cleanup_orphan_covers(self):
75
+ def _cleanup_orphan_covers(self, cover_class, cover_root, name):
72
76
  """Remove all orphan cover thumbs."""
73
77
  try:
74
- self.log.debug("Removing covers from missing comics.")
78
+ self.log.debug(f"Removing covers from missing {name}.")
75
79
  self.status_controller.start_many(self._CLEANUP_STATUS_MAP)
76
- comic_pks = Comic.objects.all().values_list("pk", flat=True)
77
- db_cover_paths = self.get_cover_paths(comic_pks)
80
+ pks = cover_class.objects.all().values_list("pk", flat=True)
81
+ db_cover_paths = self.get_cover_paths(pks)
78
82
 
79
83
  orphan_cover_paths = set()
80
- for root, _, filenames in os.walk(self.COVER_ROOT):
84
+ for root, _, filenames in os.walk(cover_root):
81
85
  root_path = Path(root)
82
86
  for fn in filenames:
83
87
  fs_cover_path = root_path / fn
@@ -86,6 +90,13 @@ class CoverPurgeMixin(CoverPathMixin):
86
90
  finally:
87
91
  self.status_controller.finish(CoverStatusTypes.FIND_ORPHAN)
88
92
 
89
- count = self.purge_cover_paths(orphan_cover_paths)
90
- self.log.info(f"Removed {count} covers for missing comics.")
93
+ count = self.purge_cover_paths(orphan_cover_paths, cover_root)
94
+ self.log.info(f"Removed {count} covers for missing {name}.")
91
95
  return count
96
+
97
+ def cleanup_orphan_covers(self):
98
+ """Cleanup both comic and custom covers."""
99
+ self._cleanup_orphan_covers(Comic, self.COVERS_ROOT, "comics")
100
+ self._cleanup_orphan_covers(
101
+ CustomCover, self.CUSTOM_COVERS_ROOT, "custom covers"
102
+ )
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
 
6
6
  @dataclass
7
7
  class CoverTask:
8
- """Handle with the CoverContributor."""
8
+ """Handle with the CoverThread."""
9
9
 
10
10
 
11
11
  @dataclass
@@ -29,7 +29,8 @@ class LibrariesTask:
29
29
  class CoverRemoveTask(CoverTask):
30
30
  """Purge a set of comic covers."""
31
31
 
32
- comic_pks: frozenset
32
+ pks: frozenset
33
+ custom: bool
33
34
 
34
35
 
35
36
  @dataclass
@@ -87,7 +87,7 @@ class AggregateMetadataMixin(CleanMetadataMixin):
87
87
  return m2m_md
88
88
 
89
89
  def _get_path_metadata(self, path, import_metadata):
90
- """Get the metatada from comicbox and munge it a little."""
90
+ """Get the metadata from comicbox and munge it a little."""
91
91
  md = {}
92
92
  fk_md = {}
93
93
  group_tree_md = {}
@@ -251,7 +251,7 @@ class AggregateMetadataMixin(CleanMetadataMixin):
251
251
  force_import_metadata,
252
252
  status=None,
253
253
  ):
254
- """Get aggregated metatada for the paths given."""
254
+ """Get aggregated metadata for the paths given."""
255
255
  total_paths = len(all_paths)
256
256
  if not total_paths:
257
257
  return 0
@@ -2,17 +2,22 @@
2
2
 
3
3
  from types import MappingProxyType
4
4
 
5
+ from bidict import bidict
6
+
5
7
  from codex.models import (
6
8
  Comic,
7
9
  Contributor,
10
+ Folder,
8
11
  Imprint,
9
12
  Publisher,
10
13
  Series,
14
+ StoryArc,
11
15
  StoryArcNumber,
12
16
  Volume,
13
17
  )
14
18
  from codex.models.groups import BrowserGroupModel
15
19
  from codex.models.named import Identifier
20
+ from codex.models.paths import CustomCover
16
21
 
17
22
  #################
18
23
  # DICT METADATA #
@@ -139,9 +144,15 @@ BULK_UPDATE_COMIC_FIELDS_WITH_VALUES = tuple(
139
144
  - frozenset(BULK_UPDATE_FOLDER_MODIFIED_FIELDS)
140
145
  )
141
146
  )
142
- MOVED_BULK_COMIC_UPDATE_FIELDS = ("path", "parent_folder")
143
- MOVED_BULK_FOLDER_UPDATE_FIELDS = ("path", "parent_folder", *GROUP_BASE_FIELDS)
144
-
147
+ MOVED_BULK_COMIC_UPDATE_FIELDS = ("path", "parent_folder", "stat", "updated_at")
148
+ MOVED_BULK_FOLDER_UPDATE_FIELDS = (
149
+ "path",
150
+ "parent_folder",
151
+ *GROUP_BASE_FIELDS,
152
+ "stat",
153
+ "updated_at",
154
+ )
155
+ CUSTOM_COVER_UPDATE_FIELDS = ("path", "stat", "updated_at", "sort_name", "group")
145
156
 
146
157
  #########
147
158
  # OTHER #
@@ -184,6 +195,15 @@ GROUP_COMPARE_FIELDS = MappingProxyType(
184
195
  Volume: ("publisher__name", "imprint__name", "series__name", "name"),
185
196
  }
186
197
  )
198
+ CLASS_CUSTOM_COVER_GROUP_MAP = bidict(
199
+ {
200
+ Publisher: CustomCover.GroupChoice.P.value,
201
+ Imprint: CustomCover.GroupChoice.I.value,
202
+ Series: CustomCover.GroupChoice.S.value,
203
+ StoryArc: CustomCover.GroupChoice.A.value,
204
+ Folder: CustomCover.GroupChoice.F.value,
205
+ }
206
+ )
187
207
 
188
208
  COMIC_FK_FIELD_NAMES = frozenset(
189
209
  field.name