codex 1.6.0a4__py3-none-any.whl → 1.6.0a6__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 (568) 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 +28 -18
  7. codex/librarian/covers/tasks.py +3 -2
  8. codex/librarian/importer/aggregate_metadata.py +2 -2
  9. codex/librarian/importer/const.py +31 -3
  10. codex/librarian/importer/covers.py +47 -0
  11. codex/librarian/importer/create_fks.py +88 -28
  12. codex/librarian/importer/deleted.py +71 -9
  13. codex/librarian/importer/importerd.py +133 -28
  14. codex/librarian/importer/link_comics.py +55 -0
  15. codex/librarian/importer/moved.py +78 -3
  16. codex/librarian/importer/query_fks.py +65 -1
  17. codex/librarian/importer/status.py +6 -0
  18. codex/librarian/importer/tasks.py +11 -3
  19. codex/librarian/importer/update_comics.py +1 -5
  20. codex/librarian/janitor/cleanup.py +24 -0
  21. codex/librarian/janitor/failed_imports.py +1 -1
  22. codex/librarian/janitor/janitor.py +5 -0
  23. codex/librarian/janitor/janitord.py +1 -1
  24. codex/librarian/janitor/status.py +1 -0
  25. codex/librarian/janitor/tasks.py +5 -0
  26. codex/librarian/librariand.py +3 -3
  27. codex/librarian/search/update.py +1 -1
  28. codex/librarian/watchdog/db_snapshot.py +9 -6
  29. codex/librarian/watchdog/emitter.py +3 -0
  30. codex/librarian/watchdog/event_batcherd.py +19 -3
  31. codex/librarian/watchdog/events.py +176 -36
  32. codex/librarian/watchdog/observers.py +13 -3
  33. codex/migrations/0027_sort_name.py +2 -12
  34. codex/migrations/0028_custom_covers.py +166 -0
  35. codex/migrations/0029_choices_adminflag_and_timestamp.py +44 -0
  36. codex/models/admin.py +0 -2
  37. codex/models/base.py +3 -0
  38. codex/models/comic.py +15 -9
  39. codex/models/groups.py +30 -14
  40. codex/models/library.py +11 -3
  41. codex/models/paths.py +62 -14
  42. codex/models/{const.py → util.py} +15 -2
  43. codex/serializers/admin.py +11 -2
  44. codex/serializers/browser/mixins.py +1 -0
  45. codex/serializers/browser/page.py +0 -1
  46. codex/serializers/browser/settings.py +3 -0
  47. codex/settings/settings.py +11 -5
  48. codex/startup.py +77 -3
  49. codex/static_root/assets/{VCheckbox-CN6Sn_k7.9bf27129091c.js → VCheckbox-BS5fdM4M.a5d446e4aeb3.js} +1 -1
  50. codex/static_root/assets/VCheckbox-BS5fdM4M.a5d446e4aeb3.js.br +0 -0
  51. codex/static_root/assets/VCheckbox-BS5fdM4M.a5d446e4aeb3.js.gz +0 -0
  52. codex/static_root/assets/{VCheckbox-CN6Sn_k7.js → VCheckbox-BS5fdM4M.js} +1 -1
  53. codex/static_root/assets/VCheckbox-BS5fdM4M.js.br +0 -0
  54. codex/static_root/assets/VCheckbox-BS5fdM4M.js.gz +0 -0
  55. codex/static_root/assets/{VCheckboxBtn-CN1RPZPQ.b763c6a7abaf.js → VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js} +1 -1
  56. codex/static_root/assets/VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js.br +0 -0
  57. codex/static_root/assets/VCheckboxBtn-m4IbdjEe.f0c4a814ded8.js.gz +0 -0
  58. codex/static_root/assets/{VCheckboxBtn-CN1RPZPQ.js → VCheckboxBtn-m4IbdjEe.js} +1 -1
  59. codex/static_root/assets/VCheckboxBtn-m4IbdjEe.js.br +0 -0
  60. codex/static_root/assets/VCheckboxBtn-m4IbdjEe.js.gz +0 -0
  61. codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js +1 -0
  62. codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js.br +0 -0
  63. codex/static_root/assets/VCombobox-jg7SI5mn.af66f18b4c54.js.gz +0 -0
  64. codex/static_root/assets/VCombobox-jg7SI5mn.js +1 -0
  65. codex/static_root/assets/VCombobox-jg7SI5mn.js.br +0 -0
  66. codex/static_root/assets/VCombobox-jg7SI5mn.js.gz +0 -0
  67. codex/static_root/assets/{VDialog-DC3KGsMT.71e2a460e44b.js → VDialog-DiixpkQL.f84fd1fd1f84.js} +1 -1
  68. codex/static_root/assets/VDialog-DiixpkQL.f84fd1fd1f84.js.br +0 -0
  69. codex/static_root/assets/VDialog-DiixpkQL.f84fd1fd1f84.js.gz +0 -0
  70. codex/static_root/assets/{VDialog-DC3KGsMT.js → VDialog-DiixpkQL.js} +1 -1
  71. codex/static_root/assets/VDialog-DiixpkQL.js.br +0 -0
  72. codex/static_root/assets/VDialog-DiixpkQL.js.gz +0 -0
  73. codex/static_root/assets/{VExpansionPanels-DJQYXCn2.f0d471a1b80a.js → VExpansionPanels-Db5mPFue.56d0ad094a62.js} +1 -1
  74. codex/static_root/assets/VExpansionPanels-Db5mPFue.56d0ad094a62.js.br +0 -0
  75. codex/static_root/assets/VExpansionPanels-Db5mPFue.56d0ad094a62.js.gz +0 -0
  76. codex/static_root/assets/{VExpansionPanels-DJQYXCn2.js → VExpansionPanels-Db5mPFue.js} +1 -1
  77. codex/static_root/assets/VExpansionPanels-Db5mPFue.js.br +0 -0
  78. codex/static_root/assets/VExpansionPanels-Db5mPFue.js.gz +0 -0
  79. codex/static_root/assets/{VRadioGroup-CgodTThy.b20290d57146.js → VRadioGroup-DULWiMSI.4ae535e01d2b.js} +1 -1
  80. codex/static_root/assets/VRadioGroup-DULWiMSI.4ae535e01d2b.js.br +0 -0
  81. codex/static_root/assets/VRadioGroup-DULWiMSI.4ae535e01d2b.js.gz +0 -0
  82. codex/static_root/assets/{VRadioGroup-CgodTThy.js → VRadioGroup-DULWiMSI.js} +1 -1
  83. codex/static_root/assets/VRadioGroup-DULWiMSI.js.br +0 -0
  84. codex/static_root/assets/VRadioGroup-DULWiMSI.js.gz +0 -0
  85. codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js +1 -0
  86. codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js.br +0 -0
  87. codex/static_root/assets/VSelect-BnoCfCLY.4d50fd11c77c.js.gz +0 -0
  88. codex/static_root/assets/VSelect-BnoCfCLY.js +1 -0
  89. codex/static_root/assets/VSelect-BnoCfCLY.js.br +0 -0
  90. codex/static_root/assets/VSelect-BnoCfCLY.js.gz +0 -0
  91. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css +1 -0
  92. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.br +0 -0
  93. codex/static_root/assets/VSelect-MGVSeLgr.7cabd30bc5e4.css.gz +0 -0
  94. codex/static_root/assets/VSelect-MGVSeLgr.css +1 -0
  95. codex/static_root/assets/VSelect-MGVSeLgr.css.br +0 -0
  96. codex/static_root/assets/VSelect-MGVSeLgr.css.gz +0 -0
  97. codex/static_root/assets/{VSelectionControl-zPp4DXWq.635b7f5aa882.js → VSelectionControl-BSL0qeL9.5b5835d286df.js} +1 -1
  98. codex/static_root/assets/VSelectionControl-BSL0qeL9.5b5835d286df.js.br +0 -0
  99. codex/static_root/assets/VSelectionControl-BSL0qeL9.5b5835d286df.js.gz +0 -0
  100. codex/static_root/assets/{VSelectionControl-zPp4DXWq.js → VSelectionControl-BSL0qeL9.js} +1 -1
  101. codex/static_root/assets/VSelectionControl-BSL0qeL9.js.br +0 -0
  102. codex/static_root/assets/VSelectionControl-BSL0qeL9.js.gz +0 -0
  103. codex/static_root/assets/{VSlideGroup-pK9Xompk.67014b86fc74.js → VSlideGroup-BQtz8h9p.ff97dbf209e1.js} +1 -1
  104. codex/static_root/assets/VSlideGroup-BQtz8h9p.ff97dbf209e1.js.br +0 -0
  105. codex/static_root/assets/VSlideGroup-BQtz8h9p.ff97dbf209e1.js.gz +0 -0
  106. codex/static_root/assets/{VSlideGroup-pK9Xompk.js → VSlideGroup-BQtz8h9p.js} +1 -1
  107. codex/static_root/assets/VSlideGroup-BQtz8h9p.js.br +0 -0
  108. codex/static_root/assets/VSlideGroup-BQtz8h9p.js.gz +0 -0
  109. codex/static_root/assets/{VTable-CMi6sDtP.8445f91d9023.js → VTable-C0gAriFq.fa6e4b60d4ef.js} +1 -1
  110. codex/static_root/assets/VTable-C0gAriFq.fa6e4b60d4ef.js.br +0 -0
  111. codex/static_root/assets/VTable-C0gAriFq.fa6e4b60d4ef.js.gz +0 -0
  112. codex/static_root/assets/{VTable-CMi6sDtP.js → VTable-C0gAriFq.js} +1 -1
  113. codex/static_root/assets/VTable-C0gAriFq.js.br +0 -0
  114. codex/static_root/assets/VTable-C0gAriFq.js.gz +0 -0
  115. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css +1 -0
  116. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css.br +0 -0
  117. codex/static_root/assets/VTextField-BssVoDhB.4cc388b4aa3c.css.gz +0 -0
  118. codex/static_root/assets/VTextField-BssVoDhB.css +1 -0
  119. codex/static_root/assets/VTextField-BssVoDhB.css.br +0 -0
  120. codex/static_root/assets/VTextField-BssVoDhB.css.gz +0 -0
  121. codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js +1 -0
  122. codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js.br +0 -0
  123. codex/static_root/assets/VTextField-Da_Hs7ls.c295f24c251e.js.gz +0 -0
  124. codex/static_root/assets/VTextField-Da_Hs7ls.js +1 -0
  125. codex/static_root/assets/VTextField-Da_Hs7ls.js.br +0 -0
  126. codex/static_root/assets/VTextField-Da_Hs7ls.js.gz +0 -0
  127. codex/static_root/assets/{VWindowItem-DNsRQx9l.c42f636c386e.js → VWindowItem-CFhKCj49.b2458002a937.js} +1 -1
  128. codex/static_root/assets/VWindowItem-CFhKCj49.b2458002a937.js.br +0 -0
  129. codex/static_root/assets/VWindowItem-CFhKCj49.b2458002a937.js.gz +0 -0
  130. codex/static_root/assets/{VWindowItem-DNsRQx9l.js → VWindowItem-CFhKCj49.js} +1 -1
  131. codex/static_root/assets/VWindowItem-CFhKCj49.js.br +0 -0
  132. codex/static_root/assets/VWindowItem-CFhKCj49.js.gz +0 -0
  133. codex/static_root/assets/{admin-BbrOU9Y9.29936748af9e.css → admin-B8Bp2Uih.82c98714bcf1.css} +1 -1
  134. codex/static_root/assets/admin-B8Bp2Uih.82c98714bcf1.css.br +0 -0
  135. codex/static_root/assets/admin-B8Bp2Uih.82c98714bcf1.css.gz +0 -0
  136. codex/static_root/assets/{admin-BbrOU9Y9.css → admin-B8Bp2Uih.css} +1 -1
  137. codex/static_root/assets/admin-B8Bp2Uih.css.br +0 -0
  138. codex/static_root/assets/admin-B8Bp2Uih.css.gz +0 -0
  139. codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js +1 -0
  140. codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js.br +0 -0
  141. codex/static_root/assets/admin-BpGFsvOH.d7193c78d0dd.js.gz +0 -0
  142. codex/static_root/assets/admin-BpGFsvOH.js +1 -0
  143. codex/static_root/assets/admin-BpGFsvOH.js.br +0 -0
  144. codex/static_root/assets/admin-BpGFsvOH.js.gz +0 -0
  145. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css +1 -0
  146. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.br +0 -0
  147. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.bd2c1d6148bb.css.gz +0 -0
  148. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css +1 -0
  149. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.br +0 -0
  150. codex/static_root/assets/admin-drawer-panel-BHdNPwzF.css.gz +0 -0
  151. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js +30 -0
  152. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js.br +0 -0
  153. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.113c50cdf2cc.js.gz +0 -0
  154. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js +30 -0
  155. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js.br +0 -0
  156. codex/static_root/assets/admin-drawer-panel-C34Ze3-5.js.gz +0 -0
  157. codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js +1 -0
  158. codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js.br +0 -0
  159. codex/static_root/assets/browser-CRPvEdqv.f355a71f4380.js.gz +0 -0
  160. codex/static_root/assets/browser-CRPvEdqv.js +1 -0
  161. codex/static_root/assets/browser-CRPvEdqv.js.br +0 -0
  162. codex/static_root/assets/browser-CRPvEdqv.js.gz +0 -0
  163. codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css +1 -0
  164. codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css.br +0 -0
  165. codex/static_root/assets/browser-Zqc9cR6T.b7d6ddeff130.css.gz +0 -0
  166. codex/static_root/assets/browser-Zqc9cR6T.css +1 -0
  167. codex/static_root/assets/browser-Zqc9cR6T.css.br +0 -0
  168. codex/static_root/assets/browser-Zqc9cR6T.css.gz +0 -0
  169. codex/static_root/assets/{change-password-dialog-BHTkpC6Z.2d5d5dd57ad3.js → change-password-dialog-JYM9aAR9.f71d378f4fa6.js} +1 -1
  170. codex/static_root/assets/change-password-dialog-JYM9aAR9.f71d378f4fa6.js.br +0 -0
  171. codex/static_root/assets/change-password-dialog-JYM9aAR9.f71d378f4fa6.js.gz +0 -0
  172. codex/static_root/assets/{change-password-dialog-BHTkpC6Z.js → change-password-dialog-JYM9aAR9.js} +1 -1
  173. codex/static_root/assets/change-password-dialog-JYM9aAR9.js.br +0 -0
  174. codex/static_root/assets/change-password-dialog-JYM9aAR9.js.gz +0 -0
  175. codex/static_root/assets/{confirm-dialog-DmXGDg4z.284812bd376d.js → confirm-dialog-ZAZ6P1Rc.515858dd21e8.js} +1 -1
  176. codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.515858dd21e8.js.br +0 -0
  177. codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.515858dd21e8.js.gz +0 -0
  178. codex/static_root/assets/{confirm-dialog-DmXGDg4z.js → confirm-dialog-ZAZ6P1Rc.js} +1 -1
  179. codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.js.br +0 -0
  180. codex/static_root/assets/confirm-dialog-ZAZ6P1Rc.js.gz +0 -0
  181. codex/static_root/assets/{datetime-column-DKaH5S3z.5c6042016200.js → datetime-column-BT1MSP82.9617f6b94ebf.js} +1 -1
  182. codex/static_root/assets/datetime-column-BT1MSP82.9617f6b94ebf.js.br +0 -0
  183. codex/static_root/assets/datetime-column-BT1MSP82.9617f6b94ebf.js.gz +0 -0
  184. codex/static_root/assets/{datetime-column-DKaH5S3z.js → datetime-column-BT1MSP82.js} +1 -1
  185. codex/static_root/assets/datetime-column-BT1MSP82.js.br +0 -0
  186. codex/static_root/assets/datetime-column-BT1MSP82.js.gz +0 -0
  187. codex/static_root/assets/{filter-DUVATQW3.e1e7aa6d0b18.js → filter-frSNsexx.eab1570bd32d.js} +1 -1
  188. codex/static_root/assets/filter-frSNsexx.eab1570bd32d.js.br +0 -0
  189. codex/static_root/assets/filter-frSNsexx.eab1570bd32d.js.gz +0 -0
  190. codex/static_root/assets/{filter-DUVATQW3.js → filter-frSNsexx.js} +1 -1
  191. codex/static_root/assets/filter-frSNsexx.js.br +0 -0
  192. codex/static_root/assets/filter-frSNsexx.js.gz +0 -0
  193. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css +1 -0
  194. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css.br +0 -0
  195. codex/static_root/assets/flag-tab-BNwbLfrN.b755a99fff7e.css.gz +0 -0
  196. codex/static_root/assets/flag-tab-BNwbLfrN.css +1 -0
  197. codex/static_root/assets/flag-tab-BNwbLfrN.css.br +0 -0
  198. codex/static_root/assets/flag-tab-BNwbLfrN.css.gz +0 -0
  199. codex/static_root/assets/flag-tab-Ci_XcDT0.a5ea640a08d6.js +1 -0
  200. codex/static_root/assets/flag-tab-Ci_XcDT0.a5ea640a08d6.js.br +0 -0
  201. codex/static_root/assets/flag-tab-Ci_XcDT0.a5ea640a08d6.js.gz +0 -0
  202. codex/static_root/assets/flag-tab-Ci_XcDT0.js +1 -0
  203. codex/static_root/assets/flag-tab-Ci_XcDT0.js.br +0 -0
  204. codex/static_root/assets/flag-tab-Ci_XcDT0.js.gz +0 -0
  205. codex/static_root/assets/{group-tab-D3CyU-Il.fbf9ce5ef593.js → group-tab-DFma2R-E.879c37b15bbd.js} +1 -1
  206. codex/static_root/assets/group-tab-DFma2R-E.879c37b15bbd.js.br +0 -0
  207. codex/static_root/assets/group-tab-DFma2R-E.879c37b15bbd.js.gz +0 -0
  208. codex/static_root/assets/{group-tab-D3CyU-Il.js → group-tab-DFma2R-E.js} +1 -1
  209. codex/static_root/assets/group-tab-DFma2R-E.js.br +0 -0
  210. codex/static_root/assets/group-tab-DFma2R-E.js.gz +0 -0
  211. codex/static_root/assets/{http-error-0ef6-Ajc.e2879bcd242f.js → http-error-B3fQGxpt.9efc29496e30.js} +1 -1
  212. codex/static_root/assets/http-error-B3fQGxpt.9efc29496e30.js.br +0 -0
  213. codex/static_root/assets/http-error-B3fQGxpt.9efc29496e30.js.gz +0 -0
  214. codex/static_root/assets/{http-error-0ef6-Ajc.js → http-error-B3fQGxpt.js} +1 -1
  215. codex/static_root/assets/http-error-B3fQGxpt.js.br +0 -0
  216. codex/static_root/assets/http-error-B3fQGxpt.js.gz +0 -0
  217. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css +1 -0
  218. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.br +0 -0
  219. codex/static_root/assets/library-tab-BN0SfPcH.913e236f625e.css.gz +0 -0
  220. codex/static_root/assets/library-tab-BN0SfPcH.css +1 -0
  221. codex/static_root/assets/library-tab-BN0SfPcH.css.br +0 -0
  222. codex/static_root/assets/library-tab-BN0SfPcH.css.gz +0 -0
  223. codex/static_root/assets/library-tab-V8DUGoHN.809c548ff347.js +1 -0
  224. codex/static_root/assets/library-tab-V8DUGoHN.809c548ff347.js.br +0 -0
  225. codex/static_root/assets/library-tab-V8DUGoHN.809c548ff347.js.gz +0 -0
  226. codex/static_root/assets/library-tab-V8DUGoHN.js +1 -0
  227. codex/static_root/assets/library-tab-V8DUGoHN.js.br +0 -0
  228. codex/static_root/assets/library-tab-V8DUGoHN.js.gz +0 -0
  229. codex/static_root/assets/{main-CuzJev9T.js → main-Cavvqkyp.a9c4df906406.js} +6 -6
  230. codex/static_root/assets/main-Cavvqkyp.a9c4df906406.js.br +0 -0
  231. codex/static_root/assets/main-Cavvqkyp.a9c4df906406.js.gz +0 -0
  232. codex/static_root/assets/{main-CuzJev9T.f58771cc37e2.js → main-Cavvqkyp.js} +6 -6
  233. codex/static_root/assets/main-Cavvqkyp.js.br +0 -0
  234. codex/static_root/assets/main-Cavvqkyp.js.gz +0 -0
  235. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.7695fafdefce.css → pagination-toolbar-ClZui9Zf.697d7ae11c21.css} +1 -1
  236. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.br +0 -0
  237. codex/static_root/assets/pagination-toolbar-ClZui9Zf.697d7ae11c21.css.gz +0 -0
  238. codex/static_root/assets/{pagination-toolbar-B3wS4_7p.css → pagination-toolbar-ClZui9Zf.css} +1 -1
  239. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.br +0 -0
  240. codex/static_root/assets/pagination-toolbar-ClZui9Zf.css.gz +0 -0
  241. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js +1 -0
  242. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js.br +0 -0
  243. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.b2eb1e0c6985.js.gz +0 -0
  244. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js +1 -0
  245. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js.br +0 -0
  246. codex/static_root/assets/pagination-toolbar-Dg9tH1Ju.js.gz +0 -0
  247. codex/static_root/assets/{pdf-doc-BlU5mm5n.0431bcdf48d1.js → pdf-doc-4PKLN-Z3.62d8de4d1fd8.js} +1 -1
  248. codex/static_root/assets/pdf-doc-4PKLN-Z3.62d8de4d1fd8.js.br +0 -0
  249. codex/static_root/assets/pdf-doc-4PKLN-Z3.62d8de4d1fd8.js.gz +0 -0
  250. codex/static_root/assets/{pdf-doc-BlU5mm5n.js → pdf-doc-4PKLN-Z3.js} +1 -1
  251. codex/static_root/assets/pdf-doc-4PKLN-Z3.js.br +0 -0
  252. codex/static_root/assets/pdf-doc-4PKLN-Z3.js.gz +0 -0
  253. codex/static_root/assets/{reader-s7jttB_F.f7b43b04961a.js → reader-C39uGGxn.67b0f324cd5e.js} +2 -2
  254. codex/static_root/assets/reader-C39uGGxn.67b0f324cd5e.js.br +0 -0
  255. codex/static_root/assets/reader-C39uGGxn.67b0f324cd5e.js.gz +0 -0
  256. codex/static_root/assets/{reader-s7jttB_F.js → reader-C39uGGxn.js} +2 -2
  257. codex/static_root/assets/reader-C39uGGxn.js.br +0 -0
  258. codex/static_root/assets/reader-C39uGGxn.js.gz +0 -0
  259. codex/static_root/assets/{reader-BiKxuxj7.css → reader-N2Ei_y2K.8c0cdc0d513f.css} +1 -1
  260. codex/static_root/assets/reader-N2Ei_y2K.8c0cdc0d513f.css.br +0 -0
  261. codex/static_root/assets/reader-N2Ei_y2K.8c0cdc0d513f.css.gz +0 -0
  262. codex/static_root/assets/{reader-BiKxuxj7.dabe592f46f1.css → reader-N2Ei_y2K.css} +1 -1
  263. codex/static_root/assets/reader-N2Ei_y2K.css.br +0 -0
  264. codex/static_root/assets/reader-N2Ei_y2K.css.gz +0 -0
  265. codex/static_root/assets/{relation-chips-D6T6FX7G.css → relation-chips-Bsc-gUxE.98e65384c3c9.css} +1 -1
  266. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.br +0 -0
  267. codex/static_root/assets/relation-chips-Bsc-gUxE.98e65384c3c9.css.gz +0 -0
  268. codex/static_root/assets/{relation-chips-D6T6FX7G.db095b2497e4.css → relation-chips-Bsc-gUxE.css} +1 -1
  269. codex/static_root/assets/relation-chips-Bsc-gUxE.css.br +0 -0
  270. codex/static_root/assets/relation-chips-Bsc-gUxE.css.gz +0 -0
  271. codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js +1 -0
  272. codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js.br +0 -0
  273. codex/static_root/assets/relation-chips-CAC5ytqH.f205d920ffea.js.gz +0 -0
  274. codex/static_root/assets/relation-chips-CAC5ytqH.js +1 -0
  275. codex/static_root/assets/relation-chips-CAC5ytqH.js.br +0 -0
  276. codex/static_root/assets/relation-chips-CAC5ytqH.js.gz +0 -0
  277. codex/static_root/assets/{settings-drawer-DbJuixzh.352aaaab5925.js → settings-drawer-CT73ieiV.fac6f69e86ff.js} +2 -2
  278. codex/static_root/assets/settings-drawer-CT73ieiV.fac6f69e86ff.js.br +0 -0
  279. codex/static_root/assets/settings-drawer-CT73ieiV.fac6f69e86ff.js.gz +0 -0
  280. codex/static_root/assets/{settings-drawer-DbJuixzh.js → settings-drawer-CT73ieiV.js} +2 -2
  281. codex/static_root/assets/settings-drawer-CT73ieiV.js.br +0 -0
  282. codex/static_root/assets/settings-drawer-CT73ieiV.js.gz +0 -0
  283. codex/static_root/assets/{settings-drawer-BJOfMtqA.6471a50fc05b.css → settings-drawer-nZ9HvKSZ.01744e131806.css} +1 -1
  284. codex/static_root/assets/settings-drawer-nZ9HvKSZ.01744e131806.css.br +0 -0
  285. codex/static_root/assets/settings-drawer-nZ9HvKSZ.01744e131806.css.gz +0 -0
  286. codex/static_root/assets/{settings-drawer-BJOfMtqA.css → settings-drawer-nZ9HvKSZ.css} +1 -1
  287. codex/static_root/assets/settings-drawer-nZ9HvKSZ.css.br +0 -0
  288. codex/static_root/assets/settings-drawer-nZ9HvKSZ.css.gz +0 -0
  289. codex/static_root/assets/{stats-tab-BhRLh7XB.7be95b5021c3.js → stats-tab-C6qAnV6-.f08c46b5f141.js} +1 -1
  290. codex/static_root/assets/stats-tab-C6qAnV6-.f08c46b5f141.js.br +0 -0
  291. codex/static_root/assets/stats-tab-C6qAnV6-.f08c46b5f141.js.gz +0 -0
  292. codex/static_root/assets/{stats-tab-BhRLh7XB.js → stats-tab-C6qAnV6-.js} +1 -1
  293. codex/static_root/assets/stats-tab-C6qAnV6-.js.br +0 -0
  294. codex/static_root/assets/stats-tab-C6qAnV6-.js.gz +0 -0
  295. codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js +1 -0
  296. codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js.br +0 -0
  297. codex/static_root/assets/task-tab-JcG2vvgt.eed6e03db487.js.gz +0 -0
  298. codex/static_root/assets/task-tab-JcG2vvgt.js +1 -0
  299. codex/static_root/assets/task-tab-JcG2vvgt.js.br +0 -0
  300. codex/static_root/assets/task-tab-JcG2vvgt.js.gz +0 -0
  301. codex/static_root/assets/{unauthorized-BgXQnNpn.f63b36d526b5.js → unauthorized-28bmiwAU.c74825b5c16f.js} +1 -1
  302. codex/static_root/assets/unauthorized-28bmiwAU.c74825b5c16f.js.br +0 -0
  303. codex/static_root/assets/unauthorized-28bmiwAU.c74825b5c16f.js.gz +0 -0
  304. codex/static_root/assets/{unauthorized-BgXQnNpn.js → unauthorized-28bmiwAU.js} +1 -1
  305. codex/static_root/assets/unauthorized-28bmiwAU.js.br +0 -0
  306. codex/static_root/assets/unauthorized-28bmiwAU.js.gz +0 -0
  307. codex/static_root/assets/{user-tab-cZoJDlb-.2a497e8b2b24.js → user-tab-BOq4BMMc.b64ac15dd772.js} +1 -1
  308. codex/static_root/assets/user-tab-BOq4BMMc.b64ac15dd772.js.br +0 -0
  309. codex/static_root/assets/user-tab-BOq4BMMc.b64ac15dd772.js.gz +0 -0
  310. codex/static_root/assets/{user-tab-cZoJDlb-.js → user-tab-BOq4BMMc.js} +1 -1
  311. codex/static_root/assets/user-tab-BOq4BMMc.js.br +0 -0
  312. codex/static_root/assets/user-tab-BOq4BMMc.js.gz +0 -0
  313. codex/static_root/js/choices-admin.ef1ff3a8b9da.json +1 -0
  314. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.br +0 -0
  315. codex/static_root/js/choices-admin.ef1ff3a8b9da.json.gz +0 -0
  316. codex/static_root/js/choices-admin.json +1 -1
  317. codex/static_root/js/choices-admin.json.br +0 -0
  318. codex/static_root/js/choices-admin.json.gz +0 -0
  319. codex/static_root/js/{choices.b60817ba0a93.json → choices.23fedac2b9ba.json} +1 -1
  320. codex/static_root/js/choices.23fedac2b9ba.json.br +0 -0
  321. codex/static_root/js/choices.23fedac2b9ba.json.gz +0 -0
  322. codex/static_root/js/choices.json +1 -1
  323. codex/static_root/js/choices.json.br +0 -0
  324. codex/static_root/js/choices.json.gz +0 -0
  325. codex/static_root/manifest.ad9da9714e46.json +642 -0
  326. codex/static_root/manifest.ad9da9714e46.json.br +0 -0
  327. codex/static_root/manifest.ad9da9714e46.json.gz +0 -0
  328. codex/static_root/manifest.json +256 -256
  329. codex/static_root/manifest.json.br +0 -0
  330. codex/static_root/manifest.json.gz +0 -0
  331. codex/static_root/staticfiles.json +1 -1
  332. codex/status_controller.py +11 -2
  333. codex/views/admin/library.py +47 -27
  334. codex/views/admin/stats.py +4 -1
  335. codex/views/admin/tasks.py +6 -1
  336. codex/views/browser/base.py +3 -3
  337. codex/views/browser/browser.py +1 -9
  338. codex/views/browser/browser_annotations.py +101 -20
  339. codex/views/browser/browser_breadcrumbs.py +2 -2
  340. codex/views/browser/browser_order_by.py +31 -20
  341. codex/views/browser/filters/field.py +3 -1
  342. codex/views/browser/filters/search.py +1 -1
  343. codex/views/browser/metadata.py +0 -1
  344. codex/views/cover.py +3 -2
  345. codex/views/session.py +1 -0
  346. {codex-1.6.0a4.dist-info → codex-1.6.0a6.dist-info}/METADATA +3 -2
  347. {codex-1.6.0a4.dist-info → codex-1.6.0a6.dist-info}/RECORD +350 -347
  348. codex/static_root/assets/VCheckbox-CN6Sn_k7.9bf27129091c.js.br +0 -0
  349. codex/static_root/assets/VCheckbox-CN6Sn_k7.9bf27129091c.js.gz +0 -0
  350. codex/static_root/assets/VCheckbox-CN6Sn_k7.js.br +0 -0
  351. codex/static_root/assets/VCheckbox-CN6Sn_k7.js.gz +0 -0
  352. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.b763c6a7abaf.js.br +0 -0
  353. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.b763c6a7abaf.js.gz +0 -0
  354. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.js.br +0 -0
  355. codex/static_root/assets/VCheckboxBtn-CN1RPZPQ.js.gz +0 -0
  356. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js +0 -1
  357. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js.br +0 -0
  358. codex/static_root/assets/VCombobox-CtLUGYtD.6e49097cea2c.js.gz +0 -0
  359. codex/static_root/assets/VCombobox-CtLUGYtD.js +0 -1
  360. codex/static_root/assets/VCombobox-CtLUGYtD.js.br +0 -0
  361. codex/static_root/assets/VCombobox-CtLUGYtD.js.gz +0 -0
  362. codex/static_root/assets/VDialog-DC3KGsMT.71e2a460e44b.js.br +0 -0
  363. codex/static_root/assets/VDialog-DC3KGsMT.71e2a460e44b.js.gz +0 -0
  364. codex/static_root/assets/VDialog-DC3KGsMT.js.br +0 -0
  365. codex/static_root/assets/VDialog-DC3KGsMT.js.gz +0 -0
  366. codex/static_root/assets/VExpansionPanels-DJQYXCn2.f0d471a1b80a.js.br +0 -0
  367. codex/static_root/assets/VExpansionPanels-DJQYXCn2.f0d471a1b80a.js.gz +0 -0
  368. codex/static_root/assets/VExpansionPanels-DJQYXCn2.js.br +0 -0
  369. codex/static_root/assets/VExpansionPanels-DJQYXCn2.js.gz +0 -0
  370. codex/static_root/assets/VRadioGroup-CgodTThy.b20290d57146.js.br +0 -0
  371. codex/static_root/assets/VRadioGroup-CgodTThy.b20290d57146.js.gz +0 -0
  372. codex/static_root/assets/VRadioGroup-CgodTThy.js.br +0 -0
  373. codex/static_root/assets/VRadioGroup-CgodTThy.js.gz +0 -0
  374. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css +0 -1
  375. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.br +0 -0
  376. codex/static_root/assets/VSelect-ARDhJiQK.372c098fb475.css.gz +0 -0
  377. codex/static_root/assets/VSelect-ARDhJiQK.css +0 -1
  378. codex/static_root/assets/VSelect-ARDhJiQK.css.br +0 -0
  379. codex/static_root/assets/VSelect-ARDhJiQK.css.gz +0 -0
  380. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js +0 -1
  381. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js.br +0 -0
  382. codex/static_root/assets/VSelect-B6s-x36h.de1030c82785.js.gz +0 -0
  383. codex/static_root/assets/VSelect-B6s-x36h.js +0 -1
  384. codex/static_root/assets/VSelect-B6s-x36h.js.br +0 -0
  385. codex/static_root/assets/VSelect-B6s-x36h.js.gz +0 -0
  386. codex/static_root/assets/VSelectionControl-zPp4DXWq.635b7f5aa882.js.br +0 -0
  387. codex/static_root/assets/VSelectionControl-zPp4DXWq.635b7f5aa882.js.gz +0 -0
  388. codex/static_root/assets/VSelectionControl-zPp4DXWq.js.br +0 -0
  389. codex/static_root/assets/VSelectionControl-zPp4DXWq.js.gz +0 -0
  390. codex/static_root/assets/VSlideGroup-pK9Xompk.67014b86fc74.js.br +0 -0
  391. codex/static_root/assets/VSlideGroup-pK9Xompk.67014b86fc74.js.gz +0 -0
  392. codex/static_root/assets/VSlideGroup-pK9Xompk.js.br +0 -0
  393. codex/static_root/assets/VSlideGroup-pK9Xompk.js.gz +0 -0
  394. codex/static_root/assets/VTable-CMi6sDtP.8445f91d9023.js.br +0 -0
  395. codex/static_root/assets/VTable-CMi6sDtP.8445f91d9023.js.gz +0 -0
  396. codex/static_root/assets/VTable-CMi6sDtP.js.br +0 -0
  397. codex/static_root/assets/VTable-CMi6sDtP.js.gz +0 -0
  398. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css +0 -1
  399. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.br +0 -0
  400. codex/static_root/assets/VTextField-DuIIp-VY.580110ad24dc.css.gz +0 -0
  401. codex/static_root/assets/VTextField-DuIIp-VY.css +0 -1
  402. codex/static_root/assets/VTextField-DuIIp-VY.css.br +0 -0
  403. codex/static_root/assets/VTextField-DuIIp-VY.css.gz +0 -0
  404. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js +0 -1
  405. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js.br +0 -0
  406. codex/static_root/assets/VTextField-dMTizQEN.e93eefec0fdf.js.gz +0 -0
  407. codex/static_root/assets/VTextField-dMTizQEN.js +0 -1
  408. codex/static_root/assets/VTextField-dMTizQEN.js.br +0 -0
  409. codex/static_root/assets/VTextField-dMTizQEN.js.gz +0 -0
  410. codex/static_root/assets/VWindowItem-DNsRQx9l.c42f636c386e.js.br +0 -0
  411. codex/static_root/assets/VWindowItem-DNsRQx9l.c42f636c386e.js.gz +0 -0
  412. codex/static_root/assets/VWindowItem-DNsRQx9l.js.br +0 -0
  413. codex/static_root/assets/VWindowItem-DNsRQx9l.js.gz +0 -0
  414. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.br +0 -0
  415. codex/static_root/assets/admin-BbrOU9Y9.29936748af9e.css.gz +0 -0
  416. codex/static_root/assets/admin-BbrOU9Y9.css.br +0 -0
  417. codex/static_root/assets/admin-BbrOU9Y9.css.gz +0 -0
  418. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js +0 -1
  419. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js.br +0 -0
  420. codex/static_root/assets/admin-CApphpty.5a41cbf0eefc.js.gz +0 -0
  421. codex/static_root/assets/admin-CApphpty.js +0 -1
  422. codex/static_root/assets/admin-CApphpty.js.br +0 -0
  423. codex/static_root/assets/admin-CApphpty.js.gz +0 -0
  424. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css +0 -1
  425. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.br +0 -0
  426. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.1514926c78e8.css.gz +0 -0
  427. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css +0 -1
  428. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.br +0 -0
  429. codex/static_root/assets/admin-drawer-panel-BF0dMvEE.css.gz +0 -0
  430. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js +0 -30
  431. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js.br +0 -0
  432. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.a79eda3055a7.js.gz +0 -0
  433. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js +0 -30
  434. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js.br +0 -0
  435. codex/static_root/assets/admin-drawer-panel-Dp8KD9D7.js.gz +0 -0
  436. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js +0 -1
  437. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js.br +0 -0
  438. codex/static_root/assets/browser-C_PHQbyN.bb065e2ce21a.js.gz +0 -0
  439. codex/static_root/assets/browser-C_PHQbyN.js +0 -1
  440. codex/static_root/assets/browser-C_PHQbyN.js.br +0 -0
  441. codex/static_root/assets/browser-C_PHQbyN.js.gz +0 -0
  442. codex/static_root/assets/browser-DVI4LUNv.css +0 -1
  443. codex/static_root/assets/browser-DVI4LUNv.css.br +0 -0
  444. codex/static_root/assets/browser-DVI4LUNv.css.gz +0 -0
  445. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css +0 -1
  446. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css.br +0 -0
  447. codex/static_root/assets/browser-DVI4LUNv.fef2b997145a.css.gz +0 -0
  448. codex/static_root/assets/change-password-dialog-BHTkpC6Z.2d5d5dd57ad3.js.br +0 -0
  449. codex/static_root/assets/change-password-dialog-BHTkpC6Z.2d5d5dd57ad3.js.gz +0 -0
  450. codex/static_root/assets/change-password-dialog-BHTkpC6Z.js.br +0 -0
  451. codex/static_root/assets/change-password-dialog-BHTkpC6Z.js.gz +0 -0
  452. codex/static_root/assets/confirm-dialog-DmXGDg4z.284812bd376d.js.br +0 -0
  453. codex/static_root/assets/confirm-dialog-DmXGDg4z.284812bd376d.js.gz +0 -0
  454. codex/static_root/assets/confirm-dialog-DmXGDg4z.js.br +0 -0
  455. codex/static_root/assets/confirm-dialog-DmXGDg4z.js.gz +0 -0
  456. codex/static_root/assets/datetime-column-DKaH5S3z.5c6042016200.js.br +0 -0
  457. codex/static_root/assets/datetime-column-DKaH5S3z.5c6042016200.js.gz +0 -0
  458. codex/static_root/assets/datetime-column-DKaH5S3z.js.br +0 -0
  459. codex/static_root/assets/datetime-column-DKaH5S3z.js.gz +0 -0
  460. codex/static_root/assets/filter-DUVATQW3.e1e7aa6d0b18.js.br +0 -0
  461. codex/static_root/assets/filter-DUVATQW3.e1e7aa6d0b18.js.gz +0 -0
  462. codex/static_root/assets/filter-DUVATQW3.js.br +0 -0
  463. codex/static_root/assets/filter-DUVATQW3.js.gz +0 -0
  464. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css +0 -1
  465. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.br +0 -0
  466. codex/static_root/assets/flag-tab-BacZLlVi.8deba6a5370d.css.gz +0 -0
  467. codex/static_root/assets/flag-tab-BacZLlVi.css +0 -1
  468. codex/static_root/assets/flag-tab-BacZLlVi.css.br +0 -0
  469. codex/static_root/assets/flag-tab-BacZLlVi.css.gz +0 -0
  470. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js +0 -1
  471. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js.br +0 -0
  472. codex/static_root/assets/flag-tab-CSKMJEn-.5d5c0604541b.js.gz +0 -0
  473. codex/static_root/assets/flag-tab-CSKMJEn-.js +0 -1
  474. codex/static_root/assets/flag-tab-CSKMJEn-.js.br +0 -0
  475. codex/static_root/assets/flag-tab-CSKMJEn-.js.gz +0 -0
  476. codex/static_root/assets/group-tab-D3CyU-Il.fbf9ce5ef593.js.br +0 -0
  477. codex/static_root/assets/group-tab-D3CyU-Il.fbf9ce5ef593.js.gz +0 -0
  478. codex/static_root/assets/group-tab-D3CyU-Il.js.br +0 -0
  479. codex/static_root/assets/group-tab-D3CyU-Il.js.gz +0 -0
  480. codex/static_root/assets/http-error-0ef6-Ajc.e2879bcd242f.js.br +0 -0
  481. codex/static_root/assets/http-error-0ef6-Ajc.e2879bcd242f.js.gz +0 -0
  482. codex/static_root/assets/http-error-0ef6-Ajc.js.br +0 -0
  483. codex/static_root/assets/http-error-0ef6-Ajc.js.gz +0 -0
  484. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css +0 -1
  485. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.br +0 -2
  486. codex/static_root/assets/library-tab-CXbkxNZt.954f2850a439.css.gz +0 -0
  487. codex/static_root/assets/library-tab-CXbkxNZt.css +0 -1
  488. codex/static_root/assets/library-tab-CXbkxNZt.css.br +0 -2
  489. codex/static_root/assets/library-tab-CXbkxNZt.css.gz +0 -0
  490. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js +0 -1
  491. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js.br +0 -0
  492. codex/static_root/assets/library-tab-IdDfLn4k.70b55460b8e1.js.gz +0 -0
  493. codex/static_root/assets/library-tab-IdDfLn4k.js +0 -1
  494. codex/static_root/assets/library-tab-IdDfLn4k.js.br +0 -0
  495. codex/static_root/assets/library-tab-IdDfLn4k.js.gz +0 -0
  496. codex/static_root/assets/main-CuzJev9T.f58771cc37e2.js.br +0 -0
  497. codex/static_root/assets/main-CuzJev9T.f58771cc37e2.js.gz +0 -0
  498. codex/static_root/assets/main-CuzJev9T.js.br +0 -0
  499. codex/static_root/assets/main-CuzJev9T.js.gz +0 -0
  500. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.br +0 -0
  501. codex/static_root/assets/pagination-toolbar-B3wS4_7p.7695fafdefce.css.gz +0 -0
  502. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.br +0 -0
  503. codex/static_root/assets/pagination-toolbar-B3wS4_7p.css.gz +0 -0
  504. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js +0 -1
  505. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js.br +0 -0
  506. codex/static_root/assets/pagination-toolbar-CLr9RVHd.66d3b756d974.js.gz +0 -0
  507. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js +0 -1
  508. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js.br +0 -0
  509. codex/static_root/assets/pagination-toolbar-CLr9RVHd.js.gz +0 -0
  510. codex/static_root/assets/pdf-doc-BlU5mm5n.0431bcdf48d1.js.br +0 -0
  511. codex/static_root/assets/pdf-doc-BlU5mm5n.0431bcdf48d1.js.gz +0 -0
  512. codex/static_root/assets/pdf-doc-BlU5mm5n.js.br +0 -0
  513. codex/static_root/assets/pdf-doc-BlU5mm5n.js.gz +0 -0
  514. codex/static_root/assets/reader-BiKxuxj7.css.br +0 -0
  515. codex/static_root/assets/reader-BiKxuxj7.css.gz +0 -0
  516. codex/static_root/assets/reader-BiKxuxj7.dabe592f46f1.css.br +0 -0
  517. codex/static_root/assets/reader-BiKxuxj7.dabe592f46f1.css.gz +0 -0
  518. codex/static_root/assets/reader-s7jttB_F.f7b43b04961a.js.br +0 -0
  519. codex/static_root/assets/reader-s7jttB_F.f7b43b04961a.js.gz +0 -0
  520. codex/static_root/assets/reader-s7jttB_F.js.br +0 -0
  521. codex/static_root/assets/reader-s7jttB_F.js.gz +0 -0
  522. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js +0 -1
  523. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js.br +0 -0
  524. codex/static_root/assets/relation-chips-BiVsYCd9.27eca8616ba4.js.gz +0 -0
  525. codex/static_root/assets/relation-chips-BiVsYCd9.js +0 -1
  526. codex/static_root/assets/relation-chips-BiVsYCd9.js.br +0 -0
  527. codex/static_root/assets/relation-chips-BiVsYCd9.js.gz +0 -0
  528. codex/static_root/assets/relation-chips-D6T6FX7G.css.br +0 -0
  529. codex/static_root/assets/relation-chips-D6T6FX7G.css.gz +0 -0
  530. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.br +0 -0
  531. codex/static_root/assets/relation-chips-D6T6FX7G.db095b2497e4.css.gz +0 -0
  532. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.br +0 -0
  533. codex/static_root/assets/settings-drawer-BJOfMtqA.6471a50fc05b.css.gz +0 -0
  534. codex/static_root/assets/settings-drawer-BJOfMtqA.css.br +0 -0
  535. codex/static_root/assets/settings-drawer-BJOfMtqA.css.gz +0 -0
  536. codex/static_root/assets/settings-drawer-DbJuixzh.352aaaab5925.js.br +0 -0
  537. codex/static_root/assets/settings-drawer-DbJuixzh.352aaaab5925.js.gz +0 -0
  538. codex/static_root/assets/settings-drawer-DbJuixzh.js.br +0 -0
  539. codex/static_root/assets/settings-drawer-DbJuixzh.js.gz +0 -0
  540. codex/static_root/assets/stats-tab-BhRLh7XB.7be95b5021c3.js.br +0 -0
  541. codex/static_root/assets/stats-tab-BhRLh7XB.7be95b5021c3.js.gz +0 -0
  542. codex/static_root/assets/stats-tab-BhRLh7XB.js.br +0 -0
  543. codex/static_root/assets/stats-tab-BhRLh7XB.js.gz +0 -0
  544. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js +0 -1
  545. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js.br +0 -0
  546. codex/static_root/assets/task-tab-DLrd54z2.c574111658b9.js.gz +0 -0
  547. codex/static_root/assets/task-tab-DLrd54z2.js +0 -1
  548. codex/static_root/assets/task-tab-DLrd54z2.js.br +0 -0
  549. codex/static_root/assets/task-tab-DLrd54z2.js.gz +0 -0
  550. codex/static_root/assets/unauthorized-BgXQnNpn.f63b36d526b5.js.br +0 -0
  551. codex/static_root/assets/unauthorized-BgXQnNpn.f63b36d526b5.js.gz +0 -0
  552. codex/static_root/assets/unauthorized-BgXQnNpn.js.br +0 -0
  553. codex/static_root/assets/unauthorized-BgXQnNpn.js.gz +0 -0
  554. codex/static_root/assets/user-tab-cZoJDlb-.2a497e8b2b24.js.br +0 -0
  555. codex/static_root/assets/user-tab-cZoJDlb-.2a497e8b2b24.js.gz +0 -0
  556. codex/static_root/assets/user-tab-cZoJDlb-.js.br +0 -0
  557. codex/static_root/assets/user-tab-cZoJDlb-.js.gz +0 -0
  558. codex/static_root/js/choices-admin.248384ece3f6.json +0 -1
  559. codex/static_root/js/choices-admin.248384ece3f6.json.br +0 -0
  560. codex/static_root/js/choices-admin.248384ece3f6.json.gz +0 -0
  561. codex/static_root/js/choices.b60817ba0a93.json.br +0 -0
  562. codex/static_root/js/choices.b60817ba0a93.json.gz +0 -0
  563. codex/static_root/manifest.3c2f342d5f5a.json +0 -642
  564. codex/static_root/manifest.3c2f342d5f5a.json.br +0 -0
  565. codex/static_root/manifest.3c2f342d5f5a.json.gz +0 -0
  566. {codex-1.6.0a4.dist-info → codex-1.6.0a6.dist-info}/LICENSE +0 -0
  567. {codex-1.6.0a4.dist-info → codex-1.6.0a6.dist-info}/WHEEL +0 -0
  568. {codex-1.6.0a4.dist-info → codex-1.6.0a6.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
@@ -1,16 +1,19 @@
1
1
  """Clean up the database after moves or imports."""
2
2
 
3
3
  from codex.librarian.covers.tasks import CoverRemoveTask
4
+ from codex.librarian.importer.const import COMIC_GROUP_FIELD_NAMES
4
5
  from codex.librarian.importer.status import ImportStatusTypes, status_notify
5
- from codex.models import Comic, Folder
6
+ from codex.models import Comic, Folder, StoryArc
7
+ from codex.models.paths import CustomCover
8
+ from codex.settings.settings import MAX_CHUNK_SIZE
6
9
  from codex.threads import QueuedThread
7
10
 
8
11
 
9
12
  class DeletedMixin(QueuedThread):
10
13
  """Clean up database methods."""
11
14
 
12
- def _remove_covers(self, delete_comic_pks):
13
- task = CoverRemoveTask(delete_comic_pks)
15
+ def _remove_covers(self, delete_pks, custom=False):
16
+ task = CoverRemoveTask(delete_pks, custom)
14
17
  self.librarian_queue.put(task)
15
18
 
16
19
  @status_notify(status_type=ImportStatusTypes.DIRS_DELETED, updates=False)
@@ -36,14 +39,47 @@ class DeletedMixin(QueuedThread):
36
39
  )
