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
@@ -3,18 +3,23 @@
3
3
  novel_downloader.core.exporters.linovelib.main_exporter
4
4
  -------------------------------------------------------
5
5
 
6
+ Exporter implementation for Linovelib novels, supporting TXT and EPUB outputs.
6
7
  """
7
8
 
8
- from collections.abc import Mapping
9
- from typing import Any
9
+ from pathlib import Path
10
10
 
11
11
  from novel_downloader.core.exporters.base import BaseExporter
12
+ from novel_downloader.core.exporters.registry import register_exporter
12
13
  from novel_downloader.models import ExporterConfig
13
- from novel_downloader.utils.chapter_storage import ChapterStorage
14
14
 
15
+ from .epub import (
16
+ export_by_volume,
17
+ export_whole_book,
18
+ )
15
19
  from .txt import linovelib_export_as_txt
16
20
 
17
21
 
22
+ @register_exporter(site_keys=["linovelib"])
18
23
  class LinovelibExporter(BaseExporter):
19
24
  """"""
20
25
 
@@ -29,10 +34,8 @@ class LinovelibExporter(BaseExporter):
29
34
  save paths, formats, and options.
30
35
  """
31
36
  super().__init__(config, "linovelib")
32
- self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
33
- self._chap_folders: list[str] = ["chapters"]
34
37
 
35
- def export_as_txt(self, book_id: str) -> None:
38
+ def export_as_txt(self, book_id: str) -> Path | None:
36
39
  """
37
40
  Compile and export a novel as a single .txt file.
38
41
 
@@ -41,23 +44,13 @@ class LinovelibExporter(BaseExporter):
41
44
  self._init_chapter_storages(book_id)
42
45
  return linovelib_export_as_txt(self, book_id)
43
46
 
44
- def export_as_epub(self, book_id: str) -> None:
47
+ def export_as_epub(self, book_id: str) -> Path | None:
45
48
  """
46
49
  Persist the assembled book as a EPUB (.epub) file.
47
50
 
48
51
  :param book_id: The book identifier.
49
52
  :raises NotImplementedError: If the method is not overridden.
50
53
  """
51
- try:
52
- from .epub import (
53
- export_by_volume,
54
- export_whole_book,
55
- )
56
- except ImportError as err:
57
- raise NotImplementedError(
58
- "EPUB export not supported. Please install 'ebooklib'"
59
- ) from err
60
-
61
54
  self._init_chapter_storages(book_id)
62
55
 
63
56
  exporters = {
@@ -71,57 +64,3 @@ class LinovelibExporter(BaseExporter):
71
64
  f"Unsupported split_mode: {self._config.split_mode!r}"
72
65
  ) from err
73
66
  return export_fn(self, book_id)
74
-
75
- @property
76
- def site(self) -> str:
77
- """
78
- Get the site identifier.
79
-
80
- :return: The site string.
81
- """
82
- return self._site
83
-
84
- @site.setter
85
- def site(self, value: str) -> None:
86
- """
87
- Set the site identifier.
88
-
89
- :param value: New site string to set.
90
- """
91
- self._site = value
92
-
93
- def _get_chapter(
94
- self,
95
- book_id: str,
96
- chap_id: str,
97
- ) -> Mapping[str, Any]:
98
- for storage in self._chapter_storage_cache[book_id]:
99
- data = storage.get(chap_id)
100
- if data:
101
- return data
102
- return {}
103
-
104
- def _init_chapter_storages(self, book_id: str) -> None:
105
- if book_id in self._chapter_storage_cache:
106
- return
107
- raw_base = self._raw_data_dir / book_id
108
- self._chapter_storage_cache[book_id] = [
109
- ChapterStorage(
110
- raw_base=raw_base,
111
- namespace=ns,
112
- backend_type=self._config.storage_backend,
113
- )
114
- for ns in self._chap_folders
115
- ]
116
-
117
- def _on_close(self) -> None:
118
- """
119
- Close all ChapterStorage connections in the cache.
120
- """
121
- for storages in self._chapter_storage_cache.values():
122
- for storage in storages:
123
- try:
124
- storage.close()
125
- except Exception as e:
126
- self.logger.warning("Failed to close storage %s: %s", storage, e)
127
- self._chapter_storage_cache.clear()
@@ -3,22 +3,20 @@
3
3
  novel_downloader.core.exporters.linovelib.txt
4
4
  ---------------------------------------------
5
5
 
6
- Contains the logic for exporting novel content as a single `.txt` file.
7
-
8
- This module defines `linovelib_export_as_txt` function, which assembles and formats
9
- a novel based on metadata and chapter files found in the raw data directory.
10
- It is intended to be used by `LinovelibExporter` as part of the save/export process.
6
+ Defines `linovelib_export_as_txt` to assemble and export a Linovelib novel
7
+ into a single `.txt` file. Intended for use by `LinovelibExporter`.
11
8
  """
