kenkui 2.3.2__tar.gz → 2.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kenkui-2.3.2/src/kenkui.egg-info → kenkui-2.3.3}/PKG-INFO +1 -1
- {kenkui-2.3.2 → kenkui-2.3.3}/pyproject.toml +1 -1
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/epub.py +55 -1
- {kenkui-2.3.2 → kenkui-2.3.3/src/kenkui.egg-info}/PKG-INFO +1 -1
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_parsing.py +48 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/LICENSE +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/MANIFEST.in +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/README.md +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/setup.cfg +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/analytics.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/audio_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/chapter_classifier.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/chapter_filter.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/cli.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/errors.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/file_finder.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/huggingface_auth.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/log.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/model_names.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/audio.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/book.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/common.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/job.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/nlp.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/models/queue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_async.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_filters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/_retry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/annotator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/attribution.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/booknlp_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/chunker.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/entities.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/llm.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/pipeline.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/_base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/_factory.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/booknlp.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/litellm.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/local.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/providers/ollama.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/quotes.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp/setup.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/nlp_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/parsing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/post_processing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/progress.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/progress_tracking.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/_pdf_extract.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/_pdf_to_epub.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/fb2.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/mobi.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/readers/pdf.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/Les Miserables - Victor Hugo.epub +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/Pride and Predjudice.epub +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/samples/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/series.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/openapi.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/server/server.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/application_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/auth_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/book_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/book_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/confirmation_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/download_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/execution_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/job_executor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/job_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/nlp_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/openrouter_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/provider_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/queue_manager.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/runtime_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/series_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/setup_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/task_coordinator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/task_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/voice_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/services/workflow_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/system_check.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/text_rules.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/utils.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_compiler.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_download.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_loader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_metadata.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_pool.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voice_registry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voices/manifest.json +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/voices/preview_phrases.json +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui/workers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/SOURCES.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/dependency_links.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/entry_points.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/requires.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/src/kenkui.egg-info/top_level.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_analytics.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_annotator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_api_attribution_options.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_application_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_audio_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_auth_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_book_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_book_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_chapter_classifier.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_chapter_filter.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_character_record_dataclass.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_credential_management.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_download_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_generation_progress.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_huggingface_auth.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_job_executor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_library_contracts.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_runtime_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_runtime_registration.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_modal_status.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_model_names.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_multi_voice_models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_name_filters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_attribution.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_booknlp_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_booknlp_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_chunker.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_coreference_pierce_brown.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_entities.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_factory.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_italic_dialogue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_litellm_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_llm_limits.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_models_enriched.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_ollama_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_pipeline.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_pipeline_split.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_providers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_providers_base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_quotes.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_quotes_non_dialogue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_retry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_segments.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_nlp_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_openrouter_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_optional_dependencies.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_extractor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_reader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_pdf_to_epub.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_post_processing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_progress_tracking.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_provider_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_qp14_efficiency.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_queue_manager.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_scare_quote_stripping.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_series_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_setup_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_speaker_voices_migration.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_suggest_cast.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_task_coordinator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_utils.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_download.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_loader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_metadata.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_pack_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_pools.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_registry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_review_ux.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_voice_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_workers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.3}/tests/test_workers_scene_breaks.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kenkui"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.3"
|
|
8
8
|
description = "Convert Ebooks to Audiobooks with [custom] voice samples"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Sumner MacArthur", email = "spn1kolat3sla@gmail.com" }]
|
|
@@ -7,6 +7,7 @@ Provides EbookReader interface for EPUB files using ebooklib.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
|
+
import tempfile
|
|
10
11
|
import warnings
|
|
11
12
|
import zipfile
|
|
12
13
|
from pathlib import Path
|
|
@@ -30,7 +31,60 @@ class EpubReader(EbookReader):
|
|
|
30
31
|
super().__init__(filepath, verbose)
|
|
31
32
|
with warnings.catch_warnings(record=True):
|
|
32
33
|
warnings.simplefilter("always")
|
|
33
|
-
|
|
34
|
+
try:
|
|
35
|
+
self.book = epub.read_epub(str(filepath))
|
|
36
|
+
except AttributeError as exc:
|
|
37
|
+
if "'NoneType' object has no attribute" not in str(exc):
|
|
38
|
+
raise
|
|
39
|
+
try:
|
|
40
|
+
self.book = self._read_epub_without_nav_parsing()
|
|
41
|
+
except Exception:
|
|
42
|
+
raise exc
|
|
43
|
+
|
|
44
|
+
def _read_epub_without_nav_parsing(self):
|
|
45
|
+
"""Load an EPUB while bypassing EbookLib's brittle NAV parser.
|
|
46
|
+
|
|
47
|
+
EbookLib parses any manifest item marked ``properties=\"nav\"`` during
|
|
48
|
+
``read_epub``. Some real EPUBs contain malformed NAV documents (for
|
|
49
|
+
example a TOC ``<nav>`` without an ``<ol>``), which makes EbookLib raise
|
|
50
|
+
an opaque ``'NoneType' object has no attribute ...`` before kenkui can
|
|
51
|
+
fall back to its own TOC/chapter extraction. Strip only the ``nav``
|
|
52
|
+
property from a temporary copy so EbookLib still loads manifest/spine
|
|
53
|
+
items while kenkui parses the original file itself.
|
|
54
|
+
"""
|
|
55
|
+
from xml.etree import ElementTree as ET
|
|
56
|
+
|
|
57
|
+
with tempfile.NamedTemporaryFile(suffix=".epub") as temp_epub:
|
|
58
|
+
with zipfile.ZipFile(str(self.filepath), "r") as source_zip:
|
|
59
|
+
names = source_zip.namelist()
|
|
60
|
+
container_path = "META-INF/container.xml"
|
|
61
|
+
if container_path not in names:
|
|
62
|
+
raise ValueError("EPUB is missing META-INF/container.xml")
|
|
63
|
+
container_tree = ET.fromstring(source_zip.read(container_path))
|
|
64
|
+
container_ns = {"container": "urn:oasis:names:tc:opendocument:xmlns:container"}
|
|
65
|
+
rootfile = container_tree.find(".//container:rootfile", container_ns)
|
|
66
|
+
if rootfile is None or rootfile.get("full-path") is None:
|
|
67
|
+
raise ValueError("EPUB container is missing rootfile full-path")
|
|
68
|
+
opf_path = rootfile.get("full-path") or ""
|
|
69
|
+
opf_tree = ET.fromstring(source_zip.read(opf_path))
|
|
70
|
+
opf_ns = {"opf": "http://www.idpf.org/2007/opf"}
|
|
71
|
+
for item in opf_tree.findall(".//opf:item", opf_ns):
|
|
72
|
+
properties = item.get("properties")
|
|
73
|
+
if not properties:
|
|
74
|
+
continue
|
|
75
|
+
remaining = [prop for prop in properties.split() if prop != "nav"]
|
|
76
|
+
if remaining:
|
|
77
|
+
item.set("properties", " ".join(remaining))
|
|
78
|
+
else:
|
|
79
|
+
item.attrib.pop("properties", None)
|
|
80
|
+
sanitized_opf = ET.tostring(opf_tree, encoding="utf-8", xml_declaration=True)
|
|
81
|
+
|
|
82
|
+
with zipfile.ZipFile(temp_epub.name, "w") as sanitized_zip:
|
|
83
|
+
for info in source_zip.infolist():
|
|
84
|
+
content = sanitized_opf if info.filename == opf_path else source_zip.read(info.filename)
|
|
85
|
+
sanitized_zip.writestr(info, content)
|
|
86
|
+
|
|
87
|
+
return epub.read_epub(temp_epub.name)
|
|
34
88
|
|
|
35
89
|
def get_metadata(self) -> EbookMetadata:
|
|
36
90
|
"""Extract metadata from EPUB."""
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests for kenkui parsing functionality."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import zipfile
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from types import SimpleNamespace
|
|
6
7
|
|
|
@@ -13,6 +14,38 @@ from kenkui.readers.epub import EpubReader
|
|
|
13
14
|
TEST_EPUB = Path("src/kenkui/samples/Les Miserables - Victor Hugo.epub")
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _write_epub_with_nav(epub_path: Path, *, nav_body: str) -> None:
|
|
18
|
+
files = {
|
|
19
|
+
"META-INF/container.xml": """<?xml version="1.0"?>
|
|
20
|
+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
21
|
+
<rootfiles>
|
|
22
|
+
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
|
23
|
+
</rootfiles>
|
|
24
|
+
</container>""",
|
|
25
|
+
"OEBPS/content.opf": """<?xml version="1.0"?>
|
|
26
|
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id">
|
|
27
|
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
28
|
+
<dc:identifier id="id">bad-nav</dc:identifier>
|
|
29
|
+
<dc:title>Bad Nav</dc:title>
|
|
30
|
+
<dc:language>en</dc:language>
|
|
31
|
+
</metadata>
|
|
32
|
+
<manifest>
|
|
33
|
+
<item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>
|
|
34
|
+
<item id="c1" href="chap.xhtml" media-type="application/xhtml+xml"/>
|
|
35
|
+
</manifest>
|
|
36
|
+
<spine>
|
|
37
|
+
<itemref idref="c1"/>
|
|
38
|
+
</spine>
|
|
39
|
+
</package>""",
|
|
40
|
+
"OEBPS/nav.xhtml": f"""<html xmlns="http://www.w3.org/1999/xhtml"><body>{nav_body}</body></html>""",
|
|
41
|
+
"OEBPS/chap.xhtml": """<html xmlns="http://www.w3.org/1999/xhtml"><body><h1>Chapter One</h1><p>Hello world.</p></body></html>""",
|
|
42
|
+
}
|
|
43
|
+
with zipfile.ZipFile(epub_path, "w") as epub_zip:
|
|
44
|
+
epub_zip.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
|
|
45
|
+
for name, content in files.items():
|
|
46
|
+
epub_zip.writestr(name, content)
|
|
47
|
+
|
|
48
|
+
|
|
16
49
|
class TestEpubReader:
|
|
17
50
|
"""Tests for the EpubReader class."""
|
|
18
51
|
|
|
@@ -53,6 +86,21 @@ class TestEpubReader:
|
|
|
53
86
|
assert isinstance(chapter.title, str)
|
|
54
87
|
assert isinstance(chapter.paragraphs, list)
|
|
55
88
|
|
|
89
|
+
def test_reader_handles_nav_document_without_ordered_list(self, tmp_path):
|
|
90
|
+
"""EPUBs with a malformed NAV document still load chapters."""
|
|
91
|
+
epub_path = tmp_path / "bad-nav.epub"
|
|
92
|
+
_write_epub_with_nav(
|
|
93
|
+
epub_path,
|
|
94
|
+
nav_body='<nav epub:type="toc" xmlns:epub="http://www.idpf.org/2007/ops"><h1>Contents</h1></nav>',
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
reader = EpubReader(epub_path)
|
|
98
|
+
|
|
99
|
+
assert reader.get_metadata().title == "Bad Nav"
|
|
100
|
+
chapters = reader.get_chapters(min_text_len=1)
|
|
101
|
+
assert len(chapters) == 1
|
|
102
|
+
assert chapters[0].paragraphs == ["Chapter One", "Hello world."]
|
|
103
|
+
|
|
56
104
|
|
|
57
105
|
class TestChapterDataclass:
|
|
58
106
|
"""Tests for the Chapter dataclass."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|