37
40
  return count
38
41
 
42
+ @staticmethod
43
+ def _populate_deleted_comic_groups(delete_qs, deleted_comic_groups):
44
+ """Populate changed groups for cover timestamp updater."""
45
+ comics_deleted_qs = delete_qs.only(*COMIC_GROUP_FIELD_NAMES).prefetch_related(
46
+ "story_arc_numbers__story_arc"
47
+ )
48
+ for comic in comics_deleted_qs.iterator(chunk_size=MAX_CHUNK_SIZE):
49
+ for field_name in COMIC_GROUP_FIELD_NAMES:
50
+ if field_name == "story_arc_numbers":
51
+ for san in comic.story_arc_numbers.select_related("story_arc").only(
52
+ "story_arc"
53
+ ):
54
+ deleted_comic_groups[StoryArc].add(san.story_arc.pk)
55
+ elif field_name == "folders":
56
+ for folder in comic.folders.only("pk"):
57
+ deleted_comic_groups[Folder].add(folder.pk)
58
+ else:
59
+ related_model = comic._meta.get_field(field_name).related_model
60
+ related_id = getattr(comic, field_name).pk
61
+ deleted_comic_groups[related_model].add(related_id)
62
+
63
+ @staticmethod
64
+ def _init_deleted_comic_groups(deleted_comic_groups):
65
+ """Init deleted_comic_groups, used later even if no deletes."""
66
+ for field_name in COMIC_GROUP_FIELD_NAMES:
67
+ related_model = Comic._meta.get_field(field_name).related_model
68
+ deleted_comic_groups[related_model] = set()
69
+
39
70
  @status_notify(status_type=ImportStatusTypes.FILES_DELETED, updates=False)