12
9
 
13
10
  from __future__ import annotations
14
11
 
15
- import json
12
+ from pathlib import Path
16
13
  from typing import TYPE_CHECKING
17
14
 
18
- from novel_downloader.utils.file_utils import save_as_txt
19
- from novel_downloader.utils.text_utils import (
20
- format_chapter,
15
+ from novel_downloader.core.exporters.txt_util import (
16
+ build_txt_chapter,
17
+ build_txt_header,
21
18
  )
19
+ from novel_downloader.utils import get_cleaner, write_file
22
20
 
23
21
  if TYPE_CHECKING:
24
22
  from .main_exporter import LinovelibExporter
@@ -27,58 +25,71 @@ if TYPE_CHECKING:
27
25
  def linovelib_export_as_txt(
28
26
  exporter: LinovelibExporter,
29
27
  book_id: str,
30
- ) -> None:
28
+ ) -> Path | None:
31
29
  """
32
- save_path 文件夹中该小说的所有章节 json 文件合并保存为一个完整的 txt 文件,
33
- 并保存到 out_path 下
34
-
35
- 处理流程:
36
- 1. book_info.json 中加载书籍信息 (包含书名、作者、简介及卷章节列表)
37
- 2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容
38
- 3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接
39
- 4. 将最终结果保存到 out_path (例如:`{book_name}.txt`)
40
-
41
- :param book_id: Identifier of the novel (used as subdirectory name).
30
+ Export a novel as a single text file by merging all chapter data.
31
+
32
+ Steps:
33
+ 1. Read metadata from `book_info.json`.
34
+ 2. For each volume:
35
+ * Clean & append the volume title.
36
+ * Clean & append optional volume intro.
37
+ * Batch-fetch all chapters in this volume to minimize SQLite overhead.
38
+ * For each chapter: clean title & content, then append.
39
+ 3. Build a header block with metadata.
40
+ 4. Concatenate header + all chapter blocks, then save as `{book_name}.txt`.
41
+
42
+ :param exporter: The LinovelibExporter instance.
43
+ :param book_id: Identifier of the novel (subdirectory under raw data).
42
44
  """
43
45
  TAG = "[exporter]"
44
46
  # --- Paths & options ---
45
- raw_base = exporter._raw_data_dir / book_id
46
47
  out_dir = exporter.output_dir
47
48
  out_dir.mkdir(parents=True, exist_ok=True)
49
+ cleaner = get_cleaner(
50
+ enabled=exporter._config.clean_text,
51
+ config=exporter._config.cleaner_cfg,
52
+ )
48
53
 
49
54
  # --- Load book_info.json ---
50
- info_path = raw_base / "book_info.json"
51
- try:
52
- info_text = info_path.read_text(encoding="utf-8")
53
- book_info = json.loads(info_text)
54
- except Exception as e:
55
- exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
56
- return
55
+ book_info = exporter._load_book_info(book_id)
56
+ if not book_info:
57
+ return None
57
58
 
58
59
  # --- Compile chapters ---
59
60
  parts: list[str] = []
60
- volumes = book_info.get("volumes", [])
61
-
62
- for vol in volumes:
63
- vol_name = vol.get("volume_name", "").strip()
64
- vol_intro = vol.get("volume_intro", "").strip()
65
- if vol_name:
66
- volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
67
- parts.append(volume_header)
68
- exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
61
+
62
+ for vol in book_info.get("volumes", []):
63
+ vol_title = cleaner.clean_title(vol.get("volume_name", ""))
64
+ if vol_title:
65
+ parts.append(f"\n\n{'=' * 6} {vol_title} {'=' * 6}\n\n")
66
+ exporter.logger.info("%s Processing volume: %s", TAG, vol_title)
67
+
68
+ vol_intro = cleaner.clean_content(vol.get("volume_intro", ""))
69
69
  if vol_intro:
70
70
  parts.append(f"{vol_intro}\n\n")
71
- for chap in vol.get("chapters", []):
72
- chap_id = chap.get("chapterId")
73
- chap_title = chap.get("title", "")
71
+
72
+ # Batch-fetch chapters for this volume
73
+ chap_ids = [
74
+ chap["chapterId"]
75
+ for chap in vol.get("chapters", [])
76
+ if chap.get("chapterId")
77
+ ]
78
+ chap_map = exporter._get_chapters(book_id, chap_ids)
79
+
80
+ for chap_meta in vol.get("chapters", []):
81
+ chap_id = chap_meta.get("chapterId")
74
82
  if not chap_id:
