novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__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 (248) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +79 -66
  6. novel_downloader/cli/export.py +17 -21
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  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/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  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 +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +17 -12
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +20 -14
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  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} +19 -12
  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} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  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} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
  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 +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  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} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  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 +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  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 +64 -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.py +64 -69
  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.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.crypto_utils.rc4
4
+ ---------------------------------------
5
+
6
+ Minimal RC4 stream cipher implementation.
7
+ """
8
+
9
+
10
+ def rc4_init(key: bytes) -> list[int]:
11
+ """
12
+ Key-Scheduling Algorithm (KSA)
13
+ """
14
+ S = list(range(256))
15
+ j = 0
16
+ klen = len(key)
17
+ for i in range(256):
18
+ j = (j + S[i] + key[i % klen]) & 0xFF
19
+ S[i], S[j] = S[j], S[i]
20
+ return S
21
+
22
+
23
+ def rc4_stream(S_init: list[int], data: bytes) -> bytes:
24
+ """
25
+ Pseudo-Random Generation Algorithm (PRGA)
26
+ """
27
+ # make a copy of S since it mutates during PRGA
28
+ S = S_init.copy()
29
+ i = 0
30
+ j = 0
31
+ out = bytearray(len(data))
32
+ for idx, ch in enumerate(data):
33
+ i = (i + 1) & 0xFF
34
+ j = (j + S[i]) & 0xFF
35
+ S[i], S[j] = S[j], S[i]
36
+ K = S[(S[i] + S[j]) & 0xFF]
37
+ out[idx] = ch ^ K
38
+
39
+ return bytes(out)
40
+
41
+
42
+ def rc4_cipher(key: bytes, data: bytes) -> bytes:
43
+ """
44
+ RC4 stream cipher.
45
+
46
+ It performs the standard Key-Scheduling Algorithm (KSA) and
47
+ Pseudo-Random Generation Algorithm (PRGA) to produce the RC4 keystream.
48
+
49
+ :param key: RC4 key as bytes (must not be empty)
50
+ :param data: plaintext or ciphertext as bytes
51
+ :return: XORed bytes (encrypt/decrypt are identical)
52
+ """
53
+ S = rc4_init(key)
54
+ return rc4_stream(S, data)
@@ -6,15 +6,14 @@ novel_downloader.utils.epub
6
6
  Top-level package for EPUB export utilities.
7
7
 
8
8
  Key components:
9
-
10
- - EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
11
- - Chapter, Volume : represent and render content sections and volume intros
9
+ * EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
10
+ * Chapter, Volume : represent and render content sections and volume intros
12
11
 
13
12
  Usage example:
14
13
 
15
14
  ```python
16
15
  builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
17
- builder.add_chapter(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
16
+ builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
18
17
  builder.export("output/my_novel.epub")
19
18
  ```
20
19
  """
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.builder
4
4
  -----------------------------------
5
5
 
6
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
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
11
 
12
12
  Provides:
13
- - methods to add chapters, volumes, images, and styles
14
- - a clean `export()` entry point that writes the final EPUB archive
13
+ * methods to add chapters, volumes, images, and styles
14
+ * a clean `export()` entry point that writes the final EPUB archive
15
15
  """
16
16
 
17
17
  import zipfile
@@ -4,14 +4,13 @@ novel_downloader.utils.epub.constants
4
4
  -------------------------------------
5
5
 
6
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
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
12
  """
13
13
 
14
- PRETTY_PRINT_FLAG = True
15
14
  ROOT_PATH = "OEBPS"
16
15
  IMAGE_FOLDER = "Images"
17
16
  TEXT_FOLDER = "Text"
@@ -24,18 +23,6 @@ NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
24
23
  OPF_NS = "http://www.idpf.org/2007/opf"
25
24
  DC_NS = "http://purl.org/dc/elements/1.1/"
26
25
 