40
- def _bulk_comics_deleted(self, delete_comic_paths, library, **kwargs):
71
+ def _bulk_comics_deleted(
72
+ self, delete_comic_paths, library, deleted_comic_groups, **kwargs
73
+ ):
41
74
  """Bulk delete comics found missing from the filesystem."""
42
75
  if not delete_comic_paths:
43
76
  return 0
44
- comics = Comic.objects.filter(library=library, path__in=delete_comic_paths)
45
- delete_comic_pks = frozenset(comics.values_list("pk", flat=True))
46
- comics.delete()
77
+ delete_qs = Comic.objects.filter(library=library, path__in=delete_comic_paths)
78
+
79
+ self._populate_deleted_comic_groups(delete_qs, deleted_comic_groups)
80
+
81
+ delete_comic_pks = frozenset(delete_qs.values_list("pk", flat=True))
82
+ delete_qs.delete()
47
83
 
48
84
  self._remove_covers(delete_comic_pks)
49
85
 
@@ -53,11 +89,37 @@ class DeletedMixin(QueuedThread):
53
89
 
54
90
  return count
55
91
 
56
- def delete(self, library, task):
92
+ @status_notify(status_type=ImportStatusTypes.COVERS_DELETED, updates=False)
93
+ def _bulk_covers_deleted(self, delete_cover_paths, library, **kwargs):
94
+ """Bulk delete comics found missing from the filesystem."""
95
+ if not delete_cover_paths:
96
+ return 0
97
+ covers = CustomCover.objects.filter(
98
+ library=library, path__in=delete_cover_paths
99
+ )
100
+ delete_cover_pks = frozenset(covers.values_list("pk", flat=True))
101
+ covers.delete()
102
+
103
+ self._remove_covers(delete_cover_pks, custom=True)
104
+
105
+ count = len(delete_cover_paths)
106
+ if count:
107
+ self.log.info(f"Deleted {count} custom covers from {library.path}")
108
+
109
+ return count
110
+
111
+ def delete(self, library, task, deleted_comic_groups):
57
112
  """Delete files and folders."""