75
- exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
83
+ exporter.logger.warning(
84
+ "%s Missing chapterId, skipping: %s", TAG, chap_meta
85
+ )
76
86
  continue
77
87
 
78
- chapter_data = exporter._get_chapter(book_id, chap_id)
79
- if not chapter_data:
88
+ chap_title = chap_meta.get("title", "")
89
+ data = chap_map.get(chap_id)
90
+ if not data:
80
91
  exporter.logger.info(
81
- "%s Missing chapter file in: %s (%s), skipping.",
92
+ "%s Missing chapter: %s (%s), skipping.",
82
93
  TAG,
83
94
  chap_title,
84
95
  chap_id,
@@ -86,33 +97,27 @@ def linovelib_export_as_txt(
86
97
  continue
87
98
 
88
99
  # Extract structured fields
89
- title = chapter_data.get("title", chap_title).strip()
90
- content = chapter_data.get("content", "").strip()
100
+ title = cleaner.clean_title(data.get("title", chap_title))
101
+ content = cleaner.clean_content(data.get("content", ""))
91
102
 
92
- parts.append(format_chapter(title, content, ""))
103
+ parts.append(build_txt_chapter(title=title, paragraphs=content, extras={}))
93
104
 
94
105
  # --- Build header ---
95
- name = book_info.get("book_name")
96
- author = book_info.get("author")
97
- words = book_info.get("word_count")
98
- updated = book_info.get("update_time")
99
- summary = book_info.get("summary")
106
+ name = book_info.get("book_name") or ""
107
+ author = book_info.get("author") or ""
108
+ words = book_info.get("word_count") or ""
109
+ updated = book_info.get("update_time") or ""
110
+ summary = book_info.get("summary") or ""
100
111
 
101
- fields = [
112
+ header_fields = [
102
113
  ("书名", name),
103
114
  ("作者", author),
104
115
  ("总字数", words),
105
116
  ("更新日期", updated),
117
+ ("内容简介", summary),
106
118
  ]
107
- header_lines = [f"{label}: {value}" for label, value in fields if value]
108
-
109
- if summary:
110
- header_lines.append("内容简介:")
111
- header_lines.append(summary)
112
-
113
- header_lines += ["", "-" * 10, ""]
114
119
 
115
- header = "\n".join(header_lines)
120
+ header = build_txt_header(header_fields)
116
121
 
117
122
  final_text = header + "\n\n" + "\n\n".join(parts).strip()
118
123
 
@@ -121,9 +126,14 @@ def linovelib_export_as_txt(
121
126
  out_path = out_dir / out_name
122
127
 
123
128
  # --- Save final text ---
124
- try:
125
- save_as_txt(content=final_text, filepath=out_path)
129
+ result = write_file(
130
+ content=final_text,
131
+ filepath=out_path,
132
+ write_mode="w",
133
+ on_exist="overwrite",
134
+ )
135
+ if result:
126
136
  exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
127
- except Exception as e:
128
- exporter.logger.error("%s Failed to save file: %s", TAG, e)
129
- return
137
+ else:
138
+ exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
139
+ return result
@@ -3,26 +3,30 @@
3
3
  novel_downloader.core.exporters.qidian
4
4
  --------------------------------------
5
5
 
6
- This module provides the `QidianExporter` class for handling the saving process
7
- of novels sourced from Qidian (起点中文网). It implements the platform-specific
8
- logic required to structure and export novel content into desired formats.
6
+ Exporter implementation for Qidian novels, supporting plain and encrypted sources.
9
7
  """
10
8
 
9
+ __all__ = ["QidianExporter"]
10
+
11
+ from novel_downloader.core.exporters.registry import register_exporter
11
12
  from novel_downloader.models import ExporterConfig
12
13
 
13
14
  from .common import CommonExporter
14
15
 
15
16
 
17
+ @register_exporter(site_keys=["qidian", "qd"])
16
18
  class QidianExporter(CommonExporter):
19
+ """ """
20
+
21
+ DEFAULT_SOURCE_ID = 0
22
+ ENCRYPTED_SOURCE_ID = 1
23
+ PRIORITIES_MAP = {
24
+ DEFAULT_SOURCE_ID: 0,
25
+ ENCRYPTED_SOURCE_ID: 1,
26
+ }
27
+
17
28
  def __init__(
18
29
  self,
19
30
  config: ExporterConfig,
20
31
  ):
21
- super().__init__(
22
- config,
23
- site="qidian",
24
- chap_folders=["chapters", "encrypted_chapters"],
25
- )
26
-
27
-
28
- __all__ = ["QidianExporter"]
32
+ super().__init__(config, site="qidian")
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.registry
4
+ ----------------------------------------
5
+
6
+ Registry and factory helpers for creating site-specific or common exporters.
7
+ """
8
+
9
+ __all__ = ["register_exporter", "get_exporter"]
10
+
11
+ from collections.abc import Callable, Sequence
12
+ from typing import TypeVar
13
+
14
+ from novel_downloader.core.exporters.common import CommonExporter
15
+ from novel_downloader.core.interfaces import ExporterProtocol
16
+ from novel_downloader.models import ExporterConfig
17
+
18
+ ExporterBuilder = Callable[[ExporterConfig], ExporterProtocol]
19
+
20
+ E = TypeVar("E", bound=ExporterProtocol)
21
+ _EXPORTER_MAP: dict[str, ExporterBuilder] = {}
22
+
23
+
24
+ def register_exporter(
25
+ site_keys: Sequence[str],
26
+ ) -> Callable[[type[E]], type[E]]:
27
+ """
28
+ Decorator to register a exporter class under given keys.
29
+
30
+ :param site_keys: Sequence of site identifiers
31
+ :return: A class decorator that populates _EXPORTER_MAP.
32
+ """
33
+
34
+ def decorator(cls: type[E]) -> type[E]:
35
+ for key in site_keys:
36
+ _EXPORTER_MAP[key.lower()] = cls
37
+ return cls
38
+
39
+ return decorator
40
+
41
+
42
+ def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
43
+ """
44
+ Returns a site-specific exporter instance.
45
+
46
+ :param site: Site name (e.g., 'qidian')
47
+ :param config: Configuration for the exporter
48
+ :return: An instance of a exporter class
49
+ """
50
+ site_key = site.lower()
51
+ try:
52
+ exporter_cls = _EXPORTER_MAP[site_key]
53
+ except KeyError:
54
+ return CommonExporter(config, site_key)
55
+ return exporter_cls(config)
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.txt_util
4
+ ----------------------------------------
5
+
6
+ Utilities for generating plain-text exports of novel content.
7
+ """
8
+
9
+ __all__ = [
10
+ "build_txt_header",
11
+ "build_txt_chapter",
12
+ ]
13
+
14
+ import re
15
+
16
+ _IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
17
+
18
+
19
+ def build_txt_header(fields: list[tuple[str, str]]) -> str:
20
+ """
21
+ Build a simple text header from label-value pairs, followed by a dashed separator.
22
+
23
+ :param fields: List of (label, value) pairs.
24
+ :return: A single string containing the formatted header.
25
+ """
26
+ header_lines = [f"{label}: {value}" for label, value in fields if value]
27
+ header_lines += ["", "-" * 10, ""]
28
+ return "\n".join(header_lines)
29
+
30
+
31
+ def build_txt_chapter(
32
+ title: str,
33
+ paragraphs: str,
34
+ extras: dict[str, str] | None = None,
35
+ ) -> str:
36
+ """
37
+ Build a formatted chapter text block including title, body paragraphs,
38
+ and optional extra sections.
39
+
40
+ * Strips any `<img...>` tags from paragraphs.
41
+ * Title appears first (stripped of surrounding whitespace).
42
+ * Each non-blank line in `paragraphs` becomes its own paragraph.
43
+
44
+ :param title: Chapter title.
45
+ :param paragraphs: Raw multi-line string. Blank lines are ignored.
46
+ :param extras: Optional dict mapping section titles to multi-line strings.
47
+ :return: A string where title, paragraphs, and extras are joined by lines.
48
+ """
49
+ parts: list[str] = [title.strip()]
50
+
51
+ # add each nonempty paragraph line
52
+ paragraphs = _IMG_TAG_RE.sub("", paragraphs)
53
+ for ln in paragraphs.splitlines():
54
+ line = ln.strip()
55
+ if line:
56
+ parts.append(line)
57
+
58
+ if extras:
59
+ for title, text in extras.items():
60
+ lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
61
+ if not lines:
62
+ continue
63
+ parts.append("---")
64
+ parts.append(title.strip())
65
+ parts.extend(lines)
66
+
67
+ return "\n\n".join(parts)
@@ -3,70 +3,71 @@
3
3
  novel_downloader.core.fetchers
4
4
  ------------------------------
5
5
 
6
- This package provides fetcher implementations for different novel platforms.
7
- Each submodule corresponds to a specific site and encapsulates the logic needed
8
- to perform network interactions, such as logging in, sending requests,
9
- or interacting with browser/session-based sources.
10
-
11
- Subpackages:
12
- - biquge (笔趣阁)
13
- - esjzone (ESJ Zone)
14
- - linovelib (哔哩轻小说)
15
- - qianbi (铅笔小说)
16
- - qidian (起点中文网)
17
- - sfacg (SF轻小说)
18
- - yamibo (百合会)
19
- - common (通用架构)
6
+ Fetcher implementations for retrieving raw data and HTML from various novel sources
20
7
  """
21
8
 
22
- from .biquge import (
23
- BiqugeBrowser,
24
- BiqugeSession,
25
- )
26
- from .common import (
27
- CommonBrowser,
28
- CommonSession,
29
- )
30
- from .esjzone import (
31
- EsjzoneBrowser,
32
- EsjzoneSession,
33
- )
34
- from .linovelib import (
35
- LinovelibBrowser,
36
- LinovelibSession,
37
- )
38
- from .qianbi import (
39
- QianbiBrowser,
40
- QianbiSession,
41
- )
42
- from .qidian import (
43
- QidianBrowser,
44
- QidianSession,
45
- )
46
- from .sfacg import (
47
- SfacgBrowser,
48
- SfacgSession,
49
- )
50
- from .yamibo import (
51
- YamiboBrowser,
52
- YamiboSession,
53
- )
54
-
55
9
  __all__ = [
56
- "BiqugeBrowser",
10
+ "get_fetcher",
11
+ "AaatxtSession",
57
12
  "BiqugeSession",
58
- "CommonBrowser",
59
- "CommonSession",
60
- "EsjzoneBrowser",
13
+ "BiquyueduSession",
14
+ "DxmwxSession",
15
+ "EightnovelSession",
61
16
  "EsjzoneSession",
62
- "LinovelibBrowser",
17
+ "GuidayeSession",
18
+ "HetushuSession",
19
+ "I25zwSession",
20
+ "Ixdzs8Session",
21
+ "Jpxs123Session",
22
+ "LewennSession",
63
23
  "LinovelibSession",
64
- "QianbiBrowser",
24
+ "PiaotiaSession",
25
+ "QbtrSession",
65
26
  "QianbiSession",
66
- "QidianBrowser",
67
27
  "QidianSession",
68
- "SfacgBrowser",
28
+ "Quanben5Session",
69
29
  "SfacgSession",
70
- "YamiboBrowser",
30
+ "ShencouSession",
31
+ "ShuhaigeSession",
32
+ "TongrenquanSession",
33
+ "TtkanSession",
34
+ "WanbengoSession",
35
+ "XiaoshuowuSession",
36
+ "XiguashuwuSession",
37
+ "Xs63bSession",
38
+ "XshbookSession",
71
39
  "YamiboSession",
40
+ "YibigeSession",
72
41
  ]
42
+
43
+ from .aaatxt import AaatxtSession
44
+ from .b520 import BiqugeSession
45
+ from .biquyuedu import BiquyueduSession
46
+ from .dxmwx import DxmwxSession
47
+ from .eightnovel import EightnovelSession
48
+ from .esjzone import EsjzoneSession
49
+ from .guidaye import GuidayeSession
50
+ from .hetushu import HetushuSession
51
+ from .i25zw import I25zwSession
52
+ from .ixdzs8 import Ixdzs8Session
53
+ from .jpxs123 import Jpxs123Session
54
+ from .lewenn import LewennSession
55
+ from .linovelib import LinovelibSession
56
+ from .piaotia import PiaotiaSession
57
+ from .qbtr import QbtrSession
58
+ from .qianbi import QianbiSession
59
+ from .qidian import QidianSession
60
+ from .quanben5 import Quanben5Session
61
+ from .registry import get_fetcher
62
+ from .sfacg import SfacgSession
63
+ from .shencou import ShencouSession
64
+ from .shuhaige import ShuhaigeSession
65
+ from .tongrenquan import TongrenquanSession
66
+ from .ttkan import TtkanSession
67
+ from .wanbengo import WanbengoSession
68
+ from .xiaoshuowu import XiaoshuowuSession
69
+ from .xiguashuwu import XiguashuwuSession
70
+ from .xs63b import Xs63bSession
71
+ from .xshbook import XshbookSession
72
+ from .yamibo import YamiboSession
73
+ from .yibige import YibigeSession