mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Implement chapter overrides and metadata merging in conversion process
- Added `_coerce_truthy` function to handle truthy value coercion. - Introduced `_apply_chapter_overrides` to apply chapter modifications based on provided overrides. - Implemented `_merge_metadata` to combine extracted metadata with overrides, ensuring proper handling of None values. - Updated `run_conversion_job` to utilize new chapter override and metadata merging functionalities. - Modified `Job` class to store chapters as dictionaries for better flexibility. - Enhanced `ConversionService` to normalize chapter input and metadata tags. - Added comprehensive tests for chapter overrides and metadata merging to ensure functionality and correctness.
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import types
|
||||
|
||||
|
||||
def _install_dependency_stubs() -> None:
|
||||
if "ebooklib" not in sys.modules:
|
||||
ebooklib_stub = types.ModuleType("ebooklib")
|
||||
epub_stub = types.ModuleType("ebooklib.epub")
|
||||
setattr(ebooklib_stub, "epub", epub_stub)
|
||||
sys.modules["ebooklib"] = ebooklib_stub
|
||||
sys.modules["ebooklib.epub"] = epub_stub
|
||||
|
||||
if "dotenv" not in sys.modules:
|
||||
dotenv_stub = types.ModuleType("dotenv")
|
||||
|
||||
def _noop(*_, **__):
|
||||
return None
|
||||
|
||||
setattr(dotenv_stub, "load_dotenv", _noop)
|
||||
setattr(dotenv_stub, "find_dotenv", lambda *_, **__: "")
|
||||
sys.modules["dotenv"] = dotenv_stub
|
||||
|
||||
if "numpy" not in sys.modules:
|
||||
numpy_stub = types.ModuleType("numpy")
|
||||
|
||||
class _DummyArray(list):
|
||||
pass
|
||||
|
||||
def _zeros(shape, dtype=None):
|
||||
size = 1
|
||||
if isinstance(shape, int):
|
||||
size = shape
|
||||
elif shape:
|
||||
size = 1
|
||||
for dimension in shape:
|
||||
size *= int(dimension)
|
||||
return [0.0] * size
|
||||
|
||||
setattr(numpy_stub, "ndarray", _DummyArray)
|
||||
setattr(numpy_stub, "zeros", _zeros)
|
||||
setattr(numpy_stub, "float32", "float32")
|
||||
setattr(numpy_stub, "array", lambda data, dtype=None: data)
|
||||
setattr(numpy_stub, "asarray", lambda data, dtype=None: data)
|
||||
setattr(numpy_stub, "concatenate", lambda seq, axis=0: sum((list(item) for item in seq), []))
|
||||
sys.modules["numpy"] = numpy_stub
|
||||
|
||||
if "soundfile" not in sys.modules:
|
||||
soundfile_stub = types.ModuleType("soundfile")
|
||||
|
||||
class _DummySoundFile:
|
||||
def __init__(self, *_, **__):
|
||||
pass
|
||||
|
||||
def write(self, *_args, **_kwargs):
|
||||
return None
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
setattr(soundfile_stub, "SoundFile", _DummySoundFile)
|
||||
setattr(soundfile_stub, "write", lambda *_args, **_kwargs: None)
|
||||
sys.modules["soundfile"] = soundfile_stub
|
||||
|
||||
if "fitz" not in sys.modules:
|
||||
sys.modules["fitz"] = types.ModuleType("fitz")
|
||||
|
||||
if "markdown" not in sys.modules:
|
||||
markdown_stub = types.ModuleType("markdown")
|
||||
|
||||
class _DummyMarkdown:
|
||||
def __init__(self, *_, **__):
|
||||
pass
|
||||
|
||||
def convert(self, text: str) -> str:
|
||||
return text
|
||||
|
||||
setattr(markdown_stub, "Markdown", _DummyMarkdown)
|
||||
sys.modules["markdown"] = markdown_stub
|
||||
|
||||
if "bs4" not in sys.modules:
|
||||
bs4_stub = types.ModuleType("bs4")
|
||||
|
||||
class _DummySoup:
|
||||
def __init__(self, *_, **__):
|
||||
pass
|
||||
|
||||
def select(self, *_, **__):
|
||||
return []
|
||||
|
||||
def find_all(self, *_, **__):
|
||||
return []
|
||||
|
||||
setattr(bs4_stub, "BeautifulSoup", _DummySoup)
|
||||
setattr(bs4_stub, "NavigableString", str)
|
||||
sys.modules["bs4"] = bs4_stub
|
||||
|
||||
|
||||
_install_dependency_stubs()
|
||||
|
||||
from abogen.text_extractor import ExtractedChapter
|
||||
from abogen.web.conversion_runner import _apply_chapter_overrides, _merge_metadata
|
||||
|
||||
|
||||
def _sample_chapters() -> list[ExtractedChapter]:
|
||||
return [
|
||||
ExtractedChapter(title="Chapter 1", text="Original one"),
|
||||
ExtractedChapter(title="Chapter 2", text="Original two"),
|
||||
ExtractedChapter(title="Chapter 3", text="Original three"),
|
||||
]
|
||||
|
||||
|
||||
def test_apply_chapter_overrides_with_custom_text() -> None:
|
||||
overrides = [
|
||||
{"index": 0, "enabled": True, "title": "Intro", "text": "Hello world"},
|
||||
{"index": 1, "enabled": False},
|
||||
]
|
||||
|
||||
selected, metadata, diagnostics = _apply_chapter_overrides(_sample_chapters(), overrides)
|
||||
|
||||
assert len(selected) == 1
|
||||
assert selected[0].title == "Intro"
|
||||
assert selected[0].text == "Hello world"
|
||||
assert overrides[0]["characters"] == len("Hello world")
|
||||
assert metadata == {}
|
||||
assert diagnostics == []
|
||||
|
||||
|
||||
def test_apply_chapter_overrides_uses_original_content_when_text_missing() -> None:
|
||||
overrides = [
|
||||
{"index": 1, "enabled": True},
|
||||
]
|
||||
|
||||
selected, metadata, diagnostics = _apply_chapter_overrides(_sample_chapters(), overrides)
|
||||
|
||||
assert len(selected) == 1
|
||||
assert selected[0].title == "Chapter 2"
|
||||
assert selected[0].text == "Original two"
|
||||
assert overrides[0]["text"] == "Original two"
|
||||
assert overrides[0]["characters"] == len("Original two")
|
||||
assert metadata == {}
|
||||
assert diagnostics == []
|
||||
|
||||
|
||||
def test_apply_chapter_overrides_collects_metadata_updates() -> None:
|
||||
overrides = [
|
||||
{
|
||||
"index": 2,
|
||||
"enabled": True,
|
||||
"metadata": {"artist": "Test Author", "year": 2024},
|
||||
}
|
||||
]
|
||||
|
||||
selected, metadata, diagnostics = _apply_chapter_overrides(_sample_chapters(), overrides)
|
||||
|
||||
assert len(selected) == 1
|
||||
assert metadata == {"artist": "Test Author", "year": "2024"}
|
||||
assert diagnostics == []
|
||||
|
||||
|
||||
def test_apply_chapter_overrides_reports_diagnostics_for_invalid_payload() -> None:
|
||||
overrides = [
|
||||
{"enabled": True, "title": "Missing"},
|
||||
]
|
||||
|
||||
selected, metadata, diagnostics = _apply_chapter_overrides(_sample_chapters(), overrides)
|
||||
|
||||
assert selected == []
|
||||
assert metadata == {}
|
||||
assert diagnostics and "Skipped chapter override" in diagnostics[0]
|
||||
|
||||
|
||||
def test_merge_metadata_prefers_overrides_and_drops_none_values() -> None:
|
||||
extracted = {"title": "Original", "artist": "Someone"}
|
||||
overrides = {"artist": "Another", "genre": "Fiction", "year": None}
|
||||
|
||||
merged = _merge_metadata(extracted, overrides)
|
||||
|
||||
assert merged["title"] == "Original"
|
||||
assert merged["artist"] == "Another"
|
||||
assert merged["genre"] == "Fiction"
|
||||
assert "year" not in merged
|
||||
Reference in New Issue
Block a user