58
113
  count = self._bulk_folders_deleted(task.dirs_deleted, library)
59
114
  task.dirs_deleted = None
60
115
 
61
- count += self._bulk_comics_deleted(task.files_deleted, library)
116
+ self._init_deleted_comic_groups(deleted_comic_groups)
117
+ count += self._bulk_comics_deleted(
118
+ task.files_deleted, library, deleted_comic_groups
119
+ )
62
120
  task.files_deleted = None
121
+
122
+ count += self._bulk_covers_deleted(task.covers_deleted, library)
123
+ task.covers_deleted = None
124
+
63
125
  return count
@@ -5,10 +5,12 @@ from pathlib import Path
5
5
  from time import sleep, time
6
6
 
7
7
  from django.core.cache import cache
8
+ from django.utils.timezone import now
8
9
  from humanize import naturaldelta
9
10
 
10
11
  from codex.librarian.importer.aggregate_metadata import AggregateMetadataMixin
11
12
  from codex.librarian.importer.const import FIS, FKS, M2M_MDS, MDS
13
+ from codex.librarian.importer.covers import CoversMixin
12
14
  from codex.librarian.importer.deleted import DeletedMixin
13
15
  from codex.librarian.importer.failed_imports import FailedImportsMixin
14
16
  from codex.librarian.importer.moved import MovedMixin