27
- OPF_PKG_ATTRIB = {
28
- "version": "3.0",
29
- "unique-identifier": "id",
30
- "prefix": "rendition: http://www.idpf.org/vocab/rendition/#",
31
- }
32
- CHAP_DOC_TYPE = (
33
- '<?xml version="1.0" encoding="utf-8"?>\n'
34
- "<!DOCTYPE html PUBLIC "
35
- '"-//W3C//DTD XHTML 1.1//EN" '
36
- '"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
37
- )
38
-
39
26
  IMAGE_MEDIA_TYPES: dict[str, str] = {
40
27
  "png": "image/png",
41
28
  "jpg": "image/jpeg",
@@ -45,13 +32,15 @@ IMAGE_MEDIA_TYPES: dict[str, str] = {
45
32
  "webp": "image/webp",
46
33
  }
47
34
 
48
- CONTAINER_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
35
+ CONTAINER_TEMPLATE = """\
36
+ <?xml version="1.0" encoding="UTF-8"?>
49
37
  <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
50
38
  <rootfiles>
51
39
  <rootfile full-path="{root_path}/content.opf"
52
40
  media-type="application/oebps-package+xml"/>
53
41
  </rootfiles>
54
- </container>"""
42
+ </container>
43
+ """
55
44
 
56
45
  COVER_IMAGE_TEMPLATE = (
57
46
  f'<div style="text-align: center; margin: 0; padding: 0;">'
@@ -66,7 +55,8 @@ CSS_TMPLATE = (
66
55
  )
67
56
 
68
57
  CHAP_TMPLATE = f"""\
69
- {CHAP_DOC_TYPE}
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">
70
60
  <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
71
61
  <head>
72
62
  <title>{{title}}</title>
@@ -75,3 +65,54 @@ CHAP_TMPLATE = f"""\
75
65
  <body>{{content}}</body>
76
66
  </html>
77
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
@@ -4,27 +4,19 @@ novel_downloader.utils.epub.documents
4
4
  -------------------------------------
5
5
 
6
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
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
10
  """
11
11
 
12
12
  from collections.abc import Sequence
13
13
  from dataclasses import dataclass, field
14
14
  from datetime import UTC, datetime
15
15
 
16
- from lxml import etree
17
- from lxml.builder import ElementMaker
18
-
19
16
  from .constants import (
20
- DC_NS,
21
- EPUB_NS,
22
- NCX_NS,
23
- OPF_NS,
24
- OPF_PKG_ATTRIB,
25
- PRETTY_PRINT_FLAG,
26
- XHTML_NS,
27
- XML_NS,
17
+ NAV_TEMPLATE,
18
+ NCX_TEMPLATE,
19
+ OPF_TEMPLATE,
28
20
  )
29
21
  from .models import (
30
22
  ChapterEntry,
@@ -35,21 +27,6 @@ from .models import (
35
27
  VolumeEntry,
36
28
  )
37
29
 
38
- NAV = ElementMaker(
39
- namespace=XHTML_NS,
40
- nsmap={None: XHTML_NS, "epub": EPUB_NS},
41
- )
42
- NCX = ElementMaker(namespace=NCX_NS, nsmap={None: NCX_NS})
43
- PKG = ElementMaker(
44
- namespace=OPF_NS,
45
- nsmap={
46
- None: OPF_NS,
47
- "dc": DC_NS,
48
- "opf": OPF_NS,
49
- },
50
- )
51
- DC = ElementMaker(namespace=DC_NS)
52
-
53
30
 
54
31
  @dataclass
55
32
  class NavDocument(EpubResource):
@@ -60,12 +37,7 @@ class NavDocument(EpubResource):
60
37
  media_type: str = field(init=False, default="application/xhtml+xml")
61
38
  content_items: list[ChapterEntry | VolumeEntry] = field(default_factory=list)
62
39
 
63
- def add_chapter(
64
- self,
65
- id: str,
66
- label: str,
67
- src: str,
68
- ) -> None:
40
+ def add_chapter(self, id: str, label: str, src: str) -> None:
69
41
  """
70
42
  Add a top-level chapter entry to the navigation.
71
43
 
@@ -100,55 +72,29 @@ class NavDocument(EpubResource):
100
72
 
101
73
  :return: A string containing the full XHTML for nav.xhtml.
102
74
  """
103
- # build the root <html> with both lang attributes
104
- html_el = NAV.html(
105
- # head/title
106
- NAV.head(NAV.title(self.title)),
107
- # body/nav/ol subtree
108
- NAV.body(
109
- NAV.nav(
110
- NAV.h2(self.title),
111
- NAV.ol(*self._render_items(self.content_items)),
112
- # namespaced + regular attributes
113
- **{
114
- f"{{{EPUB_NS}}}type": "toc",
115
- "id": self.id,
116
- "role": "doc-toc",
117
- },
118
- )
119
- ),
120
- # html attributes
75
+ items_str = self._render_items_str(self.content_items)
76
+ raw = NAV_TEMPLATE.format(
121
77
  lang=self.language,
122
- **{f"{{{XML_NS}}}lang": self.language},
78
+ id=self.id,
79
+ title=self.title,
80
+ items=items_str,
123
81
  )
124
-
125
- xml_bytes = etree.tostring(
126
- html_el,
127
- xml_declaration=True,
128
- encoding="utf-8",
129
- pretty_print=PRETTY_PRINT_FLAG,
130
- doctype="<!DOCTYPE html>",
131
- )
132
- xml_string: str = xml_bytes.decode("utf-8")
133
- return xml_string
82
+ return raw
134
83
 
135
84
  @classmethod
136
- def _render_items(
137
- cls,
138
- items: Sequence[ChapterEntry | VolumeEntry],
139
- ) -> list[etree._Element]:
140
- """
141
- Recursively build <li> elements (and nested <ol>) for each TOC entry.
142
- """
143
- elements: list[etree._Element] = []
85
+ def _render_items_str(cls, items: Sequence[ChapterEntry | VolumeEntry]) -> str:
86
+ lines: list[str] = []
144
87
  for item in items:
145
88
  if isinstance(item, VolumeEntry) and item.chapters:
146
- li = NAV.li(NAV.a(item.label, href=item.src))
147
- li.append(NAV.ol(*cls._render_items(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>")
148
95
  else:
149
- li = NAV.li(NAV.a(item.label, href=item.src))
150
- elements.append(li)
151
- return elements
96
+ lines.append(f'<li><a href="{item.src}">{item.label}</a></li>')
97
+ return "\n".join(lines)
152
98
 
153
99
 
154
100
  @dataclass
@@ -190,29 +136,19 @@ class NCXDocument(EpubResource):
190
136
 
191
137
  :return: A string containing the full NCX XML document.
192
138
  """
193
- root = NCX.ncx(version="2005-1")
194
- head = NCX.head(
195
- NCX.meta(name="dtb:uid", content=self.uid),
196
- NCX.meta(name="dtb:depth", content=str(self._depth(self.nav_points))),
197
- NCX.meta(name="dtb:totalPageCount", content="0"),
198
- NCX.meta(name="dtb:maxPageNumber", content="0"),
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,
199
150
  )
200
- root.append(head)
201
- root.append(NCX.docTitle(NCX.text(self.title)))
202
-
203
- navMap = NCX.navMap()
204
- root.append(navMap)
205
-
206
- self._render_navpoints(navMap, self.nav_points, start=1)
207
-
208
- xml_bytes = etree.tostring(
209
- root,
210
- xml_declaration=True,
211
- encoding="utf-8",
212
- pretty_print=PRETTY_PRINT_FLAG,
213
- )
214
- xml_string: str = xml_bytes.decode("utf-8")
215
- return xml_string
151
+ return raw
216
152
 
217
153
  @classmethod
218
154
  def _depth(cls, points: list[NavPoint]) -> int:
@@ -221,32 +157,21 @@ class NCXDocument(EpubResource):
221
157
  return 1 + max(cls._depth(child.children) for child in points)
222
158
 
223
159
  @classmethod
224
- def _render_navpoints(
225
- cls,
226
- parent: etree._Element,
227
- points: list[NavPoint],
228
- start: int,
229
- ) -> int:
230
- """
231
- Recursively append <navPoint> elements under `parent`,
232
- assigning playOrder starting from `start`.
233
- Returns the next unused playOrder.
234
- """
235
- play = start
236
- for pt in points:
237
- np = etree.SubElement(
238
- parent,
239
- "navPoint",
240
- id=pt.id,
241
- playOrder=str(play),
242
- )
243
- play += 1
244
- navLabel = etree.SubElement(np, "navLabel")
245
- lbl_text = etree.SubElement(navLabel, "text")
246
- lbl_text.text = pt.label
247
- etree.SubElement(np, "content", src=pt.src)
248
- play = cls._render_navpoints(np, pt.children, play)
249
- return play
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
250
175
 
251
176
 
252
177
  @dataclass
@@ -309,95 +234,64 @@ class OpfDocument(EpubResource):
309
234
  Generate the content.opf XML, which defines metadata, manifest, and spine.
310
235
 
311
236
  This function outputs a complete OPF package document that includes:
312
- - <metadata>: title, author, language, identifiers, etc.
313
- - <manifest>: all resource entries
314
- - <spine>: the reading order of the content
315
- - <guide>: optional references like cover page
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
316
241
 
317
242
  :return: A string containing the full OPF XML content.
318
243
  """
319
244
  now_iso = datetime.now(UTC).replace(microsecond=0).isoformat()
320
245
 
321
- # <package> root
322
- package = PKG.package(**OPF_PKG_ATTRIB)
323
-
324
- # <metadata>
325
- metadata = PKG.metadata()
326
- package.append(metadata)
327
-
328
- # modified timestamp
329
- modified = PKG.meta(property="dcterms:modified")
330
- modified.text = now_iso
331
- metadata.append(modified)
332
-
333
- # mandatory DC elements
334
- id_el = DC.identifier(id="id")
335
- id_el.text = self.uid
336
- title_el = DC.title()
337
- title_el.text = self.title
338
- lang_el = DC.language()
339
- lang_el.text = self.language
340
- metadata.extend([id_el, title_el, lang_el])
341
-
342
- # optional DC elements
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>")
343
252
  if self.author:
344
- creator = DC.creator(id="creator")
345
- creator.text = self.author
346
- metadata.append(creator)
253
+ meta_lines.append(f'<dc:creator id="creator">{self.author}</dc:creator>')
347
254
  if self.description:
348
- desc = DC.description()
349
- desc.text = self.description
350
- metadata.append(desc)
255
+ meta_lines.append(f"<dc:description>{self.description}</dc:description>")
351
256
  if self.subject:
352
- subj = DC.subject()
353
- subj.text = ",".join(self.subject)
354
- metadata.append(subj)
257
+ joined = ",".join(self.subject)
258
+ meta_lines.append(f"<dc:subject>{joined}</dc:subject>")
355
259
  if self.include_cover and self._cover_item:
356
- cover_meta = PKG.meta(name="cover", content=self._cover_item.id)
357
- metadata.append(cover_meta)
260
+ meta_lines.append(f'<meta name="cover" content="{self._cover_item.id}"/>')
261
+ metadata = "\n".join(meta_lines)
358
262
 
359
- # <manifest>
360
- manifest_el = PKG.manifest()
263
+ # manifest block
264
+ man_lines: list[str] = []
361
265
  for item in self.manifest:
362
- attrs = {
363
- "id": item.id,
364
- "href": item.href,
365
- "media-type": item.media_type,
366
- }
367
- if item.properties:
368
- attrs["properties"] = item.properties
369
- manifest_el.append(PKG.item(**attrs))
370
- package.append(manifest_el)
371
-
372
- # <spine>
373
- spine_attrs = {}
374
- if self._toc_item:
375
- spine_attrs["toc"] = self._toc_item.id
376
- spine_el = PKG.spine(**spine_attrs)
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] = []
377
275
  for ref in self.spine:
378
- attrs = {"idref": ref.idref}
379
- if ref.properties:
380
- attrs["properties"] = ref.properties
381
- spine_el.append(PKG.itemref(**attrs))
382
- package.append(spine_el)
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)
383
279
 
384
- # optional <guide> for cover
280
+ # guide block
385
281
  if self.include_cover and self._cover_doc:
386
- guide_el = PKG.guide()
387
- guide_el.append(
388
- PKG.reference(
389
- type="cover",
390
- title="Cover",
391
- href=self._cover_doc.href,
392
- )
282
+ guide_section = (
283
+ " <guide>\n"
284
+ f' <reference type="cover" title="Cover" href="{self._cover_doc.href}"/>\n' # noqa: E501
285
+ " </guide>\n"
393
286
  )
394
- package.append(guide_el)
395
-
396
- xml_bytes = etree.tostring(
397
- package,
398
- xml_declaration=True,
399
- encoding="utf-8",
400
- pretty_print=PRETTY_PRINT_FLAG,
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,
401
296
  )
402
- xml_string: str = xml_bytes.decode("utf-8")
403
- return xml_string
297
+ return raw
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.models
4
4
  ----------------------------------
5
5
 
6
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
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
15
  """
16
16
 
17
17
  from __future__ import annotations
@@ -65,12 +65,6 @@ class NavPoint:
65
65
  src: str
66
66
  children: list[NavPoint] = field(default_factory=list)
67
67
 
68
- def add_child(self, point: NavPoint) -> None:
69
- """
70
- Append a child nav point under this one.
71
- """
72
- self.children.append(point)
73
-
74
68
 
75
69
  @dataclass
76
70
  class EpubResource:
@@ -101,10 +95,6 @@ class Chapter(EpubResource):
101
95
  css: list[StyleSheet] = field(default_factory=list)
102
96
  media_type: str = field(init=False, default="application/xhtml+xml")
103
97
 
104
- def __post_init__(self) -> None:
105
- if not self.filename:
106
- object.__setattr__(self, "filename", f"{self.id}.xhtml")
107
-
108
98
  def to_xhtml(self, lang: str = "zh-CN") -> str:
109
99
  """
110
100
  Generate the XHTML for a chapter.
@@ -128,7 +118,3 @@ class Volume:
128
118
  intro: str = ""
129
119
  cover: Path | None = None
130
120
  chapters: list[Chapter] = field(default_factory=list)
131
-
132
- def add_chapter(self, chapter: Chapter) -> None:
133
- """Append a chapter to this volume."""
134
- self.chapters.append(chapter)