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
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.documents
4
+ -------------------------------------
5
+
6
+ Defines the classes that render EPUB navigation and packaging documents:
7
+ - NavDocument: builds the XHTML nav.xhtml (EPUB 3)
8
+ - NCXDocument: builds the NCX XML navigation map (EPUB 2)
9
+ - OpfDocument: builds the content.opf package document
10
+ """
11
+
12
+ from collections.abc import Sequence
13
+ from dataclasses import dataclass, field
14
+ from datetime import UTC, datetime
15
+
16
+ from .constants import (
17
+ NAV_TEMPLATE,
18
+ NCX_TEMPLATE,
19
+ OPF_TEMPLATE,
20
+ )
21
+ from .models import (
22
+ ChapterEntry,
23
+ EpubResource,
24
+ ManifestEntry,
25
+ NavPoint,
26
+ SpineEntry,
27
+ VolumeEntry,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class NavDocument(EpubResource):
33
+ title: str = "未命名"
34
+ language: str = "zh-CN"
35
+ id: str = "nav"
36
+ filename: str = "nav.xhtml"
37
+ media_type: str = field(init=False, default="application/xhtml+xml")
38
+ content_items: list[ChapterEntry | VolumeEntry] = field(default_factory=list)
39
+
40
+ def add_chapter(self, id: str, label: str, src: str) -> None:
41
+ """
42
+ Add a top-level chapter entry to the navigation.
43
+
44
+ :param id: The unique ID for the chapter.
45
+ :param label: The display title for the chapter.
46
+ :param src: The href target for the chapter's XHTML file.
47
+ """
48
+ self.content_items.append(ChapterEntry(id=id, label=label, src=src))
49
+
50
+ def add_volume(
51
+ self,
52
+ id: str,
53
+ label: str,
54
+ src: str,
55
+ chapters: list[ChapterEntry],
56
+ ) -> None:
57
+ """
58
+ Add a volume entry with nested chapters to the navigation.
59
+
60
+ :param id: The unique ID for the volume.
61
+ :param label: The display title for the volume.
62
+ :param src: The href target for the volume's intro XHTML file.
63
+ :param chapters: A list of chapter entries under this volume.
64
+ """
65
+ self.content_items.append(
66
+ VolumeEntry(id=id, label=label, src=src, chapters=chapters)
67
+ )
68
+
69
+ def to_xhtml(self) -> str:
70
+ """
71
+ Generate the XHTML content for nav.xhtml based on the NavDocument.
72
+
73
+ :return: A string containing the full XHTML for nav.xhtml.
74
+ """
75
+ items_str = self._render_items_str(self.content_items)
76
+ raw = NAV_TEMPLATE.format(
77
+ lang=self.language,
78
+ id=self.id,
79
+ title=self.title,
80
+ items=items_str,
81
+ )
82
+ return raw
83
+
84
+ @classmethod
85
+ def _render_items_str(cls, items: Sequence[ChapterEntry | VolumeEntry]) -> str:
86
+ lines: list[str] = []
87
+ for item in items:
88
+ if isinstance(item, VolumeEntry) and item.chapters:
89
+ lines.append(f'<li><a href="{item.src}">{item.label}</a>')
90
+ lines.append(" <ol>")
91
+ child = cls._render_items_str(item.chapters)
92
+ lines.extend(child.splitlines())
93
+ lines.append(" </ol>")
94
+ lines.append("</li>")
95
+ else:
96
+ lines.append(f'<li><a href="{item.src}">{item.label}</a></li>')
97
+ return "\n".join(lines)
98
+
99
+
100
+ @dataclass
101
+ class NCXDocument(EpubResource):
102
+ title: str = "未命名"
103
+ uid: str = ""
104
+ id: str = "ncx"
105
+ filename: str = "toc.ncx"
106
+ media_type: str = field(init=False, default="application/x-dtbncx+xml")
107
+ nav_points: list[NavPoint] = field(default_factory=list)
108
+
109
+ def add_chapter(
110
+ self,
111
+ id: str,
112
+ label: str,
113
+ src: str,
114
+ ) -> None:
115
+ """
116
+ Add a single flat chapter entry to the NCX nav map.
117
+ """
118
+ self.nav_points.append(NavPoint(id=id, label=label, src=src))
119
+
120
+ def add_volume(
121
+ self,
122
+ id: str,
123
+ label: str,
124
+ src: str,
125
+ chapters: list[ChapterEntry],
126
+ ) -> None:
127
+ """
128
+ Add a volume with nested chapters to the NCX nav map.
129
+ """
130
+ children = [NavPoint(id=c.id, label=c.label, src=c.src) for c in chapters]
131
+ self.nav_points.append(NavPoint(id=id, label=label, src=src, children=children))
132
+
133
+ def to_xml(self) -> str:
134
+ """
135
+ Generate the XML content for toc.ncx used in EPUB 2 navigation.
136
+
137
+ :return: A string containing the full NCX XML document.
138
+ """
139
+ order = 1
140
+ lines: list[str] = []
141
+ for pt in self.nav_points:
142
+ order, block = self._render_navpoint_str(pt, order)
143
+ lines.extend(block)
144
+ navpoints = "\n".join(lines)
145
+ raw = NCX_TEMPLATE.format(
146
+ uid=self.uid,
147
+ depth=self._depth(self.nav_points),
148
+ title=self.title,
149
+ navpoints=navpoints,
150
+ )
151
+ return raw
152
+
153
+ @classmethod
154
+ def _depth(cls, points: list[NavPoint]) -> int:
155
+ if not points:
156
+ return 0
157
+ return 1 + max(cls._depth(child.children) for child in points)
158
+
159
+ @classmethod
160
+ def _render_navpoint_str(cls, pt: NavPoint, order: int) -> tuple[int, list[str]]:
161
+ lines: list[str] = []
162
+ # open navPoint
163
+ lines.append(f'<navPoint id="{pt.id}" playOrder="{order}">')
164
+ order += 1
165
+ # label and content
166
+ lines.append(f"<navLabel><text>{pt.label}</text></navLabel>")
167
+ lines.append(f'<content src="{pt.src}"/>')
168
+ # children
169
+ for child in pt.children:
170
+ order, child_lines = cls._render_navpoint_str(child, order)
171
+ lines.extend(child_lines)
172
+ # close
173
+ lines.append("</navPoint>")
174
+ return order, lines
175
+
176
+
177
+ @dataclass
178
+ class OpfDocument(EpubResource):
179
+ # metadata fields
180
+ title: str = ""
181
+ author: str = ""
182
+ description: str = ""
183
+ uid: str = ""
184
+ subject: list[str] = field(default_factory=list)
185
+ language: str = "zh-CN"
186
+
187
+ # resource identity
188
+ id: str = "opf"
189
+ filename: str = "content.opf"
190
+ media_type: str = field(init=False, default="application/oebps-package+xml")
191
+
192
+ # internal state
193
+ include_cover: bool = False
194
+ manifest: list[ManifestEntry] = field(default_factory=list)
195
+ spine: list[SpineEntry] = field(default_factory=list)
196
+ _cover_item: ManifestEntry | None = field(init=False, default=None)
197
+ _toc_item: ManifestEntry | None = field(init=False, default=None)
198
+ _cover_doc: ManifestEntry | None = field(init=False, default=None)
199
+
200
+ def add_manifest_item(
201
+ self,
202
+ id: str,
203
+ href: str,
204
+ media_type: str,
205
+ properties: str | None = None,
206
+ ) -> None:
207
+ entry = ManifestEntry(
208
+ id=id,
209
+ href=href,
210
+ media_type=media_type,
211
+ properties=properties,
212
+ )
213
+ self.manifest.append(entry)
214
+
215
+ if properties == "cover-image":
216
+ self._cover_item = entry
217
+ if media_type == "application/x-dtbncx+xml":
218
+ self._toc_item = entry
219
+ if id == "cover":
220
+ self._cover_doc = entry
221
+
222
+ def add_spine_item(
223
+ self,
224
+ idref: str,
225
+ properties: str | None = None,
226
+ ) -> None:
227
+ self.spine.append(SpineEntry(idref=idref, properties=properties))
228
+
229
+ def set_subject(self, subject: list[str]) -> None:
230
+ self.subject = subject
231
+
232
+ def to_xml(self) -> str:
233
+ """
234
+ Generate the content.opf XML, which defines metadata, manifest, and spine.
235
+
236
+ This function outputs a complete OPF package document that includes:
237
+ - <metadata>: title, author, language, identifiers, etc.
238
+ - <manifest>: all resource entries
239
+ - <spine>: the reading order of the content
240
+ - <guide>: optional references like cover page
241
+
242
+ :return: A string containing the full OPF XML content.
243
+ """
244
+ now_iso = datetime.now(UTC).replace(microsecond=0).isoformat()
245
+
246
+ # metadata block
247
+ meta_lines: list[str] = []
248
+ meta_lines.append(f'<meta property="dcterms:modified">{now_iso}</meta>')
249
+ meta_lines.append(f'<dc:identifier id="id">{self.uid}</dc:identifier>')
250
+ meta_lines.append(f"<dc:title>{self.title}</dc:title>")
251
+ meta_lines.append(f"<dc:language>{self.language}</dc:language>")
252
+ if self.author:
253
+ meta_lines.append(f'<dc:creator id="creator">{self.author}</dc:creator>')
254
+ if self.description:
255
+ meta_lines.append(f"<dc:description>{self.description}</dc:description>")
256
+ if self.subject:
257
+ joined = ",".join(self.subject)
258
+ meta_lines.append(f"<dc:subject>{joined}</dc:subject>")
259
+ if self.include_cover and self._cover_item:
260
+ meta_lines.append(f'<meta name="cover" content="{self._cover_item.id}"/>')
261
+ metadata = "\n".join(meta_lines)
262
+
263
+ # manifest block
264
+ man_lines: list[str] = []
265
+ for item in self.manifest:
266
+ props = f' properties="{item.properties}"' if item.properties else ""
267
+ man_lines.append(
268
+ f'<item id="{item.id}" href="{item.href}" media-type="{item.media_type}"{props}/>' # noqa: E501
269
+ )
270
+ manifest_items = "\n".join(man_lines)
271
+
272
+ # spine block
273
+ toc_attr = f' toc="{self._toc_item.id}"' if self._toc_item else ""
274
+ spine_lines: list[str] = []
275
+ for ref in self.spine:
276
+ props = f' properties="{ref.properties}"' if ref.properties else ""
277
+ spine_lines.append(f' <itemref idref="{ref.idref}"{props}/>')
278
+ spine_items = "\n".join(spine_lines)
279
+
280
+ # guide block
281
+ if self.include_cover and self._cover_doc:
282
+ guide_section = (
283
+ " <guide>\n"
284
+ f' <reference type="cover" title="Cover" href="{self._cover_doc.href}"/>\n' # noqa: E501
285
+ " </guide>\n"
286
+ )
287
+ else:
288
+ guide_section = ""
289
+
290
+ raw = OPF_TEMPLATE.format(
291
+ metadata=metadata,
292
+ manifest_items=manifest_items,
293
+ spine_toc=toc_attr,
294
+ spine_items=spine_items,
295
+ guide_section=guide_section,
296
+ )
297
+ return raw
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.models
4
+ ----------------------------------
5
+
6
+ Defines the core EPUB data models and resource classes used by the builder:
7
+ - Typed entries for table of contents (ChapterEntry, VolumeEntry)
8
+ - Manifest and spine record types (ManifestEntry, SpineEntry)
9
+ - Hierarchical NavPoint for NCX navigation
10
+ - Base resource class (EpubResource) and specializations:
11
+ - StyleSheet
12
+ - ImageResource
13
+ - Chapter (with XHTML serialization)
14
+ - Volume container for grouping chapters with optional intro and cover
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+
22
+ from .constants import (
23
+ CHAP_TMPLATE,
24
+ CSS_TMPLATE,
25
+ )
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class ChapterEntry:
30
+ id: str
31
+ label: str
32
+ src: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class VolumeEntry:
37
+ id: str
38
+ label: str
39
+ src: str
40
+ chapters: list[ChapterEntry]
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class ManifestEntry:
45
+ id: str
46
+ href: str
47
+ media_type: str
48
+ properties: str | None = None
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class SpineEntry:
53
+ idref: str
54
+ properties: str | None = None
55
+
56
+
57
+ @dataclass
58
+ class NavPoint:
59
+ """
60
+ A table-of-contents entry, possibly with nested children.
61
+ """
62
+
63
+ id: str
64
+ label: str
65
+ src: str
66
+ children: list[NavPoint] = field(default_factory=list)
67
+
68
+
69
+ @dataclass
70
+ class EpubResource:
71
+ """
72
+ Base class for any EPUB-packaged resource.
73
+ """
74
+
75
+ id: str
76
+ filename: str
77
+ media_type: str
78
+
79
+
80
+ @dataclass
81
+ class StyleSheet(EpubResource):
82
+ content: str
83
+ media_type: str = field(init=False, default="text/css")
84
+
85
+
86
+ @dataclass
87
+ class ImageResource(EpubResource):
88
+ data: bytes
89
+
90
+
91
+ @dataclass
92
+ class Chapter(EpubResource):
93
+ title: str
94
+ content: str
95
+ css: list[StyleSheet] = field(default_factory=list)
96
+ media_type: str = field(init=False, default="application/xhtml+xml")
97
+
98
+ def to_xhtml(self, lang: str = "zh-CN") -> str:
99
+ """
100
+ Generate the XHTML for a chapter.
101
+ """
102
+ links = "\n".join(
103
+ CSS_TMPLATE.format(filename=css.filename, media_type=css.media_type)
104
+ for css in self.css
105
+ )
106
+ return CHAP_TMPLATE.format(
107
+ lang=lang,
108
+ title=self.title,
109
+ xlinks=links,
110
+ content=self.content,
111
+ )
112
+
113
+
114
+ @dataclass
115
+ class Volume:
116
+ id: str
117
+ title: str
118
+ intro: str = ""
119
+ cover: Path | None = None
120
+ chapters: list[Chapter] = field(default_factory=list)
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.utils
4
+ ---------------------------------
5
+
6
+ Pure utility functions for EPUB assembly, including:
7
+ - Computing file hashes
8
+ - Generating META-INF/container.xml
9
+ - Constructing HTML snippets for the book intro and volume intro
10
+ """
11
+
12
+ import hashlib
13
+ from html import escape
14
+ from pathlib import Path
15
+
16
+ from .constants import (
17
+ CONTAINER_TEMPLATE,
18
+ IMAGE_FOLDER,
19
+ ROOT_PATH,
20
+ )
21
+
22
+
23
+ def hash_file(file_path: Path, chunk_size: int = 8192) -> str:
24
+ """
25
+ Compute the SHA256 hash of a file.
26
+
27
+ :param file_path: The Path object of the file to hash.
28
+ :param chunk_size: The chunk size to read the file (default: 8192).
29
+ :return: The SHA256 hash string (lowercase hex) of the file content.
30
+ """
31
+ h = hashlib.sha256()
32
+ with file_path.open("rb") as f:
33
+ while chunk := f.read(chunk_size):
34
+ h.update(chunk)
35
+ return h.hexdigest()
36
+
37
+
38
+ def build_container_xml(
39
+ root_path: str = ROOT_PATH,
40
+ ) -> str:
41
+ """
42
+ Generate the XML content for META-INF/container.xml in an EPUB archive.
43
+
44
+ :param root_path: The folder where the OPF file is stored.
45
+ :return: A string containing the full XML for container.xml.
46
+ """
47
+ return CONTAINER_TEMPLATE.format(root_path=root_path)
48
+
49
+
50
+ def build_book_intro(
51
+ book_name: str,
52
+ author: str,
53
+ serial_status: str,
54
+ subject: list[str],
55
+ word_count: str,
56
+ summary: str,
57
+ ) -> str:
58
+ """
59
+ Build the HTML snippet for the overall book introduction.
60
+
61
+ This includes:
62
+ - A main heading ("Book Introduction")
63
+ - A list of metadata items (title, author, categories, word count, status)
64
+ - A "Summary" subheading and one or more paragraphs of summary text
65
+
66
+ :return: A HTML string for inclusion in `intro.xhtml`
67
+ """
68
+ lines = []
69
+
70
+ lines.append("<div>")
71
+ lines.append("<h1>书籍简介</h1>")
72
+ lines.append('<div class="intro-info">')
73
+ lines.append("<ul>")
74
+
75
+ name_val = f"《{book_name}》" if book_name else ""
76
+ subj_val = ", ".join(subject) if subject else ""
77
+
78
+ li_lines = [
79
+ _li_line("书名", name_val),
80
+ _li_line("作者", author),
81
+ _li_line("分类", subj_val),
82
+ _li_line("字数", word_count),
83
+ _li_line("状态", serial_status),
84
+ ]
85
+ for li in li_lines:
86
+ if li:
87
+ lines.append(li)
88
+
89
+ lines.append("</ul>")
90
+ lines.append("</div>")
91
+
92
+ if summary:
93
+ lines.append('<p class="new-page-after"></p>')
94
+ lines.append("<h2>简介</h2>")
95
+ lines.append('<div class="intro-summary">')
96
+ for line in summary.splitlines():
97
+ s = line.strip()
98
+ if not s:
99
+ continue
100
+ lines.append(f"<p>{escape(s, quote=True)}</p>")
101
+ lines.append("</div>")
102
+
103
+ lines.append("</div>")
104
+ return "\n".join(lines)
105
+
106
+
107
+ def build_volume_intro(
108
+ volume_title: str,
109
+ volume_intro_text: str = "",
110
+ ) -> str:
111
+ """
112
+ Build the HTML snippet for a single-volume introduction.
113
+
114
+ This includes:
115
+ - A decorative border image (top and bottom)
116
+ - A primary heading (volume main title)
117
+ - An optional secondary line (subtitle)
118
+ - One or more paragraphs of intro text
119
+
120
+ :param volume_title: e.g. "Volume 1 - The Beginning"
121
+ :param volume_intro_text: multiline intro text for this volume
122
+ :return: A HTML string for inclusion in `vol_<n>.xhtml`
123
+ """
124
+ line1, line2 = _split_volume_title(volume_title)
125
+
126
+ lines = []
127
+ lines.append("<div>")
128
+ lines.append('<div class="vol-header">')
129
+ lines.append(_vol_border_div_str(flip=False))
130
+ lines.append(f'<h1 class="vol-title-main">{escape(line1, quote=True)}</h1>')
131
+ lines.append(_vol_border_div_str(flip=True))
132
+ if line2:
133
+ lines.append(f'<h2 class="vol-title-sub">{escape(line2, quote=True)}</h2>')
134
+ lines.append("</div>")
135
+
136
+ if volume_intro_text:
137
+ lines.append('<p class="new-page-after"></p>')
138
+ lines.append('<div class="vol-intro-text">')
139
+ for line in volume_intro_text.splitlines():
140
+ s = line.strip()
141
+ if not s:
142
+ continue
143
+ lines.append(f"<p>{escape(s, quote=True)}</p>")
144
+ lines.append("</div>")
145
+
146
+ lines.append("</div>")
147
+ return "\n".join(lines)
148
+
149
+
150
+ def _li_line(label: str, value: str) -> str:
151
+ if not value:
152
+ return ""
153
+ return f"<li>{escape(label, quote=True)}: {escape(value, quote=True)}</li>"
154
+
155
+
156
+ def _vol_border_div_str(flip: bool = False) -> str:
157
+ classes = "vol-border" + (" flip" if flip else "")
158
+ return (
159
+ f'<div class="{classes}">'
160
+ f'<img src="../{IMAGE_FOLDER}/volume_border.png" alt="">'
161
+ f"</div>"
162
+ )
163
+
164
+
165
+ def _split_volume_title(volume_title: str) -> tuple[str, str]:
166
+ """
167
+ Split volume title into two parts for better display.
168
+
169
+ :param volume_title: Original volume title string.
170
+ :return: Tuple of (line1, line2)
171
+ """
172
+ if "-" in volume_title:
173
+ parts = volume_title.split("-", 1)
174
+ elif " " in volume_title:
175
+ parts = volume_title.split(" ", 1)
176
+ else:
177
+ return volume_title, ""
178
+
179
+ return parts[0], parts[1]
@@ -4,39 +4,14 @@ novel_downloader.utils.file_utils
4
4
  ---------------------------------