@@ -35,6 +37,7 @@ class ComicImporterThread(
35
37
  UpdateComicsMixin,
36
38
  FailedImportsMixin,
37
39
  MovedMixin,
40
+ CoversMixin,
38
41
  ):
39
42
  """A worker to handle all bulk database updates."""
40
43
 
@@ -117,6 +120,29 @@ class ComicImporterThread(
117
120
  log += ", ".join(dirs_log)
118
121
  self.log.debug(" " + log)
119
122
 
123
+ @staticmethod
124
+ def _init_librarian_status_moved(task, status_list):
125
+ """Initialize moved statuses."""
126
+ search_index_updates = 0
127
+ if task.dirs_moved:
128
+ status_list += [
129
+ Status(ImportStatusTypes.DIRS_MOVED, None, len(task.dirs_moved))
130
+ ]
131
+ if task.files_moved:
132
+ status_list += [
133
+ Status(ImportStatusTypes.FILES_MOVED, None, len(task.files_moved))
134
+ ]
135
+ search_index_updates += len(task.files_moved)
136
+ if task.covers_moved:
137
+ status_list += [
138
+ Status(ImportStatusTypes.COVERS_MOVED, None, len(task.covers_moved))
139
+ ]
140
+ if task.dirs_modified:
141
+ status_list += [
142
+ Status(ImportStatusTypes.DIRS_MODIFIED, None, len(task.dirs_modified))
143
+ ]
144
+ return search_index_updates
145
+
120
146
  @staticmethod
121
147
  def _init_if_modified_or_created(task, path, status_list):
122
148
  """Initialize librarian statuses for modified or created ops."""
@@ -124,6 +150,7 @@ class ComicImporterThread(
124
150
  status_list += [
125
151
  Status(ImportStatusTypes.AGGREGATE_TAGS, 0, total_paths, subtitle=path),
126
152
  Status(ImportStatusTypes.QUERY_MISSING_FKS),
153
+ Status(ImportStatusTypes.QUERY_MISSING_COVERS),
127
154
  Status(ImportStatusTypes.CREATE_FKS),
128
155
  ]
129
156
  if task.files_modified:
@@ -134,40 +161,56 @@ class ComicImporterThread(
134
161
  len(task.files_modified),
135
162
  )
136
163
  ]
137
- if task.files_created:
164
+ if task.covers_modified:
138
165
  status_list += [
139
- Status(ImportStatusTypes.FILES_CREATED, None, len(task.files_created))
166
+ Status(
167
+ ImportStatusTypes.COVERS_MODIFIED,
168
+ None,
169
+ len(task.covers_modified),
170
+ )
140
171
  ]
141
- if task.files_modified or task.files_created:
142
- status_list += [Status(ImportStatusTypes.LINK_M2M_FIELDS)]
143
- return total_paths
144
172
 
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:
173
+ if task.files_created or task.covers_created:
150
174
  status_list += [
151
- Status(ImportStatusTypes.DIRS_MOVED, None, len(task.dirs_moved))
175
+ Status(ImportStatusTypes.FILES_CREATED, None, len(task.files_created))
152
176
  ]
153
- if task.files_moved:
177
+ if task.covers_created:
154
178
  status_list += [
155
- Status(ImportStatusTypes.FILES_MOVED, None, len(task.files_moved))
179
+ Status(ImportStatusTypes.COVERS_CREATED, None, len(task.covers_created))
156
180
  ]
157
- search_index_updates += len(task.files_moved)
158
- if task.files_modified:
181
+
182
+ if task.files_modified or task.files_created:
183
+ status_list += [Status(ImportStatusTypes.LINK_M2M_FIELDS)]
184
+
185
+ num_covers_linked = (
186
+ len(task.covers_moved)
187
+ + len(task.covers_modified)
188
+ + len(task.covers_created)
189
+ )
190
+ if num_covers_linked:
159
191
  status_list += [
160
- Status(ImportStatusTypes.DIRS_MODIFIED, None, len(task.dirs_modified))
192
+ Status(ImportStatusTypes.COVERS_LINK, None, num_covers_linked)
161
193
  ]
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
- )
194
+ return total_paths
195
+
196
+ @staticmethod
197
+ def _init_librarian_status_deleted(task, status_list):
198
+ """Init deleted statuses."""
199
+ search_index_updates = 0
166
200
  if task.files_deleted:
