codex 1.4.0a1__py3-none-any.whl → 1.4.1__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/config_default.yaml +12 -4
- codex/db_functions.py +4 -2
- codex/integrity.py +17 -6
- codex/librarian/covers/create.py +6 -8
- codex/librarian/importer/aggregate_metadata.py +75 -41
- codex/librarian/importer/clean_metadata.py +30 -7
- codex/librarian/importer/create_fks.py +154 -55
- codex/librarian/importer/deleted.py +11 -2
- codex/librarian/importer/failed_imports.py +41 -5
- codex/librarian/importer/importerd.py +34 -11
- codex/librarian/importer/link_comics.py +54 -31
- codex/librarian/importer/moved.py +55 -11
- codex/librarian/importer/query_fks.py +210 -48
- codex/librarian/importer/tasks.py +7 -7
- codex/librarian/janitor/cleanup.py +17 -5
- codex/librarian/librariand.py +10 -0
- codex/librarian/watchdog/events.py +11 -14
- codex/librarian/watchdog/observers.py +5 -1
- codex/logger/loggerd.py +7 -3
- codex/logger/logging.py +1 -1
- codex/migrations/0024_comic_gtin_comic_story_arc_number.py +24 -0
- codex/migrations/0025_add_story_arc_number.py +83 -0
- codex/models.py +21 -11
- codex/search/backend.py +1 -1
- codex/search/indexes.py +1 -1
- codex/serializers/browser.py +1 -0
- codex/serializers/metadata.py +5 -1
- codex/serializers/models.py +16 -1
- codex/serializers/opds/v1.py +1 -0
- codex/serializers/opds/v2.py +5 -2
- codex/serializers/reader.py +55 -16
- codex/settings/settings.py +1 -1
- codex/static_root/assets/admin-12749881.ef0f50bac290.js +41 -0
- 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 +41 -0
- 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 +1 -0
- 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 +1 -0
- 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-41c225cc.3f84583b435b.css +1 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.3f84583b435b.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css +1 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css.br +0 -0
- codex/static_root/assets/admin-drawer-panel-41c225cc.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-522f1e6c.089d70878270.js +1 -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 +1 -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 +1 -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 +1 -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 +1 -0
- 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 +1 -0
- 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 +1 -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 +1 -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-9e76a4c3.6844a407d14c.js +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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-785c4cfc.694a251cda37.css → metadata-dialog-cb306ffd.cc304996d7bb.css} +1 -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-785c4cfc.css → metadata-dialog-cb306ffd.css} +1 -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-c603e996.ab2d147c9ae1.js → page-pdf-157ba97e.613d7c2beb77.js} +61 -51
- 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-c603e996.js → page-pdf-157ba97e.js} +61 -51
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- 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 +1 -0
- codex/static_root/assets/reader-7f004141.css.br +0 -0
- codex/static_root/assets/reader-7f004141.css.gz +0 -0
- codex/static_root/js/choices.8c58714cf5b2.json +1 -0
- codex/static_root/js/choices.8c58714cf5b2.json.br +5 -0
- codex/static_root/js/choices.8c58714cf5b2.json.gz +0 -0
- codex/static_root/js/choices.json +1 -1
- codex/static_root/js/choices.json.br +0 -0
- codex/static_root/js/choices.json.gz +0 -0
- codex/static_root/{manifest.c0e270b2e6b6.json → manifest.d2f93a519ada.json} +32 -32
- codex/static_root/manifest.d2f93a519ada.json.br +0 -0
- codex/static_root/manifest.d2f93a519ada.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/staticfiles.json +1 -1
- codex/templates/headers-script-globals.html +1 -1
- codex/templates/{opds → opds_v1}/index.xml +3 -1
- codex/templates/{opds/opensearch.xml → opds_v1/opensearch_v1.xml} +1 -1
- codex/templates/search/indexes/codex/comic_text.txt +2 -2
- codex/urls/converters.py +1 -1
- codex/urls/opds/authentication.py +1 -1
- codex/urls/opds/root.py +8 -12
- codex/urls/opds/v1.py +12 -5
- codex/urls/opds/v2.py +2 -2
- codex/views/bookmark.py +2 -2
- codex/views/browser/base.py +23 -7
- codex/views/browser/browser.py +51 -41
- codex/views/browser/browser_annotations.py +159 -50
- codex/views/browser/browser_order_by.py +50 -106
- codex/views/browser/choices.py +75 -38
- codex/views/browser/filters/bookmark.py +6 -9
- codex/views/browser/filters/field.py +9 -6
- codex/views/browser/filters/group.py +12 -27
- codex/views/browser/filters/search.py +5 -10
- codex/views/browser/metadata.py +44 -19
- codex/views/download.py +1 -1
- codex/views/frontend.py +2 -3
- codex/views/mixins.py +15 -2
- codex/views/opds/const.py +8 -1
- codex/views/opds/util.py +37 -1
- codex/views/opds/v1/__init__.py +1 -1
- codex/views/opds/v1/data.py +21 -0
- codex/views/opds/v1/entry/__init__.py +1 -0
- codex/views/opds/v1/entry/data.py +23 -0
- codex/views/opds/v1/entry/entry.py +151 -0
- codex/views/opds/v1/entry/links.py +135 -0
- codex/views/opds/v1/facets.py +190 -0
- codex/views/opds/v1/feed.py +199 -0
- codex/views/opds/v1/links.py +198 -0
- codex/views/opds/{opensearch.py → v1/opensearch_v1.py} +3 -3
- codex/views/opds/v2/__init__.py +1 -1
- codex/views/opds/v2/const.py +10 -2
- codex/views/opds/v2/feed.py +82 -21
- codex/views/opds/v2/links.py +1 -1
- codex/views/opds/v2/publications.py +1 -1
- codex/views/opds/v2/top_links.py +1 -1
- codex/views/reader/page.py +6 -7
- codex/views/reader/reader.py +191 -61
- codex/views/session.py +2 -1
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/RECORD +172 -170
- codex/librarian/importer/db_ops.py +0 -251
- codex/pdf.py +0 -115
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js +0 -48
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.br +0 -0
- codex/static_root/assets/admin-75c007ce.199fccf24c8d.js.gz +0 -0
- codex/static_root/assets/admin-75c007ce.js +0 -48
- codex/static_root/assets/admin-75c007ce.js.br +0 -0
- codex/static_root/assets/admin-75c007ce.js.gz +0 -0
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css +0 -1
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.br +0 -0
- codex/static_root/assets/admin-848d48b1.5de8a0c45636.css.gz +0 -0
- codex/static_root/assets/admin-848d48b1.css +0 -1
- codex/static_root/assets/admin-848d48b1.css.br +0 -0
- codex/static_root/assets/admin-848d48b1.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js +0 -1
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.edf187333272.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.js +0 -1
- codex/static_root/assets/admin-drawer-panel-a110c068.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-a110c068.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css +0 -1
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.br +0 -2
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.2c0814fa2a9b.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css +0 -1
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.br +0 -2
- codex/static_root/assets/admin-drawer-panel-cce8c0aa.css.gz +0 -0
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js +0 -1
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.br +0 -0
- codex/static_root/assets/browser-2c2380fd.8b515af7a743.js.gz +0 -0
- codex/static_root/assets/browser-2c2380fd.js +0 -1
- codex/static_root/assets/browser-2c2380fd.js.br +0 -0
- codex/static_root/assets/browser-2c2380fd.js.gz +0 -0
- codex/static_root/assets/browser-7325db61.css +0 -1
- codex/static_root/assets/browser-7325db61.css.br +0 -0
- codex/static_root/assets/browser-7325db61.css.gz +0 -0
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css +0 -1
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.br +0 -0
- codex/static_root/assets/browser-7325db61.ed2cfbf8e8ee.css.gz +0 -0
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js +0 -1
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.br +0 -0
- codex/static_root/assets/http-error-402decbe.9ea8de1df13f.js.gz +0 -0
- codex/static_root/assets/http-error-402decbe.js +0 -1
- codex/static_root/assets/http-error-402decbe.js.br +0 -0
- codex/static_root/assets/http-error-402decbe.js.gz +0 -0
- codex/static_root/assets/main-a7f327e9.6641fe833335.js +0 -1
- codex/static_root/assets/main-a7f327e9.6641fe833335.js.br +0 -0
- codex/static_root/assets/main-a7f327e9.6641fe833335.js.gz +0 -0
- codex/static_root/assets/main-a7f327e9.js +0 -1
- codex/static_root/assets/main-a7f327e9.js.br +0 -0
- codex/static_root/assets/main-a7f327e9.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.br +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.694a251cda37.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.css.br +0 -0
- codex/static_root/assets/metadata-dialog-785c4cfc.css.gz +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js +0 -1
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.c213b08d582f.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js +0 -1
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8a0bd8e1.js.gz +0 -0
- codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.br +0 -0
- codex/static_root/assets/page-pdf-c603e996.ab2d147c9ae1.js.gz +0 -0
- codex/static_root/assets/page-pdf-c603e996.js.br +0 -0
- codex/static_root/assets/page-pdf-c603e996.js.gz +0 -0
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js +0 -1
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js.br +0 -0
- codex/static_root/assets/reader-c2965a5f.b011260169f7.js.gz +0 -0
- codex/static_root/assets/reader-c2965a5f.js +0 -1
- codex/static_root/assets/reader-c2965a5f.js.br +0 -0
- codex/static_root/assets/reader-c2965a5f.js.gz +0 -0
- codex/static_root/assets/reader-d8534888.2821de925986.css +0 -1
- codex/static_root/assets/reader-d8534888.2821de925986.css.br +0 -0
- codex/static_root/assets/reader-d8534888.2821de925986.css.gz +0 -0
- codex/static_root/assets/reader-d8534888.css +0 -1
- codex/static_root/assets/reader-d8534888.css.br +0 -0
- codex/static_root/assets/reader-d8534888.css.gz +0 -0
- codex/static_root/js/choices.6bfc2a3d293f.json +0 -1
- codex/static_root/js/choices.6bfc2a3d293f.json.br +0 -0
- codex/static_root/js/choices.6bfc2a3d293f.json.gz +0 -0
- codex/static_root/manifest.c0e270b2e6b6.json.br +0 -0
- codex/static_root/manifest.c0e270b2e6b6.json.gz +0 -0
- codex/urls/opds/opensearch.py +0 -18
- codex/views/opds/v1/browser.py +0 -346
- codex/views/opds/v1/entry.py +0 -278
- codex/views/opds/v1/start.py +0 -28
- codex/views/opds/v1/util.py +0 -162
- codex/views/opds/v2/start.py +0 -28
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
- {codex-1.4.0a1.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""OPDS v1 feed."""
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from drf_spectacular.utils import extend_schema
|
|
5
|
+
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
6
|
+
from rest_framework.response import Response
|
|
7
|
+
|
|
8
|
+
from codex.logger.logging import get_logger
|
|
9
|
+
from codex.serializers.opds.v1 import (
|
|
10
|
+
OPDS1TemplateSerializer,
|
|
11
|
+
)
|
|
12
|
+
from codex.views.browser.browser import BrowserView
|
|
13
|
+
from codex.views.opds.const import (
|
|
14
|
+
BLANK_TITLE,
|
|
15
|
+
FALSY,
|
|
16
|
+
MimeType,
|
|
17
|
+
)
|
|
18
|
+
from codex.views.opds.v1.entry.data import OPDS1EntryData
|
|
19
|
+
from codex.views.opds.v1.entry.entry import OPDS1Entry
|
|
20
|
+
from codex.views.opds.v1.links import (
|
|
21
|
+
LinksMixin,
|
|
22
|
+
RootTopLinks,
|
|
23
|
+
TopLinks,
|
|
24
|
+
)
|
|
25
|
+
from codex.views.template import CodexXMLTemplateView
|
|
26
|
+
|
|
27
|
+
LOG = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OpdsNs:
|
|
31
|
+
"""XML Namespaces."""
|
|
32
|
+
|
|
33
|
+
CATALOG = "http://opds-spec.org/2010/catalog"
|
|
34
|
+
ACQUISITION = "http://opds-spec.org/2010/acquisition"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserAgentPrefixes:
|
|
38
|
+
"""Control whether to hack in facets with nav links."""
|
|
39
|
+
|
|
40
|
+
CLIENT_REORDERS = ("Chunky",)
|
|
41
|
+
FACET_SUPPORT = ("yar",) # kybooks
|
|
42
|
+
SIMPLE_DOWNLOAD_MIME_TYPES = ("PocketBook",)
|
|
43
|
+
# Other known valid prefixes:
|
|
44
|
+
# "Panels", "Chunky"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OPDS1FeedView(CodexXMLTemplateView, LinksMixin):
|
|
48
|
+
"""OPDS 1 Feed."""
|
|
49
|
+
|
|
50
|
+
authentication_classes = (BasicAuthentication, SessionAuthentication)
|
|
51
|
+
template_name = "opds_v1/index.xml"
|
|
52
|
+
serializer_class = OPDS1TemplateSerializer
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def opds_ns(self):
|
|
56
|
+
"""Dynamic opds namespace."""
|
|
57
|
+
try:
|
|
58
|
+
return OpdsNs.ACQUISITION if self.is_aq_feed else OpdsNs.CATALOG
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
LOG.exception(exc)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_acquisition(self):
|
|
64
|
+
"""Is acquisition."""
|
|
65
|
+
return self.is_aq_feed
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def id_tag(self):
|
|
69
|
+
"""Feed id is the url."""
|
|
70
|
+
try:
|
|
71
|
+
return self.request.build_absolute_uri()
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
LOG.exception(exc)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def title(self):
|
|
77
|
+
"""Create the feed title."""
|
|
78
|
+
result = ""
|
|
79
|
+
try:
|
|
80
|
+
if browser_title := self.obj.get("browser_title"):
|
|
81
|
+
parent_name = browser_title.get("parent_name", "All")
|
|
82
|
+
if not parent_name and self.kwargs.get("pk") == 0:
|
|
83
|
+
parent_name = "All"
|
|
84
|
+
group_name = browser_title.get("group_name")
|
|
85
|
+
result = " ".join(filter(None, (parent_name, group_name))).strip()
|
|
86
|
+
|
|
87
|
+
if not result:
|
|
88
|
+
result = BLANK_TITLE
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
LOG.exception(exc)
|
|
91
|
+
return result
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def updated(self):
|
|
95
|
+
"""Hack in feed update time from cover timestamp."""
|
|
96
|
+
try:
|
|
97
|
+
if ts := self.obj.get("covers_timestamp"):
|
|
98
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc)
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
LOG.exception(exc)
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def items_per_page(self):
|
|
104
|
+
"""Return opensearch:itemsPerPage."""
|
|
105
|
+
try:
|
|
106
|
+
if self.params.get("q"):
|
|
107
|
+
return self.MAX_OBJ_PER_PAGE
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
LOG.exception(exc)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def total_results(self):
|
|
113
|
+
"""Return opensearch:totalResults."""
|
|
114
|
+
try:
|
|
115
|
+
if self.params.get("q"):
|
|
116
|
+
return self.obj.get("total_count", 0)
|
|
117
|
+
except Exception as exc:
|
|
118
|
+
LOG.exception(exc)
|
|
119
|
+
|
|
120
|
+
def _get_entries_section(self, key, metadata):
|
|
121
|
+
"""Get entries by key section."""
|
|
122
|
+
entries = []
|
|
123
|
+
issue_max = self.obj.get("issue_max", 0)
|
|
124
|
+
data = OPDS1EntryData(
|
|
125
|
+
self.acquisition_groups, issue_max, metadata, self.mime_type_map
|
|
126
|
+
)
|
|
127
|
+
if objs := self.obj.get(key):
|
|
128
|
+
for obj in objs:
|
|
129
|
+
entries += [OPDS1Entry(obj, self.request.query_params, data)]
|
|
130
|
+
return entries
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def entries(self):
|
|
134
|
+
"""Create all the entries."""
|
|
135
|
+
entries = []
|
|
136
|
+
try:
|
|
137
|
+
at_root = self.kwargs.get("pk") == 0
|
|
138
|
+
if not self.use_facets and self.kwargs.get("page") == 1:
|
|
139
|
+
entries += self.add_top_links(TopLinks.ALL)
|
|
140
|
+
if at_root:
|
|
141
|
+
entries += self.add_top_links(RootTopLinks.ALL)
|
|
142
|
+
entries += self.facets(entries=True, root=at_root)
|
|
143
|
+
|
|
144
|
+
entries += self._get_entries_section("groups", False)
|
|
145
|
+
metadata = (
|
|
146
|
+
self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
|
|
147
|
+
)
|
|
148
|
+
entries += self._get_entries_section("books", metadata)
|
|
149
|
+
except Exception as exc:
|
|
150
|
+
LOG.exception(exc)
|
|
151
|
+
return entries
|
|
152
|
+
|
|
153
|
+
def get_object(self):
|
|
154
|
+
"""Get the browser page and serialize it for this subclass."""
|
|
155
|
+
group = self.kwargs.get("group")
|
|
156
|
+
if group == "a":
|
|
157
|
+
self.acquisition_groups = frozenset(["a"])
|
|
158
|
+
pk = self.kwargs.get("pk")
|
|
159
|
+
self.is_opds_1_acquisition = group in self.acquisition_groups and pk
|
|
160
|
+
else:
|
|
161
|
+
self.acquisition_groups = frozenset(self.valid_nav_groups[-2:])
|
|
162
|
+
self.is_opds_1_acquisition = group in self.acquisition_groups
|
|
163
|
+
self.is_opds_metadata = (
|
|
164
|
+
self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
|
|
165
|
+
)
|
|
166
|
+
self.obj = super().get_object()
|
|
167
|
+
self.is_aq_feed = self.obj.get("model_group") == "c"
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
def _set_user_agent_variables(self):
|
|
171
|
+
"""Set User Agent variables."""
|
|
172
|
+
# defaults in FacetsMixin
|
|
173
|
+
user_agent = self.request.headers.get("User-Agent")
|
|
174
|
+
if not user_agent:
|
|
175
|
+
return
|
|
176
|
+
for prefix in UserAgentPrefixes.FACET_SUPPORT:
|
|
177
|
+
if user_agent.startswith(prefix):
|
|
178
|
+
self.use_facets = True
|
|
179
|
+
break
|
|
180
|
+
for prefix in UserAgentPrefixes.CLIENT_REORDERS:
|
|
181
|
+
if user_agent.startswith(prefix):
|
|
182
|
+
self.skip_order_facets = True
|
|
183
|
+
break
|
|
184
|
+
for prefix in UserAgentPrefixes.SIMPLE_DOWNLOAD_MIME_TYPES:
|
|
185
|
+
if user_agent.startswith(prefix):
|
|
186
|
+
self.mime_type_map = MimeType.SIMPLE_FILE_TYPE_MAP
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
@extend_schema(request=BrowserView.input_serializer_class)
|
|
190
|
+
def get(self, *args, **kwargs):
|
|
191
|
+
"""Get the feed."""
|
|
192
|
+
self.parse_params()
|
|
193
|
+
self.validate_settings()
|
|
194
|
+
self._set_user_agent_variables()
|
|
195
|
+
self.skip_order_facets |= self.kwargs.get("group") == "c"
|
|
196
|
+
|
|
197
|
+
obj = self.get_object()
|
|
198
|
+
serializer = self.get_serializer(obj)
|
|
199
|
+
return Response(serializer.data, content_type=self.content_type)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""OPDS v1 Links methods."""
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from comicbox.metadata.comic_json import json
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
|
|
9
|
+
from codex.logger.logging import get_logger
|
|
10
|
+
from codex.views.opds.const import MimeType, Rel
|
|
11
|
+
from codex.views.opds.util import update_href_query_params
|
|
12
|
+
from codex.views.opds.v1.data import OPDS1Link
|
|
13
|
+
from codex.views.opds.v1.entry.data import OPDS1EntryData, OPDS1EntryObject
|
|
14
|
+
from codex.views.opds.v1.entry.entry import OPDS1Entry
|
|
15
|
+
from codex.views.opds.v1.facets import FacetsMixin
|
|
16
|
+
|
|
17
|
+
LOG = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TopRoutes:
|
|
21
|
+
"""Routes for top groups."""
|
|
22
|
+
|
|
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}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class TopLink:
|
|
32
|
+
"""A non standard root link when facets are unsupported."""
|
|
33
|
+
|
|
34
|
+
kwargs: dict
|
|
35
|
+
rel: str
|
|
36
|
+
mime_type: str
|
|
37
|
+
query_params: defaultdict[str, Union[str, bool, int]]
|
|
38
|
+
glyph: str
|
|
39
|
+
title: str
|
|
40
|
+
desc: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TopLinks:
|
|
44
|
+
"""Top link definitions."""
|
|
45
|
+
|
|
46
|
+
START = TopLink(
|
|
47
|
+
TopRoutes.ROOT,
|
|
48
|
+
Rel.START,
|
|
49
|
+
MimeType.NAV,
|
|
50
|
+
defaultdict(),
|
|
51
|
+
"⌂",
|
|
52
|
+
"Start of the catalog",
|
|
53
|
+
"",
|
|
54
|
+
)
|
|
55
|
+
ALL = (START,)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class RootTopLinks:
|
|
59
|
+
"""Top Links that only appear at the root."""
|
|
60
|
+
|
|
61
|
+
NEW = TopLink(
|
|
62
|
+
TopRoutes.SERIES,
|
|
63
|
+
Rel.SORT_NEW,
|
|
64
|
+
MimeType.ACQUISITION,
|
|
65
|
+
defaultdict(
|
|
66
|
+
None, {"orderBy": "created_at", "orderReverse": True, "limit": 100}
|
|
67
|
+
),
|
|
68
|
+
"📥",
|
|
69
|
+
"Recently Added",
|
|
70
|
+
"",
|
|
71
|
+
)
|
|
72
|
+
FEATURED = TopLink(
|
|
73
|
+
TopRoutes.SERIES,
|
|
74
|
+
Rel.FEATURED,
|
|
75
|
+
MimeType.NAV,
|
|
76
|
+
defaultdict(
|
|
77
|
+
None,
|
|
78
|
+
{
|
|
79
|
+
"orderBy": "date",
|
|
80
|
+
"filters": json.dumps({"bookmark": "UNREAD"}),
|
|
81
|
+
"limit": 100,
|
|
82
|
+
},
|
|
83
|
+
),
|
|
84
|
+
"📚",
|
|
85
|
+
"Oldest Unread",
|
|
86
|
+
"Unread issues, oldest published first",
|
|
87
|
+
)
|
|
88
|
+
LAST_READ = TopLink(
|
|
89
|
+
TopRoutes.SERIES,
|
|
90
|
+
Rel.POPULAR,
|
|
91
|
+
MimeType.NAV,
|
|
92
|
+
defaultdict(
|
|
93
|
+
None, {"orderBy": "bookmark_updated_at", "orderReverse": True, "limit": 100}
|
|
94
|
+
),
|
|
95
|
+
"👀",
|
|
96
|
+
"Last Read",
|
|
97
|
+
"Last Read issues, recently read first.",
|
|
98
|
+
)
|
|
99
|
+
ALL = (NEW, FEATURED, LAST_READ)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class LinksMixin(FacetsMixin):
|
|
103
|
+
"""OPDS 1 Links methods."""
|
|
104
|
+
|
|
105
|
+
# overwritten in get_object()
|
|
106
|
+
DEFAULT_ROUTE_NAME = "opds:v1:feed"
|
|
107
|
+
is_aq_feed = False
|
|
108
|
+
|
|
109
|
+
def is_top_link_displayed(self, top_link):
|
|
110
|
+
"""Determine if this top link should be displayed."""
|
|
111
|
+
for key, value in top_link.kwargs.items():
|
|
112
|
+
if str(self.kwargs.get(key)) != str(value):
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
for key, value in top_link.query_params.items():
|
|
116
|
+
if str(self.request.query_params.get(key)) != str(value):
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
def _link(self, kwargs, rel, query_params=None, mime_type=MimeType.NAV):
|
|
122
|
+
"""Create a link."""
|
|
123
|
+
if query_params is None:
|
|
124
|
+
query_params = self.request.query_params
|
|
125
|
+
href = reverse("opds:v1:feed", kwargs=kwargs)
|
|
126
|
+
href = update_href_query_params(href, query_params)
|
|
127
|
+
return OPDS1Link(rel, href, mime_type)
|
|
128
|
+
|
|
129
|
+
def _top_link(self, top_link):
|
|
130
|
+
"""Create a link from a top link."""
|
|
131
|
+
return self._link(
|
|
132
|
+
top_link.kwargs, top_link.rel, top_link.query_params, top_link.mime_type
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _root_links(self):
|
|
136
|
+
"""Navigation Root Links."""
|
|
137
|
+
links = []
|
|
138
|
+
if route := self.obj.get("up_route"):
|
|
139
|
+
links += [self._link(route, Rel.UP)]
|
|
140
|
+
page = self.kwargs.get("page", 1)
|
|
141
|
+
if page > 1:
|
|
142
|
+
route = {**self.kwargs, "page": page - 1}
|
|
143
|
+
links += [self._link(route, Rel.PREV)]
|
|
144
|
+
if page < self.obj.get("num_pages", 1):
|
|
145
|
+
route = {**self.kwargs, "page": page + 1}
|
|
146
|
+
links += [self._link(route, Rel.NEXT)]
|
|
147
|
+
return links
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def links(self):
|
|
151
|
+
"""Create all the links."""
|
|
152
|
+
links = []
|
|
153
|
+
try:
|
|
154
|
+
mime_type = MimeType.ACQUISITION if self.is_aq_feed else MimeType.NAV
|
|
155
|
+
links += [
|
|
156
|
+
OPDS1Link("self", self.request.get_full_path(), mime_type),
|
|
157
|
+
OPDS1Link(
|
|
158
|
+
Rel.AUTHENTICATION,
|
|
159
|
+
reverse("opds:authentication:v1"),
|
|
160
|
+
MimeType.AUTHENTICATION,
|
|
161
|
+
),
|
|
162
|
+
OPDS1Link(
|
|
163
|
+
"search", reverse("opds:v1:opensearch_v1"), MimeType.OPENSEARCH
|
|
164
|
+
),
|
|
165
|
+
]
|
|
166
|
+
links += self._root_links()
|
|
167
|
+
if self.use_facets:
|
|
168
|
+
for top_link in TopLinks.ALL + RootTopLinks.ALL:
|
|
169
|
+
if not self.is_top_link_displayed(top_link):
|
|
170
|
+
links += [self._top_link(top_link)]
|
|
171
|
+
if facets := self.facets():
|
|
172
|
+
links += facets
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
LOG.exception(exc)
|
|
175
|
+
return links
|
|
176
|
+
|
|
177
|
+
def _top_link_entry(self, top_link):
|
|
178
|
+
"""Create a entry instead of a facet."""
|
|
179
|
+
name = " ".join(filter(None, (top_link.glyph, top_link.title)))
|
|
180
|
+
entry_obj = OPDS1EntryObject(
|
|
181
|
+
group=top_link.kwargs["group"],
|
|
182
|
+
pk=top_link.kwargs["pk"],
|
|
183
|
+
name=name,
|
|
184
|
+
summary=top_link.desc,
|
|
185
|
+
)
|
|
186
|
+
issue_max = self.obj.get("issue_max", 0)
|
|
187
|
+
data = OPDS1EntryData(
|
|
188
|
+
self.acquisition_groups, issue_max, False, self.mime_type_map
|
|
189
|
+
)
|
|
190
|
+
return OPDS1Entry(entry_obj, top_link.query_params, data)
|
|
191
|
+
|
|
192
|
+
def add_top_links(self, top_links):
|
|
193
|
+
"""Add a list of top links as entries if they should be enabled."""
|
|
194
|
+
entries = []
|
|
195
|
+
for tl in top_links:
|
|
196
|
+
if not self.is_top_link_displayed(tl):
|
|
197
|
+
entries += [self._top_link_entry(tl)]
|
|
198
|
+
return entries
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Serve an opensearch document."""
|
|
1
|
+
"""Serve an opensearch v1 document."""
|
|
2
2
|
from drf_spectacular.types import OpenApiTypes
|
|
3
3
|
from drf_spectacular.utils import extend_schema
|
|
4
4
|
from rest_framework.authentication import BasicAuthentication, SessionAuthentication
|
|
@@ -8,10 +8,10 @@ from codex.views.template import CodexXMLTemplateView
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@extend_schema(responses={("200", "application/xml"): OpenApiTypes.BYTE})
|
|
11
|
-
class
|
|
11
|
+
class OpenSearch1View(CodexXMLTemplateView):
|
|
12
12
|
"""OpenSearchView."""
|
|
13
13
|
|
|
14
14
|
authentication_classes = (BasicAuthentication, SessionAuthentication)
|
|
15
15
|
permission_classes = [IsAuthenticatedOrEnabledNonUsers]
|
|
16
|
-
template_name = "
|
|
16
|
+
template_name = "opds_v1/opensearch_v1.xml"
|
|
17
17
|
content_type = "application/xml"
|
codex/views/opds/v2/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""OPDS
|
|
1
|
+
"""OPDS v2 Views."""
|
codex/views/opds/v2/const.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""OPDS
|
|
1
|
+
"""OPDS v2 consts."""
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Optional, Union
|
|
4
4
|
|
|
@@ -41,6 +41,7 @@ FACETS = (
|
|
|
41
41
|
Facet("p", "Publishers View"),
|
|
42
42
|
Facet("s", "Series View"),
|
|
43
43
|
Facet("f", "Folder View"),
|
|
44
|
+
Facet("a", "Story Arc View"),
|
|
44
45
|
),
|
|
45
46
|
)
|
|
46
47
|
# Could add Filters as well.
|
|
@@ -81,6 +82,12 @@ GROUPS = (
|
|
|
81
82
|
"s",
|
|
82
83
|
{"orderBy": "date", "orderReverse": False},
|
|
83
84
|
),
|
|
85
|
+
NavigationLink(
|
|
86
|
+
Rel.POPULAR,
|
|
87
|
+
"Last Read",
|
|
88
|
+
"s",
|
|
89
|
+
{"orderBy": "bookmark_updated_at", "orderReverse": True},
|
|
90
|
+
),
|
|
84
91
|
),
|
|
85
92
|
),
|
|
86
93
|
NavigationGroup(
|
|
@@ -89,7 +96,8 @@ GROUPS = (
|
|
|
89
96
|
NavigationLink(Rel.SUB, "Root", "r", None),
|
|
90
97
|
NavigationLink(Rel.SUB, "Publishers", "p", None),
|
|
91
98
|
NavigationLink(Rel.SUB, "Series", "s", None),
|
|
92
|
-
NavigationLink(Rel.SUB, "Folders", "f",
|
|
99
|
+
NavigationLink(Rel.SUB, "Folders", "f", {"topGroup": "f"}),
|
|
100
|
+
NavigationLink(Rel.SUB, "Story Arcs", "a", {"topGroup": "a"}),
|
|
93
101
|
),
|
|
94
102
|
),
|
|
95
103
|
)
|
codex/views/opds/v2/feed.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""OPDS
|
|
1
|
+
"""OPDS v2.0 Feed."""
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
3
|
|
|
4
4
|
from drf_spectacular.utils import extend_schema
|
|
@@ -6,6 +6,7 @@ from rest_framework.authentication import BasicAuthentication, SessionAuthentica
|
|
|
6
6
|
from rest_framework.response import Response
|
|
7
7
|
|
|
8
8
|
from codex.logger.logging import get_logger
|
|
9
|
+
from codex.models import AdminFlag
|
|
9
10
|
from codex.serializers.opds.v2 import OPDS2FeedSerializer
|
|
10
11
|
from codex.views.browser.browser import BrowserView
|
|
11
12
|
from codex.views.opds.const import BLANK_TITLE, FALSY
|
|
@@ -27,6 +28,8 @@ LOG = get_logger(__name__)
|
|
|
27
28
|
class OPDS2FeedView(PublicationMixin, TopLinksMixin):
|
|
28
29
|
"""OPDS 2.0 Feed."""
|
|
29
30
|
|
|
31
|
+
DEFAULT_ROUTE_NAME = "opds:v2:feed"
|
|
32
|
+
|
|
30
33
|
authentication_classes = (BasicAuthentication, SessionAuthentication)
|
|
31
34
|
serializer_class = OPDS2FeedSerializer
|
|
32
35
|
|
|
@@ -45,41 +48,92 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
|
|
|
45
48
|
return result
|
|
46
49
|
|
|
47
50
|
def _detect_user_agent(self):
|
|
48
|
-
|
|
51
|
+
"""Hacks for individual clients."""
|
|
49
52
|
user_agent = self.request.headers.get("User-Agent")
|
|
50
53
|
if not user_agent:
|
|
51
54
|
return
|
|
52
55
|
|
|
56
|
+
@staticmethod
|
|
57
|
+
def _is_allowed(link_spec):
|
|
58
|
+
"""Return if the link allowed."""
|
|
59
|
+
if (
|
|
60
|
+
getattr(link_spec, "group", None) == "f"
|
|
61
|
+
or getattr(link_spec, "query_param_value", None) == "f"
|
|
62
|
+
):
|
|
63
|
+
# Folder perms
|
|
64
|
+
efv_flag = (
|
|
65
|
+
AdminFlag.objects.only("on")
|
|
66
|
+
.get(key=AdminFlag.FlagChoices.FOLDER_VIEW.value)
|
|
67
|
+
.on
|
|
68
|
+
)
|
|
69
|
+
if not efv_flag:
|
|
70
|
+
return False
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _create_link_kwargs(data, link_spec):
|
|
75
|
+
"""Create link kwargs."""
|
|
76
|
+
if data.group_kwarg:
|
|
77
|
+
# Nav Groups
|
|
78
|
+
pk = getattr(link_spec, "pk", 0)
|
|
79
|
+
kwargs = {"group": link_spec.group, "pk": pk, "page": 1}
|
|
80
|
+
elif link_spec.query_param_value in ("f", "a"):
|
|
81
|
+
# Special Facets
|
|
82
|
+
kwargs = {
|
|
83
|
+
"group": link_spec.query_param_value,
|
|
84
|
+
"pk": 0,
|
|
85
|
+
"page": 1,
|
|
86
|
+
}
|
|
87
|
+
else:
|
|
88
|
+
# Regular Facets
|
|
89
|
+
kwargs = None
|
|
90
|
+
return kwargs
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _create_link_query_params(group_spec, link_spec, kwargs):
|
|
94
|
+
"""Create link query params."""
|
|
95
|
+
if qp_key := getattr(group_spec, "query_param_key", None):
|
|
96
|
+
# Facet shorthand with group
|
|
97
|
+
qps = {qp_key: link_spec.query_param_value}
|
|
98
|
+
elif ls_qps := getattr(link_spec, "query_params", None):
|
|
99
|
+
# Nav Group
|
|
100
|
+
qps = ls_qps
|
|
101
|
+
else:
|
|
102
|
+
# Regular Group
|
|
103
|
+
qps = None
|
|
104
|
+
|
|
105
|
+
# Special order by for story_arcs
|
|
106
|
+
if (
|
|
107
|
+
kwargs
|
|
108
|
+
and kwargs.get("group") == "a"
|
|
109
|
+
and kwargs.get("pk")
|
|
110
|
+
and (not qps or not qps.get("orderBy"))
|
|
111
|
+
):
|
|
112
|
+
if not qps:
|
|
113
|
+
qps = {}
|
|
114
|
+
qps["orderBy"] = "story_arc_number"
|
|
115
|
+
return qps
|
|
116
|
+
|
|
53
117
|
def _create_links_section(self, group_specs, data):
|
|
54
118
|
"""Create links sections for groups and facets."""
|
|
55
119
|
groups = []
|
|
56
120
|
for group_spec in group_specs:
|
|
57
121
|
link_dict = {}
|
|
58
122
|
for link_spec in group_spec.links:
|
|
59
|
-
if
|
|
60
|
-
|
|
61
|
-
pk = getattr(link_spec, "pk", 0)
|
|
62
|
-
kwargs = {"group": link_spec.group, "pk": pk, "page": 1}
|
|
63
|
-
else:
|
|
64
|
-
# Facets & Regular Groups
|
|
65
|
-
kwargs = None
|
|
66
|
-
rel = data.rel if data.rel else link_spec.rel
|
|
123
|
+
if not self._is_allowed(link_spec):
|
|
124
|
+
continue
|
|
67
125
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
elif ls_qps := getattr(link_spec, "query_params", None):
|
|
72
|
-
# Nav Group
|
|
73
|
-
qps = ls_qps
|
|
74
|
-
else:
|
|
75
|
-
# Regular Group
|
|
76
|
-
qps = None
|
|
126
|
+
kwargs = self._create_link_kwargs(data, link_spec)
|
|
127
|
+
|
|
128
|
+
qps = self._create_link_query_params(group_spec, link_spec, kwargs)
|
|
77
129
|
|
|
78
130
|
title = getattr(link_spec, "title", "")
|
|
79
131
|
if not title:
|
|
80
132
|
title = getattr(link_spec, "name", "")
|
|
81
133
|
|
|
82
134
|
href_data = HrefData(kwargs, qps)
|
|
135
|
+
|
|
136
|
+
rel = data.rel if data.rel else link_spec.rel
|
|
83
137
|
link_data = LinkData(rel, href_data, title=title)
|
|
84
138
|
link = self.link(link_data)
|
|
85
139
|
self.link_aggregate(link_dict, link)
|
|
@@ -117,8 +171,15 @@ class OPDS2FeedView(PublicationMixin, TopLinksMixin):
|
|
|
117
171
|
def get_object(self):
|
|
118
172
|
"""Get the browser page and serialize it for this subclass."""
|
|
119
173
|
group = self.kwargs.get("group")
|
|
120
|
-
|
|
121
|
-
|
|
174
|
+
if group in ("f", "a"):
|
|
175
|
+
self.acquisition_groups = frozenset()
|
|
176
|
+
else:
|
|
177
|
+
self.acquisition_groups = frozenset(self.valid_nav_groups[-2:])
|
|
178
|
+
if group == "a":
|
|
179
|
+
pk = self.kwargs["pk"]
|
|
180
|
+
self.is_opds_2_acquisition = bool(pk)
|
|
181
|
+
else:
|
|
182
|
+
self.is_opds_2_acquisition = group in self.acquisition_groups
|
|
122
183
|
self.is_opds_metadata = (
|
|
123
184
|
self.request.query_params.get("opdsMetadata", "").lower() not in FALSY
|
|
124
185
|
)
|
codex/views/opds/v2/links.py
CHANGED
codex/views/opds/v2/top_links.py
CHANGED
codex/views/reader/page.py
CHANGED
|
@@ -8,11 +8,11 @@ from rest_framework.negotiation import BaseContentNegotiation
|
|
|
8
8
|
|
|
9
9
|
from codex.logger.logging import get_logger
|
|
10
10
|
from codex.models import Comic
|
|
11
|
-
from codex.pdf import PDF
|
|
12
11
|
from codex.version import COMICBOX_CONFIG
|
|
13
12
|
from codex.views.bookmark import BookmarkBaseView
|
|
14
13
|
|
|
15
14
|
LOG = get_logger(__name__)
|
|
15
|
+
PDF_MIME_TYPE = "application/pdf"
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class IgnoreClientContentNegotiation(BaseContentNegotiation):
|
|
@@ -50,23 +50,22 @@ class ReaderPageView(BookmarkBaseView):
|
|
|
50
50
|
|
|
51
51
|
def _get_page_image(self):
|
|
52
52
|
"""Get the image data and content type."""
|
|
53
|
-
group_acl_filter = self.get_group_acl_filter(
|
|
53
|
+
group_acl_filter = self.get_group_acl_filter(Comic)
|
|
54
54
|
pk = self.kwargs.get("pk")
|
|
55
55
|
comic = Comic.objects.filter(group_acl_filter).only("path").get(pk=pk)
|
|
56
56
|
page = self.kwargs.get("page")
|
|
57
57
|
if comic.file_type == Comic.FileType.PDF.value:
|
|
58
|
-
|
|
59
|
-
content_type = PDF.MIME_TYPE
|
|
58
|
+
content_type = PDF_MIME_TYPE
|
|
60
59
|
else:
|
|
61
|
-
car = ComicArchive(comic.path, config=COMICBOX_CONFIG)
|
|
62
60
|
content_type = self.content_type
|
|
63
|
-
|
|
61
|
+
with ComicArchive(comic.path, config=COMICBOX_CONFIG) as car:
|
|
62
|
+
page_image = car.get_page_by_index(page)
|
|
64
63
|
return page_image, content_type
|
|
65
64
|
|
|
66
65
|
@extend_schema(
|
|
67
66
|
responses={
|
|
68
67
|
(200, content_type): OpenApiTypes.BINARY,
|
|
69
|
-
(200,
|
|
68
|
+
(200, PDF_MIME_TYPE): OpenApiTypes.BINARY,
|
|
70
69
|
}
|
|
71
70
|
)
|
|
72
71
|
def get(self, *args, **kwargs):
|