Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
406 commits
Select commit Hold shift + click to select a range
16eeb72
Scroll to the right page when reading Bottom to Top (#836)
ajslater Sep 2, 2026
85f47ff
update deps
ajslater Sep 8, 2026
79122e7
Comicbox 5 (#839)
ajslater Sep 9, 2026
36f9794
update deps
ajslater Sep 9, 2026
72af4b2
Clear basedpyright, ty, and radon complexity warnings
ajslater Sep 9, 2026
c5a2254
Auto Update on by default outside Docker, honest per-source tagging s…
ajslater Sep 9, 2026
b0a92e1
Make the stored-id prepass report what it did (#841)
ajslater Sep 9, 2026
419126b
update deps
ajslater Sep 9, 2026
6dce467
Merge branch 'main' into develop
ajslater Sep 9, 2026
5184e0d
update deps
ajslater Sep 10, 2026
cb0fdd4
Fix `make docs`: build the site outside docs_dir (#843)
ajslater Sep 12, 2026
c7cb4bd
Republish a deprecated Docker Hub mirror and warn its admins to move …
ajslater Sep 12, 2026
4d590ec
update version to 2.3.1
ajslater Sep 12, 2026
9e3c9ca
Let the color prop work on snackbars (#845)
ajslater Sep 12, 2026
14df7e8
Warn the logs, not just the web UI, about the deprecated Docker Hub i…
ajslater Sep 12, 2026
33b14b7
bump news
ajslater Sep 12, 2026
b1ad810
news bump
ajslater Sep 12, 2026
edaa437
Merge branch 'main' into develop
ajslater Sep 12, 2026
2916299
update devenv deps
ajslater Sep 13, 2026
d0ebdef
update deps
ajslater Sep 13, 2026
590e3e8
run fix
ajslater Sep 13, 2026
43f7d2f
fix typecheck & complexity warnings
ajslater Sep 13, 2026
11b3210
Fix nightly janitor running dozens of times each midnight (#848)
ajslater Sep 13, 2026
1b7c8db
make news more terse
ajslater Sep 13, 2026
7402dfc
bump version
ajslater Sep 13, 2026
f92bbf0
Fix the ASGI pool concurrency test racing on WAL conversion (#850)
ajslater Sep 13, 2026
ba7484c
Merge branch 'main' into develop
ajslater Sep 14, 2026
1ece089
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Sep 14, 2026
84d6329
update deps
ajslater Sep 17, 2026
c204ebe
Adapt to comicbox 5.1 and @vueuse/core 15 (#851)
ajslater Sep 17, 2026
b53a7e4
Apply an online-tagging match prompt to every issue of its series (#852)
ajslater Sep 17, 2026
0f466a4
Merge branch 'main' into develop
ajslater Sep 17, 2026
a5d45fc
update deps
ajslater Sep 18, 2026
08ffb02
Fix mark-read confirm gate never firing from a browser card (#857)
ajslater Sep 19, 2026
e5f06ff
update deps
ajslater Sep 20, 2026
552a321
dont mount comics readonly in the compose example for docker
ajslater Sep 20, 2026
0790a72
fix(importer): an unreadable directory is not a deleted one (#858)
ajslater Sep 20, 2026
de09c59
Restore OPDS library browsing for every client (#855) (#859)
ajslater Sep 20, 2026
d8804aa
fix(importer): look twice before deleting a comic (#860)
ajslater Sep 20, 2026
bdccb5c
fix(poller): pair a move that lost its inode, and stop truncating mti…
ajslater Sep 20, 2026
3783a84
fix(janitor): stop pruning folders that hold uppercase comics (#861)
ajslater Sep 20, 2026
bbd1118
fix(tagging): answer the four things #854 reported (#863)
ajslater Sep 20, 2026
4c67da4
fix(janitor): make the orphan-bookmark sweep able to run (#864)
ajslater Sep 20, 2026
e646ef0
update deps
ajslater Sep 20, 2026
d145654
feat(tagging): link a refused conversion to the comic it became (#865)
ajslater Sep 20, 2026
ec8ca32
fix(bookmarks): one bookmark per owner per comic (#866)
ajslater Sep 20, 2026
b8f3fcf
fix(auth): enforce the register-mode password minimum (#867)
ajslater Sep 20, 2026
cc7a92f
refactor(auth): remove the auth-form mixin's never-run setup() (#868)
ajslater Sep 21, 2026
74761f7
fix(metadata): gate edit saves on field validation (#869)
ajslater Sep 21, 2026
4d5f89f
Install Vuetify's rules plugin and convert the validation call sites …
ajslater Sep 21, 2026
2af1834
Theme consolidation: WS1–WS9 (#871)
ajslater Sep 21, 2026
813be2b
depend on comicbox 5.2.0
ajslater Sep 21, 2026
243b54d
feat(tagging): enlarge a match candidate's cover on hover (#872)
ajslater Sep 21, 2026
86ae49f
comicbox 5.2.0 backend adoption (#873)
ajslater Sep 21, 2026
85db3ad
refactor(covers): move both legacy cover popups onto the shared compo…
ajslater Sep 21, 2026
3fc61ff
Cascade layers, app-wide (#875)
ajslater Sep 21, 2026
9c377bf
fix constant name in claude.md
ajslater Sep 21, 2026
2a773c0
fix(admin): two live client validation bugs (#876)
ajslater Sep 21, 2026
b910070
feat(models): add the missing_since column (#878)
ajslater Sep 21, 2026
0adda84
chore(theme): declare the on-primary token (#877)
ajslater Sep 21, 2026
3663908
feat(choices): a shared limits transport (#885)
ajslater Sep 21, 2026
4c884a9
fix(admin): reject values that break the app (#886)
ajslater Sep 21, 2026
a1982f8
fix(admin): type the from address, bound the lengths (#887)
ajslater Sep 21, 2026
75ad92e
fix(admin): show the server's reason on the field that caused it (#888)
ajslater Sep 21, 2026
a1a493d
feat(views): hide scanner-stamped rows from ordinary users (#879)
ajslater Sep 21, 2026
fffa588
feat(scribe): keep a vanished row instead of deleting it (#880)
ajslater Sep 21, 2026
1fa4a54
feat(poller): revive a row whose file came back (#881)
ajslater Sep 21, 2026
83ab1c0
feat(janitor): reap expired pending deletes nightly (#882)
ajslater Sep 21, 2026
ed1c908
feat(admin): a Pending Deletes panel in the Libraries tab (#883)
ajslater Sep 21, 2026
396464d
fix(janitor): custom cover cleanup fails closed on an unreadable path…
ajslater Sep 21, 2026
47b609d
fix(admin): a cleared poll-interval part no longer emits "null" (#889)
ajslater Sep 21, 2026
37128ae
update deps
ajslater Sep 21, 2026
af272c1
fix(tests): give each run its own fixture directories (#892)
ajslater Sep 21, 2026
912e52f
fix(importer): scope every path lookup to its own library (#893)
ajslater Sep 21, 2026
5d887b4
fix(poller): carry Force Update as a diff flag (#894)
ajslater Sep 21, 2026
64f197d
fix(admin): library delete and membership changes broadcast again (#899)
ajslater Sep 21, 2026
5f03625
fix(browser): container names use the full ACL, not just the missing …
ajslater Sep 21, 2026
63ff6a5
fix(onlinetag): release Comic Vine connections with Metron's (#903)
ajslater Sep 21, 2026
4f5d2bf
fix(views): ACL the raw pks that reach the librarian queues (#896)
ajslater Sep 21, 2026
8e68afa
fix(views): a comic file that cannot be read is Not Found (#897)
ajslater Sep 21, 2026
f2133e8
fix(browser): collection rows summarize only visible comics (#901)
ajslater Sep 21, 2026
99270b6
fix(opds): one unreadable comic no longer empties a v1 feed (#898)
ajslater Sep 21, 2026
bbd2536
fix(covers): derive the cover namespace from one custom flag (#900)
ajslater Sep 21, 2026
e3b703e
fix(browser): an unresolvable collection redirects up a level (#902)
ajslater Sep 21, 2026
cdfff8b
fix import in breadcrumbs
ajslater Sep 21, 2026
c37b041
fix(librarian): the importer's FTS statuses are actually finished
ajslater Sep 22, 2026
90a4533
refactor(reader): extract the settings comic prefetch
ajslater Sep 22, 2026
1c5730e
refactor(tests): split the two rank-C test blocks
ajslater Sep 22, 2026
e2dd2d6
fix(tests): breadcrumbs fixtures go back to per-run directories
ajslater Sep 22, 2026
e176e56
docs(news): condense the v2.3.4 fixes
ajslater Sep 22, 2026
a124914
fix(tests): the last six fixture dirs go per-run (#904)
ajslater Sep 22, 2026
a8e36b0
fix(browser): collection row aggregates were summing distinct values …
ajslater Sep 22, 2026
76a5add
feat(browser): show read state under every cover (#906)
ajslater Sep 22, 2026
0940587
docs(news): read state and collection counting for v2.3.4 (#908)
ajslater Sep 22, 2026
c2baca4
Fold the post-v2.3.3 migrations into one (#909)
ajslater Sep 22, 2026
7b85655
chore(settings): pin five options on and take their toggles away (#910)
ajslater Sep 22, 2026
71f8f1e
fix(browser): hide pending-delete rows from admins too (#911)
ajslater Sep 22, 2026
f9aea1b
fix(scribe): the watcher hard-deleted, so retention never happened (#…
ajslater Sep 22, 2026
03cc3a8
update version to v2.4.0 and add feature titles
ajslater Sep 22, 2026
e152993
chore(settings): remove show_read_state as if it never existed
ajslater Sep 22, 2026
d7355b6
Merge branch 'main' into develop
ajslater Sep 22, 2026
3991389
fix(browser): restore scrolling — Vuetify ships one unlayered stylesh…
ajslater Sep 22, 2026
0427c7e
bump version and news. 2.4.1
ajslater Sep 22, 2026
abc9833
news language
ajslater Sep 22, 2026
a68f10b
test(rename-toggle): cancel the real debounce timer mountEditPanel le…
ajslater Sep 22, 2026
0a9d642
Merge branch 'main' into develop
ajslater Sep 22, 2026
db89f5d
fix(opds): position endpoint 404s for session-less clients (#917)
ajslater Sep 23, 2026
1a89299
Merge branch 'main' into develop
ajslater Sep 23, 2026
dc62db5
Merge branch 'main' into develop
ajslater Sep 23, 2026
9d33d74
fix(opds): no-position GET is 200 + empty payload; Keep Reading lists…
ajslater Sep 24, 2026
0dd611f
feat(opds): position PUT returns the stored document; errors are Prob…
ajslater Sep 24, 2026
a421fbe
fix(bookmarks): UNREAD is one identity-scoped NOT EXISTS (#921)
ajslater Sep 24, 2026
0365a03
build(frontend): drop the Vuetify unlayered-CSS plugin; 4.2.2 fixes i…
ajslater Sep 24, 2026
4f0c9bd
bump news
ajslater Sep 24, 2026
916196a
Merge branch 'main' into develop
ajslater Sep 24, 2026
7888061
update deps
ajslater Sep 27, 2026
6570b98
feat(onlinetag): show the file's own cover in match review (#927)
ajslater Sep 27, 2026
2a74752
ci: release job (tag, GitHub Release, merge-back) + Release Preflight…
ajslater Sep 27, 2026
015e8dc
ci: split "Lint, Test & Build Dist" into gate, fail-fast check matrix…
ajslater Sep 27, 2026
5c195fd
feat(admin): Defaults tab for site-wide default browser and reader se…
ajslater Sep 27, 2026
46c189f
update deps
ajslater Sep 27, 2026
a1d604d
organize news better
ajslater Sep 27, 2026
2b52129
fix(admin): explain the locked Show Collections box on the Defaults t…
ajslater Sep 27, 2026
266027e
refactor(onlinetag): move the file's cover into the prompt's panel ti…
ajslater Sep 27, 2026
eb88907
fix(onlinetag): let the title cover enlarge; tint the file under revi…
ajslater Sep 27, 2026
69454e9
fix(lint): make the ESLint base presets actually apply (#931)
ajslater Sep 28, 2026
63f0600
update deps
ajslater Sep 28, 2026
51f1567
refactor(frontend): fix the findings parked by the ESLint preset fix …
ajslater Sep 28, 2026
30e3468
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Sep 28, 2026
e368049
fix(admin): keep the Add/Edit dialog open when the server rejects a s…
ajslater Sep 28, 2026
0e5d1fd
fix(reader): stop the one-page step from moving past the book's ends …
ajslater Sep 28, 2026
2e67818
fix(reader): write each debounced bookmark to the book it was read in…
ajslater Sep 28, 2026
5c96f59
fix(reader): land settings responses only on the book they were for (…
ajslater Sep 28, 2026
2dd8a9b
fix(reader): resolve loadGlobalSettings after the settings land (#937)
ajslater Sep 28, 2026
dbdfe8d
fix(browser): send only the filters in use with bulk and metadata req…
ajslater Sep 28, 2026
1c7d069
fix(admin): Add/Edit submit button follows the newest edit (#939)
ajslater Sep 28, 2026
2240f36
fix(auth): let kiosk sessions save their timezone again (#940)
ajslater Sep 28, 2026
056e688
test(admin): cover the table engine (#947)
ajslater Sep 28, 2026
04d5d36
test(browser): characterize top-collection validation (#941)
ajslater Sep 28, 2026
d32a7c2
chore(browser): delete dead browser store state and actions (#942)
ajslater Sep 28, 2026
ad3b526
fix(reader): drop dead store bindings; make the "b" shortcut set bott…
ajslater Sep 28, 2026
a2b2389
fix(reader): stop a redundant settings request and a stale zoom fit (…
ajslater Sep 28, 2026
b63f09c
fix(admin): keep the folder picker's root when its path is cleared (#…
ajslater Sep 28, 2026
7275993
perf(frontend): keep the admin store out of the browser and reader bu…
ajslater Sep 28, 2026
faf4c84
refactor(admin): share one sticky-cache test across the admin loads (…
ajslater Sep 28, 2026
f2f4a19
update deps
ajslater Sep 28, 2026
37dec20
perf(frontend): keep the admin API client out of the browser bundle (…
ajslater Sep 28, 2026
fba3862
fix(admin): stop a slow table read from overwriting newer rows (#949)
ajslater Sep 28, 2026
4adced9
fix(reader): name the Original Size fit class fitToOrig (#951)
ajslater Sep 28, 2026
94defbd
fix(reader): fit wide vector PDF pages to the screen (#953)
ajslater Sep 28, 2026
24a6764
fix(admin): stop a slow tag-write errors read from overwriting newer …
ajslater Sep 28, 2026
8c63260
ci: compose CI from devenv's building blocks (#954)
ajslater Sep 29, 2026
5e88c40
update deps
ajslater Sep 29, 2026
4178d14
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Sep 29, 2026
3614f27
feat(browser): mark finished items with a check cap, not a thick bar …
ajslater Sep 29, 2026
bf19c90
update deps
ajslater Sep 29, 2026
7f2bb45
docs(news): title v2.5.0 and order its fixes by user interest
ajslater Sep 29, 2026
d7feeeb
update devenv
ajslater Sep 29, 2026
da3bb6a
update deps
ajslater Sep 29, 2026
99b1ff1
bump version to 2.5.0
ajslater Sep 29, 2026
c01bfa5
Merge branch 'main' into develop
github-actions[bot] Sep 29, 2026
722ca0d
fix(browser): an unresolvable collection redirects to the top, not it…
ajslater Sep 29, 2026
7c0f8e3
fix(browser): clearing a search or loading a saved view no longer str…
ajslater Sep 29, 2026
e76ad66
feat(browser): saved views can be deleted (#959)
ajslater Sep 29, 2026
cd292e6
version 2.5.1
ajslater Sep 29, 2026
66b7040
fix(browser): picking a saved view no longer reopens its menu (#960)
ajslater Sep 29, 2026
49927c4
update devenv
ajslater Sep 30, 2026
ea8484e
docs: fix markdown lint errors and readthedocs links
ajslater Sep 30, 2026
d85156c
ci: let the deploy job publish to PyPI with trusted publishing (#961)
ajslater Sep 30, 2026
1554524
update devenv
ajslater Sep 30, 2026
d39b85f
exclude static from codespell
ajslater Sep 30, 2026
74ec962
codespell fixes
ajslater Sep 30, 2026
e71fa6f
fix codespell
ajslater Sep 30, 2026
c010cee
update comicbox
ajslater Sep 30, 2026
dc24718
Adopt comicbox 5.2.2: 404 a reader page past the end (#962)
ajslater Sep 30, 2026
88646dc
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Sep 30, 2026
1eb6a7c
update deps
ajslater Sep 30, 2026
db32443
Merge branch 'main' into develop
github-actions[bot] Sep 30, 2026
942f509
chore(docker): drop two dockerignore negations that do nothing (#964)
ajslater Oct 2, 2026
51ba70c
update devenv
ajslater Oct 2, 2026
67f0e7e
Merge branch 'develop' of github.com:ajslater/codex into develop
ajslater Oct 2, 2026
3b43595
perf(docker): uv.lock-pinned venv, ~490 MB smaller image; declare pac…
ajslater Oct 5, 2026
4cc8cec
update devenv
ajslater Oct 5, 2026
83b4b6b
update devenv and deps. fix a slew of unicorn errors
ajslater Oct 5, 2026
a1aa04b
bump version to 2.5.2
ajslater Oct 5, 2026
7ed0b55
Merge branch 'main' into develop
github-actions[bot] Oct 5, 2026
12b215f
update deps
ajslater Oct 6, 2026
c30c36e
Run on (and use) the Python 3.12 floor (#967)
ajslater Oct 6, 2026
aa35ab8
version 2.5.3
ajslater Oct 6, 2026
8330feb
NEWS for v2.5.3
ajslater Oct 6, 2026
7cd2632
update deps
ajslater Oct 7, 2026
8a01e7c
update deps. comicbox 5.3.0
ajslater Oct 7, 2026
c120c28
Adapt to comicbox 5.3.x: doctor tab, probe() credentials, ask match m…
ajslater Oct 7, 2026
3a9c27f
Merge branch 'main' into develop
github-actions[bot] Oct 7, 2026
42836c9
Online tagging: toolbar Review button instead of an auto-opening dial…
ajslater Oct 8, 2026
8cd6ea6
Reader: keys meant for a dialog no longer drive the reader (#971)
ajslater Oct 8, 2026
1b2cd41
fix(make): drop the install recipe whose OpenSSL flags never applied …
ajslater Oct 8, 2026
a136479
move prod-server makefile target from django to codex
ajslater Oct 8, 2026
02605fe
fix indendation
ajslater Oct 8, 2026
47ea14a
update devenv
ajslater Oct 8, 2026
128c3fc
update version
ajslater Oct 8, 2026
1f71dd0
Merge branch 'main' into develop
github-actions[bot] Oct 8, 2026
f9d0382
Read stale pickled tagging-cache entries as misses, not 500s (#974)
ajslater Oct 8, 2026
4bd148d
update version to 2.5.5
ajslater Oct 8, 2026
ed238ed
update devenv
ajslater Oct 8, 2026
a8db51c
update devenv
ajslater Oct 10, 2026
307824e
update devenv
ajslater Oct 10, 2026
3c42f6a
update devenv
ajslater Oct 12, 2026
a74ea21
update python description for 3.15
ajslater Oct 12, 2026
7dc20c1
update deps
ajslater Oct 12, 2026
3a64058
update devenv
ajslater Oct 12, 2026
1083236
update deps
ajslater Oct 15, 2026
7dcf53b
use ububuntu-latest
ajslater Oct 17, 2026
834f24d
update deps and devenv
ajslater Oct 17, 2026
0eb4fc8
lint fixes
ajslater Oct 17, 2026
d28c8f9
first pass for python 3.15
ajslater Oct 18, 2026
df658e5
Install the CI lint tools; update devenv to 0.2.9 (#975)
ajslater Oct 19, 2026
bf6ddc3
update devenv
ajslater Oct 19, 2026
c36f194
update deps
ajslater Oct 19, 2026
bbb0d1f
Clear ty's 99 override errors (#976)
ajslater Oct 19, 2026
dbf7052
Adapt to comicbox 5.3.2 (#977)
ajslater Oct 19, 2026
3463ffb
Add a csrf_trusted_origins setting for reverse proxies (#978)
ajslater Oct 19, 2026
4715894
Note that a correctly configured proxy doesn't need csrf_trusted_orig…
ajslater Oct 19, 2026
0f78256
Remove the four pinned settings from the schema (#981)
ajslater Oct 19, 2026
86080f3
Run the Python suite in parallel and stop vitest's double work (#980)
ajslater Oct 19, 2026
fa3d8c6
remove news item
ajslater Oct 19, 2026
9aec2ce
update devenv
ajslater Oct 19, 2026
26cf6dc
Split the credentials doctor check below radon's C rank
ajslater Oct 19, 2026
82bcd00
add news item
ajslater Oct 19, 2026
4fc33f3
update devenv
ajslater Oct 19, 2026
f0f7f98
Merge branch 'main' into develop
github-actions[bot] Oct 19, 2026
b7207c5
bump version to 2.5.6
ajslater Oct 20, 2026
09b0230
update deps
ajslater Oct 20, 2026
c4704c5
Show series and issue detail under each match candidate (#983)
ajslater Oct 20, 2026
361a236
update deps to use comicbox 5.4.0
ajslater Oct 20, 2026
5d56afc
Import and show series years, status and summary (#984)
ajslater Oct 20, 2026
b1ee555
Tidy series facts in the metadata header; Tags button on browser titl…
ajslater Oct 20, 2026
f3d0f98
Tag editor edits series end year, status and summary in the database …
ajslater Oct 20, 2026
c24d99a
Online tagging saves series years, status and summary (#987)
ajslater Oct 20, 2026
e07387b
Drop dead select_related from the importer's foreign-key query (#988)
ajslater Oct 20, 2026
c353b18
feat: filter and search by series years, status and summary (#989)
ajslater Oct 20, 2026
b573634
update deps
ajslater Oct 20, 2026
547afb2
compact news for 2.5.6
ajslater Oct 20, 2026
d790deb
fix: merge FTS re-sync tags across import chunks (#990)
ajslater Oct 20, 2026
a1bded5
update devenv
ajslater Oct 20, 2026
fc83dcf
fix: index universe designations on every imported comic (#991)
ajslater Oct 20, 2026
9a61bcd
update deps
ajslater Oct 20, 2026
dc8e04a
update deps
ajslater Oct 22, 2026
00d8971
fix: stop browsing from re-importing tagless comics and reloading the…
ajslater Oct 22, 2026
dde59c5
chore(migrations): fold the post-v2.5.5 migrations into one (#994)
ajslater Oct 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ width: 128px;
border-radius: 128px;
" />

## v2.5.6

- Features
- Filter, search, view, edit, and online tag by series years, series status,
and series summary. End year, status and summary are saved in Codex only,
not comic metadata.
- Browser title shows series years and a Tags button.
- Online tag match picker shows series and issue details to help
disambiguate similar covers and titles.
- Fixes
- Browsing comics without embedded tags, or re-importing unchanged comics,
no longer rebuilds their covers and reloads the page.
- Searches for a universe designation no longer miss imported comics.

## v2.5.5

- Features
Expand Down
5 changes: 5 additions & 0 deletions bin/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ def _discover() -> str:

def main():
"""Run the server."""
# Python puts the script's own bin/ first on sys.path, not the root, so
# the project would come from whatever copy the venv installed (another
# worktree's editable install, say). Put the root first, as a root-level
# manage.py gets for free.
sys.path.insert(0, str(ROOT))
# Not setdefault: its argument runs first, and _discover() raises in a
# project with two settings modules even when the variable picks one.
if not os.environ.get("DJANGO_SETTINGS_MODULE"):
Expand Down
90 changes: 59 additions & 31 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions codex/choices/choices_to_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
IDENTIFIER_TYPE_BY_CODEX_NAME,
IDENTIFIER_TYPES,
LANGUAGES,
SERIES_STATUSES,
)

_DEFAULTS = MappingProxyType(
Expand Down Expand Up @@ -81,6 +82,7 @@
"reader-defaults.json": READER_DEFAULTS,
"reader-map.json": READER_CHOICES,
"search-map.json": SEARCH_FIELDS,
"series-statuses.json": SERIES_STATUSES,
"tagging-estimate.json": TAGGING_ESTIMATE,
}
)
Expand Down
10 changes: 10 additions & 0 deletions codex/choices/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"month",
"year",
"page_count",
"series_end_year",
"series_start_year",
"size",
"decade",
"volume",
Expand Down Expand Up @@ -114,7 +116,15 @@ def _get_fieldmap_values(*args) -> tuple:
"review": (),
"scan_info": ("scan",),
"series": (),
"series_end_year": _get_fieldmap_values("series_end_year", "end_year"),
"series_groups": _get_fieldmap_values("series_groups"),
"series_start_year": _get_fieldmap_values("series_start_year", "start_year"),
# Hand-written: the generator would strip the final "s" into a
# bogus singular.
"series_status": ("series-status", "seriesstatus", "status"),
"series_summary": _get_fieldmap_values(
"series_summary", "series_desc", "series_description"
),
"size": (),
"sources": _get_fieldmap_values("sources", "id_sources"),
"stories": ("story",),
Expand Down
21 changes: 15 additions & 6 deletions codex/choices/tagging.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from comicbox.formats.metron_info.transform import MetronInfoTransform

from codex.choices.reader import READER_CHOICES
from codex.models.choices import SeriesStatusChoices
from codex.models.identifier import IdentifierType, to_comicbox_id_type

# comicbox transform per codex write-format id.
Expand Down Expand Up @@ -84,6 +85,13 @@ def _vuetify_choices(pairs: Iterable[tuple[str, str]]) -> tuple[MappingProxyType
{id_type.value: to_comicbox_id_type(id_type.value) for id_type in IdentifierType}
)

# A series' publication status (Ongoing, Completed, ...). No format codex
# writes holds it, so the editor saves it to the database only and offers
# it under every format.
SERIES_STATUSES = _vuetify_choices(
(str(label), str(value)) for value, label in SeriesStatusChoices.choices
)


# ---------------------------------------------------------------------------
# Per-format field support, derived from the comicbox transforms
Expand Down Expand Up @@ -156,16 +164,17 @@ def _vuetify_choices(pairs: Iterable[tuple[str, str]]) -> tuple[MappingProxyType
# Editor sub-fields comicbox stores only for some formats and which its
# transform specs don't surface at the SPECS_TO top level (the
# hand-maintained support data): MetronInfo's Series carries a volume
# count, only MetronInfo's CommunityRating persists a rating count, and
# only MetronInfo encodes a reprint's volume number (in Reprints) and
# language (in Series/AlternativeNames). Whether a reprint row is one of
# the series' other names is MetronInfo's too, and for the same reason:
# ComicInfo has no reprint tag at all, since comicbox 5 reads its
# AlternateSeries and AlternateNumber as a story arc.
# count and a start year, only MetronInfo's CommunityRating persists a
# rating count, and only MetronInfo encodes a reprint's volume number (in
# Reprints) and language (in Series/AlternativeNames). Whether a reprint
# row is one of the series' other names is MetronInfo's too, and for the
# same reason: ComicInfo has no reprint tag at all, since comicbox 5 reads
# its AlternateSeries and AlternateNumber as a story arc.
_EXTRA_FORMAT_FIELDS: MappingProxyType[str, tuple[str, ...]] = MappingProxyType(
{
"METRON_INFO": (
"volume_count",
"start_year",
"community_rating_count",
"reprint_volume",
"reprint_language",
Expand Down
15 changes: 15 additions & 0 deletions codex/librarian/onlinetag/session_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,21 @@ def serialize_candidate(c) -> dict[str, Any]:
# The series' ordinal volume. Metron's issue rows carry one;
# Comic Vine's search results have no equivalent.
"volume": getattr(summary, "volume", None),
# What tells apart candidates whose covers and titles read
# identically: a reprint volume, a foreign or a collected
# edition. Comic Vine fills most of these from rows it already
# fetched. Metron always has the start year, but gets the
# format, publisher and issue count only when titles collide.
# Absent before comicbox 5.4, and None wherever a source has
# nothing.
"series_start_year": getattr(summary, "series_start_year", None),
"series_publisher": getattr(summary, "series_publisher", None),
"series_issue_count": getattr(summary, "series_issue_count", None),
# Publication format, e.g. "Trade Paperback". Metron only.
"series_type": getattr(summary, "series_type", None),
# The issue's own title and one-line deck. Comic Vine only.
"issue_title": getattr(summary, "issue_title", None),
"issue_summary": getattr(summary, "issue_summary", None),
},
"score": float(c.score),
# What the blended score is made of. Two candidates can read
Expand Down
46 changes: 20 additions & 26 deletions codex/librarian/scribe/importer/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
PARENT_FOLDER_FIELD_NAME = "parent_folder"
VOLUME_COUNT_FIELD_NAME = "volume_count"
ISSUE_COUNT_FIELD_NAME = "issue_count"
START_YEAR_FIELD_NAME = "start_year"
END_YEAR_FIELD_NAME = "end_year"
STATUS_FIELD_NAME = "status"
SUMMARY_FIELD_NAME = "summary"
PATH_FIELD_NAME = "path"
IDENTIFIERS_FIELD_NAME = "identifiers"
CREDIT_PRIMARY_FIELD_NAME = "primary"
Expand Down Expand Up @@ -153,20 +157,25 @@ def _metron_fts_values(values: tuple) -> tuple:
FTS_UPDATE = "fts_update"
FTS_CREATE = "fts_create"
FTS_EXISTING_M2MS = "fts_existing_m2ms"
FTS_CREATED_M2MS = "fts_created_m2ms"
FTS_UPDATED_M2MS = "fts_updated_m2ms"

#######
# M2M #
#######
COLLECTION_MODEL_COUNT_FIELDS: MappingProxyType[
type[BrowserCollectionModel], str | None
COLLECTION_MODEL_ATTR_FIELDS: MappingProxyType[
type[BrowserCollectionModel], tuple[str, ...]
] = MappingProxyType(
{
Publisher: None,
Imprint: None,
Series: VOLUME_COUNT_FIELD_NAME,
Volume: ISSUE_COUNT_FIELD_NAME,
Publisher: (),
Imprint: (),
Series: (
VOLUME_COUNT_FIELD_NAME,
START_YEAR_FIELD_NAME,
END_YEAR_FIELD_NAME,
STATUS_FIELD_NAME,
SUMMARY_FIELD_NAME,
),
Volume: (ISSUE_COUNT_FIELD_NAME,),
}
)
COMIC_M2M_FIELDS: tuple[ManyToManyField, ...] = cast(
Expand Down Expand Up @@ -201,6 +210,8 @@ def _metron_fts_values(values: tuple) -> tuple:
REPRINT_LANGUAGE_FIELD_NAME = "language"
# Reprints index only their series names, under their own ComicFTS column.
ALTERNATE_SERIES_FTS_FIELD_NAME = "alternate_series"
# The linked series' summary, indexed on each of its comics.
SERIES_SUMMARY_FTS_FIELD_NAME = "series_summary"
UNIVERSES_FIELD_NAME = "universes"
NAME_FIELD_NAME = "name"
NUMBER_TO_FIELD_NAME = "number_to"
Expand Down Expand Up @@ -282,7 +293,7 @@ def _metron_fts_values(values: tuple) -> tuple:
NAME_FIELD_NAME,
),
_IDENTIFIER_RELS,
VOLUME_COUNT_FIELD_NAME,
*COLLECTION_MODEL_ATTR_FIELDS[Series],
),
Volume: (
(
Expand All @@ -293,7 +304,7 @@ def _metron_fts_values(values: tuple) -> tuple:
NUMBER_TO_FIELD_NAME,
),
"",
ISSUE_COUNT_FIELD_NAME,
*COLLECTION_MODEL_ATTR_FIELDS[Volume],
),
Folder: (
(PATH_FIELD_NAME,),
Expand Down Expand Up @@ -324,23 +335,6 @@ def _metron_fts_values(values: tuple) -> tuple:
Universe: ((NAME_FIELD_NAME,), _IDENTIFIER_RELS, DESIGNATION_FIELD_NAME),
}
)
_IDENTIFIED_SELECT_RELATED = ("identifier", "identifier__source")
MODEL_SELECT_RELATED: MappingProxyType[type[BaseModel], tuple[str, ...]] = (
MappingProxyType(
{
**dict.fromkeys(IDENTIFIED_MODELS, _IDENTIFIED_SELECT_RELATED),
Identifier: (IDENTIFIER_SOURCE_FIELD_NAME,),
Publisher: _IDENTIFIED_SELECT_RELATED,
Imprint: ("publisher",),
Series: ("publisher", "imprint"),
Volume: ("publisher", "imprint", "series"),
Credit: (CREDIT_PERSON_FIELD_NAME, CREDIT_ROLE_FIELD_NAME),
Reprint: _IDENTIFIED_SELECT_RELATED,
StoryArcNumber: (STORY_ARC_FIELD_NAME,),
Universe: _IDENTIFIED_SELECT_RELATED,
}
)
)
FIELD_NAME_KEYS_REL_MAP = MappingProxyType(
{
# ``related_model`` is typed ``type[Model] | Literal['self']
Expand Down
61 changes: 58 additions & 3 deletions codex/librarian/scribe/importer/create/comics.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
BULK_UPDATE_COMIC_FIELDS_SET,
COLLECTION_FIELD_NAMES,
CREATE_COMICS,
DELETE_M2MS,
FTS_CREATE,
FTS_UPDATE,
LINK_FKS,
LINK_M2MS,
NON_FTS_FIELDS,
PATH_FIELD_NAME,
UPDATE_COMICS,
Expand All @@ -33,6 +35,10 @@
if TYPE_CHECKING:
from codex.models.collections import BrowserCollectionModel

# What ``presave`` derives from the file and the row. An update that would
# change none of them, the timestamp aside, writes nothing new.
_NOOP_CHECK_FIELDS = tuple(sorted(ALWAYS_UPDATE_COMIC_FIELDS - {"updated_at"}))


class CreateComicsImporter(CreateForeignKeyLinksImporter):
"""Create comics methods."""
Expand Down Expand Up @@ -66,10 +72,55 @@ def _record_moved_source_collections(
)
self.moved_source_collections.setdefault(model, set()).add(old_pk)

def _is_noop_update(
self,
comic: Comic,
md: dict,
link_md: dict,
m2m_deleted_pks: frozenset[int],
) -> bool:
"""
Return whether updating this comic would write nothing new.

A forced or lazy import of an unchanged comic, typically one with no
embedded tags, used to rewrite the row anyway: that purged and rebuilt
its cover and broadcast LIBRARY_CHANGED, reloading every browser. The
query phase has already pruned every link that is already correct, so
no scalar, FK or m2m change remains. If ``presave`` then derives the
same stat and the same values from the file on disk, the row is
current. A chunk that changed tag or collection rows keeps every comic
(see ``chunk_fks_updated``).
"""
if (
self.chunk_fks_updated
or md
or link_md
or comic.path in self.task.series_attrs
):
return False
if (
comic.path in self.metadata.get(LINK_M2MS, {})
or comic.pk in m2m_deleted_pks
):
return False
before = tuple(getattr(comic, field_name) for field_name in _NOOP_CHECK_FIELDS)
comic.presave()
return before == tuple(
getattr(comic, field_name) for field_name in _NOOP_CHECK_FIELDS
)

def _update_comic_values(
self, comic: Comic, update_comics: list, comic_pks: list, changed_fields: set
self,
comic: Comic,
update_comics: list,
comic_pks: list,
changed_fields: set,
m2m_deleted_pks: frozenset[int],
) -> None:
md = self.metadata[UPDATE_COMICS].pop(comic.pk, {})
link_md = self.get_comic_fk_links(comic.pk, comic.path, for_create=False)
if self._is_noop_update(comic, md, link_md, m2m_deleted_pks):
return
for field_name, value in md.items():
setattr(comic, field_name, value)
changed_fields.update(md)
Expand All @@ -82,7 +133,6 @@ def _update_comic_values(
field_name: getattr(comic, f"{field_name}_id")
for field_name in COLLECTION_FIELD_NAMES
}
link_md = self.get_comic_fk_links(comic.pk, comic.path, for_create=False)
changed_fields.update(link_md)
for field_name, value in link_md.items():
set_value = value
Expand Down Expand Up @@ -125,12 +175,17 @@ def update_comics(self) -> int:
update_comics = []
comic_pks = []
changed_fields: set[str] = set()
m2m_deleted_pks = frozenset(
comic_pk
for rows in self.metadata.get(DELETE_M2MS, {}).values()
for comic_pk, _ in rows
)
for comic in comics:
if self.abort_event.is_set():
return count
try:
self._update_comic_values(
comic, update_comics, comic_pks, changed_fields
comic, update_comics, comic_pks, changed_fields, m2m_deleted_pks
)
except Exception:
self.log.exception(f"Error preparing {comic} for update.")
Expand Down
8 changes: 6 additions & 2 deletions codex/librarian/scribe/importer/create/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from types import MappingProxyType

from codex.librarian.scribe.importer.const import (
COLLECTION_MODEL_ATTR_FIELDS,
CREDIT_PERSON_FIELD_NAME,
CREDIT_PRIMARY_FIELD_NAME,
CREDIT_ROLE_FIELD_NAME,
Expand All @@ -28,7 +29,6 @@
REPRINT_VOLUME_NUMBER_FIELD_NAME,
SERIES_FIELD_NAME,
STORY_ARC_FIELD_NAME,
VOLUME_COUNT_FIELD_NAME,
)
from codex.models import (
Credit,
Expand Down Expand Up @@ -68,6 +68,7 @@
{
Universe: frozenset({"designation"}),
Identifier: frozenset({"id_type", "key", "url"}),
Series: DEFAULT_NON_NULL_CHARFIELD_NAMES | {"status", "summary"},
Comic: frozenset({"path"}),
Volume: frozenset({}),
}
Expand Down Expand Up @@ -104,7 +105,10 @@
IMPRINT_FIELD_NAME: Imprint,
NAME_FIELD_NAME: None,
},
{IDENTIFIER_FIELD_NAME: Identifier, VOLUME_COUNT_FIELD_NAME: None},
{
IDENTIFIER_FIELD_NAME: Identifier,
**dict.fromkeys(COLLECTION_MODEL_ATTR_FIELDS[Series]),
},
),
Volume: (
{
Expand Down
Loading
Loading