167
201
  status_list += [
168
202
  Status(ImportStatusTypes.FILES_DELETED, None, len(task.files_deleted))
169
203
  ]
170
204
  search_index_updates += len(task.files_deleted)
205
+ if task.covers_deleted:
206
+ status_list += [
207
+ Status(ImportStatusTypes.COVERS_DELETED, None, len(task.covers_deleted))
208
+ ]
209
+ return search_index_updates
210
+
211
+ @staticmethod
212
+ def _init_librarian_status_search_index(search_index_updates, status_list):
213
+ """Init search index statuses."""
171
214
  status_list += [
172
215
  Status(
173
216
  SearchIndexStatusTypes.SEARCH_INDEX_UPDATE,
@@ -176,6 +219,23 @@ class ComicImporterThread(
176
219
  ),
177
220
  Status(SearchIndexStatusTypes.SEARCH_INDEX_REMOVE),
178
221
  ]
222
+
223
+ def _init_librarian_status(self, task, path):
224
+ """Update the librarian status tasks."""
225
+ status_list = []
226
+ search_index_updates = 0
227
+ search_index_updates += self._init_librarian_status_moved(task, status_list)
228
+ if (
229
+ task.files_modified
230
+ or task.files_created
231
+ or task.covers_modified
232
+ or task.covers_created
233
+ ):
234
+ search_index_updates += self._init_if_modified_or_created(
235
+ task, path, status_list
236
+ )
237
+ search_index_updates += self._init_librarian_status_deleted(task, status_list)
238
+ self._init_librarian_status_search_index(search_index_updates, status_list)
179
239
  self.status_controller.start_many(status_list)
180
240
 
181
241
  def _init_apply(self, library, task):
@@ -193,12 +253,38 @@ class ComicImporterThread(
193
253
  self._log_task(library.path, task)
194
254
  self._init_librarian_status(task, library.path)
195
255
 
196
- def _create_comic_relations(self, library, fks) -> int:
256
+ def _create_comic_and_cover_relations(
257
+ self, library, fks, cover_paths: frozenset[int], link_cover_pks: set[int]
258
+ ) -> int:
197
259
  """Query all foreign keys to determine what needs creating, then create them."""
198
- if not fks:
199
- return 0
260
+ count = 0
261
+ if not fks and not cover_paths:
262
+ return count
200
263
  create_data = self.query_all_missing_fks(library.path, fks)
201
- return self.create_all_fks(library, create_data)
264
+ query_cover_data = []
265
+ count += self.query_missing_custom_covers(
266
+ cover_paths,
267
+ library,
268
+ query_cover_data,
269
+ )
270
+ count += self.create_all_fks(library, create_data)
271
+ if query_cover_data:
272
+ update_covers_qs, create_cover_paths = query_cover_data
273
+ count += self.update_custom_covers(update_covers_qs, link_cover_pks)
274
+ link_covers_status = Status(
275
+ ImportStatusTypes.COVERS_LINK, 0, len(link_cover_pks)
276
+ )
277
+ self.status_controller.update(link_covers_status, notify=False)
278
+ count += self.create_custom_covers(
279
+ create_cover_paths,
280
+ library,
281
+ link_cover_pks,
282
+ )
283
+ link_covers_status = Status(
284
+ ImportStatusTypes.COVERS_LINK, 0, len(link_cover_pks)
285
+ )
286
+ self.status_controller.update(link_covers_status, notify=False)
287
+ return count
202
288
 
203
289
  def _finish_apply_status(self, library):
204
290
  """Finish all librarian statuses."""
@@ -239,13 +325,16 @@ class ComicImporterThread(
239
325
  library = Library.objects.get(pk=task.library_id)
240
326
  try:
241
327
  self._init_apply(library, task)
328
+ import_start_time = now()
242
329
 
243
330
  changed: int = 0
244
- changed += self.move_and_modify_dirs(library, task)
331
+ link_cover_pks: set[int] = set()
332
+ changed += self.move_and_modify_dirs(library, task, link_cover_pks)
245
333
 
246
334
  modified_paths = task.files_modified
247
335
  created_paths = task.files_created
248
336
  task.files_modified = task.files_created = None
337
+
249
338
  mds = {}
250
339
  m2m_mds = {}
251
340
  fks = {}
@@ -266,8 +355,13 @@ class ComicImporterThread(
266
355
  modified_paths -= fis.keys()
267
356
  created_paths -= fis.keys()
268
357
 
269
- changed += self._create_comic_relations(library, fks)
270
- fks = None
358
+ cover_paths = frozenset(task.covers_modified | task.covers_created)
359
+ task.covers_modified = task.covers_created = None
360
+
361
+ changed += self._create_comic_and_cover_relations(
362
+ library, fks, cover_paths, link_cover_pks
363
+ )
364
+ fks = cover_paths = None
271
365
 
272
366
  imported_count = self.bulk_update_comics(
273
367
  modified_paths,
@@ -282,11 +376,18 @@ class ComicImporterThread(
282
376
  m2m_mds = None
283
377
  changed += imported_count
284
378
 
379
+ self.link_custom_covers(link_cover_pks)
380
+ link_cover_pks = set()
381
+
285
382
  new_failed_imports = self.fail_imports(
286
383
  library, fis, bool(task.files_deleted)
287
384
  )
288
385
 
289
- changed += self.delete(library, task)
386
+ deleted_comic_groups = {}
387
+ changed += self.delete(library, task, deleted_comic_groups)
388
+
389
+ self.update_groups_for_covers(import_start_time, deleted_comic_groups)
390
+
290
391
  if changed:
291
392
  cache.clear()
292
393
  finally:
@@ -310,11 +411,15 @@ class ComicImporterThread(
310
411
  library_id=library_id,
311
412
  dirs_moved={},
312
413
  files_moved={},
414
+ covers_moved={},
313
415
  dirs_modified=frozenset(),
314
416
  files_modified=frozenset(paths),
417
+ covers_modified=frozenset(),
315
418
  files_created=frozenset(),
419
+ covers_created=frozenset(),
316
420
  dirs_deleted=frozenset(),
317
421
  files_deleted=frozenset(),
422
+ covers_deleted=frozenset(),
318
423
  force_import_metadata=True,
319
424
  )
320
425
  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