novel-downloader 1.4.5__py3-none-any.whl → 2.0.0__py3-none-any.whl

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 (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/archived/qidian/searcher.py +79 -0
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.utils.crypto_utils
4
- -----------------------------------
3
+ novel_downloader.utils.crypto_utils.rc4
4
+ ---------------------------------------
5
5
 
6
- Generic cryptographic utilities
6
+ RC4 stream cipher for simple text encryption and decryption.
7
7
  """
8
8
 
9
- from __future__ import annotations
10
-
11
9
  import base64
12
10
 
13
11
 
@@ -22,16 +20,11 @@ def rc4_crypt(
22
20
  Encrypt or decrypt data using RC4 and Base64.
23
21
 
24
22
  :param key: RC4 key (will be encoded using the specified encoding).
25
- :type key: str
26
23
  :param data: Plain-text (for 'encrypt') or Base64 cipher-text (for 'decrypt').
27
- :type data: str
28
24
  :param mode: Operation mode, either 'encrypt' or 'decrypt'. Defaults to 'encrypt'.
29
- :type mode: str, optional
30
25
  :param encoding: Character encoding for key and returned string. Defaults 'utf-8'.
31
- :type encoding: str, optional
32
26
 
33
27
  :return: Base64 cipher-text (for encryption) or decoded plain-text (for decryption).
34
- :rtype: str
35
28
 
36
29
  :raises ValueError: If mode is not 'encrypt' or 'decrypt'.
37
30
  """
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub
4
+ ---------------------------
5
+
6
+ Top-level package for EPUB export utilities.
7
+
8
+ Key components:
9
+
10
+ - EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
11
+ - Chapter, Volume : represent and render content sections and volume intros
12
+
13
+ Usage example:
14
+
15
+ ```python
16
+ builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
17
+ builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
18
+ builder.export("output/my_novel.epub")
19
+ ```
20
+ """
21
+
22
+ __all__ = [
23
+ "EpubBuilder",
24
+ "Chapter",
25
+ "Volume",
26
+ "StyleSheet",
27
+ ]
28
+
29
+ from .builder import EpubBuilder
30
+ from .models import (
31
+ Chapter,
32
+ StyleSheet,
33
+ Volume,
34
+ )
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.builder
4
+ -----------------------------------
5
+
6
+ Orchestrates the end-to-end EPUB build process by:
7
+ - Managing metadata (title, author, description, language, etc.)
8
+ - Collecting and deduplicating resources (chapters, images, stylesheets)
9
+ - Registering everything in the OPF manifest and spine
10
+ - Generating nav.xhtml, toc.ncx, content.opf, and the zipped .epub file
11
+
12
+ Provides:
13
+ - methods to add chapters, volumes, images, and styles
14
+ - a clean `export()` entry point that writes the final EPUB archive
15
+ """
16
+
17
+ import zipfile
18
+ from pathlib import Path
19
+ from zipfile import ZIP_DEFLATED, ZIP_STORED
20
+
21
+ from novel_downloader.utils.constants import (
22
+ CSS_INTRO_PATH,
23
+ VOLUME_BORDER_IMAGE_PATH,
24
+ )
25
+
26
+ from .constants import (
27
+ COVER_IMAGE_TEMPLATE,
28
+ CSS_FOLDER,
29
+ IMAGE_FOLDER,
30
+ IMAGE_MEDIA_TYPES,
31
+ ROOT_PATH,
32
+ TEXT_FOLDER,
33
+ )
34
+ from .documents import (
35
+ NavDocument,
36
+ NCXDocument,
37
+ OpfDocument,
38
+ )
39
+ from .models import (
40
+ Chapter,
41
+ ChapterEntry,
42
+ EpubResource,
43
+ ImageResource,
44
+ StyleSheet,
45
+ Volume,
46
+ )
47
+ from .utils import (
48
+ build_book_intro,
49
+ build_container_xml,
50
+ build_volume_intro,
51
+ hash_file,
52
+ )
53
+
54
+
55
+ class EpubBuilder:
56
+ def __init__(
57
+ self,
58
+ title: str,
59
+ author: str = "",
60
+ description: str = "",
61
+ cover_path: Path | None = None,
62
+ subject: list[str] | None = None,
63
+ serial_status: str = "",
64
+ word_count: str = "0",
65
+ uid: str = "",
66
+ language: str = "zh-CN",
67
+ ):
68
+ # metadata
69
+ self.title = title
70
+ self.author = author
71
+ self.description = description
72
+ self.language = language
73
+ self.subject = subject or []
74
+ self.serial_status = serial_status
75
+ self.word_count = word_count
76
+ self.uid = uid
77
+
78
+ # builder state
79
+ self.chapters: list[Chapter] = []
80
+ self.images: list[ImageResource] = []
81
+ self.styles: list[StyleSheet] = []
82
+ self._img_map: dict[str, str] = {}
83
+ self._img_idx = 0
84
+ self._vol_idx = 0
85
+
86
+ # core EPUB documents
87
+ self.nav = NavDocument(title=title, language=language)
88
+ self.ncx = NCXDocument(title=title, uid=uid)
89
+ self.opf = OpfDocument(
90
+ title=title,
91
+ author=author,
92
+ description=description,
93
+ uid=uid,
94
+ subject=self.subject,
95
+ language=language,
96
+ )
97
+
98
+ # register the nav & ncx items
99
+ self.opf.add_manifest_item(
100
+ "nav",
101
+ "nav.xhtml",
102
+ self.nav.media_type,
103
+ properties="nav",
104
+ )
105
+ self.opf.add_manifest_item("ncx", "toc.ncx", self.ncx.media_type)
106
+
107
+ self._init_styles()
108
+ self._init_cover(cover_path)
109
+ self._init_intro()
110
+
111
+ def add_image(self, image_path: Path) -> str:
112
+ """
113
+ Add an image resource (deduped by hash) and register it.
114
+ """
115
+ if not (image_path.exists() and image_path.is_file()):
116
+ return ""
117
+ h = hash_file(image_path)
118
+ if h in self._img_map:
119
+ return self._img_map[h]
120
+
121
+ ext = image_path.suffix.lower().lstrip(".")
122
+ mtype = IMAGE_MEDIA_TYPES.get(ext)
123
+ if not mtype:
124
+ return ""
125
+
126
+ res_id = f"img_{self._img_idx}"
127
+ filename = f"{res_id}.{ext}"
128
+ data = image_path.read_bytes()
129
+ img = ImageResource(id=res_id, data=data, media_type=mtype, filename=filename)
130
+ self.images.append(img)
131
+ self._register(img, folder=IMAGE_FOLDER, in_spine=False)
132
+
133
+ self._img_map[h] = filename
134
+ self._img_idx += 1
135
+ return filename
136
+
137
+ def add_chapter(self, chap: Chapter) -> None:
138
+ self.chapters.append(chap)
139
+ self._register(chap, folder=TEXT_FOLDER)
140
+ self.nav.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
141
+ self.ncx.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
142
+
143
+ def add_volume(self, volume: Volume) -> None:
144
+ """Add a volume cover, intro, and all its chapters to the EPUB."""
145
+ # volume-specific cover
146
+ if volume.cover:
147
+ filename = self.add_image(volume.cover)
148
+ cover_html = f'<img class="width100" src="../{IMAGE_FOLDER}/{filename}"/>'
149
+ cover_chap = Chapter(
150
+ id=f"vol_{self._vol_idx}_cover",
151
+ title=volume.title,
152
+ content=cover_html,
153
+ filename=f"vol_{self._vol_idx}_cover.xhtml",
154
+ )
155
+ self.chapters.append(cover_chap)
156
+ self._register(
157
+ cover_chap,
158
+ folder=TEXT_FOLDER,
159
+ properties="duokan-page-fullscreen",
160
+ )
161
+
162
+ # volume intro page
163
+ intro_content = build_volume_intro(volume.title, volume.intro)
164
+ vol_intro = Chapter(
165
+ id=f"vol_{self._vol_idx}",
166
+ title=volume.title,
167
+ content=intro_content,
168
+ css=[self.intro_css],
169
+ filename=f"vol_{self._vol_idx}.xhtml",
170
+ )
171
+ self.chapters.append(vol_intro)
172
+ self._register(vol_intro, folder=TEXT_FOLDER)
173
+
174
+ # nested chapters
175
+ entries: list[ChapterEntry] = []
176
+ for chap in volume.chapters:
177
+ self.chapters.append(chap)
178
+ self._register(chap, folder=TEXT_FOLDER)
179
+ entries.append(
180
+ ChapterEntry(
181
+ id=chap.id,
182
+ label=chap.title,
183
+ src=f"{TEXT_FOLDER}/{chap.filename}",
184
+ )
185
+ )
186
+
187
+ # TOC updates
188
+ self.ncx.add_volume(
189
+ id=f"vol_{self._vol_idx}",
190
+ label=volume.title,
191
+ src=f"{TEXT_FOLDER}/{vol_intro.filename}",
192
+ chapters=entries,
193
+ )
194
+ self.nav.add_volume(
195
+ id=f"vol_{self._vol_idx}",
196
+ label=volume.title,
197
+ src=f"{TEXT_FOLDER}/{vol_intro.filename}",
198
+ chapters=entries,
199
+ )
200
+
201
+ self._vol_idx += 1
202
+
203
+ def add_stylesheet(self, css: StyleSheet) -> None:
204
+ """
205
+ Register an external CSS file in the EPUB.
206
+ """
207
+ self.styles.append(css)
208
+ self._register(css, folder=CSS_FOLDER, in_spine=False)
209
+
210
+ def export(self, output_path: str | Path) -> Path:
211
+ """
212
+ Build and export the current book as an EPUB file.
213
+
214
+ :param output_path: Path to save the final .epub file.
215
+ """
216
+ return self._build_epub(output_path=Path(output_path))
217
+
218
+ def _register(
219
+ self,
220
+ res: EpubResource,
221
+ folder: str,
222
+ in_spine: bool = True,
223
+ properties: str | None = None,
224
+ ) -> None:
225
+ """
226
+ Add resource to the manifest—and optionally to the spine.
227
+ """
228
+ href = f"{folder}/{res.filename}"
229
+ self.opf.add_manifest_item(res.id, href, res.media_type, properties)
230
+ if in_spine:
231
+ self.opf.add_spine_item(res.id, properties)
232
+
233
+ def _init_styles(self) -> None:
234
+ # volume border & intro CSS
235
+ self.intro_css = StyleSheet(
236
+ id="intro_style",
237
+ content=CSS_INTRO_PATH.read_text("utf-8"),
238
+ filename="intro_style.css",
239
+ )
240
+ self.styles.append(self.intro_css)
241
+ self._register(self.intro_css, folder=CSS_FOLDER, in_spine=False)
242
+
243
+ try:
244
+ border_bytes = VOLUME_BORDER_IMAGE_PATH.read_bytes()
245
+ except FileNotFoundError:
246
+ return
247
+ border = ImageResource(
248
+ id="img-volume-border",
249
+ data=border_bytes,
250
+ media_type="image/png",
251
+ filename="volume_border.png",
252
+ )
253
+ self.images.append(border)
254
+ self._register(border, folder=IMAGE_FOLDER, in_spine=False)
255
+
256
+ def _init_cover(self, cover_path: Path | None) -> None:
257
+ if not cover_path or not cover_path.is_file():
258
+ return
259
+ ext = cover_path.suffix.lower().lstrip(".")
260
+ mtype = IMAGE_MEDIA_TYPES.get(ext)
261
+ if not mtype:
262
+ return
263
+
264
+ data = cover_path.read_bytes()
265
+ cover_img = ImageResource(
266
+ id="cover-img",
267
+ data=data,
268
+ media_type=mtype,
269
+ filename=f"cover.{ext}",
270
+ )
271
+ self.images.append(cover_img)
272
+ self._register(
273
+ cover_img,
274
+ folder=IMAGE_FOLDER,
275
+ in_spine=False,
276
+ properties="cover-image",
277
+ )
278
+
279
+ cover_chapter = Chapter(
280
+ id="cover",
281
+ title="Cover",
282
+ content=COVER_IMAGE_TEMPLATE.format(ext=ext),
283
+ filename="cover.xhtml",
284
+ )
285
+ self.chapters.append(cover_chapter)
286
+ self._register(
287
+ cover_chapter,
288
+ folder=TEXT_FOLDER,
289
+ properties="duokan-page-fullscreen",
290
+ )
291
+ self.nav.add_chapter(
292
+ cover_chapter.id,
293
+ cover_chapter.title,
294
+ f"{TEXT_FOLDER}/{cover_chapter.filename}",
295
+ )
296
+ self.ncx.add_chapter(
297
+ cover_chapter.id,
298
+ cover_chapter.title,
299
+ f"{TEXT_FOLDER}/{cover_chapter.filename}",
300
+ )
301
+ self.opf.include_cover = True
302
+
303
+ def _init_intro(self) -> None:
304
+ intro_html = build_book_intro(
305
+ book_name=self.title,
306
+ author=self.author,
307
+ serial_status=self.serial_status,
308
+ subject=self.subject,
309
+ word_count=self.word_count,
310
+ summary=self.description,
311
+ )
312
+ intro = Chapter(
313
+ id="intro",
314
+ title="书籍简介",
315
+ content=intro_html,
316
+ filename="intro.xhtml",
317
+ css=[self.intro_css],
318
+ )
319
+ self.chapters.append(intro)
320
+ self._register(intro, folder=TEXT_FOLDER)
321
+ self.nav.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
322
+ self.ncx.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
323
+
324
+ def _build_epub(self, output_path: Path) -> Path:
325
+ """
326
+ Write out the .epub ZIP file.
327
+ """
328
+ output_path.parent.mkdir(parents=True, exist_ok=True)
329
+
330
+ with zipfile.ZipFile(output_path, "w") as epub:
331
+ # must be first and uncompressed
332
+ epub.writestr(
333
+ "mimetype",
334
+ "application/epub+zip",
335
+ compress_type=ZIP_STORED,
336
+ )
337
+
338
+ # container
339
+ epub.writestr(
340
+ "META-INF/container.xml",
341
+ build_container_xml(),
342
+ compress_type=ZIP_DEFLATED,
343
+ )
344
+
345
+ # core documents
346
+ epub.writestr(
347
+ f"{ROOT_PATH}/nav.xhtml",
348
+ self.nav.to_xhtml(),
349
+ compress_type=ZIP_DEFLATED,
350
+ )
351
+ epub.writestr(
352
+ f"{ROOT_PATH}/toc.ncx",
353
+ self.ncx.to_xml(),
354
+ compress_type=ZIP_DEFLATED,
355
+ )
356
+ epub.writestr(
357
+ f"{ROOT_PATH}/content.opf",
358
+ self.opf.to_xml(),
359
+ compress_type=ZIP_DEFLATED,
360
+ )
361
+
362
+ # stylesheets
363
+ for css in self.styles:
364
+ path = f"{ROOT_PATH}/{CSS_FOLDER}/{css.filename}"
365
+ epub.writestr(path, css.content, compress_type=ZIP_DEFLATED)
366
+
367
+ # chapters
368
+ for chap in self.chapters:
369
+ path = f"{ROOT_PATH}/{TEXT_FOLDER}/{chap.filename}"
370
+ epub.writestr(path, chap.to_xhtml(), compress_type=ZIP_DEFLATED)
371
+
372
+ # images
373
+ for img in self.images:
374
+ path = f"{ROOT_PATH}/{IMAGE_FOLDER}/{img.filename}"
375
+ epub.writestr(path, img.data, compress_type=ZIP_DEFLATED)
376
+
377
+ return output_path
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.constants
4
+ -------------------------------------
5
+
6
+ EPUB-specific constants used by the builder, including:
7
+ - Directory names for OEBPS structure
8
+ - XML namespace URIs
9
+ - Package attributes and document-type declarations
10
+ - Media type mappings for images
11
+ - Template strings for container.xml and cover image HTML
12
+ """
13
+
14
+ ROOT_PATH = "OEBPS"
15
+ IMAGE_FOLDER = "Images"
16
+ TEXT_FOLDER = "Text"
17
+ CSS_FOLDER = "Styles"
18
+
19
+ XHTML_NS = "http://www.w3.org/1999/xhtml"
20
+ EPUB_NS = "http://www.idpf.org/2007/ops"
21
+ XML_NS = "http://www.w3.org/XML/1998/namespace"
22
+ NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
23
+ OPF_NS = "http://www.idpf.org/2007/opf"
24
+ DC_NS = "http://purl.org/dc/elements/1.1/"
25
+
26
+ IMAGE_MEDIA_TYPES: dict[str, str] = {
27
+ "png": "image/png",
28
+ "jpg": "image/jpeg",
29
+ "jpeg": "image/jpeg",
30
+ "gif": "image/gif",
31
+ "svg": "image/svg+xml",
32
+ "webp": "image/webp",
33
+ }
34
+
35
+ CONTAINER_TEMPLATE = """\
36
+ <?xml version="1.0" encoding="UTF-8"?>
37
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
38
+ <rootfiles>
39
+ <rootfile full-path="{root_path}/content.opf"
40
+ media-type="application/oebps-package+xml"/>
41
+ </rootfiles>
42
+ </container>
43
+ """
44
+
45
+ COVER_IMAGE_TEMPLATE = (
46
+ f'<div style="text-align: center; margin: 0; padding: 0;">'
47
+ f'<img src="../{IMAGE_FOLDER}/cover.{{ext}}" alt="cover" '
48
+ f'style="max-width: 100%; height: auto;" />'
49
+ f"</div>"
50
+ )
51
+
52
+ CSS_TMPLATE = (
53
+ f'<link href="../{CSS_FOLDER}/{{filename}}" '
54
+ f'rel="stylesheet" type="{{media_type}}"/>'
55
+ )
56
+
57
+ CHAP_TMPLATE = f"""\
58
+ <?xml version="1.0" encoding="utf-8"?>
59
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
60
+ <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
61
+ <head>
62
+ <title>{{title}}</title>
63
+ {{xlinks}}
64
+ </head>
65
+ <body>{{content}}</body>
66
+ </html>
67
+ """
68
+
69
+ NAV_TEMPLATE = f"""\
70
+ <?xml version='1.0' encoding='utf-8'?>
71
+ <!DOCTYPE html>
72
+ <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
73
+ <head>
74
+ <title>{{title}}</title>
75
+ </head>
76
+ <body>
77
+ <nav epub:type="toc" id="{{id}}" role="doc-toc">
78
+ <h2>{{title}}</h2>
79
+ <ol>
80
+ {{items}}
81
+ </ol>
82
+ </nav>
83
+ </body>
84
+ </html>
85
+ """
86
+
87
+ NCX_TEMPLATE = f"""\
88
+ <?xml version='1.0' encoding='utf-8'?>
89
+ <ncx xmlns="{NCX_NS}" version="2005-1">
90
+ <head>
91
+ <meta name="dtb:uid" content="{{uid}}"/>
92
+ <meta name="dtb:depth" content="{{depth}}"/>
93
+ <meta name="dtb:totalPageCount" content="0"/>
94
+ <meta name="dtb:maxPageNumber" content="0"/>
95
+ </head>
96
+ <docTitle>
97
+ <text>{{title}}</text>
98
+ </docTitle>
99
+ <navMap>
100
+ {{navpoints}}
101
+ </navMap>
102
+ </ncx>
103
+ """
104
+
105
+ OPF_TEMPLATE = f"""\
106
+ <?xml version='1.0' encoding='utf-8'?>
107
+ <package xmlns="{OPF_NS}" xmlns:dc="{DC_NS}" xmlns:opf="{OPF_NS}" version="3.0" unique-identifier="id" prefix="rendition: http://www.idpf.org/vocab/rendition/#">
108
+ <metadata>
109
+ {{metadata}}
110
+ </metadata>
111
+ <manifest>
112
+ {{manifest_items}}
113
+ </manifest>
114
+ <spine{{spine_toc}}>
115
+ {{spine_items}}
116
+ </spine>
117
+ {{guide_section}}</package>
118
+ """ # noqa: E501