refactor: unify duplicated logic between WebUI and PyQt

domain/output_paths.py:
- Add sanitize_filename_for_chapter() with OS safety + smart truncation
- Keep existing slugify() for backward compatibility

domain/text_chapters.py (NEW):
- parse_chapters_from_text() combines intro preservation (PyQt) + clean_text (WebUI)

PyQt/conversion.py:
- Replace 3x copy-pasted ASS/SRT headers with create_subtitle_writer()
- Replace inline M4B muxing with ExportService.embed_m4b_metadata()
- Replace inline chapter splitting with parse_chapters_from_text()
- Replace inline chapter filename sanitization with sanitize_filename_for_chapter()
- Remove unused _CHAPTER_MARKER_SEARCH_PATTERN import

Tests: 1152 passed (+21 new)
This commit is contained in:
Artem Akymenko
2026-07-19 07:50:02 +00:00
parent a299947bb1
commit c76cf74efc
7 changed files with 488 additions and 316 deletions
+49
View File
@@ -0,0 +1,49 @@
"""Tests for sanitize_filename_for_chapter in domain/output_paths.py"""
from __future__ import annotations
from abogen.domain.output_paths import sanitize_filename_for_chapter
class TestSanitizeFilenameForChapter:
def test_basic_title(self):
result = sanitize_filename_for_chapter("The Beginning", 1)
assert result == "01_The_Beginning"
def test_special_chars_removed(self):
result = sanitize_filename_for_chapter("Ch. 1: Hello!", 2)
assert result.startswith("02_")
assert "Ch" in result
assert "Hello" in result
def test_empty_title_uses_fallback(self):
result = sanitize_filename_for_chapter("", 3)
assert result == "03_chapter_03"
def test_index_prefix_zero_padded(self):
result = sanitize_filename_for_chapter("Test", 10)
assert result.startswith("10_")
def test_long_title_truncated_at_word_boundary(self):
long_title = "a" * 50 + "_" + "b" * 50
result = sanitize_filename_for_chapter(long_title, 1, max_len=60)
# Should truncate at word boundary
suffix = result[3:] # Remove "01_"
assert len(suffix) <= 60
def test_custom_max_len(self):
result = sanitize_filename_for_chapter("Hello World", 1, max_len=5)
suffix = result[3:] # Remove "01_"
assert len(suffix) <= 5
def test_hyphens_and_spaces_collapsed(self):
result = sanitize_filename_for_chapter("mid-night story", 1)
assert result == "01_mid_night_story"
def test_whitespace_collapsed(self):
result = sanitize_filename_for_chapter("hello world", 1)
assert "hello_world" in result
def test_leading_trailing_underscores_stripped(self):
result = sanitize_filename_for_chapter(" hello ", 1)
assert result == "01_hello"
+86
View File
@@ -0,0 +1,86 @@
"""Tests for domain/text_chapters.py"""
from __future__ import annotations
from abogen.domain.text_chapters import parse_chapters_from_text
class TestParseChaptersFromText:
def test_no_markers_returns_single_chapter(self):
result = parse_chapters_from_text("Hello world")
assert len(result) == 1
assert result[0][0] == "text"
assert result[0][1] == "Hello world"
def test_no_markers_custom_default_title(self):
result = parse_chapters_from_text("Hello", default_title="intro")
assert result[0][0] == "intro"
def test_single_marker(self):
text = "<<CHAPTER_MARKER:Chapter 1>>\nSome text here"
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 1
assert result[0][0] == "Chapter 1"
assert result[0][1] == "Some text here"
def test_multiple_markers(self):
text = (
"<<CHAPTER_MARKER:Chapter 1>>\nText 1\n"
"<<CHAPTER_MARKER:Chapter 2>>\nText 2\n"
)
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 2
assert result[0][0] == "Chapter 1"
assert result[0][1] == "Text 1"
assert result[1][0] == "Chapter 2"
assert result[1][1] == "Text 2"
def test_intro_preserved_before_first_marker(self):
text = "Introduction text\n<<CHAPTER_MARKER:Chapter 1>>\nMain text"
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 2
assert result[0][0] == "Introduction"
assert result[0][1] == "Introduction text"
assert result[1][0] == "Chapter 1"
def test_empty_intro_not_added(self):
text = "<<CHAPTER_MARKER:Chapter 1>>\nText"
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 1
assert result[0][0] == "Chapter 1"
def test_clean_text_applied_by_default(self):
text = "<<CHAPTER_MARKER:Ch 1>>\n some messy text "
result = parse_chapters_from_text(text, clean=True)
# clean_text normalizes whitespace
assert "some" in result[0][1]
assert "messy" in result[0][1]
def test_clean_disabled(self):
text = "<<CHAPTER_MARKER:Ch 1>>\n some text "
result = parse_chapters_from_text(text, clean=False)
assert result[0][1] == "some text"
def test_empty_marker_title_uses_default(self):
text = "<<CHAPTER_MARKER:>>\nSome text"
result = parse_chapters_from_text(text, clean=False)
assert result[0][0] == "text"
def test_case_insensitive_markers(self):
text = "<<chapter_marker:Chapter 1>>\nText"
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 1
assert result[0][0] == "Chapter 1"
def test_empty_text(self):
result = parse_chapters_from_text("")
assert len(result) == 1
assert result[0][0] == "text"
assert result[0][1] == ""
def test_only_markers_no_text(self):
text = "<<CHAPTER_MARKER:Ch 1>><<CHAPTER_MARKER:Ch 2>>"
result = parse_chapters_from_text(text, clean=False)
assert len(result) == 2
assert result[0][0] == "Ch 1"
assert result[1][0] == "Ch 2"