codex 1.4.1__py3-none-any.whl → 1.4.3__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 (237) hide show
  1. codex/applications/websocket.py +1 -1
  2. codex/librarian/importer/aggregate_metadata.py +38 -25
  3. codex/librarian/importer/clean_metadata.py +26 -13
  4. codex/librarian/importer/create_fks.py +1 -1
  5. codex/librarian/librariand.py +7 -4
  6. codex/librarian/watchdog/db_snapshot.py +1 -1
  7. codex/librarian/watchdog/event_batcherd.py +14 -11
  8. codex/librarian/watchdog/events.py +1 -1
  9. codex/logger/loggerd.py +14 -11
  10. codex/migrations/0001_init.py +3 -2
  11. codex/migrations/0002_auto_20200826_0622.py +4 -2
  12. codex/migrations/0003_auto_20200831_2033.py +4 -2
  13. codex/migrations/0004_failedimport.py +4 -2
  14. codex/migrations/0005_auto_20200918_0146.py +4 -2
  15. codex/migrations/0006_update_default_names_and_remove_duplicate_comics.py +12 -7
  16. codex/migrations/0007_auto_20211210_1710.py +3 -2
  17. codex/migrations/0008_alter_comic_created_at_alter_comic_format_and_more.py +4 -2
  18. codex/migrations/0009_alter_comic_parent_folder.py +4 -2
  19. codex/migrations/0010_haystack.py +4 -2
  20. codex/migrations/0011_library_groups_and_metadata_changes.py +3 -2
  21. codex/migrations/0012_rename_description_comic_comments.py +4 -2
  22. codex/migrations/0013_int_issue_count_longer_charfields.py +3 -3
  23. codex/migrations/0014_pdf_issue_suffix_remove_cover_image_sort_name.py +3 -2
  24. codex/migrations/0015_link_comics_to_top_level_folders.py +5 -2
  25. codex/migrations/0016_remove_comic_cover_path_librarianstatus.py +3 -3
  26. codex/migrations/0017_alter_timestamp_options_alter_adminflag_name_and_more.py +3 -3
  27. codex/migrations/0018_rename_userbookmark_bookmark.py +3 -2
  28. codex/migrations/0019_delete_queuejob.py +3 -2
  29. codex/migrations/0020_remove_search_tables.py +3 -2
  30. codex/migrations/0021_bookmark_fit_to_choices_read_in_reverse.py +3 -2
  31. codex/migrations/0022_bookmark_vertical_useractive_null_statuses.py +3 -2
  32. codex/migrations/0023_rename_credit_creator_and_more.py +3 -2
  33. codex/migrations/0024_comic_gtin_comic_story_arc_number.py +4 -2
  34. codex/migrations/0025_add_story_arc_number.py +4 -2
  35. codex/models.py +3 -4
  36. codex/search/backend.py +34 -31
  37. codex/serializers/auth.py +2 -1
  38. codex/serializers/choices.py +1 -0
  39. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js +41 -0
  40. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.br +0 -0
  41. codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.gz +0 -0
  42. codex/static_root/assets/admin-b2b56cd6.js +41 -0
  43. codex/static_root/assets/admin-b2b56cd6.js.br +0 -0
  44. codex/static_root/assets/admin-b2b56cd6.js.gz +0 -0
  45. codex/static_root/assets/{admin-drawer-panel-522f1e6c.089d70878270.js → admin-drawer-panel-efc525ec.ddab36a24e08.js} +1 -1
  46. codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.br +0 -0
  47. codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.gz +0 -0
  48. codex/static_root/assets/{admin-drawer-panel-522f1e6c.js → admin-drawer-panel-efc525ec.js} +1 -1
  49. codex/static_root/assets/admin-drawer-panel-efc525ec.js.br +0 -0
  50. codex/static_root/assets/admin-drawer-panel-efc525ec.js.gz +0 -0
  51. codex/static_root/assets/admin-f2bb1dc8.css +1 -0
  52. codex/static_root/assets/admin-f2bb1dc8.css.br +0 -0
  53. codex/static_root/assets/admin-f2bb1dc8.css.gz +0 -0
  54. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css +1 -0
  55. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.br +0 -0
  56. codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.gz +0 -0
  57. codex/static_root/assets/{browser-7f7d7134.0fe3749b0f2f.css → browser-198df919.css} +1 -1
  58. codex/static_root/assets/browser-198df919.css.br +0 -0
  59. codex/static_root/assets/browser-198df919.css.gz +0 -0
  60. codex/static_root/assets/{browser-7f7d7134.css → browser-198df919.f06301531790.css} +1 -1
  61. codex/static_root/assets/browser-198df919.f06301531790.css.br +0 -0
  62. codex/static_root/assets/browser-198df919.f06301531790.css.gz +0 -0
  63. codex/static_root/assets/browser-ca158ba5.980d652eb174.js +1 -0
  64. codex/static_root/assets/browser-ca158ba5.980d652eb174.js.br +0 -0
  65. codex/static_root/assets/browser-ca158ba5.980d652eb174.js.gz +0 -0
  66. codex/static_root/assets/browser-ca158ba5.js +1 -0
  67. codex/static_root/assets/browser-ca158ba5.js.br +0 -0
  68. codex/static_root/assets/browser-ca158ba5.js.gz +0 -0
  69. codex/static_root/assets/{http-error-5e17b794.77ceeb2d4641.js → http-error-d31fd3bd.6ab9acf65973.js} +1 -1
  70. codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.br +0 -0
  71. codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.gz +0 -0
  72. codex/static_root/assets/{http-error-5e17b794.js → http-error-d31fd3bd.js} +1 -1
  73. codex/static_root/assets/http-error-d31fd3bd.js.br +0 -0
  74. codex/static_root/assets/http-error-d31fd3bd.js.gz +0 -0
  75. codex/static_root/assets/{main-0898f4bb.181e0145c642.css → main-c11eb0f1.776522baac3b.css} +1 -1
  76. codex/static_root/assets/main-c11eb0f1.776522baac3b.css.br +0 -0
  77. codex/static_root/assets/{main-0898f4bb.181e0145c642.css.gz → main-c11eb0f1.776522baac3b.css.gz} +0 -0
  78. codex/static_root/assets/{main-0898f4bb.css → main-c11eb0f1.css} +1 -1
  79. codex/static_root/assets/main-c11eb0f1.css.br +0 -0
  80. codex/static_root/assets/{main-0898f4bb.css.gz → main-c11eb0f1.css.gz} +0 -0
  81. codex/static_root/assets/main-c5736dea.a4790dbdb569.js +1 -0
  82. codex/static_root/assets/main-c5736dea.a4790dbdb569.js.br +0 -0
  83. codex/static_root/assets/main-c5736dea.a4790dbdb569.js.gz +0 -0
  84. codex/static_root/assets/main-c5736dea.js +1 -0
  85. codex/static_root/assets/main-c5736dea.js.br +0 -0
  86. codex/static_root/assets/main-c5736dea.js.gz +0 -0
  87. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css +1 -0
  88. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.br +0 -0
  89. codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.gz +0 -0
  90. codex/static_root/assets/metadata-dialog-83c74d48.css +1 -0
  91. codex/static_root/assets/metadata-dialog-83c74d48.css.br +0 -0
  92. codex/static_root/assets/metadata-dialog-83c74d48.css.gz +0 -0
  93. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js +1 -0
  94. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.br +0 -0
  95. codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.gz +0 -0
  96. codex/static_root/assets/metadata-dialog-8c0a11ff.js +1 -0
  97. codex/static_root/assets/metadata-dialog-8c0a11ff.js.br +0 -0
  98. codex/static_root/assets/metadata-dialog-8c0a11ff.js.gz +0 -0
  99. codex/static_root/assets/{page-pdf-157ba97e.613d7c2beb77.js → page-pdf-ed976750.730244f14d16.js} +1 -1
  100. codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.br +0 -0
  101. codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.gz +0 -0
  102. codex/static_root/assets/{page-pdf-157ba97e.js → page-pdf-ed976750.js} +1 -1
  103. codex/static_root/assets/page-pdf-ed976750.js.br +0 -0
  104. codex/static_root/assets/page-pdf-ed976750.js.gz +0 -0
  105. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css +1 -0
  106. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.br +0 -0
  107. codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.gz +0 -0
  108. codex/static_root/assets/reader-5540ffcb.css +1 -0
  109. codex/static_root/assets/reader-5540ffcb.css.br +0 -0
  110. codex/static_root/assets/reader-5540ffcb.css.gz +0 -0
  111. codex/static_root/assets/reader-c562377d.7f78718f4c63.js +1 -0
  112. codex/static_root/assets/reader-c562377d.7f78718f4c63.js.br +0 -0
  113. codex/static_root/assets/reader-c562377d.7f78718f4c63.js.gz +0 -0
  114. codex/static_root/assets/reader-c562377d.js +1 -0
  115. codex/static_root/assets/reader-c562377d.js.br +0 -0
  116. codex/static_root/assets/reader-c562377d.js.gz +0 -0
  117. codex/static_root/{manifest.d2f93a519ada.json → manifest.55457ccaa01c.json} +32 -32
  118. codex/static_root/manifest.55457ccaa01c.json.br +0 -0
  119. codex/static_root/manifest.55457ccaa01c.json.gz +0 -0
  120. codex/static_root/manifest.json +32 -32
  121. codex/static_root/manifest.json.br +0 -0
  122. codex/static_root/manifest.json.gz +0 -0
  123. codex/static_root/pwa/{offline.37a4206d79f0.html → offline.7bfaf9f94bf9.html} +1 -1
  124. codex/static_root/pwa/offline.7bfaf9f94bf9.html.br +0 -0
  125. codex/static_root/pwa/offline.7bfaf9f94bf9.html.gz +0 -0
  126. codex/static_root/pwa/offline.html +1 -1
  127. codex/static_root/pwa/offline.html.br +0 -0
  128. codex/static_root/pwa/offline.html.gz +0 -0
  129. codex/static_root/staticfiles.json +1 -1
  130. codex/threads.py +1 -1
  131. codex/views/admin/api_key.py +3 -1
  132. codex/views/admin/flag.py +3 -1
  133. codex/views/admin/group.py +3 -1
  134. codex/views/admin/library.py +5 -4
  135. codex/views/admin/stats.py +10 -6
  136. codex/views/admin/tasks.py +35 -30
  137. codex/views/admin/user.py +4 -2
  138. codex/views/bookmark.py +6 -4
  139. codex/views/browser/base.py +30 -28
  140. codex/views/browser/browser.py +78 -80
  141. codex/views/browser/browser_annotations.py +15 -10
  142. codex/views/browser/browser_order_by.py +21 -16
  143. codex/views/browser/choices.py +37 -22
  144. codex/views/browser/filters/search.py +19 -16
  145. codex/views/browser/metadata.py +50 -41
  146. codex/views/cover.py +3 -1
  147. codex/views/download.py +4 -2
  148. codex/views/frontend.py +3 -2
  149. codex/views/mixins.py +13 -9
  150. codex/views/opds/authentication_v1.py +45 -41
  151. codex/views/opds/const.py +20 -13
  152. codex/views/opds/v1/entry/data.py +2 -1
  153. codex/views/opds/v1/facets.py +2 -1
  154. codex/views/opds/v1/feed.py +11 -4
  155. codex/views/opds/v1/links.py +8 -6
  156. codex/views/opds/v1/opensearch_v1.py +1 -1
  157. codex/views/opds/v2/feed.py +2 -1
  158. codex/views/opds/v2/publications.py +15 -12
  159. codex/views/reader/page.py +1 -1
  160. codex/views/session.py +50 -43
  161. codex/views/template.py +2 -2
  162. codex/websockets/listener.py +10 -7
  163. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/METADATA +24 -28
  164. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/RECORD +167 -167
  165. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/WHEEL +1 -1
  166. codex/static_root/assets/admin-12749881.ef0f50bac290.js +0 -41
  167. codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
  168. codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
  169. codex/static_root/assets/admin-12749881.js +0 -41
  170. codex/static_root/assets/admin-12749881.js.br +0 -0
  171. codex/static_root/assets/admin-12749881.js.gz +0 -0
  172. codex/static_root/assets/admin-beda768d.a614eee46307.css +0 -1
  173. codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
  174. codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
  175. codex/static_root/assets/admin-beda768d.css +0 -1
  176. codex/static_root/assets/admin-beda768d.css.br +0 -0
  177. codex/static_root/assets/admin-beda768d.css.gz +0 -0
  178. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
  179. codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
  180. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
  181. codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
  182. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
  183. codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
  184. codex/static_root/assets/browser-7f7d7134.css.br +0 -0
  185. codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
  186. codex/static_root/assets/browser-af622672.d51aca96d64d.js +0 -1
  187. codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
  188. codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
  189. codex/static_root/assets/browser-af622672.js +0 -1
  190. codex/static_root/assets/browser-af622672.js.br +0 -0
  191. codex/static_root/assets/browser-af622672.js.gz +0 -0
  192. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
  193. codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
  194. codex/static_root/assets/http-error-5e17b794.js.br +0 -0
  195. codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
  196. codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
  197. codex/static_root/assets/main-0898f4bb.css.br +0 -0
  198. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +0 -1
  199. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
  200. codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
  201. codex/static_root/assets/main-9e76a4c3.js +0 -1
  202. codex/static_root/assets/main-9e76a4c3.js.br +0 -0
  203. codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
  204. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +0 -1
  205. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
  206. codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
  207. codex/static_root/assets/metadata-dialog-62c29ce0.js +0 -1
  208. codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
  209. codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
  210. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css +0 -1
  211. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
  212. codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
  213. codex/static_root/assets/metadata-dialog-cb306ffd.css +0 -1
  214. codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
  215. codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
  216. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
  217. codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
  218. codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
  219. codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
  220. codex/static_root/assets/reader-36266549.0b2cf1291f27.js +0 -1
  221. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
  222. codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
  223. codex/static_root/assets/reader-36266549.js +0 -1
  224. codex/static_root/assets/reader-36266549.js.br +0 -0
  225. codex/static_root/assets/reader-36266549.js.gz +0 -0
  226. codex/static_root/assets/reader-7f004141.506eecc6954b.css +0 -1
  227. codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
  228. codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
  229. codex/static_root/assets/reader-7f004141.css +0 -1
  230. codex/static_root/assets/reader-7f004141.css.br +0 -0
  231. codex/static_root/assets/reader-7f004141.css.gz +0 -0
  232. codex/static_root/manifest.d2f93a519ada.json.br +0 -0
  233. codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
  234. codex/static_root/pwa/offline.37a4206d79f0.html.br +0 -0
  235. codex/static_root/pwa/offline.37a4206d79f0.html.gz +0 -0
  236. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/LICENSE +0 -0
  237. {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/entry_points.txt +0 -0
@@ -18,7 +18,7 @@ WEBSOCKET_APPLICATION = AllowedHostsOriginValidator(
18
18
  [
19
19
  path(
20
20
  f"{ROOT_PREFIX}api/v3/ws",
21
- NotifierConsumer.as_asgi(), # type: ignore
21
+ NotifierConsumer.as_asgi(),
22
22
  name="websocket",
23
23
  ),
24
24
  ]
@@ -1,5 +1,6 @@
1
1
  """Aggregate metadata from comics to prepare for importing."""
2
2
  from pathlib import Path
3
+ from types import MappingProxyType
3
4
  from zipfile import BadZipFile
4
5
 
5
6
  from comicbox.comic_archive import ComicArchive
@@ -20,9 +21,11 @@ class AggregateMetadataMixin(CleanMetadataMixin):
20
21
 
21
22
  _BROWSER_GROUPS = (Publisher, Imprint, Series, Volume)
22
23
  _BROWSER_GROUP_TREE_COUNT_FIELDS = frozenset(["volume_count", "issue_count"])
23
- _GROUP_TREES_INIT = {
24
- "group_trees": {Publisher: {}, Imprint: {}, Series: {}, Volume: {}},
25
- }
24
+ _GROUP_TREES_INIT = MappingProxyType(
25
+ {
26
+ "group_trees": {Publisher: {}, Imprint: {}, Series: {}, Volume: {}},
27
+ }
28
+ )
26
29
  _AGGREGATE_COMICBOX_CONFIG = AttrDict({**COMICBOX_CONFIG, "close_fd": False})
27
30
 
28
31
  @staticmethod
@@ -36,6 +39,36 @@ class AggregateMetadataMixin(CleanMetadataMixin):
36
39
  file_type = suffix
37
40
  return file_type
38
41
 
42
+ @classmethod
43
+ def _get_group_tree(cls, md):
44
+ """Create the group tree to counts map for a single comic."""
45
+ # Create group tree
46
+ group_tree = []
47
+ for group_cls in cls._BROWSER_GROUPS:
48
+ group_field = group_cls.__name__.lower()
49
+ # some volumes are read by ComicArchive as ints, cast
50
+ group_name = str(md.get(group_field, Publisher.DEFAULT_NAME))
51
+ # This fixes no imprint or whatever being in md
52
+ md[group_field] = group_name
53
+ group_tree.append(group_name)
54
+
55
+ # Add counts to group tree.
56
+ groups_md = {}
57
+ md_group_count_fields = cls._BROWSER_GROUP_TREE_COUNT_FIELDS & md.keys()
58
+ for key in md_group_count_fields:
59
+ groups_md[key] = md.pop(key)
60
+ return {tuple(group_tree): groups_md}
61
+
62
+ @staticmethod
63
+ def _get_m2m_metadata(md, path):
64
+ """Many_to_many fields get moved into a separate dict."""
65
+ m2m_md = {}
66
+ md_m2m_fields = COMIC_M2M_FIELD_NAMES & md.keys()
67
+ for field in md_m2m_fields:
68
+ m2m_md[field] = md.pop(field)
69
+ m2m_md["folders"] = Path(path).parents
70
+ return m2m_md
71
+
39
72
  def _get_path_metadata(self, path):
40
73
  """Get the metatada from comicbox and munge it a little."""
41
74
  md = {}
@@ -50,28 +83,8 @@ class AggregateMetadataMixin(CleanMetadataMixin):
50
83
  md["path"] = path
51
84
  md = self.clean_md(md)
52
85
 
53
- # Create group tree
54
- group_tree = []
55
- for group_cls in self._BROWSER_GROUPS:
56
- group_field = group_cls.__name__.lower()
57
- # some volumes are read by ComicArchive as ints, cast
58
- group_name = str(md.get(group_field, Publisher.DEFAULT_NAME))
59
- # This fixes no imprint or whatever being in md
60
- md[group_field] = group_name
61
- group_tree.append(group_name)
62
-
63
- # Add counts to group tree.
64
- groups_md = {}
65
- md_group_count_fields = self._BROWSER_GROUP_TREE_COUNT_FIELDS & md.keys()
66
- for key in md_group_count_fields:
67
- groups_md[key] = md.pop(key)
68
- group_tree_md[tuple(group_tree)] = groups_md
69
-
70
- # Many_to_many fields get moved into a separate dict
71
- md_m2m_fields = COMIC_M2M_FIELD_NAMES & md.keys()
72
- for field in md_m2m_fields:
73
- m2m_md[field] = md.pop(field)
74
- m2m_md["folders"] = Path(path).parents
86
+ group_tree_md = self._get_group_tree(md)
87
+ m2m_md = self._get_m2m_metadata(md, path)
75
88
 
76
89
  except (UnsupportedArchiveTypeError, BadRarFile, BadZipFile, OSError) as exc:
77
90
  self.log.warning(f"Failed to import {path}: {exc}")
@@ -2,7 +2,7 @@
2
2
  import re
3
3
  from contextlib import suppress
4
4
  from decimal import Decimal
5
- from typing import Any, Optional
5
+ from typing import Any, Optional, Union
6
6
 
7
7
  from comicbox.metadata.comic_base import ComicBaseMetadata
8
8
  from django.db.models.fields import CharField, DecimalField, PositiveSmallIntegerField
@@ -54,7 +54,6 @@ _MD_CHAR_KEYS = frozenset(
54
54
  _TWO_PLACES = Decimal("0.01")
55
55
  _PSI_MAX = 2147483647
56
56
  _GROUPS = frozenset(("publisher", "imprint", "series", "volume"))
57
- _URL_MAX_LENGTH = 200
58
57
  _M2M_NAMED_KEYS = frozenset(
59
58
  (
60
59
  "characters",
@@ -74,10 +73,21 @@ class CleanMetadataMixin(QueuedThread):
74
73
  """Clean metadata before importing."""
75
74
 
76
75
  @staticmethod
77
- def _clean_decimal(value, field_name: str):
76
+ def _clean_string(value: Union[bytes, str]):
77
+ """Replace unstorable, unprintable characters from metadata."""
78
+ # https://stackoverflow.com/questions/27366479/python-3-os-walk-file-paths-unicodeencodeerror-utf-8-codec-cant-encode-s
79
+ if isinstance(value, str):
80
+ value = value.encode("utf8", "replace")
81
+
82
+ return value.decode("utf8", "replace")
83
+
84
+ @classmethod
85
+ def _clean_decimal(cls, value, field_name: str):
78
86
  field: DecimalField = Comic._meta.get_field(field_name) # type: ignore
79
87
  try:
80
88
  # Comicbox now gives issues as strings, convert them to decimal here.
89
+ if isinstance(value, (str, bytes)):
90
+ value = cls._clean_string(value)
81
91
  value = ComicBaseMetadata.parse_decimal(value)
82
92
  value = value.quantize(_TWO_PLACES)
83
93
  value = value.max(_DECIMAL_ZERO)
@@ -89,7 +99,9 @@ class CleanMetadataMixin(QueuedThread):
89
99
 
90
100
  def _parse_comic_issue(self, md: dict[str, Any]):
91
101
  """Parse the issue field."""
92
- issue_str = md.get("issue", "").strip()
102
+ issue_str = md.get("issue", "")
103
+ issue_str = self._clean_string(issue_str)
104
+ issue_str = issue_str.strip()
93
105
  try:
94
106
  match = _PARSE_ISSUE_MATCHER.match(issue_str)
95
107
  issue, issue_suffix = match.groups() # type: ignore
@@ -151,13 +163,14 @@ class CleanMetadataMixin(QueuedThread):
151
163
  with suppress(KeyError):
152
164
  md["name"] = md.pop("title")
153
165
 
154
- @staticmethod
155
- def _clean_charfield(value: Optional[str], field: CharField) -> Optional[str]:
166
+ @classmethod
167
+ def _clean_charfield(cls, value: Optional[str], field: CharField) -> Optional[str]:
156
168
  try:
157
169
  if value is None:
158
170
  raise ValueError # noqa TRY301
159
171
  value = str(value)
160
172
  value = value[: field.max_length].strip()
173
+ value = cls._clean_string(value)
161
174
  except Exception:
162
175
  value = None if field.null else ""
163
176
  return value
@@ -202,14 +215,14 @@ class CleanMetadataMixin(QueuedThread):
202
215
  good_story_arc_numbers[good_story_arc_name] = number
203
216
  md["story_arc_numbers"] = good_story_arc_numbers
204
217
 
205
- @staticmethod
206
- def _clean_comic_web(md):
218
+ @classmethod
219
+ def _clean_comic_web(cls, md):
207
220
  """URL field is a special charfield."""
208
- if "web" not in md:
209
- return
210
- try:
211
- md["web"] = md["web"][:_URL_MAX_LENGTH]
212
- except Exception:
221
+ web = md.get("web")
222
+ if web:
223
+ field: CharField = Comic._meta.get_field("web") # type:ignore
224
+ web = cls._clean_charfield(web, field)
225
+ if not web and web in md:
213
226
  del md["web"]
214
227
 
215
228
  @classmethod
@@ -161,7 +161,7 @@ class CreateForeignKeysMixin(QueuedThread):
161
161
  parent = Folder.objects.get(path=parent_path)
162
162
  except Folder.DoesNotExist:
163
163
  if path.parent != Path(library.path):
164
- self.log.exception(
164
+ self.log.warning(
165
165
  f"Can't find parent folder {parent_path}"
166
166
  f" for {path} in library {library.path}"
167
167
  )
@@ -2,6 +2,7 @@
2
2
  from collections import namedtuple
3
3
  from multiprocessing import Manager, Process
4
4
  from threading import active_count
5
+ from types import MappingProxyType
5
6
 
6
7
  from caseconverter import snakecase
7
8
  from comicbox.comic_archive import ComicArchive
@@ -53,10 +54,12 @@ class LibrarianDaemon(Process, LoggerBaseMixin):
53
54
  LibraryPollingObserver,
54
55
  JanitorThread,
55
56
  )
56
- _THREAD_CLASS_MAP = {
57
- snakecase(thread_class.__name__): thread_class
58
- for thread_class in _THREAD_CLASSES
59
- }
57
+ _THREAD_CLASS_MAP = MappingProxyType(
58
+ {
59
+ snakecase(thread_class.__name__): thread_class
60
+ for thread_class in _THREAD_CLASSES
61
+ }
62
+ )
60
63
  LibrarianThreads = namedtuple("LibrarianThreads", _THREAD_CLASS_MAP.keys())
61
64
 
62
65
  proc = None
@@ -41,7 +41,7 @@ class CodexDatabaseSnapshot(DirectorySnapshot, LoggerBaseMixin):
41
41
  f"Force delete missing path with missing db stat: {path}"
42
42
  )
43
43
  # This will trigger a deleted event
44
- stat = Comic.ZERO_STAT
44
+ stat = list(Comic.ZERO_STAT)
45
45
 
46
46
  if force:
47
47
  # Fake mtime will trigger modified event
@@ -7,6 +7,7 @@ and then re-serialize everything in this batcher and the event Handler
7
7
  """
8
8
  from contextlib import suppress
9
9
  from copy import deepcopy
10
+ from types import MappingProxyType
10
11
 
11
12
  from watchdog.events import EVENT_TYPE_MOVED
12
13
 
@@ -19,16 +20,18 @@ class WatchdogEventBatcherThread(AggregateMessageQueuedThread):
19
20
  """Batch watchdog events into bulk database tasks."""
20
21
 
21
22
  CLS_SUFFIX = -len("Event")
22
- DBDIFF_TASK_PARAMS = {
23
- "library_id": None,
24
- "dirs_moved": {},
25
- "files_moved": {},
26
- "files_modified": set(),
27
- "files_created": set(),
28
- "dirs_deleted": set(),
29
- "dirs_modified": set(),
30
- "files_deleted": set(),
31
- }
23
+ DBDIFF_TASK_PARAMS = MappingProxyType(
24
+ {
25
+ "library_id": None,
26
+ "dirs_moved": {},
27
+ "files_moved": {},
28
+ "files_modified": set(),
29
+ "files_created": set(),
30
+ "dirs_deleted": set(),
31
+ "dirs_modified": set(),
32
+ "files_deleted": set(),
33
+ }
34
+ )
32
35
  MAX_DELAY = 60
33
36
  MAX_ITEMS_PER_GB = 50000
34
37
 
@@ -42,7 +45,7 @@ class WatchdogEventBatcherThread(AggregateMessageQueuedThread):
42
45
  def _ensure_library_args(self, library_id):
43
46
  if library_id in self.cache:
44
47
  return
45
- args = deepcopy(self.DBDIFF_TASK_PARAMS)
48
+ args = deepcopy(dict(self.DBDIFF_TASK_PARAMS))
46
49
  args["library_id"] = library_id
47
50
  self.cache[library_id] = args
48
51
 
@@ -19,7 +19,7 @@ from codex.logger_base import LoggerBaseMixin
19
19
  class CodexLibraryEventHandler(FileSystemEventHandler, LoggerBaseMixin):
20
20
  """Handle watchdog events for comics in a library."""
21
21
 
22
- IGNORED_EVENTS = {EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED}
22
+ IGNORED_EVENTS = frozenset((EVENT_TYPE_CLOSED, EVENT_TYPE_OPENED))
23
23
 
24
24
  def __init__(self, library, *args, **kwargs):
25
25
  """Let us send along he library id."""
codex/logger/loggerd.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Central Logging Thread."""
2
2
  import logging
3
3
  from logging.handlers import QueueListener, RotatingFileHandler
4
+ from types import MappingProxyType
4
5
 
5
6
  from colors import color
6
7
 
@@ -10,28 +11,30 @@ from codex.settings.settings import DEBUG, LOG_DIR, LOG_TO_CONSOLE, LOG_TO_FILE
10
11
  class ColorFormatter(logging.Formatter):
11
12
  """Logging Formatter to add colors and count warning / errors."""
12
13
 
13
- FORMAT_COLORS = {
14
- "CRITICAL": {"fg": "red", "style": "bold"},
15
- "ERROR": {"fg": "red"},
16
- "WARNING": {"fg": "yellow"},
17
- "INFO": {"fg": "green"},
18
- "DEBUG": {"fg": "black", "style": "bold"},
19
- "NOTSET": {"fg": "blue"},
20
- }
21
- FORMATTERS = {}
14
+ FORMAT_COLORS = MappingProxyType(
15
+ {
16
+ "CRITICAL": {"fg": "red", "style": "bold"},
17
+ "ERROR": {"fg": "red"},
18
+ "WARNING": {"fg": "yellow"},
19
+ "INFO": {"fg": "green"},
20
+ "DEBUG": {"fg": "black", "style": "bold"},
21
+ "NOTSET": {"fg": "blue"},
22
+ }
23
+ )
22
24
 
23
25
  def __init__(self, fmt, **kwargs):
24
26
  """Set up the FORMATS dict."""
25
27
  super().__init__(**kwargs)
28
+ self.formatters = {}
26
29
  for level_name, args in self.FORMAT_COLORS.items():
27
30
  levelno = getattr(logging, level_name)
28
31
  template = color(fmt, **args)
29
32
  formatter = logging.Formatter(fmt=template, **kwargs)
30
- self.FORMATTERS[levelno] = formatter
33
+ self.formatters[levelno] = formatter
31
34
 
32
35
  def format(self, record): # noqa A003
33
36
  """Format each log message."""
34
- formatter = self.FORMATTERS[record.levelno]
37
+ formatter = self.formatters[record.levelno]
35
38
  return formatter.format(record)
36
39
 
37
40
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import datetime
4
4
  from decimal import Decimal
5
+ from typing import ClassVar
5
6
 
6
7
  import django.db.models.deletion
7
8
  from django.conf import settings
@@ -15,12 +16,12 @@ class Migration(migrations.Migration):
15
16
 
16
17
  initial = True
17
18
 
18
- dependencies = [
19
+ dependencies: ClassVar[list] = [
19
20
  ("sessions", "0001_initial"),
20
21
  migrations.swappable_dependency(settings.AUTH_USER_MODEL),
21
22
  ]
22
23
 
23
- operations = [
24
+ operations: ClassVar[list] = [
24
25
  migrations.CreateModel(
25
26
  name="Character",
26
27
  fields=[
@@ -1,16 +1,18 @@
1
1
  """Generated by Django 3.1 on 2020-08-26 06:22."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  from django.db import migrations
4
6
 
5
7
 
6
8
  class Migration(migrations.Migration):
7
9
  """Change libraries verbose name."""
8
10
 
9
- dependencies = [
11
+ dependencies: ClassVar[list] = [
10
12
  ("codex", "0001_init"),
11
13
  ]
12
14
 
13
- operations = [
15
+ operations: ClassVar[list] = [
14
16
  migrations.AlterModelOptions(
15
17
  name="library",
16
18
  options={"verbose_name_plural": "libraries"},
@@ -1,5 +1,7 @@
1
1
  """Generated by Django 3.1 on 2020-08-31 20:33."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  import django.db.models.deletion
4
6
  from django.db import migrations, models
5
7
 
@@ -7,11 +9,11 @@ from django.db import migrations, models
7
9
  class Migration(migrations.Migration):
8
10
  """Credit roles can be none."""
9
11
 
10
- dependencies = [
12
+ dependencies: ClassVar[list] = [
11
13
  ("codex", "0002_auto_20200826_0622"),
12
14
  ]
13
15
 
14
- operations = [
16
+ operations: ClassVar[list] = [
15
17
  migrations.AlterField(
16
18
  model_name="credit",
17
19
  name="role",
@@ -1,5 +1,7 @@
1
1
  """Generated by Django 3.1.1 on 2020-09-14 22:15."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  import django.db.models.deletion
4
6
  from django.db import migrations, models
5
7
 
@@ -7,11 +9,11 @@ from django.db import migrations, models
7
9
  class Migration(migrations.Migration):
8
10
  """Keep track of failed imports."""
9
11
 
10
- dependencies = [
12
+ dependencies: ClassVar[list] = [
11
13
  ("codex", "0003_auto_20200831_2033"),
12
14
  ]
13
15
 
14
- operations = [
16
+ operations: ClassVar[list] = [
15
17
  migrations.CreateModel(
16
18
  name="FailedImport",
17
19
  fields=[
@@ -1,16 +1,18 @@
1
1
  """Generated by Django 3.1.1 on 2020-09-18 01:46."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  from django.db import migrations
4
6
 
5
7
 
6
8
  class Migration(migrations.Migration):
7
9
  """Update verbose names."""
8
10
 
9
- dependencies = [
11
+ dependencies: ClassVar[list] = [
10
12
  ("codex", "0004_failedimport"),
11
13
  ]
12
14
 
13
- operations = [
15
+ operations: ClassVar[list] = [
14
16
  migrations.AlterModelOptions(
15
17
  name="comic",
16
18
  options={"verbose_name": "Issue"},
@@ -1,13 +1,18 @@
1
1
  """Generated by Django 3.2.9 on 2021-11-04 03:03."""
2
2
 
3
+ from types import MappingProxyType
4
+ from typing import ClassVar
5
+
3
6
  from django.db import migrations
4
7
  from django.db.models.functions import Now
5
8
 
6
- MODEL_NAMES = {
7
- "Series": "Default Series",
8
- "Imprint": "Main Imprint",
9
- "Publisher": "No Publisher",
10
- }
9
+ MODEL_NAMES = MappingProxyType(
10
+ {
11
+ "Series": "Default Series",
12
+ "Imprint": "Main Imprint",
13
+ "Publisher": "No Publisher",
14
+ }
15
+ )
11
16
  NEW_DEFAULT_NAME = ""
12
17
  UPDATE_FIELDS = ("stat", "updated_at")
13
18
 
@@ -58,11 +63,11 @@ def remove_duplicate_comics(apps, _schema_editor):
58
63
  class Migration(migrations.Migration):
59
64
  """Change default names to ''."""
60
65
 
61
- dependencies = [
66
+ dependencies: ClassVar[list] = [
62
67
  ("codex", "0005_auto_20200918_0146"),
63
68
  ]
64
69
 
65
- operations = [
70
+ operations: ClassVar[list] = [
66
71
  migrations.RunPython(update_default_names),
67
72
  migrations.RunPython(remove_duplicate_comics),
68
73
  ]
@@ -1,6 +1,7 @@
1
1
  """Generated by Django 3.2.9 on 2021-12-11 01:10."""
2
2
 
3
3
  import datetime
4
+ from typing import ClassVar
4
5
 
5
6
  import django.db.models.deletion
6
7
  from django.db import migrations, models
@@ -9,11 +10,11 @@ from django.db import migrations, models
9
10
  class Migration(migrations.Migration):
10
11
  """Large migration for v0.7.0."""
11
12
 
12
- dependencies = [
13
+ dependencies: ClassVar[list] = [
13
14
  ("codex", "0006_update_default_names_and_remove_duplicate_comics"),
14
15
  ]
15
16
 
16
- operations = [
17
+ operations: ClassVar[list] = [
17
18
  migrations.CreateModel(
18
19
  name="LatestVersion",
19
20
  fields=[
@@ -1,5 +1,7 @@
1
1
  """Generated by Django 4.0 on 2021-12-17 04:36."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  import django.db.models.deletion
4
6
  from django.db import migrations, models
5
7
 
@@ -7,11 +9,11 @@ from django.db import migrations, models
7
9
  class Migration(migrations.Migration):
8
10
  """Indexes for filtered and sorted comic fields."""
9
11
 
10
- dependencies = [
12
+ dependencies: ClassVar[list] = [
11
13
  ("codex", "0007_auto_20211210_1710"),
12
14
  ]
13
15
 
14
- operations = [
16
+ operations: ClassVar[list] = [
15
17
  migrations.AlterField(
16
18
  model_name="comic",
17
19
  name="created_at",
@@ -1,5 +1,7 @@
1
1
  """Generated by Django 4.0 on 2021-12-19 17:47."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  import django.db.models.deletion
4
6
  from django.db import migrations, models
5
7
 
@@ -7,11 +9,11 @@ from django.db import migrations, models
7
9
  class Migration(migrations.Migration):
8
10
  """Override related_in becauese interferes with comic.folders."""
9
11
 
10
- dependencies = [
12
+ dependencies: ClassVar[list] = [
11
13
  ("codex", "0008_alter_comic_created_at_alter_comic_format_and_more"),
12
14
  ]
13
15
 
14
- operations = [
16
+ operations: ClassVar[list] = [
15
17
  migrations.AlterField(
16
18
  model_name="comic",
17
19
  name="parent_folder",
@@ -1,5 +1,7 @@
1
1
  """Generated by Django 4.0.1 on 2022-01-16 05:31."""
2
2
 
3
+ from typing import ClassVar
4
+
3
5
  import django.db.models.deletion
4
6
  from django.db import migrations, models
5
7
 
@@ -7,11 +9,11 @@ from django.db import migrations, models
7
9
  class Migration(migrations.Migration):
8
10
  """Haystack search engine."""
9
11
 
10
- dependencies = [
12
+ dependencies: ClassVar[list] = [
11
13
  ("codex", "0009_alter_comic_parent_folder"),
12
14
  ]
13
15
 
14
- operations = [
16
+ operations: ClassVar[list] = [
15
17
  migrations.CreateModel(
16
18
  # Fake model. Not managed.
17
19
  name="QueueJob",
@@ -1,5 +1,6 @@
1
1
  """Generated by Django 4.0.1 on 2022-01-31 22:09 & tweaked by aj."""
2
2
  from decimal import Decimal
3
+ from typing import ClassVar
3
4
 
4
5
  from django.db import migrations, models
5
6
 
@@ -26,12 +27,12 @@ def critical_rating_to_decimal(apps, _schema_editor):
26
27
  class Migration(migrations.Migration):
27
28
  """Library group ACLS and metadata changes."""
28
29
 
29
- dependencies = [
30
+ dependencies: ClassVar[list] = [
30
31
  ("auth", "0012_alter_user_first_name_max_length"),
31
32
  ("codex", "0010_haystack"),
32
33
  ]
33
34
 
34
- operations = [
35
+ operations: ClassVar[list] = [
35
36
  migrations.AddField(
36
37
  model_name="comic",
37
38
  name="community_rating",
@@ -1,15 +1,17 @@
1
1
  """Generated by Django 4.0.2 on 2022-02-24 20:58."""
2
+ from typing import ClassVar
3
+
2
4
  from django.db import migrations
3
5
 
4
6
 
5
7
  class Migration(migrations.Migration):
6
8
  """Rename comic description to comic comments."""
7
9
 
8
- dependencies = [
10
+ dependencies: ClassVar[list] = [
9
11
  ("codex", "0011_library_groups_and_metadata_changes"),
10
12
  ]
11
13
 
12
- operations = [
14
+ operations: ClassVar[list] = [
13
15
  migrations.RenameField(
14
16
  model_name="comic",
15
17
  old_name="description",
@@ -1,6 +1,6 @@
1
1
  """Generated by Django 4.0.2 on 2022-03-25 23:16."""
2
-
3
2
  from decimal import Decimal
3
+ from typing import ClassVar
4
4
 
5
5
  from django.db import migrations, models
6
6
 
@@ -28,11 +28,11 @@ def cast_issue_count(apps, _schema_editor):
28
28
  class Migration(migrations.Migration):
29
29
  """Larger valid fields."""
30
30
 
31
- dependencies = [
31
+ dependencies: ClassVar[list] = [
32
32
  ("codex", "0012_rename_description_comic_comments"),
33
33
  ]
34
34
 
35
- operations = [
35
+ operations: ClassVar[list] = [
36
36
  migrations.RenameField(
37
37
  model_name="volume", old_name="issue_count", new_name="issue_count_decimal"
38
38
  ),