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.
Files changed (187) hide show
  1. {kenkui-2.3.2/src/kenkui.egg-info → kenkui-2.3.4}/PKG-INFO +1 -1
  2. {kenkui-2.3.2 → kenkui-2.3.4}/pyproject.toml +1 -1
  3. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/epub.py +41 -0
  4. {kenkui-2.3.2 → kenkui-2.3.4/src/kenkui.egg-info}/PKG-INFO +1 -1
  5. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_parsing.py +56 -0
  6. {kenkui-2.3.2 → kenkui-2.3.4}/LICENSE +0 -0
  7. {kenkui-2.3.2 → kenkui-2.3.4}/MANIFEST.in +0 -0
  8. {kenkui-2.3.2 → kenkui-2.3.4}/README.md +0 -0
  9. {kenkui-2.3.2 → kenkui-2.3.4}/setup.cfg +0 -0
  10. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/__init__.py +0 -0
  11. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/analytics.py +0 -0
  12. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/api.py +0 -0
  13. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/audio_build.py +0 -0
  14. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/chapter_classifier.py +0 -0
  15. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/chapter_filter.py +0 -0
  16. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/cli.py +0 -0
  17. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/config.py +0 -0
  18. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/errors.py +0 -0
  19. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/file_finder.py +0 -0
  20. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/huggingface_auth.py +0 -0
  21. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/log.py +0 -0
  22. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/model_names.py +0 -0
  23. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/__init__.py +0 -0
  24. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/api.py +0 -0
  25. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/audio.py +0 -0
  26. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/book.py +0 -0
  27. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/common.py +0 -0
  28. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/config.py +0 -0
  29. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/job.py +0 -0
  30. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/nlp.py +0 -0
  31. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/models/queue.py +0 -0
  32. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/__init__.py +0 -0
  33. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_async.py +0 -0
  34. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_cache.py +0 -0
  35. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_filters.py +0 -0
  36. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/_retry.py +0 -0
  37. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/annotator.py +0 -0
  38. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/attribution.py +0 -0
  39. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/booknlp_roster.py +0 -0
  40. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/chunker.py +0 -0
  41. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/entities.py +0 -0
  42. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/llm.py +0 -0
  43. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/models.py +0 -0
  44. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/pipeline.py +0 -0
  45. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/__init__.py +0 -0
  46. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/_base.py +0 -0
  47. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/_factory.py +0 -0
  48. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/base.py +0 -0
  49. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/booknlp.py +0 -0
  50. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/litellm.py +0 -0
  51. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/local.py +0 -0
  52. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/providers/ollama.py +0 -0
  53. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/quotes.py +0 -0
  54. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp/setup.py +0 -0
  55. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/nlp_config.py +0 -0
  56. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/parsing.py +0 -0
  57. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/post_processing.py +0 -0
  58. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/progress.py +0 -0
  59. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/progress_tracking.py +0 -0
  60. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/__init__.py +0 -0
  61. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/_pdf_extract.py +0 -0
  62. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/_pdf_to_epub.py +0 -0
  63. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/fb2.py +0 -0
  64. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/mobi.py +0 -0
  65. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/readers/pdf.py +0 -0
  66. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/Les Miserables - Victor Hugo.epub +0 -0
  67. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/Pride and Predjudice.epub +0 -0
  68. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/samples/__init__.py +0 -0
  69. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/series.py +0 -0
  70. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/__init__.py +0 -0
  71. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/api.py +0 -0
  72. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/openapi.py +0 -0
  73. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/server/server.py +0 -0
  74. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/__init__.py +0 -0
  75. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/application_service.py +0 -0
  76. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/auth_service.py +0 -0
  77. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/book_cache.py +0 -0
  78. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/book_service.py +0 -0
  79. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/confirmation_service.py +0 -0
  80. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/download_service.py +0 -0
  81. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/execution_service.py +0 -0
  82. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/job_executor.py +0 -0
  83. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/job_service.py +0 -0
  84. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/nlp_service.py +0 -0
  85. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/openrouter_service.py +0 -0
  86. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/provider_service.py +0 -0
  87. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/queue_manager.py +0 -0
  88. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/runtime_service.py +0 -0
  89. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/series_service.py +0 -0
  90. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/setup_service.py +0 -0
  91. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/task_coordinator.py +0 -0
  92. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/task_service.py +0 -0
  93. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/voice_service.py +0 -0
  94. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/services/workflow_service.py +0 -0
  95. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/system_check.py +0 -0
  96. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/text_rules.py +0 -0
  97. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/utils.py +0 -0
  98. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_compiler.py +0 -0
  99. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_download.py +0 -0
  100. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_loader.py +0 -0
  101. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_metadata.py +0 -0
  102. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_pool.py +0 -0
  103. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voice_registry.py +0 -0
  104. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voices/manifest.json +0 -0
  105. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/voices/preview_phrases.json +0 -0
  106. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui/workers.py +0 -0
  107. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/SOURCES.txt +0 -0
  108. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/dependency_links.txt +0 -0
  109. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/entry_points.txt +0 -0
  110. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/requires.txt +0 -0
  111. {kenkui-2.3.2 → kenkui-2.3.4}/src/kenkui.egg-info/top_level.txt +0 -0
  112. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_analytics.py +0 -0
  113. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_annotator.py +0 -0
  114. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_api_attribution_options.py +0 -0
  115. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_application_service.py +0 -0
  116. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_audio_build.py +0 -0
  117. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_auth_service.py +0 -0
  118. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_book_cache.py +0 -0
  119. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_book_service.py +0 -0
  120. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_chapter_classifier.py +0 -0
  121. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_chapter_filter.py +0 -0
  122. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_character_record_dataclass.py +0 -0
  123. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_credential_management.py +0 -0
  124. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_download_service.py +0 -0
  125. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_generation_progress.py +0 -0
  126. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_huggingface_auth.py +0 -0
  127. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_job_executor.py +0 -0
  128. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_library_contracts.py +0 -0
  129. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_runtime_config.py +0 -0
  130. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_runtime_registration.py +0 -0
  131. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_modal_status.py +0 -0
  132. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_model_names.py +0 -0
  133. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_models.py +0 -0
  134. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_multi_voice_models.py +0 -0
  135. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_name_filters.py +0 -0
  136. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_attribution.py +0 -0
  137. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_booknlp_adapters.py +0 -0
  138. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_booknlp_roster.py +0 -0
  139. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_cache.py +0 -0
  140. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_chunker.py +0 -0
  141. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_config.py +0 -0
  142. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_coreference_pierce_brown.py +0 -0
  143. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_entities.py +0 -0
  144. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_factory.py +0 -0
  145. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_italic_dialogue.py +0 -0
  146. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_litellm_adapters.py +0 -0
  147. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_llm_limits.py +0 -0
  148. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_models_enriched.py +0 -0
  149. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_ollama_adapters.py +0 -0
  150. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_pipeline.py +0 -0
  151. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_pipeline_split.py +0 -0
  152. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_providers.py +0 -0
  153. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_providers_base.py +0 -0
  154. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_quotes.py +0 -0
  155. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_quotes_non_dialogue.py +0 -0
  156. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_retry.py +0 -0
  157. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_segments.py +0 -0
  158. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_nlp_service.py +0 -0
  159. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_openrouter_service.py +0 -0
  160. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_optional_dependencies.py +0 -0
  161. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_extractor.py +0 -0
  162. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_reader.py +0 -0
  163. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_pdf_to_epub.py +0 -0
  164. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_post_processing.py +0 -0
  165. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_progress_tracking.py +0 -0
  166. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_provider_service.py +0 -0
  167. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_qp14_efficiency.py +0 -0
  168. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_queue_manager.py +0 -0
  169. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_scare_quote_stripping.py +0 -0
  170. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series.py +0 -0
  171. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series_roster.py +0 -0
  172. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_series_service.py +0 -0
  173. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_setup_service.py +0 -0
  174. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_speaker_voices_migration.py +0 -0
  175. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_suggest_cast.py +0 -0
  176. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_task_coordinator.py +0 -0
  177. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_utils.py +0 -0
  178. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_download.py +0 -0
  179. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_loader.py +0 -0
  180. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_metadata.py +0 -0
  181. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_pack_build.py +0 -0
  182. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_pools.py +0 -0
  183. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_registry.py +0 -0
  184. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_review_ux.py +0 -0
  185. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_voice_service.py +0 -0
  186. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_workers.py +0 -0
  187. {kenkui-2.3.2 → kenkui-2.3.4}/tests/test_workers_scene_breaks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kenkui
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: Convert Ebooks to Audiobooks with [custom] voice samples
5
5
  Author-email: Sumner MacArthur <spn1kolat3sla@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kenkui"
7
- version = "2.3.2"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kenkui
3
- Version: 2.3.2
3
+ Version: 2.3.4
4
4
  Summary: Convert Ebooks to Audiobooks with [custom] voice samples
5
5
  Author-email: Sumner MacArthur <spn1kolat3sla@gmail.com>
6
6
  License-Expression: MIT
@@ -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