From a501e96b12de134dde296acf12961c23066518fe Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 2 Dec 2025 10:51:51 -0800 Subject: [PATCH] feat: Add metadata fields for title, subtitle, author, series, publisher, and description in book form --- abogen/web/routes/api.py | 13 +++++ abogen/web/routes/utils/form.py | 40 ++++++++++++++++ abogen/web/service.py | 30 ++---------- .../templates/partials/new_job_step_book.html | 47 +++++++++++++++++++ 4 files changed, 103 insertions(+), 27 deletions(-) diff --git a/abogen/web/routes/api.py b/abogen/web/routes/api.py index 4057a3e..938e15d 100644 --- a/abogen/web/routes/api.py +++ b/abogen/web/routes/api.py @@ -225,6 +225,7 @@ def api_calibre_opds_import() -> ResponseReturnValue: raw_series = metadata_payload.get("series") or metadata_payload.get("series_name") series_name = str(raw_series or "").strip() + if series_name: metadata_overrides["series"] = series_name metadata_overrides.setdefault("series_name", series_name) @@ -258,6 +259,18 @@ def api_calibre_opds_import() -> ResponseReturnValue: metadata_overrides.setdefault("description", description_text) metadata_overrides.setdefault("summary", description_text) + subtitle_value = metadata_payload.get("subtitle") + if subtitle_value: + subtitle_text = _stringify_metadata_value(subtitle_value) + if subtitle_text: + metadata_overrides.setdefault("subtitle", subtitle_text) + + publisher_value = metadata_payload.get("publisher") + if publisher_value: + publisher_text = _stringify_metadata_value(publisher_value) + if publisher_text: + metadata_overrides.setdefault("publisher", publisher_text) + settings = load_settings() integrations = load_integration_settings() calibre_settings = integrations.get("calibre_opds", {}) diff --git a/abogen/web/routes/utils/form.py b/abogen/web/routes/utils/form.py index 16c3f8c..c8cb724 100644 --- a/abogen/web/routes/utils/form.py +++ b/abogen/web/routes/utils/form.py @@ -600,6 +600,46 @@ def apply_book_step_form( pending.applied_speaker_config = (form.get("speaker_config") or "").strip() or None + # Metadata updates + if "meta_title" in form: + pending.metadata_tags["title"] = str(form.get("meta_title", "")).strip() + + if "meta_subtitle" in form: + pending.metadata_tags["subtitle"] = str(form.get("meta_subtitle", "")).strip() + + if "meta_author" in form: + authors = str(form.get("meta_author", "")).strip() + pending.metadata_tags["authors"] = authors + pending.metadata_tags["author"] = authors + + if "meta_series" in form: + series = str(form.get("meta_series", "")).strip() + pending.metadata_tags["series"] = series + pending.metadata_tags["series_name"] = series + pending.metadata_tags["seriesname"] = series + pending.metadata_tags["series_title"] = series + pending.metadata_tags["seriestitle"] = series + # If user manually edits series, update opds_series too so it persists + if "opds_series" in pending.metadata_tags: + pending.metadata_tags["opds_series"] = series + + if "meta_series_index" in form: + idx = str(form.get("meta_series_index", "")).strip() + pending.metadata_tags["series_index"] = idx + pending.metadata_tags["series_sequence"] = idx + + if "meta_publisher" in form: + pending.metadata_tags["publisher"] = str(form.get("meta_publisher", "")).strip() + + if "meta_description" in form: + desc = str(form.get("meta_description", "")).strip() + pending.metadata_tags["description"] = desc + pending.metadata_tags["summary"] = desc + + if coerce_bool(form.get("remove_cover"), False): + pending.cover_image_path = None + pending.cover_image_mime = None + def persist_cover_image(extraction_result: Any, stored_path: Path) -> tuple[Optional[Path], Optional[str]]: cover_bytes = getattr(extraction_result, "cover_image", None) if not cover_bytes: diff --git a/abogen/web/service.py b/abogen/web/service.py index 3e053e4..3b47995 100644 --- a/abogen/web/service.py +++ b/abogen/web/service.py @@ -389,33 +389,7 @@ def build_audiobookshelf_metadata(job: Job) -> Dict[str, Any]: tags.get("series_title"), tags.get("seriestitle"), ) - if series_name: - normalized_series = series_name.strip() - if normalized_series: - author_candidates: set[str] = set() - for name in authors: - author_name = name.strip() - if author_name: - author_candidates.add(author_name.casefold()) - raw_author_fields = ( - tags.get("authors"), - tags.get("author"), - tags.get("album_artist"), - tags.get("artist"), - tags.get("writer"), - tags.get("composer"), - ) - for raw_author in raw_author_fields: - if not raw_author: - continue - author_text = str(raw_author).strip() - if author_text: - author_candidates.add(author_text.casefold()) - joined_authors = ", ".join(authors) - if joined_authors: - author_candidates.add(joined_authors.casefold()) - if normalized_series.casefold() in author_candidates: - series_name = None + series_sequence = None for key in _SERIES_SEQUENCE_TAG_KEYS: raw_value = tags.get(key) @@ -427,9 +401,11 @@ def build_audiobookshelf_metadata(job: Job) -> Dict[str, Any]: series_sequence = None data: Dict[str, Any] = { "title": title, + "subtitle": tags.get("subtitle"), "authors": authors, "narrators": narrators, "description": description, + "publisher": tags.get("publisher"), "genres": genres, "tags": keywords, "language": language, diff --git a/abogen/web/templates/partials/new_job_step_book.html b/abogen/web/templates/partials/new_job_step_book.html index c4b2879..13b46d3 100644 --- a/abogen/web/templates/partials/new_job_step_book.html +++ b/abogen/web/templates/partials/new_job_step_book.html @@ -220,6 +220,53 @@ +
+

Book Metadata

+
+
+ + +
+
+ + +
+
+ + +

Comma separated for multiple authors

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + {% if pending.cover_image_path %} +

Cover image currently exists.

+ {% else %} +

No cover image found.

+ {% endif %} +
+
+
+

Narrator defaults