Run download jobs and deliver books to CWA

Jobs download new chapters, recheck chapters, replace the book, update
metadata or rebuild from stored chapters. Locked metadata fields keep
their values. A scheduler queues updates. An update finds the library
book of a story through CWA, and fails with an error if it cannot, so
that it never makes a duplicate book.
This commit is contained in:
tim
2026-09-27 23:38:28 +02:00
parent 312dc3efc5
commit 49cd18a09f
8 changed files with 2581 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
import pytest
from bindery import orchestrator as backend
SITE = {
"title": "Site Title",
"authors": ["Writer"],
"comments": "<p>New blurb</p>",
"tags": ["Fantasy", "Completed"],
"series": "Saga",
"series_index": 2.0,
"pubdate": "2020-01-01T00:00:00",
"languages": "en",
}
def story(**extra):
base = {"locked_fields": ["title"], "pinned": {}, "site_values": {}}
base.update(extra)
return base
def test_first_download_keeps_site_value():
assert backend._resolve_metadata(story(), SITE, []) == {"title": "Site Title"}
def test_locked_field_keeps_its_value():
assert backend._resolve_metadata(story(pinned={"title": "Old"}), SITE, []) == {"title": "Old"}
def test_forced_field_takes_site_value_and_stays_locked():
s = story(pinned={"title": "Old"})
assert backend._resolve_metadata(s, SITE, ["title"]) == {"title": "Site Title"}
def test_edited_unlocked_field_stays_until_the_site_changes_it():
s = story(pinned={"title": "Old", "tags": ["mine"]}, site_values=SITE)
assert backend._resolve_metadata(s, SITE, []) == {"title": "Old", "tags": ["mine"]}
changed = {**SITE, "tags": ["Fantasy", "Completed", "LitRPG"]}
assert backend._resolve_metadata(s, changed, []) == {"title": "Old"}
# A change to another field keeps the edit.
assert backend._resolve_metadata(s, {**SITE, "comments": "<p>x</p>"}, []) == {"title": "Old", "tags": ["mine"]}
def test_forced_update_replaces_an_edited_unlocked_field():
s = story(pinned={"tags": ["mine"]}, site_values=SITE)
assert backend._resolve_metadata(s, SITE, ["tags"]) == {"title": "Site Title"}
def test_edit_before_the_first_site_values_stays():
s = story(pinned={"tags": ["mine"]})
assert backend._resolve_metadata(s, SITE, []) == {"title": "Site Title", "tags": ["mine"]}
def test_edited_values_are_checked_and_normalized():
assert backend._edited_value("authors", "A & B ") == ["A", "B"]
assert backend._edited_value("tags", "x, y,,") == ["x", "y"]
assert backend._edited_value("series", "Saga [2]") == ["Saga", 2.0]
assert backend._edited_value("series", ["Saga", "3"]) == ["Saga", 3.0]
assert backend._edited_value("series", "") == ["", None]
assert backend._edited_value("pubdate", "2024-01-31") == "2024-01-31"
assert backend._edited_value("title", " New ") == "New"
for field, value in (("title", " "), ("authors", "&"), ("pubdate", "soon")):
with pytest.raises(ValueError):
backend._edited_value(field, value)
def test_series_keeps_name_and_index():
s = story(locked_fields=["series"])
assert backend._resolve_metadata(s, SITE, []) == {"series": ["Saga", 2.0]}
def test_resolve_without_site_values_does_not_fail():
assert backend._resolve_metadata(story(pinned={"title": "Old"}), {}, []) == {"title": "Old"}