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
@@ -17,6 +17,7 @@ from novel_downloader.core.exporters.epub_util import (
17
17
  finalize_export,
18
18
  inline_remote_images,
19
19
  prepare_builder,
20
+ remove_all_images,
20
21
  )
21
22
  from novel_downloader.utils import (
22
23
  download,
@@ -41,7 +42,7 @@ _IMG_HEADERS["Referer"] = "https://www.linovelib.com/"
41
42
  def export_whole_book(
42
43
  exporter: LinovelibExporter,
43
44
  book_id: str,
44
- ) -> None:
45
+ ) -> Path | None:
45
46
  """
46
47
  Export a single novel (identified by `book_id`) to an EPUB file.
47
48
 
@@ -73,7 +74,7 @@ def export_whole_book(
73
74
  # --- Load book_info.json ---
74
75
  book_info = exporter._load_book_info(book_id)
75
76
  if not book_info:
76
- return
77
+ return None
77
78
 
78
79
  book_name = book_info.get("book_name", book_id)
79
80
  book_author = book_info.get("author", "")
@@ -99,7 +100,7 @@ def export_whole_book(
99
100
  title=book_name,
100
101
  author=book_author,
101
102
  description=book_info.get("summary", ""),
102
- subject=book_info.get("subject", []),
103
+ subject=book_info.get("tags", []),
103
104
  serial_status=book_info.get("serial_status", ""),
104
105
  word_count=book_info.get("word_count", ""),
105
106
  cover_path=cover_path,
@@ -118,7 +119,7 @@ def export_whole_book(
118
119
 
119
120
  # Batch-fetch chapters for this volume
120
121
  chap_ids = [
121
- chap.get("chapterId")
122
+ chap["chapterId"]
122
123
  for chap in vol.get("chapters", [])
123
124
  if chap.get("chapterId")
124
125
  ]
@@ -152,7 +153,7 @@ def export_whole_book(
152
153
  )
153
154
  continue
154
155
 
155
- chap_title = cleaner.clean_title(chap_meta.get("title", ""))
156
+ chap_title = chap_meta.get("title", "")
156
157
  data = chap_map.get(chap_id)
157
158
  if not data:
158
159
  exporter.logger.info(
@@ -165,11 +166,10 @@ def export_whole_book(
165
166
 
166
167
  title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
167
168
  content = cleaner.clean_content(data.get("content", ""))
168
- content = inline_remote_images(
169
- book,
170
- content,
171
- img_dir,
172
- headers=_IMG_HEADERS,
169
+ content = (
170
+ inline_remote_images(book, content, img_dir, headers=_IMG_HEADERS)
171
+ if config.include_picture
172
+ else remove_all_images(content)
173
173
  )
174
174
 
175
175
  chap_html = build_epub_chapter(
@@ -177,7 +177,7 @@ def export_whole_book(
177
177
  paragraphs=content,
178
178
  extras={},
179
179
  )
180
- curr_vol.add_chapter(
180
+ curr_vol.chapters.append(
181
181
  Chapter(
182
182
  id=f"c_{chap_id}",
183
183
  filename=f"c{chap_id}.xhtml",
@@ -195,20 +195,19 @@ def export_whole_book(
195
195
  author=book_info.get("author"),
196
196
  ext="epub",
197
197
  )
198
- finalize_export(
198
+ return finalize_export(
199
199
  book=book,
200
200
  out_dir=out_dir,
201
201
  filename=out_name,
202
202
  logger=exporter.logger,
203
203
  tag=TAG,
204
204
  )
205
- return
206
205
 
207
206
 
208
207
  def export_by_volume(
209
208
  exporter: LinovelibExporter,
210
209
  book_id: str,
211
- ) -> None:
210
+ ) -> Path | None:
212
211
  """
213
212
  Export each volume of a novel as a separate EPUB file.
214
213
 
