kenkui 2.3.2__tar.gz → 2.3.4__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.4}/PKG-INFO +1 -1
- {kenkui-2.3.2 → kenkui-2.3.4}/pyproject.toml +1 -1
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/epub.py +41 -0
- {kenkui-2.3.2 → kenkui-2.3.4/src/kenkui.egg-info}/PKG-INFO +1 -1
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_parsing.py +56 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/LICENSE +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/MANIFEST.in +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/README.md +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/setup.cfg +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/analytics.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/audio_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/chapter_classifier.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/chapter_filter.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/cli.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/errors.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/file_finder.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/huggingface_auth.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/log.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/model_names.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/audio.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/book.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/common.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/job.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/nlp.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/queue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_async.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_filters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_retry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/annotator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/attribution.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/booknlp_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/chunker.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/entities.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/llm.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/pipeline.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/_base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/_factory.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/booknlp.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/litellm.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/local.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/ollama.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/quotes.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/setup.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/parsing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/post_processing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/progress.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/progress_tracking.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/_pdf_extract.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/_pdf_to_epub.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/fb2.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/mobi.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/pdf.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/Les Miserables - Victor Hugo.epub +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/Pride and Predjudice.epub +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/series.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/api.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/openapi.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/server.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/__init__.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/application_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/auth_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/book_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/book_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/confirmation_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/download_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/execution_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/job_executor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/job_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/nlp_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/openrouter_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/provider_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/queue_manager.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/runtime_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/series_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/setup_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/task_coordinator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/task_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/voice_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/workflow_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/system_check.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/text_rules.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/utils.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_compiler.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_download.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_loader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_metadata.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_pool.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_registry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voices/manifest.json +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voices/preview_phrases.json +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/workers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/SOURCES.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/dependency_links.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/entry_points.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/requires.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/top_level.txt +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_analytics.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_annotator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_api_attribution_options.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_application_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_audio_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_auth_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_book_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_book_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_chapter_classifier.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_chapter_filter.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_character_record_dataclass.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_credential_management.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_download_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_generation_progress.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_huggingface_auth.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_job_executor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_library_contracts.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_runtime_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_runtime_registration.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_status.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_model_names.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_multi_voice_models.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_name_filters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_attribution.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_booknlp_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_booknlp_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_cache.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_chunker.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_config.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_coreference_pierce_brown.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_entities.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_factory.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_italic_dialogue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_litellm_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_llm_limits.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_models_enriched.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_ollama_adapters.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_pipeline.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_pipeline_split.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_providers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_providers_base.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_quotes.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_quotes_non_dialogue.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_retry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_segments.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_openrouter_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_optional_dependencies.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_extractor.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_reader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_to_epub.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_post_processing.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_progress_tracking.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_provider_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_qp14_efficiency.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_queue_manager.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_scare_quote_stripping.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series_roster.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_setup_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_speaker_voices_migration.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_suggest_cast.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_task_coordinator.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_utils.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_download.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_loader.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_metadata.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_pack_build.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_pools.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_registry.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_review_ux.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_service.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_workers.py +0 -0
- {kenkui-2.3.2 → kenkui-2.3.4}/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.4"
|
|
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" }]
|
|
@@ -15,10 +15,21 @@ from bs4 import BeautifulSoup
|
|
|
15
15
|
from ebooklib import epub
|
|
16
16
|
|
|
17
17
|
from ..chapter_classifier import ChapterClassifier
|
|
18
|
+
from ..errors import KenkuiReaderError
|
|
18
19
|
from ..models import Chapter
|
|
19
20
|
from ..utils import extract_epub_cover
|
|
20
21
|
from . import EbookMetadata, EbookReader, Registry, TocEntry
|
|
21
22
|
|
|
23
|
+
# Known DRM signatures found inside META-INF/encryption.xml. These schemes
|
|
24
|
+
# encrypt the book's content documents with keys kenkui does not (and should
|
|
25
|
+
# not) hold, so the book cannot be parsed. Detecting them lets us fail with an
|
|
26
|
+
# actionable message instead of an opaque parser crash deep inside ebooklib.
|
|
27
|
+
_DRM_SIGNATURES: tuple[tuple[str, str], ...] = (
|
|
28
|
+
("http://ns.adobe.com/adept", "Adobe ADEPT"),
|
|
29
|
+
("http://www.apple.com/2009/fpk", "Apple FairPlay"),
|
|
30
|
+
("http://www.barnesandnoble.com/2010", "Barnes & Noble"),
|
|
31
|
+
)
|
|
32
|
+
|
|
22
33
|
|
|
23
34
|
@Registry.register
|
|
24
35
|
class EpubReader(EbookReader):
|
|
@@ -28,10 +39,40 @@ class EpubReader(EbookReader):
|
|
|
28
39
|
|
|
29
40
|
def __init__(self, filepath: Path, verbose: bool = False):
|
|
30
41
|
super().__init__(filepath, verbose)
|
|
42
|
+
self._raise_if_drm_protected(Path(filepath))
|
|
31
43
|
with warnings.catch_warnings(record=True):
|
|
32
44
|
warnings.simplefilter("always")
|
|
33
45
|
self.book = epub.read_epub(str(filepath))
|
|
34
46
|
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _raise_if_drm_protected(filepath: Path) -> None:
|
|
49
|
+
"""Raise ``KenkuiReaderError`` if the EPUB is DRM-encrypted.
|
|
50
|
+
|
|
51
|
+
Commercial EPUBs (Adobe ADEPT, Apple FairPlay, Barnes & Noble) encrypt
|
|
52
|
+
their content documents and record it in ``META-INF/encryption.xml``.
|
|
53
|
+
ebooklib cannot decrypt these and instead fails partway through parsing
|
|
54
|
+
with a cryptic ``'NoneType' object has no attribute ...`` error. We
|
|
55
|
+
detect the DRM signature up front and surface a clear message.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
with zipfile.ZipFile(str(filepath), "r") as book_zip:
|
|
59
|
+
if "META-INF/encryption.xml" not in book_zip.namelist():
|
|
60
|
+
return
|
|
61
|
+
encryption_xml = book_zip.read("META-INF/encryption.xml").decode(
|
|
62
|
+
"utf-8", errors="ignore"
|
|
63
|
+
)
|
|
64
|
+
except (zipfile.BadZipFile, OSError):
|
|
65
|
+
# Let ebooklib produce the definitive error for unreadable archives.
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
for signature, scheme in _DRM_SIGNATURES:
|
|
69
|
+
if signature in encryption_xml:
|
|
70
|
+
raise KenkuiReaderError(
|
|
71
|
+
f"'{filepath.name}' is DRM-protected ({scheme}) and cannot be "
|
|
72
|
+
"processed. kenkui can only read books without DRM; remove the "
|
|
73
|
+
"DRM using software you are licensed to use, then try again."
|
|
74
|
+
)
|
|
75
|
+
|
|
35
76
|
def get_metadata(self) -> EbookMetadata:
|
|
36
77
|
"""Extract metadata from EPUB."""
|
|
37
78
|
title = self._get_title()
|
|
@@ -1,18 +1,62 @@
|
|
|
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
|
|
|
7
8
|
import pytest
|
|
8
9
|
|
|
9
10
|
from kenkui.chapter_filter import FilterOperation
|
|
11
|
+
from kenkui.errors import KenkuiReaderError
|
|
10
12
|
from kenkui.models import Chapter, ProcessingConfig
|
|
11
13
|
from kenkui.readers.epub import EpubReader
|
|
12
14
|
|
|
13
15
|
TEST_EPUB = Path("src/kenkui/samples/Les Miserables - Victor Hugo.epub")
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
def _write_adept_drm_epub(epub_path: Path) -> None:
|
|
19
|
+
"""Write a minimal EPUB whose content is encrypted with Adobe ADEPT DRM."""
|
|
20
|
+
encryption_xml = """<?xml version="1.0"?>
|
|
21
|
+
<encryption xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
22
|
+
<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#">
|
|
23
|
+
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>
|
|
24
|
+
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
|
|
25
|
+
<resource xmlns="http://ns.adobe.com/adept">urn:uuid:2b63d91e-1c6f-4e2d-ae4b-f73c8ad67fd7</resource>
|
|
26
|
+
</KeyInfo>
|
|
27
|
+
<CipherData>
|
|
28
|
+
<CipherReference URI="OEBPS/chap.xhtml"/>
|
|
29
|
+
</CipherData>
|
|
30
|
+
</EncryptedData>
|
|
31
|
+
</encryption>"""
|
|
32
|
+
files = {
|
|
33
|
+
"META-INF/container.xml": """<?xml version="1.0"?>
|
|
34
|
+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
35
|
+
<rootfiles>
|
|
36
|
+
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
|
37
|
+
</rootfiles>
|
|
38
|
+
</container>""",
|
|
39
|
+
"META-INF/encryption.xml": encryption_xml,
|
|
40
|
+
"OEBPS/content.opf": """<?xml version="1.0"?>
|
|
41
|
+
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="id">
|
|
42
|
+
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
43
|
+
<dc:identifier id="id">drm-book</dc:identifier>
|
|
44
|
+
<dc:title>DRM Book</dc:title>
|
|
45
|
+
</metadata>
|
|
46
|
+
<manifest>
|
|
47
|
+
<item id="c1" href="chap.xhtml" media-type="application/xhtml+xml"/>
|
|
48
|
+
</manifest>
|
|
49
|
+
<spine><itemref idref="c1"/></spine>
|
|
50
|
+
</package>""",
|
|
51
|
+
# Ciphertext stand-in — deliberately not valid XHTML, as in a real DRM'd book.
|
|
52
|
+
"OEBPS/chap.xhtml": b"\x9c\x1e\xd3encrypted-bytes\x00\xff",
|
|
53
|
+
}
|
|
54
|
+
with zipfile.ZipFile(epub_path, "w") as epub_zip:
|
|
55
|
+
epub_zip.writestr("mimetype", "application/epub+zip", compress_type=zipfile.ZIP_STORED)
|
|
56
|
+
for name, content in files.items():
|
|
57
|
+
epub_zip.writestr(name, content)
|
|
58
|
+
|
|
59
|
+
|
|
16
60
|
class TestEpubReader:
|
|
17
61
|
"""Tests for the EpubReader class."""
|
|
18
62
|
|
|
@@ -53,6 +97,18 @@ class TestEpubReader:
|
|
|
53
97
|
assert isinstance(chapter.title, str)
|
|
54
98
|
assert isinstance(chapter.paragraphs, list)
|
|
55
99
|
|
|
100
|
+
def test_adept_drm_epub_raises_actionable_error(self, tmp_path):
|
|
101
|
+
"""A DRM-protected EPUB fails with a clear message, not a cryptic crash."""
|
|
102
|
+
epub_path = tmp_path / "drm.epub"
|
|
103
|
+
_write_adept_drm_epub(epub_path)
|
|
104
|
+
|
|
105
|
+
with pytest.raises(KenkuiReaderError) as excinfo:
|
|
106
|
+
EpubReader(epub_path)
|
|
107
|
+
|
|
108
|
+
message = str(excinfo.value)
|
|
109
|
+
assert "DRM" in message
|
|
110
|
+
assert "Adobe" in message
|
|
111
|
+
|
|
56
112
|
|
|
57
113
|
class TestChapterDataclass:
|
|
58
114
|
"""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
|