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
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""OPDS v1 Facets methods."""
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
from django.urls import reverse
|
|
5
|
+
|
|
6
|
+
from codex.models import AdminFlag
|
|
7
|
+
from codex.views.browser.browser import BrowserView
|
|
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
|
+
from codex.views.opds.v1.entry.entry import OPDS1Entry
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class FacetGroup:
|
|
17
|
+
"""An opds:facetGroup."""
|
|
18
|
+
|
|
19
|
+
title_prefix: str
|
|
20
|
+
query_param: str
|
|
21
|
+
glyph: str
|
|
22
|
+
facets: tuple
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Facet:
|
|
27
|
+
"""An OPDS facet."""
|
|
28
|
+
|
|
29
|
+
value: str
|
|
30
|
+
title: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class FacetGroups:
|
|
34
|
+
"""Facet Group definitions."""
|
|
35
|
+
|
|
36
|
+
ORDER_BY = FacetGroup(
|
|
37
|
+
"Order By",
|
|
38
|
+
"orderBy",
|
|
39
|
+
"➠",
|
|
40
|
+
(
|
|
41
|
+
Facet("date", "Date"),
|
|
42
|
+
Facet("sort_name", "Name"),
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
ORDER_REVERSE = FacetGroup(
|
|
46
|
+
"Order",
|
|
47
|
+
"orderReverse",
|
|
48
|
+
"⇕",
|
|
49
|
+
(Facet("false", "Ascending"), Facet("true", "Descending")),
|
|
50
|
+
)
|
|
51
|
+
ALL = (ORDER_BY, ORDER_REVERSE)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class RootFacetGroups:
|
|
55
|
+
"""Facet Groups that only appear at the root."""
|
|
56
|
+
|
|
57
|
+
TOP_GROUP = FacetGroup(
|
|
58
|
+
"",
|
|
59
|
+
"topGroup",
|
|
60
|
+
"⊙",
|
|
61
|
+
(
|
|
62
|
+
Facet("p", "Publishers View"),
|
|
63
|
+
Facet("s", "Series View"),
|
|
64
|
+
Facet("f", "Folder View"),
|
|
65
|
+
Facet("a", "Story Arc View"),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
ALL = (TOP_GROUP,)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
DEFAULT_FACETS = {
|
|
72
|
+
"topGroup": "p",
|
|
73
|
+
"orderBy": "sort_name",
|
|
74
|
+
"orderReverse": "false",
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class FacetsMixin(BrowserView):
|
|
79
|
+
"""OPDS 1 Facets methods."""
|
|
80
|
+
|
|
81
|
+
# Overwritten by get_object()
|
|
82
|
+
use_facets = False
|
|
83
|
+
skip_order_facets = False
|
|
84
|
+
acquisition_groups = frozenset()
|
|
85
|
+
mime_type_map = MimeType.FILE_TYPE_MAP
|
|
86
|
+
obj = {}
|
|
87
|
+
|
|
88
|
+
def _facet(self, kwargs, facet_group, facet_title, new_query_params):
|
|
89
|
+
href = reverse("opds:v1:feed", kwargs=kwargs)
|
|
90
|
+
facet_active = False
|
|
91
|
+
for key, val in new_query_params.items():
|
|
92
|
+
if self.request.query_params.get(key) == val:
|
|
93
|
+
facet_active = True
|
|
94
|
+
break
|
|
95
|
+
href = update_href_query_params(
|
|
96
|
+
href, self.request.query_params, new_query_params
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
title = " ".join(filter(None, (facet_group.title_prefix, facet_title))).strip()
|
|
100
|
+
return OPDS1Link(
|
|
101
|
+
Rel.FACET,
|
|
102
|
+
href,
|
|
103
|
+
MimeType.NAV,
|
|
104
|
+
title=title,
|
|
105
|
+
facet_group=facet_group.query_param,
|
|
106
|
+
facet_active=facet_active,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def _facet_entry(self, item, facet_group, facet, query_params):
|
|
110
|
+
name = " ".join(
|
|
111
|
+
filter(None, (facet_group.glyph, facet_group.title_prefix, facet.title))
|
|
112
|
+
).strip()
|
|
113
|
+
entry_obj = OPDS1EntryObject(
|
|
114
|
+
group=item.get("group"),
|
|
115
|
+
pk=item.get("pk"),
|
|
116
|
+
name=name,
|
|
117
|
+
)
|
|
118
|
+
qps = {**self.request.query_params}
|
|
119
|
+
qps.update(query_params)
|
|
120
|
+
issue_max = self.obj.get("issue_max", 0)
|
|
121
|
+
data = OPDS1EntryData(
|
|
122
|
+
self.acquisition_groups, issue_max, False, self.mime_type_map
|
|
123
|
+
)
|
|
124
|
+
return OPDS1Entry(entry_obj, qps, data)
|
|
125
|
+
|
|
126
|
+
def _is_facet_active(self, facet_group, facet):
|
|
127
|
+
compare = [facet.value]
|
|
128
|
+
default_val = DEFAULT_FACETS.get(facet_group.query_param)
|
|
129
|
+
if facet.value == default_val:
|
|
130
|
+
compare += [None]
|
|
131
|
+
return self.request.query_params.get(facet_group.query_param) in compare
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _did_special_group_change(group, facet_group):
|
|
135
|
+
"""Test if one of the special groups changed."""
|
|
136
|
+
for test_group in ("f", "a"):
|
|
137
|
+
if (
|
|
138
|
+
group == test_group
|
|
139
|
+
and facet_group != test_group
|
|
140
|
+
or group != test_group
|
|
141
|
+
and facet_group == test_group
|
|
142
|
+
):
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def _facet_or_facet_entry(self, facet_group, facet, entries):
|
|
147
|
+
# This logic preempts facet:activeFacet but no one uses it.
|
|
148
|
+
# don't add default facets if in default mode.
|
|
149
|
+
if self._is_facet_active(facet_group, facet):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
group = self.kwargs.get("group")
|
|
153
|
+
if facet_group.query_param == "topGroup" and self._did_special_group_change(
|
|
154
|
+
group, facet.value
|
|
155
|
+
):
|
|
156
|
+
kwargs = {"group": facet.value, "pk": 0, "page": 1}
|
|
157
|
+
else:
|
|
158
|
+
kwargs = self.kwargs
|
|
159
|
+
|
|
160
|
+
qps = {facet_group.query_param: facet.value}
|
|
161
|
+
if entries and self.kwargs.get("page") == 1:
|
|
162
|
+
facet = self._facet_entry(kwargs, facet_group, facet, qps)
|
|
163
|
+
else:
|
|
164
|
+
facet = self._facet(kwargs, facet_group, facet.title, qps)
|
|
165
|
+
return facet
|
|
166
|
+
|
|
167
|
+
def _facet_group(self, facet_group, entries):
|
|
168
|
+
facets = []
|
|
169
|
+
for facet in facet_group.facets:
|
|
170
|
+
if facet.value == "f":
|
|
171
|
+
efv_flag = (
|
|
172
|
+
AdminFlag.objects.only("on")
|
|
173
|
+
.get(key=AdminFlag.FlagChoices.FOLDER_VIEW.value)
|
|
174
|
+
.on
|
|
175
|
+
)
|
|
176
|
+
if not efv_flag:
|
|
177
|
+
continue
|
|
178
|
+
if facet_obj := self._facet_or_facet_entry(facet_group, facet, entries):
|
|
179
|
+
facets += [facet_obj]
|
|
180
|
+
return facets
|
|
181
|
+
|
|
182
|
+
def facets(self, entries=False, root=True):
|
|
183
|
+
"""Return facets."""
|
|
184
|
+
facets = []
|
|
185
|
+
if not self.skip_order_facets:
|
|
186
|
+
facets += self._facet_group(FacetGroups.ORDER_BY, entries)
|
|
187
|
+
facets += self._facet_group(FacetGroups.ORDER_REVERSE, entries)
|
|
188
|
+
if root:
|
|
189
|
+
facets += self._facet_group(RootFacetGroups.TOP_GROUP, entries)
|
|
190
|
+
return facets
|
|
@@ -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
|
)
|