@@ -241,7 +240,7 @@ def export_by_volume(
241
240
  # --- Load book_info.json ---
242
241
  book_info = exporter._load_book_info(book_id)
243
242
  if not book_info:
244
- return
243
+ return None
245
244
 
246
245
  book_name = book_info.get("book_name", book_id)
247
246
  book_author = book_info.get("author", "")
@@ -263,7 +262,7 @@ def export_by_volume(
263
262
 
264
263
  # Batch-fetch chapters for this volume
265
264
  chap_ids = [
266
- chap.get("chapterId")
265
+ chap["chapterId"]
267
266
  for chap in vol.get("chapters", [])
268
267
  if chap.get("chapterId")
269
268
  ]
@@ -286,8 +285,8 @@ def export_by_volume(
286
285
  title=book_name,
287
286
  author=book_author,
288
287
  description=vol.get("volume_intro") or book_summary,
289
- subject=book_info.get("subject", []),
290
- serial_status=vol.get("serial_status", ""),
288
+ subject=book_info.get("tags", []),
289
+ serial_status=book_info.get("serial_status", ""),
291
290
  word_count=vol.get("word_count", ""),
292
291
  cover_path=vol_cover,
293
292
  )
@@ -302,7 +301,7 @@ def export_by_volume(
302
301
  )
303
302
  continue
304
303
 
305
- chap_title = cleaner.clean_title(chap_meta.get("title", ""))
304
+ chap_title = chap_meta.get("title", "")
306
305
  data = chap_map.get(chap_id)
307
306
  if not data:
308
307
  exporter.logger.info(
@@ -315,11 +314,10 @@ def export_by_volume(
315
314
 
316
315
  title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
317
316
  content = cleaner.clean_content(data.get("content", ""))
318
- content = inline_remote_images(
319
- book,
320
- content,
321
- img_dir,
322
- headers=_IMG_HEADERS,
317
+ content = (
318
+ inline_remote_images(book, content, img_dir, headers=_IMG_HEADERS)
319
+ if config.include_picture
320
+ else remove_all_images(content)
323
321
  )
324
322
  chap_html = build_epub_chapter(
325
323
  title=title,
@@ -348,4 +346,4 @@ def export_by_volume(
348
346
  logger=exporter.logger,
349
347
  tag=TAG,
350
348
  )
351
- return
349
+ return None
@@ -3,13 +3,19 @@
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
 
9
+ from pathlib import Path
8
10
 
9
11
  from novel_downloader.core.exporters.base import BaseExporter
10
12
  from novel_downloader.core.exporters.registry import register_exporter
11
13
  from novel_downloader.models import ExporterConfig
12
14
 
15
+ from .epub import (
16
+ export_by_volume,
17
+ export_whole_book,
18
+ )
13
19
  from .txt import linovelib_export_as_txt
14
20
 
15
21
 
@@ -29,7 +35,7 @@ class LinovelibExporter(BaseExporter):
29
35
  """
30
36
  super().__init__(config, "linovelib")
31
37
 
32
- def export_as_txt(self, book_id: str) -> None:
38
+ def export_as_txt(self, book_id: str) -> Path | None:
33
39
  """
34
40
  Compile and export a novel as a single .txt file.
35
41
 
@@ -38,23 +44,13 @@ class LinovelibExporter(BaseExporter):
38
44
  self._init_chapter_storages(book_id)
39
45
  return linovelib_export_as_txt(self, book_id)
40
46
 
41
- def export_as_epub(self, book_id: str) -> None:
47
+ def export_as_epub(self, book_id: str) -> Path | None:
42
48
  """
43
49
  Persist the assembled book as a EPUB (.epub) file.
44
50
 
45
51
  :param book_id: The book identifier.
46
52
  :raises NotImplementedError: If the method is not overridden.
47
53
  """
48
- try:
49
- from .epub import (
50
- export_by_volume,
51
- export_whole_book,
52
- )
53
- except ImportError as err:
54
- raise NotImplementedError(
55
- "EPUB export not supported. Please install 'ebooklib'"
56
- ) from err
57
-
58
54
  self._init_chapter_storages(book_id)
59
55
 
60
56
  exporters = {
@@ -9,13 +9,14 @@ into a single `.txt` file. Intended for use by `LinovelibExporter`.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ from pathlib import Path
12
13
  from typing import TYPE_CHECKING
13
14
 
14
15
  from novel_downloader.core.exporters.txt_util import (
15
16
  build_txt_chapter,
16
17
  build_txt_header,
17
18
  )
18
- from novel_downloader.utils import get_cleaner, save_as_txt
19
+ from novel_downloader.utils import get_cleaner, write_file
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from .main_exporter import LinovelibExporter
@@ -24,17 +25,17 @@ if TYPE_CHECKING:
24
25
  def linovelib_export_as_txt(
25
26
  exporter: LinovelibExporter,
26
27
  book_id: str,
27
- ) -> None:
28
+ ) -> Path | None:
28
29
  """
29
30
  Export a novel as a single text file by merging all chapter data.
30
31
 
31
32
  Steps:
32
33
  1. Read metadata from `book_info.json`.
33
34
  2. For each volume:
34
- - Clean & append the volume title.
35
- - Clean & append optional volume intro.
36
- - Batch-fetch all chapters in this volume to minimize SQLite overhead.
37
- - For each chapter: clean title & content, then append.
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.
38
39
  3. Build a header block with metadata.
39
40
  4. Concatenate header + all chapter blocks, then save as `{book_name}.txt`.
40
41
 
@@ -53,7 +54,7 @@ def linovelib_export_as_txt(
53
54
  # --- Load book_info.json ---
54
55
  book_info = exporter._load_book_info(book_id)
55
56
  if not book_info:
56
- return
57
+ return None
57
58
 
58
59
  # --- Compile chapters ---
59
60
  parts: list[str] = []
@@ -70,7 +71,7 @@ def linovelib_export_as_txt(
70
71
 
71
72
  # Batch-fetch chapters for this volume
72
73
  chap_ids = [
73
- chap.get("chapterId")
74
+ chap["chapterId"]
74
75
  for chap in vol.get("chapters", [])
75
76
  if chap.get("chapterId")
76
77
  ]
@@ -84,7 +85,7 @@ def linovelib_export_as_txt(
84
85
  )
85
86
  continue
86
87
 
87
- chap_title = cleaner.clean_title(chap_meta.get("title", ""))
88
+ chap_title = chap_meta.get("title", "")
88
89
  data = chap_map.get(chap_id)
89
90
  if not data:
90
91
  exporter.logger.info(
@@ -125,9 +126,14 @@ def linovelib_export_as_txt(
125
126
  out_path = out_dir / out_name
126
127
 
127
128
  # --- Save final text ---
128
- result = save_as_txt(content=final_text, filepath=out_path)
129
- if result:
129
+ try:
130
+ result = write_file(
131
+ content=final_text,
132
+ filepath=out_path,
133
+ on_exist="overwrite",
134
+ )
130
135
  exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
131
- else:
132
- exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
133
- return
136
+ except Exception as e:
137
+ exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
138
+ return None
139
+ return result
@@ -3,9 +3,7 @@
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
 
11
9
  __all__ = ["QidianExporter"]
@@ -31,8 +29,4 @@ class QidianExporter(CommonExporter):
31
29
  self,
32
30
  config: ExporterConfig,
33
31
  ):
34
- super().__init__(
35
- config,
36
- site="qidian",
37
- priorities=self.PRIORITIES_MAP,
38
- )
32
+ super().__init__(config, site="qidian")
@@ -3,6 +3,7 @@
3
3
  novel_downloader.core.exporters.registry
4
4
  ----------------------------------------
5
5
 
6
+ Registry and factory helpers for creating site-specific or common exporters.
6
7
  """
7
8
 
8
9
  __all__ = ["register_exporter", "get_exporter"]
@@ -10,6 +11,7 @@ __all__ = ["register_exporter", "get_exporter"]
10
11
  from collections.abc import Callable, Sequence
11
12
  from typing import TypeVar
12
13
 
14
+ from novel_downloader.core.exporters.common import CommonExporter
13
15
  from novel_downloader.core.interfaces import ExporterProtocol
14
16
  from novel_downloader.models import ExporterConfig
15
17
 
@@ -48,6 +50,6 @@ def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
48
50
  site_key = site.lower()
49
51
  try:
50
52
  exporter_cls = _EXPORTER_MAP[site_key]
51
- except KeyError as err:
52
- raise ValueError(f"Unsupported site: {site}") from err
53
+ except KeyError:
54
+ return CommonExporter(config, site_key)
53
55
  return exporter_cls(config)
@@ -13,7 +13,7 @@ __all__ = [
13
13
 
14
14
  import re
15
15
 
16
- _IMG_TAG_RE = re.compile(r"<img[^>]*>")
16
+ _IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
17
17
 
18
18
 
19
19
  def build_txt_header(fields: list[tuple[str, str]]) -> str:
@@ -37,14 +37,14 @@ def build_txt_chapter(
37
37
  Build a formatted chapter text block including title, body paragraphs,
38
38
  and optional extra sections.
39
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.
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
43
 
44
- :param title: Chapter title.
44
+ :param title: Chapter title.
45
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.
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
48
  """
49
49
  parts: list[str] = [title.strip()]
50
50
 
@@ -3,65 +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 (百合会)
6
+ Fetcher implementations for retrieving raw data and HTML from various novel sources
19
7
  """
20
8
 
21
9
  __all__ = [
22
10
  "get_fetcher",
23
- "BiqugeBrowser",
11
+ "AaatxtSession",
24
12
  "BiqugeSession",
25
- "EsjzoneBrowser",
13
+ "BiquyueduSession",
14
+ "DxmwxSession",
15
+ "EightnovelSession",
26
16
  "EsjzoneSession",
27
- "LinovelibBrowser",
17
+ "GuidayeSession",
18
+ "HetushuSession",
19
+ "I25zwSession",
20
+ "Ixdzs8Session",
21
+ "Jpxs123Session",
22
+ "LewennSession",
28
23
  "LinovelibSession",
29
- "QianbiBrowser",
24
+ "PiaotiaSession",
25
+ "QbtrSession",
30
26
  "QianbiSession",
31
- "QidianBrowser",
32
27
  "QidianSession",
33
- "SfacgBrowser",
28
+ "Quanben5Session",
34
29
  "SfacgSession",
35
- "YamiboBrowser",
30
+ "ShencouSession",
31
+ "ShuhaigeSession",
32
+ "TongrenquanSession",
33
+ "TtkanSession",
34
+ "WanbengoSession",
35
+ "XiaoshuowuSession",
36
+ "XiguashuwuSession",
37
+ "Xs63bSession",
38
+ "XshbookSession",
36
39
  "YamiboSession",
40
+ "YibigeSession",
37
41
  ]
38
42
 
39
- from .biquge import (
40
- BiqugeBrowser,
41
- BiqugeSession,
42
- )
43
- from .esjzone import (
44
- EsjzoneBrowser,
45
- EsjzoneSession,
46
- )
47
- from .linovelib import (
48
- LinovelibBrowser,
49
- LinovelibSession,
50
- )
51
- from .qianbi import (
52
- QianbiBrowser,
53
- QianbiSession,
54
- )
55
- from .qidian import (
56
- QidianBrowser,
57
- QidianSession,
58
- )
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
59
61
  from .registry import get_fetcher
60
- from .sfacg import (
61
- SfacgBrowser,
62
- SfacgSession,
63
- )
64
- from .yamibo import (
65
- YamiboBrowser,
66
- YamiboSession,
67
- )
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
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.aaatxt
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from novel_downloader.core.fetchers.base import BaseSession
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
+ from novel_downloader.models import FetcherConfig
13
+
14
+
15
+ @register_fetcher(
16
+ site_keys=["aaatxt"],
17
+ )
18
+ class AaatxtSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 3A电子书 (www.aaatxt.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "http://www.aaatxt.com/shu/{book_id}.html"
24
+ CHAPTER_URL = "http://www.aaatxt.com/yuedu/{chapter_id}.html"
25
+
26
+ def __init__(
27
+ self,
28
+ config: FetcherConfig,
29
+ cookies: dict[str, str] | None = None,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ super().__init__("aaatxt", config, cookies, **kwargs)
33
+
34
+ async def get_book_info(
35
+ self,
36
+ book_id: str,
37
+ **kwargs: Any,
38
+ ) -> list[str]:
39
+ """
40
+ Fetch the raw HTML of the book info page asynchronously.
41
+
42
+ :param book_id: The book identifier.
43
+ :return: The page content as string list.
44
+ """
45
+ url = self.book_info_url(book_id=book_id)
46
+ return [await self.fetch(url, **kwargs)]
47
+
48
+ async def get_book_chapter(
49
+ self,
50
+ book_id: str,
51
+ chapter_id: str,
52
+ **kwargs: Any,
53
+ ) -> list[str]:
54
+ """
55
+ Fetch the raw HTML of a single chapter asynchronously.
56
+
57
+ :param book_id: The book identifier.
58
+ :param chapter_id: The chapter identifier.
59
+ :return: The page content as string list.
60
+ """
61
+ url = self.chapter_url(chapter_id=chapter_id)
62
+ return [await self.fetch(url, encoding="gb2312", **kwargs)]
63
+
64
+ @classmethod
65
+ def book_info_url(cls, book_id: str) -> str:
66
+ """
67
+ Construct the URL for fetching a book's info page.
68
+
69
+ :param book_id: The identifier of the book.
70
+ :return: Fully qualified URL for the book info page.
71
+ """
72
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
73
+
74
+ @classmethod
75
+ def chapter_url(cls, chapter_id: str) -> str:
76
+ """
77
+ Construct the URL for fetching a specific chapter.
78
+
79
+ :param book_id: The identifier of the book.
80
+ :param chapter_id: The identifier of the chapter.
81
+ :return: Fully qualified chapter URL.
82
+ """
83
+ return cls.CHAPTER_URL.format(chapter_id=chapter_id)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.biquge.session
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.b520
4
+ -----------------------------------
5
5
 
6
6
  """
7
7
 
@@ -13,12 +13,11 @@ from novel_downloader.models import FetcherConfig
13
13
 
14
14
 
15
15
  @register_fetcher(
16
- site_keys=["biquge", "bqg"],
17
- backends=["session"],
16
+ site_keys=["biquge", "bqg", "b520"],
18
17
  )
19
18
  class BiqugeSession(BaseSession):
20
19
  """
21
- A session class for interacting with the Biquge (www.b520.cc) novel website.
20
+ A session class for interacting with the 笔趣阁 (www.b520.cc) novel website.
22
21
  """
23
22
 
24
23
  BOOK_INFO_URL = "http://www.b520.cc/{book_id}/"
@@ -41,7 +40,7 @@ class BiqugeSession(BaseSession):
41
40
  Fetch the raw HTML of the book info page asynchronously.
42
41
 
43
42
  :param book_id: The book identifier.
44
- :return: The page content as a string.
43
+ :return: The page content as string list.
45
44
  """
46
45
  url = self.book_info_url(book_id=book_id)
47
46
  return [await self.fetch(url, **kwargs)]
@@ -57,7 +56,7 @@ class BiqugeSession(BaseSession):
57
56
 
58
57
  :param book_id: The book identifier.
59
58
  :param chapter_id: The chapter identifier.
60
- :return: The chapter content as a string.
59
+ :return: The page content as string list.
61
60
  """
62
61
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
63
62
  return [await self.fetch(url, encoding="gbk", **kwargs)]
@@ -82,7 +81,3 @@ class BiqugeSession(BaseSession):
82
81
  :return: Fully qualified chapter URL.
83
82
  """
84
83
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
85
-
86
- @property
87
- def hostname(self) -> str:
88
- return "www.b520.cc"