diff options
| -rw-r--r-- | converter/extractors.py | 92 | ||||
| -rw-r--r-- | tests/test_extractors.py | 40 |
2 files changed, 36 insertions, 96 deletions
diff --git a/converter/extractors.py b/converter/extractors.py index 9139626..cd270a1 100644 --- a/converter/extractors.py +++ b/converter/extractors.py @@ -43,12 +43,11 @@ def extract_sections(file_path: Path) -> List[Section]: one chapter at a time. TXT and PDF files have no chapter structure and always yield a single section. """ - extension = file_path.suffix.lower() - if extension == ".epub": + if file_path.suffix.lower() == ".epub": chapters = _extract_epub_chapters(file_path) - if len(chapters) > 1: - return chapters - return [Section(file_path.stem, extract_epub(file_path))] + if not chapters: + raise RuntimeError("All EPUB extraction methods failed") + return chapters return [Section(file_path.stem, extract_text(file_path))] @@ -199,49 +198,11 @@ def clean_html(html_content: str) -> str: def extract_epub(file_path: Path) -> str: - """Extract text from EPUB, trying several methods in order.""" - methods = [ - _extract_epub_ebooklib, - _extract_epub_zipfile, - _extract_epub_manual, - ] - - for method in methods: - try: - text = method(file_path) - if text and text.strip(): - logger.info("EPUB extraction successful (%s): %d characters", method.__name__, len(text)) - return text - except Exception as exc: - logger.warning("EPUB method %s failed: %s", method.__name__, exc) - - raise RuntimeError("All EPUB extraction methods failed") - - -def _extract_epub_ebooklib(file_path: Path) -> str: - """Extract using ebooklib, following the spine (reading) order.""" - import ebooklib - from ebooklib import epub - - book = epub.read_epub(str(file_path)) - text_parts = [] - - for entry in book.spine: - item_id = entry[0] if isinstance(entry, (tuple, list)) else entry - try: - item = book.get_item_with_id(item_id) - if item and item.get_type() == ebooklib.ITEM_DOCUMENT: - content = item.get_body_content() - if content: - if isinstance(content, bytes): - content = content.decode("utf-8", errors="ignore") - cleaned = clean_html(str(content)) - if cleaned.strip(): - text_parts.append(cleaned) - except Exception as exc: - logger.debug("Skipping EPUB spine item %r: %s", item_id, exc) - - return "\n\n".join(text_parts) + """Extract the book's text from EPUB, trying several methods in order.""" + chapters = _extract_epub_chapters(file_path) + if not chapters: + raise RuntimeError("All EPUB extraction methods failed") + return "\n\n".join(section.text for section in chapters) def _natural_key(name: str): @@ -250,41 +211,6 @@ def _natural_key(name: str): for part in re.split(r"(\d+)", name)] -def _extract_epub_zipfile(file_path: Path) -> str: - """Extract by parsing HTML members of the EPUB zip directly.""" - text_parts = [] - with zipfile.ZipFile(file_path, "r") as epub_zip: - for file_name in sorted(epub_zip.namelist(), key=_natural_key): - if file_name.lower().endswith((".html", ".xhtml", ".htm")): - try: - content = epub_zip.read(file_name).decode("utf-8", errors="ignore") - cleaned = clean_html(content) - if cleaned.strip(): - text_parts.append(cleaned) - except Exception as exc: - logger.debug("Skipping EPUB member %r: %s", file_name, exc) - return "\n\n".join(text_parts) - - -def _extract_epub_manual(file_path: Path) -> str: - """Last-resort extraction from any markup-looking EPUB member.""" - skipped_extensions = (".jpg", ".jpeg", ".png", ".gif", ".css", ".js") - text_parts = [] - with zipfile.ZipFile(file_path, "r") as epub_zip: - for file_name in sorted(epub_zip.namelist(), key=_natural_key): - if file_name.lower().endswith(skipped_extensions): - continue - try: - content = epub_zip.read(file_name).decode("utf-8", errors="ignore") - if "<" in content and len(content.strip()) > 100: - cleaned = clean_html(content) - if cleaned: - text_parts.append(cleaned) - except Exception as exc: - logger.debug("Skipping EPUB member %r: %s", file_name, exc) - return "\n\n".join(text_parts) - - def _extract_txt(file_path: Path) -> str: """Extract from TXT, handling BOMs and common encodings (latin-1 is the catch-all). diff --git a/tests/test_extractors.py b/tests/test_extractors.py index 09b688e..7b307c0 100644 --- a/tests/test_extractors.py +++ b/tests/test_extractors.py @@ -35,7 +35,8 @@ class TxtExtractionTests(unittest.TestCase): extract_text(path) -def _build_test_epub(path: Path) -> None: +def _build_test_epub(path: Path, chapters=(("One", "First chapter text."), + ("Two", "Second chapter text."))) -> None: from ebooklib import epub book = epub.EpubBook() @@ -43,15 +44,15 @@ def _build_test_epub(path: Path) -> None: book.set_title("Test Book") book.set_language("en") - chapter1 = epub.EpubHtml(title="One", file_name="chap1.xhtml", lang="en") - chapter1.content = "<html><body><p>First chapter text.</p></body></html>" - chapter2 = epub.EpubHtml(title="Two", file_name="chap2.xhtml", lang="en") - chapter2.content = "<html><body><p>Second chapter text.</p></body></html>" + items = [] + for index, (title, text) in enumerate(chapters, 1): + chapter = epub.EpubHtml(title=title, file_name=f"chap{index}.xhtml", lang="en") + chapter.content = f"<html><body><p>{text}</p></body></html>" + book.add_item(chapter) + items.append(chapter) - book.add_item(chapter1) - book.add_item(chapter2) - book.toc = (chapter1, chapter2) - book.spine = ["nav", chapter1, chapter2] + book.toc = tuple(items) + book.spine = ["nav", *items] book.add_item(epub.EpubNcx()) book.add_item(epub.EpubNav()) @@ -68,15 +69,16 @@ class EpubExtractionTests(unittest.TestCase): def test_ebooklib_extraction(self): # Regression test: the ebooklib path used to silently return "" due to # isinstance(item, ebooklib.ITEM_DOCUMENT) (an int, not a class). - from converter.extractors import _extract_epub_ebooklib + from converter.extractors import _read_epub_ebooklib with tempfile.TemporaryDirectory() as tmp: path = Path(tmp) / "book.epub" _build_test_epub(path) - text = _extract_epub_ebooklib(path) + items = _read_epub_ebooklib(path) - self.assertIn("First chapter text.", text) - self.assertIn("Second chapter text.", text) + html = "\n".join(content for _, content in items) + self.assertIn("First chapter text.", html) + self.assertIn("Second chapter text.", html) def test_epub_extraction_follows_spine_order(self): with tempfile.TemporaryDirectory() as tmp: @@ -123,6 +125,18 @@ class ExtractSectionsTests(unittest.TestCase): self.assertEqual(sections[0].title, "book") self.assertEqual(sections[0].text, "Hello world.") + def test_single_chapter_epub_keeps_chapter_title(self): + from converter.extractors import extract_sections + + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "book.epub" + _build_test_epub(path, chapters=(("Only", "Just one chapter."),)) + sections = extract_sections(path) + + self.assertEqual(len(sections), 1) + self.assertEqual(sections[0].title, "Only") + self.assertIn("Just one chapter.", sections[0].text) + if __name__ == "__main__": unittest.main() |
