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.
- codex/applications/websocket.py +1 -1
- codex/librarian/importer/aggregate_metadata.py +38 -25
- codex/librarian/importer/clean_metadata.py +26 -13
- codex/librarian/importer/create_fks.py +1 -1
- codex/librarian/librariand.py +7 -4
- codex/librarian/watchdog/db_snapshot.py +1 -1
- codex/librarian/watchdog/event_batcherd.py +14 -11
- codex/librarian/watchdog/events.py +1 -1
- codex/logger/loggerd.py +14 -11
- codex/migrations/0001_init.py +3 -2
- codex/migrations/0002_auto_20200826_0622.py +4 -2
- codex/migrations/0003_auto_20200831_2033.py +4 -2
- codex/migrations/0004_failedimport.py +4 -2
- codex/migrations/0005_auto_20200918_0146.py +4 -2
- codex/migrations/0006_update_default_names_and_remove_duplicate_comics.py +12 -7
- codex/migrations/0007_auto_20211210_1710.py +3 -2
- codex/migrations/0008_alter_comic_created_at_alter_comic_format_and_more.py +4 -2
- codex/migrations/0009_alter_comic_parent_folder.py +4 -2
- codex/migrations/0010_haystack.py +4 -2
- codex/migrations/0011_library_groups_and_metadata_changes.py +3 -2
- codex/migrations/0012_rename_description_comic_comments.py +4 -2
- codex/migrations/0013_int_issue_count_longer_charfields.py +3 -3
- codex/migrations/0014_pdf_issue_suffix_remove_cover_image_sort_name.py +3 -2
- codex/migrations/0015_link_comics_to_top_level_folders.py +5 -2
- codex/migrations/0016_remove_comic_cover_path_librarianstatus.py +3 -3
- codex/migrations/0017_alter_timestamp_options_alter_adminflag_name_and_more.py +3 -3
- codex/migrations/0018_rename_userbookmark_bookmark.py +3 -2
- codex/migrations/0019_delete_queuejob.py +3 -2
- codex/migrations/0020_remove_search_tables.py +3 -2
- codex/migrations/0021_bookmark_fit_to_choices_read_in_reverse.py +3 -2
- codex/migrations/0022_bookmark_vertical_useractive_null_statuses.py +3 -2
- codex/migrations/0023_rename_credit_creator_and_more.py +3 -2
- codex/migrations/0024_comic_gtin_comic_story_arc_number.py +4 -2
- codex/migrations/0025_add_story_arc_number.py +4 -2
- codex/models.py +3 -4
- codex/search/backend.py +34 -31
- codex/serializers/auth.py +2 -1
- codex/serializers/choices.py +1 -0
- codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js +41 -0
- codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.br +0 -0
- codex/static_root/assets/admin-b2b56cd6.f68d07d2bf93.js.gz +0 -0
- codex/static_root/assets/admin-b2b56cd6.js +41 -0
- codex/static_root/assets/admin-b2b56cd6.js.br +0 -0
- codex/static_root/assets/admin-b2b56cd6.js.gz +0 -0
- codex/static_root/assets/{admin-drawer-panel-522f1e6c.089d70878270.js → admin-drawer-panel-efc525ec.ddab36a24e08.js} +1 -1
- codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-efc525ec.ddab36a24e08.js.gz +0 -0
- codex/static_root/assets/{admin-drawer-panel-522f1e6c.js → admin-drawer-panel-efc525ec.js} +1 -1
- codex/static_root/assets/admin-drawer-panel-efc525ec.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-efc525ec.js.gz +0 -0
- codex/static_root/assets/admin-f2bb1dc8.css +1 -0
- codex/static_root/assets/admin-f2bb1dc8.css.br +0 -0
- codex/static_root/assets/admin-f2bb1dc8.css.gz +0 -0
- codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css +1 -0
- codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.br +0 -0
- codex/static_root/assets/admin-f2bb1dc8.ecec18791c01.css.gz +0 -0
- codex/static_root/assets/{browser-7f7d7134.0fe3749b0f2f.css → browser-198df919.css} +1 -1
- codex/static_root/assets/browser-198df919.css.br +0 -0
- codex/static_root/assets/browser-198df919.css.gz +0 -0
- codex/static_root/assets/{browser-7f7d7134.css → browser-198df919.f06301531790.css} +1 -1
- codex/static_root/assets/browser-198df919.f06301531790.css.br +0 -0
- codex/static_root/assets/browser-198df919.f06301531790.css.gz +0 -0
- codex/static_root/assets/browser-ca158ba5.980d652eb174.js +1 -0
- codex/static_root/assets/browser-ca158ba5.980d652eb174.js.br +0 -0
- codex/static_root/assets/browser-ca158ba5.980d652eb174.js.gz +0 -0
- codex/static_root/assets/browser-ca158ba5.js +1 -0
- codex/static_root/assets/browser-ca158ba5.js.br +0 -0
- codex/static_root/assets/browser-ca158ba5.js.gz +0 -0
- codex/static_root/assets/{http-error-5e17b794.77ceeb2d4641.js → http-error-d31fd3bd.6ab9acf65973.js} +1 -1
- codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.br +0 -0
- codex/static_root/assets/http-error-d31fd3bd.6ab9acf65973.js.gz +0 -0
- codex/static_root/assets/{http-error-5e17b794.js → http-error-d31fd3bd.js} +1 -1
- codex/static_root/assets/http-error-d31fd3bd.js.br +0 -0
- codex/static_root/assets/http-error-d31fd3bd.js.gz +0 -0
- codex/static_root/assets/{main-0898f4bb.181e0145c642.css → main-c11eb0f1.776522baac3b.css} +1 -1
- codex/static_root/assets/main-c11eb0f1.776522baac3b.css.br +0 -0
- codex/static_root/assets/{main-0898f4bb.181e0145c642.css.gz → main-c11eb0f1.776522baac3b.css.gz} +0 -0
- codex/static_root/assets/{main-0898f4bb.css → main-c11eb0f1.css} +1 -1
- codex/static_root/assets/main-c11eb0f1.css.br +0 -0
- codex/static_root/assets/{main-0898f4bb.css.gz → main-c11eb0f1.css.gz} +0 -0
- codex/static_root/assets/main-c5736dea.a4790dbdb569.js +1 -0
- codex/static_root/assets/main-c5736dea.a4790dbdb569.js.br +0 -0
- codex/static_root/assets/main-c5736dea.a4790dbdb569.js.gz +0 -0
- codex/static_root/assets/main-c5736dea.js +1 -0
- codex/static_root/assets/main-c5736dea.js.br +0 -0
- codex/static_root/assets/main-c5736dea.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css +1 -0
- codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.br +0 -0
- codex/static_root/assets/metadata-dialog-83c74d48.b5cccc13c737.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-83c74d48.css +1 -0
- codex/static_root/assets/metadata-dialog-83c74d48.css.br +0 -0
- codex/static_root/assets/metadata-dialog-83c74d48.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js +1 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.b281b7635db5.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.js +1 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8c0a11ff.js.gz +0 -0
- codex/static_root/assets/{page-pdf-157ba97e.613d7c2beb77.js → page-pdf-ed976750.730244f14d16.js} +1 -1
- codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.br +0 -0
- codex/static_root/assets/page-pdf-ed976750.730244f14d16.js.gz +0 -0
- codex/static_root/assets/{page-pdf-157ba97e.js → page-pdf-ed976750.js} +1 -1
- codex/static_root/assets/page-pdf-ed976750.js.br +0 -0
- codex/static_root/assets/page-pdf-ed976750.js.gz +0 -0
- codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css +1 -0
- codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.br +0 -0
- codex/static_root/assets/reader-5540ffcb.8ea3c63a3154.css.gz +0 -0
- codex/static_root/assets/reader-5540ffcb.css +1 -0
- codex/static_root/assets/reader-5540ffcb.css.br +0 -0
- codex/static_root/assets/reader-5540ffcb.css.gz +0 -0
- codex/static_root/assets/reader-c562377d.7f78718f4c63.js +1 -0
- codex/static_root/assets/reader-c562377d.7f78718f4c63.js.br +0 -0
- codex/static_root/assets/reader-c562377d.7f78718f4c63.js.gz +0 -0
- codex/static_root/assets/reader-c562377d.js +1 -0
- codex/static_root/assets/reader-c562377d.js.br +0 -0
- codex/static_root/assets/reader-c562377d.js.gz +0 -0
- codex/static_root/{manifest.d2f93a519ada.json → manifest.55457ccaa01c.json} +32 -32
- codex/static_root/manifest.55457ccaa01c.json.br +0 -0
- codex/static_root/manifest.55457ccaa01c.json.gz +0 -0
- codex/static_root/manifest.json +32 -32
- codex/static_root/manifest.json.br +0 -0
- codex/static_root/manifest.json.gz +0 -0
- codex/static_root/pwa/{offline.37a4206d79f0.html → offline.7bfaf9f94bf9.html} +1 -1
- codex/static_root/pwa/offline.7bfaf9f94bf9.html.br +0 -0
- codex/static_root/pwa/offline.7bfaf9f94bf9.html.gz +0 -0
- codex/static_root/pwa/offline.html +1 -1
- codex/static_root/pwa/offline.html.br +0 -0
- codex/static_root/pwa/offline.html.gz +0 -0
- codex/static_root/staticfiles.json +1 -1
- codex/threads.py +1 -1
- codex/views/admin/api_key.py +3 -1
- codex/views/admin/flag.py +3 -1
- codex/views/admin/group.py +3 -1
- codex/views/admin/library.py +5 -4
- codex/views/admin/stats.py +10 -6
- codex/views/admin/tasks.py +35 -30
- codex/views/admin/user.py +4 -2
- codex/views/bookmark.py +6 -4
- codex/views/browser/base.py +30 -28
- codex/views/browser/browser.py +78 -80
- codex/views/browser/browser_annotations.py +15 -10
- codex/views/browser/browser_order_by.py +21 -16
- codex/views/browser/choices.py +37 -22
- codex/views/browser/filters/search.py +19 -16
- codex/views/browser/metadata.py +50 -41
- codex/views/cover.py +3 -1
- codex/views/download.py +4 -2
- codex/views/frontend.py +3 -2
- codex/views/mixins.py +13 -9
- codex/views/opds/authentication_v1.py +45 -41
- codex/views/opds/const.py +20 -13
- codex/views/opds/v1/entry/data.py +2 -1
- codex/views/opds/v1/facets.py +2 -1
- codex/views/opds/v1/feed.py +11 -4
- codex/views/opds/v1/links.py +8 -6
- codex/views/opds/v1/opensearch_v1.py +1 -1
- codex/views/opds/v2/feed.py +2 -1
- codex/views/opds/v2/publications.py +15 -12
- codex/views/reader/page.py +1 -1
- codex/views/session.py +50 -43
- codex/views/template.py +2 -2
- codex/websockets/listener.py +10 -7
- {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/METADATA +24 -28
- {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/RECORD +167 -167
- {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/WHEEL +1 -1
- codex/static_root/assets/admin-12749881.ef0f50bac290.js +0 -41
- codex/static_root/assets/admin-12749881.ef0f50bac290.js.br +0 -0
- codex/static_root/assets/admin-12749881.ef0f50bac290.js.gz +0 -0
- codex/static_root/assets/admin-12749881.js +0 -41
- codex/static_root/assets/admin-12749881.js.br +0 -0
- codex/static_root/assets/admin-12749881.js.gz +0 -0
- codex/static_root/assets/admin-beda768d.a614eee46307.css +0 -1
- codex/static_root/assets/admin-beda768d.a614eee46307.css.br +0 -0
- codex/static_root/assets/admin-beda768d.a614eee46307.css.gz +0 -0
- codex/static_root/assets/admin-beda768d.css +0 -1
- codex/static_root/assets/admin-beda768d.css.br +0 -0
- codex/static_root/assets/admin-beda768d.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.js.gz +0 -0
- codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.br +0 -0
- codex/static_root/assets/browser-7f7d7134.0fe3749b0f2f.css.gz +0 -0
- codex/static_root/assets/browser-7f7d7134.css.br +0 -0
- codex/static_root/assets/browser-7f7d7134.css.gz +0 -0
- codex/static_root/assets/browser-af622672.d51aca96d64d.js +0 -1
- codex/static_root/assets/browser-af622672.d51aca96d64d.js.br +0 -0
- codex/static_root/assets/browser-af622672.d51aca96d64d.js.gz +0 -0
- codex/static_root/assets/browser-af622672.js +0 -1
- codex/static_root/assets/browser-af622672.js.br +0 -0
- codex/static_root/assets/browser-af622672.js.gz +0 -0
- codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.br +0 -0
- codex/static_root/assets/http-error-5e17b794.77ceeb2d4641.js.gz +0 -0
- codex/static_root/assets/http-error-5e17b794.js.br +0 -0
- codex/static_root/assets/http-error-5e17b794.js.gz +0 -0
- codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
- codex/static_root/assets/main-0898f4bb.css.br +0 -0
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js +0 -1
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.br +0 -0
- codex/static_root/assets/main-9e76a4c3.6844a407d14c.js.gz +0 -0
- codex/static_root/assets/main-9e76a4c3.js +0 -1
- codex/static_root/assets/main-9e76a4c3.js.br +0 -0
- codex/static_root/assets/main-9e76a4c3.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js +0 -1
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.br +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.8418785c0453.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.js +0 -1
- codex/static_root/assets/metadata-dialog-62c29ce0.js.br +0 -0
- codex/static_root/assets/metadata-dialog-62c29ce0.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css +0 -1
- codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.br +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.cc304996d7bb.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.css +0 -1
- codex/static_root/assets/metadata-dialog-cb306ffd.css.br +0 -0
- codex/static_root/assets/metadata-dialog-cb306ffd.css.gz +0 -0
- codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.br +0 -0
- codex/static_root/assets/page-pdf-157ba97e.613d7c2beb77.js.gz +0 -0
- codex/static_root/assets/page-pdf-157ba97e.js.br +0 -0
- codex/static_root/assets/page-pdf-157ba97e.js.gz +0 -0
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js +0 -1
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js.br +0 -0
- codex/static_root/assets/reader-36266549.0b2cf1291f27.js.gz +0 -0
- codex/static_root/assets/reader-36266549.js +0 -1
- codex/static_root/assets/reader-36266549.js.br +0 -0
- codex/static_root/assets/reader-36266549.js.gz +0 -0
- codex/static_root/assets/reader-7f004141.506eecc6954b.css +0 -1
- codex/static_root/assets/reader-7f004141.506eecc6954b.css.br +0 -0
- codex/static_root/assets/reader-7f004141.506eecc6954b.css.gz +0 -0
- codex/static_root/assets/reader-7f004141.css +0 -1
- codex/static_root/assets/reader-7f004141.css.br +0 -0
- codex/static_root/assets/reader-7f004141.css.gz +0 -0
- codex/static_root/manifest.d2f93a519ada.json.br +0 -0
- codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
- codex/static_root/pwa/offline.37a4206d79f0.html.br +0 -0
- codex/static_root/pwa/offline.37a4206d79f0.html.gz +0 -0
- {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/LICENSE +0 -0
- {codex-1.4.1.dist-info → codex-1.4.3.dist-info}/entry_points.txt +0 -0
|
@@ -3,6 +3,7 @@ from django.db.models import Q
|
|
|
3
3
|
from haystack.query import SearchQuerySet
|
|
4
4
|
|
|
5
5
|
from codex.logger.logging import get_logger
|
|
6
|
+
from codex.models import Comic
|
|
6
7
|
|
|
7
8
|
LOG = get_logger(__name__)
|
|
8
9
|
|
|
@@ -10,8 +11,17 @@ LOG = get_logger(__name__)
|
|
|
10
11
|
class SearchFilterMixin:
|
|
11
12
|
"""Search Filters Methods."""
|
|
12
13
|
|
|
13
|
-
def
|
|
14
|
+
def get_search_scores(self) -> dict:
|
|
14
15
|
"""Perform the search and return the scores as a dict."""
|
|
16
|
+
search_scores = {}
|
|
17
|
+
text = self.params.get("q", "") # type: ignore
|
|
18
|
+
if not text:
|
|
19
|
+
# for opds 2
|
|
20
|
+
text = self.params.get("query", "") # type: ignore
|
|
21
|
+
text = text.strip()
|
|
22
|
+
if not text:
|
|
23
|
+
return search_scores
|
|
24
|
+
|
|
15
25
|
sqs = SearchQuerySet().auto_query(text)
|
|
16
26
|
comic_scores = sqs.values("pk", "score")
|
|
17
27
|
try:
|
|
@@ -22,30 +32,23 @@ class SearchFilterMixin:
|
|
|
22
32
|
except Exception as exc:
|
|
23
33
|
LOG.warning("While searching:")
|
|
24
34
|
LOG.exception(exc)
|
|
35
|
+
return search_scores
|
|
25
36
|
|
|
26
|
-
def _get_search_query_filter(self,
|
|
37
|
+
def _get_search_query_filter(self, model, search_scores: dict):
|
|
27
38
|
"""Get the search filter and scores."""
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
prefix = "" if model == Comic else self.rel_prefix # type: ignore
|
|
40
|
+
rel = prefix + "pk__in"
|
|
30
41
|
query_dict = {rel: search_scores.keys()}
|
|
31
42
|
return Q(**query_dict)
|
|
32
43
|
|
|
33
|
-
def get_search_filter(self):
|
|
44
|
+
def get_search_filter(self, model, search_scores: dict):
|
|
34
45
|
"""Preparse search, search and return the filter and scores."""
|
|
35
46
|
search_filter = Q()
|
|
36
|
-
search_scores = {}
|
|
37
47
|
try:
|
|
38
|
-
|
|
39
|
-
if not query_string:
|
|
40
|
-
# for opds 2
|
|
41
|
-
query_string = self.params.get("query", "") # type: ignore
|
|
42
|
-
|
|
43
|
-
if query_string:
|
|
48
|
+
if search_scores:
|
|
44
49
|
# Query haystack
|
|
45
|
-
search_filter = self._get_search_query_filter(
|
|
46
|
-
query_string, search_scores
|
|
47
|
-
)
|
|
50
|
+
search_filter = self._get_search_query_filter(model, search_scores)
|
|
48
51
|
except Exception as exc:
|
|
49
52
|
LOG.warning(exc)
|
|
50
53
|
|
|
51
|
-
return search_filter
|
|
54
|
+
return search_filter
|
codex/views/browser/metadata.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""View for marking comics read and unread."""
|
|
2
2
|
from copy import copy
|
|
3
|
+
from types import MappingProxyType
|
|
4
|
+
from typing import ClassVar
|
|
3
5
|
|
|
4
6
|
from django.db.models import Count, F, IntegerField, Subquery, Value
|
|
5
7
|
from drf_spectacular.utils import extend_schema
|
|
@@ -16,45 +18,53 @@ from codex.views.browser.browser_annotations import BrowserAnnotationsView
|
|
|
16
18
|
class MetadataView(BrowserAnnotationsView):
|
|
17
19
|
"""Comic metadata."""
|
|
18
20
|
|
|
19
|
-
permission_classes = [IsAuthenticatedOrEnabledNonUsers]
|
|
21
|
+
permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
|
|
20
22
|
serializer_class = MetadataSerializer
|
|
21
23
|
|
|
22
24
|
# DO NOT USE BY ITSELF. USE _get_comic_value_fields() instead.
|
|
23
|
-
_COMIC_VALUE_FIELDS =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
25
|
+
_COMIC_VALUE_FIELDS = frozenset(
|
|
26
|
+
{
|
|
27
|
+
"age_rating",
|
|
28
|
+
"comments",
|
|
29
|
+
"community_rating",
|
|
30
|
+
"country",
|
|
31
|
+
"critical_rating",
|
|
32
|
+
"day",
|
|
33
|
+
"file_type",
|
|
34
|
+
"gtin",
|
|
35
|
+
"issue",
|
|
36
|
+
"issue_suffix",
|
|
37
|
+
"language",
|
|
38
|
+
"month",
|
|
39
|
+
"notes",
|
|
40
|
+
"original_format",
|
|
41
|
+
"read_ltr",
|
|
42
|
+
"scan_info",
|
|
43
|
+
"summary",
|
|
44
|
+
"web",
|
|
45
|
+
"year",
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
_ADMIN_OR_FILE_VIEW_ENABLED_COMIC_VALUE_FIELDS = frozenset({"path"})
|
|
49
|
+
_COMIC_VALUE_FIELDS_CONFLICTING = frozenset(
|
|
50
|
+
{
|
|
51
|
+
"name",
|
|
52
|
+
"updated_at",
|
|
53
|
+
"created_at",
|
|
54
|
+
}
|
|
55
|
+
)
|
|
50
56
|
_COMIC_VALUE_FIELDS_CONFLICTING_PREFIX = "conflict_"
|
|
51
|
-
_COMIC_FK_FIELDS_MAP =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
_COMIC_FK_FIELDS_MAP = MappingProxyType(
|
|
58
|
+
{
|
|
59
|
+
"p": "publisher",
|
|
60
|
+
"i": "imprint",
|
|
61
|
+
"s": "series",
|
|
62
|
+
"v": "volume",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
_COMIC_RELATED_VALUE_FIELDS = frozenset(
|
|
66
|
+
{"series__volume_count", "volume__issue_count"}
|
|
67
|
+
)
|
|
58
68
|
_PATH_GROUPS = ("c", "f")
|
|
59
69
|
_CREATOR_RELATIONS = ("role", "person")
|
|
60
70
|
_STORY_ARC_NUMBER_RELATIONS = ("story_arc",)
|
|
@@ -109,8 +119,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
109
119
|
if not self.is_model_comic:
|
|
110
120
|
size_func = self.get_aggregate_func(self.model, "size")
|
|
111
121
|
qs = qs.annotate(size=size_func)
|
|
112
|
-
|
|
113
|
-
return qs
|
|
122
|
+
return self.annotate_common_aggregates(qs, self.model, {})
|
|
114
123
|
|
|
115
124
|
def _annotate_values_and_fks(self, qs, simple_qs):
|
|
116
125
|
"""Annotate comic values and comic foreign key values."""
|
|
@@ -211,7 +220,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
211
220
|
def _highlight_current_group(self, obj):
|
|
212
221
|
"""Values for highlighting the current group."""
|
|
213
222
|
obj.group = self.group
|
|
214
|
-
if not self.is_model_comic:
|
|
223
|
+
if self.model and not self.is_model_comic:
|
|
215
224
|
# move the name of the group to the correct field
|
|
216
225
|
group_field = self.model.__name__.lower()
|
|
217
226
|
group_obj = {"pk": obj.pk, "name": obj.name}
|
|
@@ -259,7 +268,8 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
259
268
|
if self.model is None:
|
|
260
269
|
raise NotFound(detail=f"Cannot get metadata for {self.group=}")
|
|
261
270
|
|
|
262
|
-
|
|
271
|
+
search_scores: dict = self.get_search_scores()
|
|
272
|
+
object_filter = self.get_query_filters_without_group(self.model, search_scores)
|
|
263
273
|
pk = self.kwargs["pk"]
|
|
264
274
|
qs = self.model.objects.filter(object_filter, pk=pk)
|
|
265
275
|
|
|
@@ -279,8 +289,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
279
289
|
) from exc
|
|
280
290
|
|
|
281
291
|
m2m_intersections = self._query_m2m_intersections(simple_qs)
|
|
282
|
-
|
|
283
|
-
return obj
|
|
292
|
+
return self._copy_annotations_into_comic_fields(obj, m2m_intersections)
|
|
284
293
|
|
|
285
294
|
def _is_enabled_folder_view(self):
|
|
286
295
|
if self._efv_flag is None:
|
codex/views/cover.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""Comic cover thumbnail view."""
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
2
4
|
from django.http import HttpResponse
|
|
3
5
|
from drf_spectacular.types import OpenApiTypes
|
|
4
6
|
from drf_spectacular.utils import extend_schema
|
|
@@ -31,7 +33,7 @@ class WEBPRenderer(BaseRenderer):
|
|
|
31
33
|
class CoverView(APIView, GroupACLMixin):
|
|
32
34
|
"""ComicCover View."""
|
|
33
35
|
|
|
34
|
-
permission_classes = [IsAuthenticatedOrEnabledNonUsers]
|
|
36
|
+
permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
|
|
35
37
|
renderer_classes = (WEBPRenderer,)
|
|
36
38
|
content_type = "image/webp"
|
|
37
39
|
|
codex/views/download.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Download a comic archive."""
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
from typing import ClassVar
|
|
3
4
|
|
|
4
5
|
from django.http import FileResponse, Http404
|
|
5
6
|
from drf_spectacular.types import OpenApiTypes
|
|
@@ -14,7 +15,7 @@ from codex.views.mixins import GroupACLMixin
|
|
|
14
15
|
class DownloadView(APIView, GroupACLMixin):
|
|
15
16
|
"""Return the comic archive file as an attachment."""
|
|
16
17
|
|
|
17
|
-
permission_classes = [IsAuthenticatedOrEnabledNonUsers]
|
|
18
|
+
permission_classes: ClassVar[list] = [IsAuthenticatedOrEnabledNonUsers]
|
|
18
19
|
content_type = "application/vnd.comicbook+zip"
|
|
19
20
|
|
|
20
21
|
_DOWNLOAD_SELECT_RELATED = ("series", "volume")
|
|
@@ -36,7 +37,8 @@ class DownloadView(APIView, GroupACLMixin):
|
|
|
36
37
|
reason = f"Comic {pk} not not found."
|
|
37
38
|
raise Http404(reason) from err
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
# FileResponse requires file handle not be closed in this method.
|
|
41
|
+
comic_file = Path(comic.path).open("rb") # noqa: SIM115
|
|
40
42
|
return FileResponse(
|
|
41
43
|
comic_file,
|
|
42
44
|
as_attachment=True,
|
codex/views/frontend.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""Frontend views."""
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
2
4
|
from rest_framework.renderers import TemplateHTMLRenderer
|
|
3
5
|
from rest_framework.response import Response
|
|
4
6
|
|
|
@@ -8,9 +10,8 @@ from codex.views.session import BrowserSessionViewBase
|
|
|
8
10
|
class IndexView(BrowserSessionViewBase):
|
|
9
11
|
"""The main app."""
|
|
10
12
|
|
|
11
|
-
renderer_classes = [TemplateHTMLRenderer]
|
|
13
|
+
renderer_classes: ClassVar[list] = [TemplateHTMLRenderer]
|
|
12
14
|
template_name = "index.html"
|
|
13
|
-
main_urls = {}
|
|
14
15
|
|
|
15
16
|
def get(self, *args, **kwargs):
|
|
16
17
|
"""Get the app index page."""
|
codex/views/mixins.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""A filter for group ACLS."""
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
|
|
2
4
|
from django.db.models import Q
|
|
3
5
|
|
|
4
6
|
from codex.models import Comic, Folder, StoryArc
|
|
@@ -11,15 +13,17 @@ class GroupACLMixin:
|
|
|
11
13
|
FOLDER_GROUP = "f"
|
|
12
14
|
STORY_ARC_GROUP = "a"
|
|
13
15
|
COMIC_GROUP = "c"
|
|
14
|
-
GROUP_RELATION =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
16
|
+
GROUP_RELATION = MappingProxyType(
|
|
17
|
+
{
|
|
18
|
+
"p": "publisher",
|
|
19
|
+
"i": "imprint",
|
|
20
|
+
"s": "series",
|
|
21
|
+
"v": "volume",
|
|
22
|
+
COMIC_GROUP: "pk",
|
|
23
|
+
FOLDER_GROUP: "parent_folder",
|
|
24
|
+
STORY_ARC_GROUP: "story_arc_numbers__story_arc",
|
|
25
|
+
}
|
|
26
|
+
)
|
|
23
27
|
|
|
24
28
|
def get_rel_prefix(self, model):
|
|
25
29
|
"""Return the relation prfiex for most fields."""
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"""OPDS Authentication 1.0."""
|
|
2
|
+
from types import MappingProxyType
|
|
3
|
+
|
|
2
4
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
3
5
|
from django.urls import reverse_lazy
|
|
4
6
|
from rest_framework.generics import GenericAPIView
|
|
@@ -12,47 +14,49 @@ class OPDSAuthentication1View(GenericAPIView):
|
|
|
12
14
|
|
|
13
15
|
serializer_class = OPDSAuthentication1Serializer
|
|
14
16
|
|
|
15
|
-
DOC =
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
17
|
+
DOC = MappingProxyType(
|
|
18
|
+
{
|
|
19
|
+
"id": reverse_lazy("opds:authentication:v1"),
|
|
20
|
+
"title": "Codex",
|
|
21
|
+
"description": "Codex OPDS Syndication",
|
|
22
|
+
"links": [
|
|
23
|
+
{
|
|
24
|
+
"rel": "logo",
|
|
25
|
+
"href": staticfiles_storage.url("img/logo.svg"),
|
|
26
|
+
"type": "image/svg+xml",
|
|
27
|
+
"width": 512,
|
|
28
|
+
"height": 512,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"rel": "help",
|
|
32
|
+
"href": "https://github.com/ajslater/codex",
|
|
33
|
+
"type": "text/html",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"rel": "register",
|
|
37
|
+
"href": reverse_lazy("app:start"),
|
|
38
|
+
"type": "text/html",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
"authentication": [
|
|
42
|
+
{
|
|
43
|
+
"type": "http://opds-spec.org/auth/basic",
|
|
44
|
+
"labels": {"login": "Username", "password": "Password"},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
# XXX Out of spec type
|
|
48
|
+
"type": "cookie",
|
|
49
|
+
"links": [
|
|
50
|
+
{
|
|
51
|
+
"rel": "authenticate",
|
|
52
|
+
"href": reverse_lazy("app:start"),
|
|
53
|
+
"type": "text/html",
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
)
|
|
56
60
|
|
|
57
61
|
def get(self, *args, **kwargs):
|
|
58
62
|
"""Fill in the authentication dict."""
|
codex/views/opds/const.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""OPDS Common consts."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
3
6
|
BLANK_TITLE = "Unknown"
|
|
4
7
|
FALSY = {"", "false", "0"}
|
|
5
8
|
AUTHOR_ROLES = {"Writer"}
|
|
@@ -51,17 +54,21 @@ class MimeType:
|
|
|
51
54
|
HTML = "text/html"
|
|
52
55
|
AUTH_BASIC = "http://opds-spec.org/auth/basic"
|
|
53
56
|
COOKIE = "cookie"
|
|
54
|
-
FILE_TYPE_MAP =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
FILE_TYPE_MAP: Mapping[str, str] = MappingProxyType(
|
|
58
|
+
{
|
|
59
|
+
"CBZ": "application/vnd.comicbook+zip",
|
|
60
|
+
"CBR": "application/vnd.comicbook+rar",
|
|
61
|
+
"CBT": "application/vnd.comicbook+tar",
|
|
62
|
+
"PDF": "application/pdf",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
SIMPLE_FILE_TYPE_MAP: Mapping[str, str] = MappingProxyType(
|
|
66
|
+
{
|
|
67
|
+
# PocketBooks needs app/zip
|
|
68
|
+
"CBZ": "application/zip",
|
|
69
|
+
"CBR": "application/x-rar-compressed",
|
|
70
|
+
"CBT": "application/x-tar",
|
|
71
|
+
"PDF": "application/pdf",
|
|
72
|
+
}
|
|
73
|
+
)
|
|
67
74
|
OCTET = "application/octet-stream"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""OPDS v1 Entry Data classes."""
|
|
2
|
+
from collections.abc import Mapping
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
|
|
4
5
|
|
|
@@ -20,4 +21,4 @@ class OPDS1EntryData:
|
|
|
20
21
|
acquisition_groups: frozenset
|
|
21
22
|
issue_max: int
|
|
22
23
|
metadata: bool
|
|
23
|
-
mime_type_map:
|
|
24
|
+
mime_type_map: Mapping[str, str]
|
codex/views/opds/v1/facets.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""OPDS v1 Facets methods."""
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from types import MappingProxyType
|
|
3
4
|
|
|
4
5
|
from django.urls import reverse
|
|
5
6
|
|
|
@@ -83,7 +84,7 @@ class FacetsMixin(BrowserView):
|
|
|
83
84
|
skip_order_facets = False
|
|
84
85
|
acquisition_groups = frozenset()
|
|
85
86
|
mime_type_map = MimeType.FILE_TYPE_MAP
|
|
86
|
-
obj = {}
|
|
87
|
+
obj = MappingProxyType({})
|
|
87
88
|
|
|
88
89
|
def _facet(self, kwargs, facet_group, facet_title, new_query_params):
|
|
89
90
|
href = reverse("opds:v1:feed", kwargs=kwargs)
|
codex/views/opds/v1/feed.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""OPDS v1 feed."""
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
3
4
|
|
|
4
5
|
from drf_spectacular.utils import extend_schema
|
|
5
6
|
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
@@ -24,6 +25,9 @@ from codex.views.opds.v1.links import (
|
|
|
24
25
|
)
|
|
25
26
|
from codex.views.template import CodexXMLTemplateView
|
|
26
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
from collections.abc import Mapping
|
|
30
|
+
|
|
27
31
|
LOG = get_logger(__name__)
|
|
28
32
|
|
|
29
33
|
|
|
@@ -77,7 +81,10 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
|
|
|
77
81
|
"""Create the feed title."""
|
|
78
82
|
result = ""
|
|
79
83
|
try:
|
|
80
|
-
|
|
84
|
+
browser_title: Mapping[str, Any] = self.obj.get(
|
|
85
|
+
"browser_title"
|
|
86
|
+
) # type: ignore
|
|
87
|
+
if browser_title:
|
|
81
88
|
parent_name = browser_title.get("parent_name", "All")
|
|
82
89
|
if not parent_name and self.kwargs.get("pk") == 0:
|
|
83
90
|
parent_name = "All"
|
|
@@ -95,7 +102,7 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
|
|
|
95
102
|
"""Hack in feed update time from cover timestamp."""
|
|
96
103
|
try:
|
|
97
104
|
if ts := self.obj.get("covers_timestamp"):
|
|
98
|
-
return datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
105
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc) # type: ignore
|
|
99
106
|
except Exception as exc:
|
|
100
107
|
LOG.exception(exc)
|
|
101
108
|
|
|
@@ -120,12 +127,12 @@ class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
|
|
|
120
127
|
def _get_entries_section(self, key, metadata):
|
|
121
128
|
"""Get entries by key section."""
|
|
122
129
|
entries = []
|
|
123
|
-
issue_max = self.obj.get("issue_max", 0)
|
|
130
|
+
issue_max: int = self.obj.get("issue_max", 0) # type: ignore
|
|
124
131
|
data = OPDS1EntryData(
|
|
125
132
|
self.acquisition_groups, issue_max, metadata, self.mime_type_map
|
|
126
133
|
)
|
|
127
134
|
if objs := self.obj.get(key):
|
|
128
|
-
for obj in objs:
|
|
135
|
+
for obj in objs: # type: ignore
|
|
129
136
|
entries += [OPDS1Entry(obj, self.request.query_params, data)]
|
|
130
137
|
return entries
|
|
131
138
|
|
codex/views/opds/v1/links.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""OPDS v1 Links methods."""
|
|
2
2
|
from collections import defaultdict
|
|
3
|
+
from collections.abc import Mapping
|
|
3
4
|
from dataclasses import dataclass
|
|
5
|
+
from types import MappingProxyType
|
|
4
6
|
from typing import Union
|
|
5
7
|
|
|
6
8
|
from comicbox.metadata.comic_json import json
|
|
@@ -20,18 +22,18 @@ LOG = get_logger(__name__)
|
|
|
20
22
|
class TopRoutes:
|
|
21
23
|
"""Routes for top groups."""
|
|
22
24
|
|
|
23
|
-
PUBLISHER = {"group": "p", "pk": 0, "page": 1}
|
|
24
|
-
SERIES = {"group": "s", "pk": 0, "page": 1}
|
|
25
|
-
FOLDER = {"group": "f", "pk": 0, "page": 1}
|
|
26
|
-
ROOT = {"group": "r", "pk": 0, "page": 1}
|
|
27
|
-
STORY_ARC = {"group": "a", "pk": 0, "page": 1}
|
|
25
|
+
PUBLISHER = MappingProxyType({"group": "p", "pk": 0, "page": 1})
|
|
26
|
+
SERIES = MappingProxyType({"group": "s", "pk": 0, "page": 1})
|
|
27
|
+
FOLDER = MappingProxyType({"group": "f", "pk": 0, "page": 1})
|
|
28
|
+
ROOT = MappingProxyType({"group": "r", "pk": 0, "page": 1})
|
|
29
|
+
STORY_ARC = MappingProxyType({"group": "a", "pk": 0, "page": 1})
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
@dataclass
|
|
31
33
|
class TopLink:
|
|
32
34
|
"""A non standard root link when facets are unsupported."""
|
|
33
35
|
|
|
34
|
-
kwargs:
|
|
36
|
+
kwargs: Mapping
|
|
35
37
|
rel: str
|
|
36
38
|
mime_type: str
|
|
37
39
|
query_params: defaultdict[str, Union[str, bool, int]]
|
|
@@ -12,6 +12,6 @@ class OpenSearch1View(CodexXMLTemplateView):
|
|
|
12
12
|
"""OpenSearchView."""
|
|
13
13
|
|
|
14
14
|
authentication_classes = (BasicAuthentication, SessionAuthentication)
|
|
15
|
-
permission_classes =
|
|
15
|
+
permission_classes = (IsAuthenticatedOrEnabledNonUsers,)
|
|
16
16
|
template_name = "opds_v1/opensearch_v1.xml"
|
|
17
17
|
content_type = "application/xml"
|
codex/views/opds/v2/feed.py
CHANGED
|
@@ -189,7 +189,8 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
|
|
|
189
189
|
|
|
190
190
|
self.is_aq_feed = browser_page.get("model_group") == "c"
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
ts: int = browser_page["covers_timestamp"] # type: ignore
|
|
193
|
+
datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
193
194
|
self.num_pages = browser_page["num_pages"]
|
|
194
195
|
number_of_items = browser_page["total_count"]
|
|
195
196
|
title = self._title(browser_page.get("browser_title"))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Publication Methods for OPDS v2.0 feed."""
|
|
2
|
+
from types import MappingProxyType
|
|
2
3
|
from urllib.parse import quote_plus
|
|
3
4
|
|
|
4
5
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
@@ -13,18 +14,20 @@ from codex.views.opds.v2.links import HrefData, LinkData, LinksMixin
|
|
|
13
14
|
class PublicationMixin(LinksMixin):
|
|
14
15
|
"""Publication Methods for OPDS 2.0 feed."""
|
|
15
16
|
|
|
16
|
-
_MD_CONTRIBUTOR_MAP =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
17
|
+
_MD_CONTRIBUTOR_MAP = MappingProxyType(
|
|
18
|
+
{
|
|
19
|
+
"author": AUTHOR_ROLES,
|
|
20
|
+
# "translator": {},
|
|
21
|
+
"editor": {"Editor"},
|
|
22
|
+
"artist": {"CoverArtist"},
|
|
23
|
+
# "illustrator": {},
|
|
24
|
+
"letterer": {"Letterer"},
|
|
25
|
+
"penciller": {"Penciller"},
|
|
26
|
+
"colorist": {"Colorist"},
|
|
27
|
+
"inker": {"Inker"},
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
_CONTRIBUTOR_ROLES = frozenset({x for s in _MD_CONTRIBUTOR_MAP.values() for x in s})
|
|
28
31
|
is_opds_metadata = False
|
|
29
32
|
|
|
30
33
|
def _get_big_image_link(self, obj, cover_pk):
|
codex/views/reader/page.py
CHANGED
|
@@ -30,7 +30,7 @@ class IgnoreClientContentNegotiation(BaseContentNegotiation):
|
|
|
30
30
|
class ReaderPageView(BookmarkBaseView):
|
|
31
31
|
"""Display a comic page from the archive itself."""
|
|
32
32
|
|
|
33
|
-
X_MOZ_PRE_HEADERS = {"prefetch", "preload", "prerender", "subresource"}
|
|
33
|
+
X_MOZ_PRE_HEADERS = frozenset({"prefetch", "preload", "prerender", "subresource"})
|
|
34
34
|
content_type = "image/jpeg"
|
|
35
35
|
content_negotiation_class = IgnoreClientContentNegotiation # type: ignore
|
|
36
36
|
|