codex 1.4.0a0__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/coverd.py +3 -11
- codex/librarian/covers/create.py +15 -21
- codex/librarian/covers/tasks.py +2 -16
- 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 +44 -5
- codex/librarian/importer/importerd.py +37 -12
- 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-a6ac9581.2fd9e52cbcc3.css → main-0898f4bb.181e0145c642.css} +1 -1
- codex/static_root/assets/main-0898f4bb.181e0145c642.css.br +0 -0
- codex/static_root/assets/{main-a6ac9581.2fd9e52cbcc3.css.gz → main-0898f4bb.181e0145c642.css.gz} +0 -0
- codex/static_root/assets/{main-a6ac9581.css → main-0898f4bb.css} +1 -1
- codex/static_root/assets/main-0898f4bb.css.br +0 -0
- codex/static_root/assets/{main-a6ac9581.css.gz → main-0898f4bb.css.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-abfd509d.3870dab8eaf4.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-abfd509d.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-admin.24cecf0a0568.json +1 -0
- codex/static_root/js/choices-admin.24cecf0a0568.json.br +0 -0
- codex/static_root/js/choices-admin.24cecf0a0568.json.gz +0 -0
- codex/static_root/js/choices-admin.json +1 -1
- codex/static_root/js/choices-admin.json.br +0 -0
- codex/static_root/js/choices-admin.json.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.64a989215af8.json → manifest.d2f93a519ada.json} +34 -34
- codex/static_root/manifest.d2f93a519ada.json.br +0 -0
- codex/static_root/manifest.d2f93a519ada.json.gz +0 -0
- codex/static_root/manifest.json +34 -34
- 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/admin/tasks.py +6 -1
- codex/views/bookmark.py +2 -2
- codex/views/browser/base.py +23 -7
- codex/views/browser/browser.py +66 -56
- codex/views/browser/browser_annotations.py +159 -50
- codex/views/browser/browser_order_by.py +51 -105
- 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.0a0.dist-info → codex-1.4.1.dist-info}/METADATA +10 -41
- {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/RECORD +187 -185
- codex/librarian/importer/db_ops.py +0 -248
- codex/pdf.py +0 -115
- codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js +0 -48
- codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.br +0 -0
- codex/static_root/assets/admin-73d93dc7.2c3eb62e50a0.js.gz +0 -0
- codex/static_root/assets/admin-73d93dc7.js +0 -48
- codex/static_root/assets/admin-73d93dc7.js.br +0 -0
- codex/static_root/assets/admin-73d93dc7.js.gz +0 -0
- codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css +0 -1
- codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.br +0 -0
- codex/static_root/assets/admin-79555229.5f2c4cb3a73c.css.gz +0 -0
- codex/static_root/assets/admin-79555229.css +0 -1
- codex/static_root/assets/admin-79555229.css.br +0 -0
- codex/static_root/assets/admin-79555229.css.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js +0 -1
- codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-64bcc083.a85324c9ccd8.js.gz +0 -0
- codex/static_root/assets/admin-drawer-panel-64bcc083.js +0 -1
- codex/static_root/assets/admin-drawer-panel-64bcc083.js.br +0 -0
- codex/static_root/assets/admin-drawer-panel-64bcc083.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-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/browser-d2caeed7.2262000a6d55.js +0 -1
- codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.br +0 -0
- codex/static_root/assets/browser-d2caeed7.2262000a6d55.js.gz +0 -0
- codex/static_root/assets/browser-d2caeed7.js +0 -1
- codex/static_root/assets/browser-d2caeed7.js.br +0 -0
- codex/static_root/assets/browser-d2caeed7.js.gz +0 -0
- codex/static_root/assets/http-error-0221c37d.480d5066da92.js +0 -1
- codex/static_root/assets/http-error-0221c37d.480d5066da92.js.br +0 -0
- codex/static_root/assets/http-error-0221c37d.480d5066da92.js.gz +0 -0
- codex/static_root/assets/http-error-0221c37d.js +0 -1
- codex/static_root/assets/http-error-0221c37d.js.br +0 -0
- codex/static_root/assets/http-error-0221c37d.js.gz +0 -0
- codex/static_root/assets/main-a6ac9581.2fd9e52cbcc3.css.br +0 -0
- codex/static_root/assets/main-a6ac9581.css.br +0 -0
- codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js +0 -1
- codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.br +0 -0
- codex/static_root/assets/main-e33dcfb0.a65044fc1a08.js.gz +0 -0
- codex/static_root/assets/main-e33dcfb0.js +0 -1
- codex/static_root/assets/main-e33dcfb0.js.br +0 -0
- codex/static_root/assets/main-e33dcfb0.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-8b0e8aaa.d12b42b1c9da.js +0 -1
- codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8b0e8aaa.d12b42b1c9da.js.gz +0 -0
- codex/static_root/assets/metadata-dialog-8b0e8aaa.js +0 -1
- codex/static_root/assets/metadata-dialog-8b0e8aaa.js.br +0 -0
- codex/static_root/assets/metadata-dialog-8b0e8aaa.js.gz +0 -0
- codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.br +0 -0
- codex/static_root/assets/page-pdf-abfd509d.3870dab8eaf4.js.gz +0 -0
- codex/static_root/assets/page-pdf-abfd509d.js.br +0 -0
- codex/static_root/assets/page-pdf-abfd509d.js.gz +0 -0
- codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css +0 -1
- codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.br +0 -0
- codex/static_root/assets/reader-a8b8f766.875abdd0d22e.css.gz +0 -0
- codex/static_root/assets/reader-a8b8f766.css +0 -1
- codex/static_root/assets/reader-a8b8f766.css.br +0 -0
- codex/static_root/assets/reader-a8b8f766.css.gz +0 -0
- codex/static_root/assets/reader-fe9345d2.759c31f82998.js +0 -1
- codex/static_root/assets/reader-fe9345d2.759c31f82998.js.br +0 -0
- codex/static_root/assets/reader-fe9345d2.759c31f82998.js.gz +0 -0
- codex/static_root/assets/reader-fe9345d2.js +0 -1
- codex/static_root/assets/reader-fe9345d2.js.br +0 -0
- codex/static_root/assets/reader-fe9345d2.js.gz +0 -0
- codex/static_root/js/choices-admin.3d958ea7f83b.json +0 -1
- codex/static_root/js/choices-admin.3d958ea7f83b.json.br +0 -0
- codex/static_root/js/choices-admin.3d958ea7f83b.json.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.64a989215af8.json.br +0 -0
- codex/static_root/manifest.64a989215af8.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.0a0.dist-info → codex-1.4.1.dist-info}/LICENSE +0 -0
- {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/WHEEL +0 -0
- {codex-1.4.0a0.dist-info → codex-1.4.1.dist-info}/entry_points.txt +0 -0
codex/views/browser/metadata.py
CHANGED
|
@@ -7,7 +7,7 @@ from rest_framework.exceptions import NotFound
|
|
|
7
7
|
from rest_framework.response import Response
|
|
8
8
|
|
|
9
9
|
from codex.comic_field_names import COMIC_M2M_FIELD_NAMES
|
|
10
|
-
from codex.models import AdminFlag, Comic
|
|
10
|
+
from codex.models import AdminFlag, Comic, StoryArc
|
|
11
11
|
from codex.serializers.metadata import MetadataSerializer
|
|
12
12
|
from codex.views.auth import IsAuthenticatedOrEnabledNonUsers
|
|
13
13
|
from codex.views.browser.browser_annotations import BrowserAnnotationsView
|
|
@@ -28,6 +28,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
28
28
|
"critical_rating",
|
|
29
29
|
"day",
|
|
30
30
|
"file_type",
|
|
31
|
+
"gtin",
|
|
31
32
|
"issue",
|
|
32
33
|
"issue_suffix",
|
|
33
34
|
"language",
|
|
@@ -56,6 +57,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
56
57
|
_COMIC_RELATED_VALUE_FIELDS = {"series__volume_count", "volume__issue_count"}
|
|
57
58
|
_PATH_GROUPS = ("c", "f")
|
|
58
59
|
_CREATOR_RELATIONS = ("role", "person")
|
|
60
|
+
_STORY_ARC_NUMBER_RELATIONS = ("story_arc",)
|
|
59
61
|
|
|
60
62
|
def _get_comic_value_fields(self):
|
|
61
63
|
"""Include the path field for staff."""
|
|
@@ -79,7 +81,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
79
81
|
# Have to use simple_qs because every annotation in the loop
|
|
80
82
|
# corrupts the the main qs
|
|
81
83
|
# If 1 variant, annotate value, otherwise None
|
|
82
|
-
full_field =
|
|
84
|
+
full_field = self.rel_prefix + field
|
|
83
85
|
|
|
84
86
|
sq = (
|
|
85
87
|
simple_qs.values("id")
|
|
@@ -105,7 +107,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
105
107
|
def _annotate_aggregates(self, qs):
|
|
106
108
|
"""Annotate aggregate values."""
|
|
107
109
|
if not self.is_model_comic:
|
|
108
|
-
size_func = self.get_aggregate_func("size"
|
|
110
|
+
size_func = self.get_aggregate_func(self.model, "size")
|
|
109
111
|
qs = qs.annotate(size=size_func)
|
|
110
112
|
qs = self.annotate_common_aggregates(qs, self.model, {})
|
|
111
113
|
return qs
|
|
@@ -135,18 +137,32 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
135
137
|
)
|
|
136
138
|
return qs
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
140
|
+
@staticmethod
|
|
141
|
+
def _get_intersection_queryset(qs, values, count_rel, comic_pks):
|
|
142
|
+
"""Create an intersection queryset."""
|
|
143
|
+
return (
|
|
144
|
+
qs.only(*values)
|
|
145
|
+
.annotate(count=Count(count_rel))
|
|
146
|
+
.order_by()
|
|
147
|
+
.filter(count=comic_pks.count())
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def _get_story_arc_intersection_queryset(cls, comic_pks):
|
|
152
|
+
"""Hoist Story Arc intersections up from StoryArcNumber."""
|
|
153
|
+
qs = StoryArc.objects.filter(storyarcnumber__comic__pk__in=comic_pks)
|
|
154
|
+
return cls._get_intersection_queryset(
|
|
155
|
+
qs,
|
|
156
|
+
("name",),
|
|
157
|
+
"storyarcnumber__comic",
|
|
158
|
+
comic_pks,
|
|
159
|
+
)
|
|
144
160
|
|
|
145
161
|
def _query_m2m_intersections(self, simple_qs):
|
|
146
162
|
"""Query the through models to figure out m2m intersections."""
|
|
147
163
|
# Speed ok, but still does a query per m2m model
|
|
148
164
|
m2m_intersections = {}
|
|
149
|
-
pk_field =
|
|
165
|
+
pk_field = self.rel_prefix + "pk"
|
|
150
166
|
comic_pks = simple_qs.values_list(pk_field, flat=True)
|
|
151
167
|
for field_name in COMIC_M2M_FIELD_NAMES:
|
|
152
168
|
model = Comic._meta.get_field(field_name).related_model
|
|
@@ -156,19 +172,28 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
156
172
|
|
|
157
173
|
intersection_qs = model.objects.filter(comic__pk__in=comic_pks)
|
|
158
174
|
if field_name == "creators":
|
|
175
|
+
# XXX This doesn't prevent an n+1 warning
|
|
159
176
|
intersection_qs = intersection_qs.select_related(
|
|
160
177
|
*self._CREATOR_RELATIONS
|
|
161
178
|
)
|
|
162
179
|
values = self._CREATOR_RELATIONS
|
|
180
|
+
elif field_name == "story_arc_numbers":
|
|
181
|
+
# XXX This doesn't prevent an n+1 warning
|
|
182
|
+
intersection_qs = intersection_qs.select_related(
|
|
183
|
+
*self._STORY_ARC_NUMBER_RELATIONS
|
|
184
|
+
)
|
|
185
|
+
values = self._STORY_ARC_NUMBER_RELATIONS
|
|
186
|
+
|
|
187
|
+
# Extra add on m2m
|
|
188
|
+
m2m_intersections[
|
|
189
|
+
"story_arcs"
|
|
190
|
+
] = self._get_story_arc_intersection_queryset(comic_pks)
|
|
163
191
|
else:
|
|
164
192
|
values = ("name",)
|
|
165
193
|
|
|
166
194
|
# order_by() is very important for grouping
|
|
167
|
-
intersection_qs = (
|
|
168
|
-
intersection_qs
|
|
169
|
-
.annotate(count=Count("comic"))
|
|
170
|
-
.order_by()
|
|
171
|
-
.filter(count=comic_pks.count())
|
|
195
|
+
intersection_qs = self._get_intersection_queryset(
|
|
196
|
+
intersection_qs, values, "comic", comic_pks
|
|
172
197
|
)
|
|
173
198
|
m2m_intersections[field_name] = intersection_qs
|
|
174
199
|
return m2m_intersections
|
|
@@ -221,7 +246,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
221
246
|
|
|
222
247
|
# filename
|
|
223
248
|
if self.model == Comic:
|
|
224
|
-
obj.filename =
|
|
249
|
+
obj.filename = obj.filename()
|
|
225
250
|
|
|
226
251
|
return obj
|
|
227
252
|
|
|
@@ -234,7 +259,7 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
234
259
|
if self.model is None:
|
|
235
260
|
raise NotFound(detail=f"Cannot get metadata for {self.group=}")
|
|
236
261
|
|
|
237
|
-
object_filter, _ = self.get_query_filters_without_group(self.
|
|
262
|
+
object_filter, _ = self.get_query_filters_without_group(self.model)
|
|
238
263
|
pk = self.kwargs["pk"]
|
|
239
264
|
qs = self.model.objects.filter(object_filter, pk=pk)
|
|
240
265
|
|
|
@@ -242,10 +267,8 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
242
267
|
simple_qs = qs
|
|
243
268
|
|
|
244
269
|
qs = self._annotate_values_and_fks(qs, simple_qs)
|
|
245
|
-
qs = self._annotate_for_filename(qs)
|
|
246
270
|
|
|
247
271
|
try:
|
|
248
|
-
# obj = qs.values()[0]
|
|
249
272
|
obj = qs.first()
|
|
250
273
|
if not obj:
|
|
251
274
|
reason = "Empty obj"
|
|
@@ -282,6 +305,8 @@ class MetadataView(BrowserAnnotationsView):
|
|
|
282
305
|
self.parse_params()
|
|
283
306
|
self.group = self.kwargs["group"]
|
|
284
307
|
self._validate()
|
|
308
|
+
self.rel_prefix = self.get_rel_prefix(self.model)
|
|
309
|
+
self.set_order_key()
|
|
285
310
|
|
|
286
311
|
obj = self.get_object()
|
|
287
312
|
|
codex/views/download.py
CHANGED
|
@@ -25,7 +25,7 @@ class DownloadView(APIView, GroupACLMixin):
|
|
|
25
25
|
"""Download a comic archive."""
|
|
26
26
|
pk = kwargs.get("pk")
|
|
27
27
|
try:
|
|
28
|
-
group_acl_filter = self.get_group_acl_filter(
|
|
28
|
+
group_acl_filter = self.get_group_acl_filter(Comic)
|
|
29
29
|
comic = (
|
|
30
30
|
Comic.objects.filter(group_acl_filter)
|
|
31
31
|
.select_related(*self._DOWNLOAD_SELECT_RELATED)
|
codex/views/frontend.py
CHANGED
|
@@ -14,7 +14,6 @@ class IndexView(BrowserSessionViewBase):
|
|
|
14
14
|
|
|
15
15
|
def get(self, *args, **kwargs):
|
|
16
16
|
"""Get the app index page."""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
17
|
+
last_route = self.get_from_session("route")
|
|
18
|
+
extra_context = {"last_route": last_route}
|
|
20
19
|
return Response(extra_context)
|
codex/views/mixins.py
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""A filter for group ACLS."""
|
|
2
2
|
from django.db.models import Q
|
|
3
3
|
|
|
4
|
+
from codex.models import Comic, Folder, StoryArc
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
class GroupACLMixin:
|
|
6
8
|
"""Filter group ACLS for views."""
|
|
7
9
|
|
|
8
10
|
ROOT_GROUP = "r"
|
|
9
11
|
FOLDER_GROUP = "f"
|
|
12
|
+
STORY_ARC_GROUP = "a"
|
|
10
13
|
COMIC_GROUP = "c"
|
|
11
14
|
GROUP_RELATION = {
|
|
12
15
|
"p": "publisher",
|
|
@@ -15,12 +18,22 @@ class GroupACLMixin:
|
|
|
15
18
|
"v": "volume",
|
|
16
19
|
COMIC_GROUP: "pk",
|
|
17
20
|
FOLDER_GROUP: "parent_folder",
|
|
21
|
+
STORY_ARC_GROUP: "story_arc_numbers__story_arc",
|
|
18
22
|
}
|
|
19
23
|
|
|
20
|
-
def
|
|
24
|
+
def get_rel_prefix(self, model):
|
|
25
|
+
"""Return the relation prfiex for most fields."""
|
|
26
|
+
prefix = ""
|
|
27
|
+
if model != Comic:
|
|
28
|
+
if model == StoryArc:
|
|
29
|
+
prefix += "storyarcnumber__"
|
|
30
|
+
prefix += "comic__"
|
|
31
|
+
return prefix
|
|
32
|
+
|
|
33
|
+
def get_group_acl_filter(self, model):
|
|
21
34
|
"""Generate the group acl filter for comics."""
|
|
22
35
|
# The rel prefix
|
|
23
|
-
prefix =
|
|
36
|
+
prefix = self.get_rel_prefix(model) if model != Folder else ""
|
|
24
37
|
groups_rel = f"{prefix}library__groups"
|
|
25
38
|
|
|
26
39
|
# Libraries with no groups are always visible
|
codex/views/opds/const.py
CHANGED
|
@@ -15,6 +15,7 @@ class Rel:
|
|
|
15
15
|
IMAGE = "http://opds-spec.org/image"
|
|
16
16
|
STREAM = "http://vaemendis.net/opds-pse/stream"
|
|
17
17
|
SORT_NEW = "http://opds-spec.org/sort/new"
|
|
18
|
+
POPULAR = "http://opds-spec.org/sort/popular"
|
|
18
19
|
FEATURED = "http://opds-spec.org/featured"
|
|
19
20
|
SELF = "self"
|
|
20
21
|
START = "start"
|
|
@@ -40,7 +41,6 @@ class MimeType:
|
|
|
40
41
|
ENTRY_CATALOG = ";".join((ATOM, "type=entry", _PROFILE_CATALOG))
|
|
41
42
|
AUTHENTICATION = "application/opds-authentication+json"
|
|
42
43
|
OPENSEARCH = "application/opensearchdescription+xml"
|
|
43
|
-
DOWNLOAD = "application/zip" # PocketBooks needs app/zip
|
|
44
44
|
STREAM = "image/jpeg"
|
|
45
45
|
OPDS_JSON = "application/opds+json"
|
|
46
46
|
OPDS_PUB = "application/opds-publication+json"
|
|
@@ -57,4 +57,11 @@ class MimeType:
|
|
|
57
57
|
"CBT": "application/vnd.comicbook+tar",
|
|
58
58
|
"PDF": "application/pdf",
|
|
59
59
|
}
|
|
60
|
+
SIMPLE_FILE_TYPE_MAP = {
|
|
61
|
+
# PocketBooks needs app/zip
|
|
62
|
+
"CBZ": "application/zip",
|
|
63
|
+
"CBR": "application/x-rar-compressed",
|
|
64
|
+
"CBT": "application/x-tar",
|
|
65
|
+
"PDF": "application/pdf",
|
|
66
|
+
}
|
|
60
67
|
OCTET = "application/octet-stream"
|
codex/views/opds/util.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""OPDS Utility classes."""
|
|
2
2
|
from django.db.models import F
|
|
3
|
+
from django.http.response import HttpResponseRedirect
|
|
4
|
+
from django.urls import reverse
|
|
3
5
|
from django.utils.http import urlencode
|
|
4
6
|
|
|
5
7
|
from codex.models import (
|
|
@@ -13,6 +15,7 @@ from codex.models import (
|
|
|
13
15
|
Tag,
|
|
14
16
|
Team,
|
|
15
17
|
)
|
|
18
|
+
from codex.serializers.choices import DEFAULTS
|
|
16
19
|
|
|
17
20
|
OPDS_M2M_MODELS = (Character, Genre, Location, SeriesGroup, StoryArc, Tag, Team)
|
|
18
21
|
|
|
@@ -61,6 +64,39 @@ def get_m2m_objects(pk) -> dict:
|
|
|
61
64
|
cats = {}
|
|
62
65
|
for model in OPDS_M2M_MODELS:
|
|
63
66
|
table = model.__name__.lower()
|
|
64
|
-
|
|
67
|
+
rel = "comic"
|
|
68
|
+
if model == StoryArc:
|
|
69
|
+
rel = "storyarcnumber__" + rel
|
|
70
|
+
comic_filter = {rel: pk}
|
|
71
|
+
qs = model.objects.filter(**comic_filter).order_by("name").only("name")
|
|
65
72
|
cats[table] = qs
|
|
73
|
+
|
|
66
74
|
return cats
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def full_redirect_view(url_name):
|
|
78
|
+
"""Redirect to view, for a url name."""
|
|
79
|
+
|
|
80
|
+
def func(request):
|
|
81
|
+
"""Redirect to view, forwarding query strings and auth."""
|
|
82
|
+
kwargs = DEFAULTS["route"]
|
|
83
|
+
url = reverse(url_name, kwargs=kwargs)
|
|
84
|
+
|
|
85
|
+
# Forward the query string.
|
|
86
|
+
path = request.get_full_path()
|
|
87
|
+
if path:
|
|
88
|
+
parts = path.split("?")
|
|
89
|
+
if len(parts) >= 2: # noqa PLR2004
|
|
90
|
+
parts[0] = url
|
|
91
|
+
url = "?".join(parts)
|
|
92
|
+
|
|
93
|
+
response = HttpResponseRedirect(url)
|
|
94
|
+
|
|
95
|
+
# Forward authorization.
|
|
96
|
+
auth_header = request.META.get("HTTP_AUTHORIZATION")
|
|
97
|
+
if auth_header:
|
|
98
|
+
response["HTTP_AUTHORIZATION"] = auth_header
|
|
99
|
+
|
|
100
|
+
return response
|
|
101
|
+
|
|
102
|
+
return func
|
codex/views/opds/v1/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"""OPDS
|
|
1
|
+
"""OPDS v1 Views."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""OPDS v1 Data classes."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class OPDS1Link:
|
|
9
|
+
"""An OPDS Link."""
|
|
10
|
+
|
|
11
|
+
rel: str
|
|
12
|
+
href: str
|
|
13
|
+
mime_type: str
|
|
14
|
+
title: str = ""
|
|
15
|
+
length: int = 0
|
|
16
|
+
facet_group: str = ""
|
|
17
|
+
facet_active: bool = False
|
|
18
|
+
thr_count: int = 0
|
|
19
|
+
pse_count: int = 0
|
|
20
|
+
pse_last_read: int = 0
|
|
21
|
+
pse_last_read_date: Optional[datetime] = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OPDS v1 Entries."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""OPDS v1 Entry Data classes."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class OPDS1EntryObject:
|
|
7
|
+
"""Fake entry db object for top link & facet entries."""
|
|
8
|
+
|
|
9
|
+
group: str = ""
|
|
10
|
+
pk: int = 0
|
|
11
|
+
name: str = ""
|
|
12
|
+
summary: str = ""
|
|
13
|
+
fake: bool = True
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class OPDS1EntryData:
|
|
18
|
+
"""Entry Data class to avoid to many args."""
|
|
19
|
+
|
|
20
|
+
acquisition_groups: frozenset
|
|
21
|
+
issue_max: int
|
|
22
|
+
metadata: bool
|
|
23
|
+
mime_type_map: dict
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""OPDS v1 Entry."""
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from urllib.parse import urlencode
|
|
5
|
+
|
|
6
|
+
from django.urls import reverse
|
|
7
|
+
|
|
8
|
+
from codex.logger.logging import get_logger
|
|
9
|
+
from codex.models import Comic
|
|
10
|
+
from codex.views.opds.const import (
|
|
11
|
+
AUTHOR_ROLES,
|
|
12
|
+
BLANK_TITLE,
|
|
13
|
+
)
|
|
14
|
+
from codex.views.opds.util import (
|
|
15
|
+
get_creator_people,
|
|
16
|
+
get_m2m_objects,
|
|
17
|
+
)
|
|
18
|
+
from codex.views.opds.v1.entry.links import OPDS1EntryLinksMixin
|
|
19
|
+
|
|
20
|
+
LOG = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OPDS1Entry(OPDS1EntryLinksMixin):
|
|
24
|
+
"""An OPDS entry object."""
|
|
25
|
+
|
|
26
|
+
_DATE_FORMAT_BASE = "%Y-%m-%dT%H:%M:%S"
|
|
27
|
+
_DATE_FORMAT_MS = _DATE_FORMAT_BASE + ".%f%z"
|
|
28
|
+
_DATE_FORMAT = _DATE_FORMAT_BASE + "%z"
|
|
29
|
+
_DATE_FORMATS = (_DATE_FORMAT_MS, _DATE_FORMAT)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def id_tag(self):
|
|
33
|
+
"""GUID is a nav url."""
|
|
34
|
+
# Id top links by query params but not regular entries.
|
|
35
|
+
return self._nav_href(metadata=self.metadata)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def title(self):
|
|
39
|
+
"""Compute the item title."""
|
|
40
|
+
result = ""
|
|
41
|
+
try:
|
|
42
|
+
parts = []
|
|
43
|
+
if not self.fake:
|
|
44
|
+
group = self.obj.group
|
|
45
|
+
if group == "i":
|
|
46
|
+
parts.append(self.obj.publisher_name)
|
|
47
|
+
elif group == "v":
|
|
48
|
+
parts.append(self.obj.series_name)
|
|
49
|
+
elif group == "c":
|
|
50
|
+
title = Comic.get_title(self.obj, issue_max=self.issue_max)
|
|
51
|
+
parts.append(title)
|
|
52
|
+
|
|
53
|
+
if name := self.obj.name:
|
|
54
|
+
parts.append(name)
|
|
55
|
+
|
|
56
|
+
result = " ".join(filter(None, parts))
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
LOG.exception(exc)
|
|
59
|
+
|
|
60
|
+
if not result:
|
|
61
|
+
result = BLANK_TITLE
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def issued(self):
|
|
66
|
+
"""Return the published date."""
|
|
67
|
+
if self.obj.group == "c":
|
|
68
|
+
return self.obj.date
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def publisher(self):
|
|
73
|
+
"""Return the publisher."""
|
|
74
|
+
return self.obj.publisher_name
|
|
75
|
+
|
|
76
|
+
def _get_datefield(self, key):
|
|
77
|
+
result = None
|
|
78
|
+
if not self.fake and (value := getattr(self.obj, key, None)):
|
|
79
|
+
for date_format in self._DATE_FORMATS:
|
|
80
|
+
try:
|
|
81
|
+
if isinstance(value, str):
|
|
82
|
+
result = datetime.strptime(value, date_format).astimezone(
|
|
83
|
+
timezone.utc
|
|
84
|
+
)
|
|
85
|
+
if isinstance(value, datetime):
|
|
86
|
+
result = value.astimezone(timezone.utc).strftime(date_format)
|
|
87
|
+
break
|
|
88
|
+
except ValueError:
|
|
89
|
+
pass
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def updated(self):
|
|
94
|
+
"""When the entry was last updated."""
|
|
95
|
+
return self._get_datefield("updated_at")
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def published(self):
|
|
99
|
+
"""When the entry was created."""
|
|
100
|
+
return self._get_datefield("created_at")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def language(self):
|
|
104
|
+
"""Return the entry language."""
|
|
105
|
+
return self.obj.language
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def summary(self):
|
|
109
|
+
"""Return a child count or comic summary."""
|
|
110
|
+
if self.obj.group == "c":
|
|
111
|
+
desc = self.obj.summary
|
|
112
|
+
else:
|
|
113
|
+
children = self.obj.child_count
|
|
114
|
+
desc = f"{children} issues"
|
|
115
|
+
return desc
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _add_url_to_obj(objs, filter_key):
|
|
119
|
+
"""Add filter urls to objects."""
|
|
120
|
+
kwargs = {"group": "s", "pk": 0, "page": 1}
|
|
121
|
+
url_base = reverse("opds:v1:feed", kwargs=kwargs)
|
|
122
|
+
result = []
|
|
123
|
+
for obj in objs:
|
|
124
|
+
qp = {"filters": json.dumps({filter_key: [obj.pk]})}
|
|
125
|
+
qp = urlencode(qp)
|
|
126
|
+
obj.url = url_base + "?" + qp
|
|
127
|
+
result.append(obj)
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def authors(self):
|
|
132
|
+
"""Get Author names."""
|
|
133
|
+
if not self.metadata:
|
|
134
|
+
return []
|
|
135
|
+
people = get_creator_people(self.obj.pk, AUTHOR_ROLES)
|
|
136
|
+
return self._add_url_to_obj(people, "creators")
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def contributors(self):
|
|
140
|
+
"""Get Contributor names."""
|
|
141
|
+
if not self.metadata:
|
|
142
|
+
return []
|
|
143
|
+
people = get_creator_people(self.obj.pk, AUTHOR_ROLES, exclude=True)
|
|
144
|
+
return self._add_url_to_obj(people, "creators")
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def category_groups(self):
|
|
148
|
+
"""Get Category labels."""
|
|
149
|
+
if not self.metadata:
|
|
150
|
+
return {}
|
|
151
|
+
return get_m2m_objects(self.obj.pk)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""OPDS v1 Entry Links Methods."""
|
|
2
|
+
from urllib.parse import quote_plus
|
|
3
|
+
|
|
4
|
+
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
5
|
+
from django.urls import reverse
|
|
6
|
+
|
|
7
|
+
from codex.models import Comic
|
|
8
|
+
from codex.views.opds.const import MimeType, Rel
|
|
9
|
+
from codex.views.opds.util import update_href_query_params
|
|
10
|
+
from codex.views.opds.v1.data import OPDS1Link
|
|
11
|
+
from codex.views.opds.v1.entry.data import OPDS1EntryData, OPDS1EntryObject
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OPDS1EntryLinksMixin:
|
|
15
|
+
"""OPDS v1 Entry Links Methods."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, obj, query_params, data: OPDS1EntryData):
|
|
18
|
+
"""Initialize params."""
|
|
19
|
+
self.obj = obj
|
|
20
|
+
self.fake = isinstance(self.obj, OPDS1EntryObject)
|
|
21
|
+
self.query_params = query_params
|
|
22
|
+
self.acquision_groups = data.acquisition_groups
|
|
23
|
+
self.issue_max = data.issue_max
|
|
24
|
+
self.metadata = data.metadata
|
|
25
|
+
self.mime_type_map = data.mime_type_map
|
|
26
|
+
|
|
27
|
+
def _thumb_link(self):
|
|
28
|
+
if self.fake:
|
|
29
|
+
return None
|
|
30
|
+
cover_pk = self.obj.cover_pk
|
|
31
|
+
if cover_pk:
|
|
32
|
+
kwargs = {"pk": cover_pk}
|
|
33
|
+
href = reverse("opds:bin:cover", kwargs=kwargs)
|
|
34
|
+
elif cover_pk == 0:
|
|
35
|
+
href = staticfiles_storage.url("img/missing_cover.webp")
|
|
36
|
+
else:
|
|
37
|
+
return None
|
|
38
|
+
return OPDS1Link(Rel.THUMBNAIL, href, "image/webp")
|
|
39
|
+
|
|
40
|
+
def _image_link(self):
|
|
41
|
+
if self.fake:
|
|
42
|
+
return None
|
|
43
|
+
cover_pk = self.obj.cover_pk
|
|
44
|
+
if cover_pk:
|
|
45
|
+
kwargs = {"pk": cover_pk, "page": 0}
|
|
46
|
+
href = reverse("opds:bin:page", kwargs=kwargs)
|
|
47
|
+
mime_type = "image/jpeg"
|
|
48
|
+
elif cover_pk == 0:
|
|
49
|
+
href = staticfiles_storage.url("img/missing_cover.webp")
|
|
50
|
+
mime_type = "image/webp"
|
|
51
|
+
else:
|
|
52
|
+
return None
|
|
53
|
+
return OPDS1Link(Rel.IMAGE, href, mime_type)
|
|
54
|
+
|
|
55
|
+
def _nav_href(self, metadata=False):
|
|
56
|
+
kwargs = {"group": self.obj.group, "pk": self.obj.pk, "page": 1}
|
|
57
|
+
href = reverse("opds:v1:feed", kwargs=kwargs)
|
|
58
|
+
qps = {}
|
|
59
|
+
if (
|
|
60
|
+
self.obj.group == "a"
|
|
61
|
+
and self.obj.pk
|
|
62
|
+
and not self.query_params.get("orderBy")
|
|
63
|
+
):
|
|
64
|
+
# story arcs get ordered by story_arc_number by default
|
|
65
|
+
qps.update({"orderBy": "story_arc_number"})
|
|
66
|
+
if metadata:
|
|
67
|
+
qps.update({"opdsMetadata": 1})
|
|
68
|
+
return update_href_query_params(href, self.query_params, qps)
|
|
69
|
+
|
|
70
|
+
def _nav_link(self, metadata=False):
|
|
71
|
+
group = self.obj.group
|
|
72
|
+
|
|
73
|
+
if group in self.acquision_groups:
|
|
74
|
+
mime_type = MimeType.ENTRY_CATALOG if metadata else MimeType.ACQUISITION
|
|
75
|
+
else:
|
|
76
|
+
mime_type = MimeType.NAV
|
|
77
|
+
|
|
78
|
+
href = self._nav_href(metadata)
|
|
79
|
+
thr_count = 0 if self.fake else self.obj.child_count
|
|
80
|
+
rel = Rel.ALTERNATE if metadata else "subsection"
|
|
81
|
+
|
|
82
|
+
return OPDS1Link(rel, href, mime_type, thr_count=thr_count)
|
|
83
|
+
|
|
84
|
+
def _download_link(self):
|
|
85
|
+
pk = self.obj.pk
|
|
86
|
+
if not pk:
|
|
87
|
+
return None
|
|
88
|
+
fn = Comic.get_filename(self.obj)
|
|
89
|
+
fn = quote_plus(fn)
|
|
90
|
+
kwargs = {"pk": pk, "filename": fn}
|
|
91
|
+
href = reverse("opds:bin:download", kwargs=kwargs)
|
|
92
|
+
mime_type = self.mime_type_map.get(self.obj.file_type, MimeType.OCTET)
|
|
93
|
+
return OPDS1Link(Rel.ACQUISITION, href, mime_type, length=self.obj.size)
|
|
94
|
+
|
|
95
|
+
def _stream_link(self):
|
|
96
|
+
pk = self.obj.pk
|
|
97
|
+
if not pk:
|
|
98
|
+
return None
|
|
99
|
+
kwargs = {"pk": pk, "page": 0}
|
|
100
|
+
qps = {"bookmark": 1}
|
|
101
|
+
href = reverse("opds:bin:page", kwargs=kwargs)
|
|
102
|
+
href = update_href_query_params(href, {}, qps)
|
|
103
|
+
href = href.replace("0/page.jpg", "{pageNumber}/page.jpg")
|
|
104
|
+
page = self.obj.page
|
|
105
|
+
count = self.obj.page_count
|
|
106
|
+
bookmark_updated_at = self.obj.bookmark_updated_at
|
|
107
|
+
return OPDS1Link(
|
|
108
|
+
Rel.STREAM,
|
|
109
|
+
href,
|
|
110
|
+
MimeType.STREAM,
|
|
111
|
+
pse_count=count,
|
|
112
|
+
pse_last_read=page,
|
|
113
|
+
pse_last_read_date=bookmark_updated_at,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def links(self):
|
|
118
|
+
"""Create all entry links."""
|
|
119
|
+
result = []
|
|
120
|
+
if thumb := self._thumb_link():
|
|
121
|
+
result += [thumb]
|
|
122
|
+
if image := self._image_link():
|
|
123
|
+
result += [image]
|
|
124
|
+
|
|
125
|
+
if self.obj.group == "c" and not self.fake:
|
|
126
|
+
if download := self._download_link():
|
|
127
|
+
result += [download]
|
|
128
|
+
if stream := self._stream_link():
|
|
129
|
+
result += [stream]
|
|
130
|
+
if not self.metadata and (metadata := self._nav_link(metadata=True)):
|
|
131
|
+
result += [metadata]
|
|
132
|
+
elif nav := self._nav_link():
|
|
133
|
+
result += [nav]
|
|
134
|
+
|
|
135
|
+
return result
|