mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
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:
@@ -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()
|
||||
@@ -62,6 +62,10 @@ from abogen.domain.metadata_helpers import (
|
||||
extract_series_metadata as _extract_series_metadata,
|
||||
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
|
||||
@@ -167,81 +171,6 @@ def _coerce_truthy(value: Any, default: bool = True) -> bool:
|
||||
_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]:
|
||||
text = str(spec or "").strip()
|
||||
if not text:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user