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
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
|
|
@@ -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
|