refactor: extract title/outro builders into domain/title_builder.py

- Extract build_title_intro_text and build_outro_text into domain/title_builder.py
- Uses metadata_helpers for metadata processing
- Remove _build_title_intro_text and _build_outro_text from conversion_runner.py
- Add tests/test_title_builder.py with 12 tests
- All tests match old behavior
This commit is contained in:
Artem Akymenko
2026-07-14 10:27:48 +00:00
parent 364c179bd6
commit 7777e58f1d
3 changed files with 160 additions and 75 deletions
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List, Mapping, Optional
from .metadata_helpers import (
ensure_sentence,
extract_series_metadata,
format_author_sentence,
format_series_sentence,
normalize_metadata_map,
)
def build_title_intro_text(
metadata: Optional[Mapping[str, Any]],
fallback_basename: str,
) -> str:
"""Build the title introduction text from metadata."""
normalized = normalize_metadata_map(metadata)
fallback_title = Path(fallback_basename).stem if fallback_basename else ""
title = (
normalized.get("title")
or normalized.get("book_title")
or normalized.get("album")
or fallback_title
)
if not title:
title = fallback_title
subtitle = normalized.get("subtitle") or normalized.get("sub_title")
if subtitle and title and subtitle.casefold() == title.casefold():
subtitle = ""
author_value = ""
for candidate in ("artist", "album_artist", "author", "authors", "writer", "composer"):
value = normalized.get(candidate)
if value:
author_value = value
break
series_name, series_number = extract_series_metadata(normalized)
series_sentence = format_series_sentence(series_name, series_number)
sentences: List[str] = []
if series_sentence:
sentences.append(ensure_sentence(series_sentence))
if title:
sentences.append(ensure_sentence(title))
if subtitle:
sentences.append(ensure_sentence(subtitle))
author_sentence = format_author_sentence(author_value)
if author_sentence:
sentences.append(ensure_sentence(author_sentence))
return " ".join(sentences).strip()
def build_outro_text(
metadata: Optional[Mapping[str, Any]],
fallback_basename: str,
) -> str:
"""Build the outro/closing text from metadata."""
normalized = normalize_metadata_map(metadata)
fallback_title = Path(fallback_basename).stem if fallback_basename else ""
title = (
normalized.get("title")
or normalized.get("book_title")
or normalized.get("album")
or fallback_title
)
author_value = ""
for candidate in ("authors", "author", "album_artist", "artist", "writer", "composer"):
value = normalized.get(candidate)
if value:
author_value = value
break
author_sentence = format_author_sentence(author_value)
authors_fragment = (
author_sentence[3:].strip() if author_sentence.lower().startswith("by ") else author_sentence.strip()
)
if title and authors_fragment:
closing_line = f"The end of {title} from {authors_fragment}"
elif title:
closing_line = f"The end of {title}"
elif authors_fragment:
closing_line = f"The end from {authors_fragment}"
else:
closing_line = "The end"
series_name, series_number = extract_series_metadata(normalized)
series_sentence = format_series_sentence(series_name, series_number)
sentences: List[str] = [ensure_sentence(closing_line)]
if series_sentence:
sentences.append(ensure_sentence(series_sentence))
return " ".join(sentence for sentence in sentences if sentence).strip()
+4 -75
View File
@@ -62,6 +62,10 @@ from abogen.domain.metadata_helpers import (
extract_series_metadata as _extract_series_metadata, extract_series_metadata as _extract_series_metadata,
format_series_sentence as _format_series_sentence, format_series_sentence as _format_series_sentence,
) )
from abogen.domain.title_builder import (
build_title_intro_text as _build_title_intro_text,
build_outro_text as _build_outro_text,
)
from .service import Job, JobStatus from .service import Job, JobStatus
@@ -167,81 +171,6 @@ def _coerce_truthy(value: Any, default: bool = True) -> bool:
_OUTPUT_SANITIZE_RE = re.compile(r"[^\w\-_.]+") _OUTPUT_SANITIZE_RE = re.compile(r"[^\w\-_.]+")
def _build_title_intro_text(
metadata: Optional[Mapping[str, Any]],
fallback_basename: str,
) -> str:
normalized = _normalize_metadata_map(metadata)
fallback_title = Path(fallback_basename).stem if fallback_basename else ""
title = normalized.get("title") or normalized.get("book_title") or normalized.get("album") or fallback_title
if not title:
title = fallback_title
subtitle = normalized.get("subtitle") or normalized.get("sub_title")
if subtitle and title and subtitle.casefold() == title.casefold():
subtitle = ""
author_value = ""
for candidate in ("artist", "album_artist", "author", "authors", "writer", "composer"):
value = normalized.get(candidate)
if value:
author_value = value
break
series_name, series_number = _extract_series_metadata(normalized)
series_sentence = _format_series_sentence(series_name, series_number)
sentences: List[str] = []
if series_sentence:
sentences.append(_ensure_sentence(series_sentence))
if title:
sentences.append(_ensure_sentence(title))
if subtitle:
sentences.append(_ensure_sentence(subtitle))
author_sentence = _format_author_sentence(author_value)
if author_sentence:
sentences.append(_ensure_sentence(author_sentence))
return " ".join(sentences).strip()
def _build_outro_text(
metadata: Optional[Mapping[str, Any]],
fallback_basename: str,
) -> str:
normalized = _normalize_metadata_map(metadata)
fallback_title = Path(fallback_basename).stem if fallback_basename else ""
title = (
normalized.get("title")
or normalized.get("book_title")
or normalized.get("album")
or fallback_title
)
author_value = ""
for candidate in ("authors", "author", "album_artist", "artist", "writer", "composer"):
value = normalized.get(candidate)
if value:
author_value = value
break
author_sentence = _format_author_sentence(author_value)
authors_fragment = author_sentence[3:].strip() if author_sentence.lower().startswith("by ") else author_sentence.strip()
if title and authors_fragment:
closing_line = f"The end of {title} from {authors_fragment}"
elif title:
closing_line = f"The end of {title}"
elif authors_fragment:
closing_line = f"The end from {authors_fragment}"
else:
closing_line = "The end"
series_name, series_number = _extract_series_metadata(normalized)
series_sentence = _format_series_sentence(series_name, series_number)
sentences: List[str] = [_ensure_sentence(closing_line)]
if series_sentence:
sentences.append(_ensure_sentence(series_sentence))
return " ".join(sentence for sentence in sentences if sentence).strip()
def _spec_to_voice_ids(spec: Any) -> Set[str]: def _spec_to_voice_ids(spec: Any) -> Set[str]:
text = str(spec or "").strip() text = str(spec or "").strip()
if not text: if not text:
+59
View File
@@ -0,0 +1,59 @@
"""Tests for domain/title_builder.py."""
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from abogen.domain.title_builder import build_title_intro_text, build_outro_text
class TestBuildTitleIntroText:
def test_empty_metadata(self):
result = build_title_intro_text({}, "book.epub")
assert result == "book."
def test_title_from_metadata(self):
result = build_title_intro_text({"title": "My Book"}, "book.epub")
assert result == "My Book."
def test_title_fallback_basename(self):
result = build_title_intro_text({}, "my_book.epub")
assert result == "my_book."
def test_with_author(self):
result = build_title_intro_text({"title": "My Book", "author": "John Doe"}, "book.epub")
assert result == "My Book. By John Doe."
def test_with_subtitle(self):
result = build_title_intro_text({"title": "My Book", "subtitle": "A Tale"}, "book.epub")
assert result == "My Book. A Tale."
def test_duplicate_title_subtitle(self):
result = build_title_intro_text({"title": "My Book", "subtitle": "My Book"}, "book.epub")
assert result == "My Book."
def test_with_series(self):
result = build_title_intro_text({"title": "My Book", "series": "Series", "series_index": "3"}, "book.epub")
assert result == "Book 3 of the Series. My Book."
class TestBuildOutroText:
def test_empty(self):
result = build_outro_text({}, "book.epub")
assert result == "The end of book."
def test_title_only(self):
result = build_outro_text({"title": "My Book"}, "book.epub")
assert result == "The end of My Book."
def test_author_only(self):
result = build_outro_text({"author": "John Doe"}, "book.epub")
assert result == "The end of book from John Doe."
def test_title_and_author(self):
result = build_outro_text({"title": "My Book", "author": "John Doe"}, "book.epub")
assert result == "The end of My Book from John Doe."
def test_with_series(self):
result = build_outro_text({"title": "My Book", "series": "Series", "series_index": "3"}, "book.epub")
assert "The end of My Book." in result
assert "Book 3 of the Series." in result