5
5
 
6
6
  High-level file I/O utility re-exports for convenience.
7
-
8
- This module aggregates commonly used low-level file utilities such as:
9
- - Path sanitization (for safe filenames)
10
- - Text normalization (e.g. Windows/Linux line endings)
11
- - JSON, plain text, and binary file reading/writing
12
-
13
- Included utilities:
14
- - sanitize_filename: remove invalid characters from filenames
15
- - normalize_txt_line_endings: standardize line endings in text files
16
- - save_as_json / save_as_txt: write dict or text to file
17
- - read_text_file / read_json_file / read_binary_file: load content from file
18
7
  """
19
8
 
20
- from .io import (
21
- load_blacklisted_words,
22
- load_text_resource,
23
- read_binary_file,
24
- read_json_file,
25
- read_text_file,
26
- save_as_json,
27
- save_as_txt,
28
- )
29
- from .normalize import normalize_txt_line_endings
30
- from .sanitize import sanitize_filename
31
-
32
9
  __all__ = [
33
10
  "sanitize_filename",
34
- "save_as_json",
35
- "save_as_txt",
36
- "read_text_file",
37
- "read_json_file",
38
- "read_binary_file",
39
- "load_text_resource",
40
- "load_blacklisted_words",
11
+ "write_file",
41
12
  "normalize_txt_line_endings",
42
13
  ]
14
+
15
+ from .io import write_file
16
+ from .normalize import normalize_txt_line_endings
17
+ from .sanitize import sanitize_filename