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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/archived/qidian/searcher.py +79 -0
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,26 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.esjzone.session
4
- ----------------------------------------------
3
+ novel_downloader.core.fetchers.esjzone
4
+ --------------------------------------
5
5
 
6
6
  """
7
7
 
8
8
  import re
9
+ from collections.abc import Mapping
9
10
  from typing import Any
10
11
 
11
12
  from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
14
  from novel_downloader.models import FetcherConfig, LoginField
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
15
+ from novel_downloader.utils import async_jitter_sleep
14
16
 
15
17
 
18
+ @register_fetcher(
19
+ site_keys=["esjzone"],
20
+ )
16
21
  class EsjzoneSession(BaseSession):
17
22
  """
18
- A session class for interacting with the esjzone (www.esjzone.cc) novel website.
23
+ A session class for interacting with the ESJ Zone (www.esjzone.cc) novel website.
19
24
  """
20
25
 
21
26
  BOOKCASE_URL = "https://www.esjzone.cc/my/favorite"
@@ -63,7 +68,7 @@ class EsjzoneSession(BaseSession):
63
68
  ):
64
69
  self._is_logged_in = True
65
70
  return True
66
- await async_sleep_with_random_delay(
71
+ await async_jitter_sleep(
67
72
  self.backoff_factor,
68
73
  mul_spread=1.1,
69
74
  max_sleep=self.backoff_factor + 2,
@@ -81,7 +86,7 @@ class EsjzoneSession(BaseSession):
81
86
  Fetch the raw HTML of the book info page asynchronously.
82
87
 
83
88
  :param book_id: The book identifier.
84
- :return: The page content as a string.
89
+ :return: The page content as string list.
85
90
  """
86
91
  url = self.book_info_url(book_id=book_id)
87
92
  return [await self.fetch(url, **kwargs)]
@@ -97,7 +102,7 @@ class EsjzoneSession(BaseSession):
97
102
 
98
103
  :param book_id: The book identifier.
99
104
  :param chapter_id: The chapter identifier.
100
- :return: The chapter content as a string.
105
+ :return: The page content as string list.
101
106
  """
102
107
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
103
108
  return [await self.fetch(url, **kwargs)]
@@ -165,10 +170,6 @@ class EsjzoneSession(BaseSession):
165
170
  """
166
171
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
167
172
 
168
- @property
169
- def hostname(self) -> str:
170
- return "www.esjzone.cc"
171
-
172
173
  async def _api_login(self, username: str, password: str) -> bool:
173
174
  """
174
175
  Login to the API using a 2-step token-based process.
@@ -229,3 +230,14 @@ class EsjzoneSession(BaseSession):
229
230
  def _extract_token(self, text: str) -> str:
230
231
  match = re.search(r"<JinJing>(.+?)</JinJing>", text)
231
232
  return match.group(1) if match else ""
233
+
234
+ @staticmethod
235
+ def _filter_cookies(
236
+ raw_cookies: list[Mapping[str, Any]],
237
+ ) -> dict[str, str]:
238
+ ALLOWED_DOMAINS = {".www.esjzone.cc", "www.esjzone.cc", ".esjzone.cc", ""}
239
+ return {
240
+ c["name"]: c["value"]
241
+ for c in raw_cookies
242
+ if c.get("domain", "") in ALLOWED_DOMAINS
243
+ }
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.guidaye
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=["guidaye"],
17
+ )
18
+ class GuidayeSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 名著阅读 (b.guidaye.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://b.guidaye.com/{book_id}/"
24
+ CHAPTER_URL = "https://b.guidaye.com/{book_id}/{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__("guidaye", 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
+ book_id = book_id.replace("-", "/")
46
+ url = self.book_info_url(book_id=book_id)
47
+ return [await self.fetch(url, **kwargs)]
48
+
49
+ async def get_book_chapter(
50
+ self,
51
+ book_id: str,
52
+ chapter_id: str,
53
+ **kwargs: Any,
54
+ ) -> list[str]:
55
+ """
56
+ Fetch the raw HTML of a single chapter asynchronously.
57
+
58
+ :param book_id: The book identifier.
59
+ :param chapter_id: The chapter identifier.
60
+ :return: The page content as string list.
61
+ """
62
+ book_id = book_id.replace("-", "/")
63
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
64
+ return [await self.fetch(url, **kwargs)]
65
+
66
+ @classmethod
67
+ def book_info_url(cls, book_id: str) -> str:
68
+ """
69
+ Construct the URL for fetching a book's info page.
70
+
71
+ :param book_id: The identifier of the book.
72
+ :return: Fully qualified URL for the book info page.
73
+ """
74
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
75
+
76
+ @classmethod
77
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
78
+ """
79
+ Construct the URL for fetching a specific chapter.
80
+
81
+ :param book_id: The identifier of the book.
82
+ :param chapter_id: The identifier of the chapter.
83
+ :return: Fully qualified chapter URL.
84
+ """
85
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.hetushu
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=["hetushu"],
17
+ )
18
+ class HetushuSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 和图书 (www.hetushu.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://{base_url}/book/{book_id}/index.html"
24
+ CHAPTER_URL = "https://{base_url}/book/{book_id}/{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__("hetushu", config, cookies, **kwargs)
33
+ self.base_url = (
34
+ "www.hetushu.com"
35
+ if config.locale_style == "simplified"
36
+ else "www.hetubook.com"
37
+ )
38
+
39
+ async def get_book_info(
40
+ self,
41
+ book_id: str,
42
+ **kwargs: Any,
43
+ ) -> list[str]:
44
+ """
45
+ Fetch the raw HTML of the book info page asynchronously.
46
+
47
+ :param book_id: The book identifier.
48
+ :return: The page content as string list.
49
+ """
50
+ url = self.book_info_url(base_url=self.base_url, book_id=book_id)
51
+ return [await self.fetch(url, **kwargs)]
52
+
53
+ async def get_book_chapter(
54
+ self,
55
+ book_id: str,
56
+ chapter_id: str,
57
+ **kwargs: Any,
58
+ ) -> list[str]:
59
+ """
60
+ Fetch the raw HTML of a single chapter asynchronously.
61
+
62
+ :param book_id: The book identifier.
63
+ :param chapter_id: The chapter identifier.
64
+ :return: The page content as string list.
65
+ """
66
+ url = self.chapter_url(
67
+ base_url=self.base_url, book_id=book_id, chapter_id=chapter_id
68
+ )
69
+ return [await self.fetch(url, **kwargs)]
70
+
71
+ @classmethod
72
+ def book_info_url(cls, base_url: str, book_id: str) -> str:
73
+ """
74
+ Construct the URL for fetching a book's info page.
75
+
76
+ :param book_id: The identifier of the book.
77
+ :return: Fully qualified URL for the book info page.
78
+ """
79
+ return cls.BOOK_INFO_URL.format(base_url=base_url, book_id=book_id)
80
+
81
+ @classmethod
82
+ def chapter_url(cls, base_url: str, book_id: str, chapter_id: str) -> str:
83
+ """
84
+ Construct the URL for fetching a specific chapter.
85
+
86
+ :param book_id: The identifier of the book.
87
+ :param chapter_id: The identifier of the chapter.
88
+ :return: Fully qualified chapter URL.
89
+ """
90
+ return cls.CHAPTER_URL.format(
91
+ base_url=base_url, book_id=book_id, chapter_id=chapter_id
92
+ )
@@ -1,37 +1,37 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.qianbi.browser
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.i25zw
4
+ ------------------------------------
5
5
 
6
6
  """
7
7
 
8
+ import asyncio
8
9
  from typing import Any
9
10
 
10
- from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
13
  from novel_downloader.models import FetcherConfig
12
14
 
13
15
 
14
- class QianbiBrowser(BaseBrowser):
16
+ @register_fetcher(
17
+ site_keys=["i25zw"],
18
+ )
19
+ class I25zwSession(BaseSession):
15
20
  """
16
- A browser class for interacting with the Qianbi (www.23qb.com) novel website.
21
+ A session class for interacting with the 25中文网 (www.i25zw.com) novel website.
17
22
  """
18
23
 
19
- BASE_URLS = [
20
- "www.23qb.com",
21
- "www.23qb.net",
22
- ]
23
-
24
- BOOK_INFO_URL = "https://www.23qb.com/book/{book_id}/"
25
- BOOK_CATALOG_URL = "https://www.23qb.com/book/{book_id}/catalog"
26
- CHAPTER_URL = "https://www.23qb.com/book/{book_id}/{chapter_id}.html"
24
+ BOOK_INFO_URL = "https://www.i25zw.com/book/{book_id}.html"
25
+ BOOK_CATALOG_URL = "https://www.i25zw.com/{book_id}/"
26
+ CHAPTER_URL = "https://www.i25zw.com/{book_id}/{chapter_id}.html"
27
27
 
28
28
  def __init__(
29
29
  self,
30
30
  config: FetcherConfig,
31
- reuse_page: bool = False,
31
+ cookies: dict[str, str] | None = None,
32
32
  **kwargs: Any,
33
33
  ) -> None:
34
- super().__init__("qianbi", config, reuse_page, **kwargs)
34
+ super().__init__("i25zw", config, cookies, **kwargs)
35
35
 
36
36
  async def get_book_info(
37
37
  self,
@@ -44,14 +44,15 @@ class QianbiBrowser(BaseBrowser):
44
44
  Order: [info, catalog]
45
45
 
46
46
  :param book_id: The book identifier.
47
- :return: The page content as a string.
47
+ :return: The page content as string list.
48
48
  """
49
49
  info_url = self.book_info_url(book_id=book_id)
50
50
  catalog_url = self.book_catalog_url(book_id=book_id)
51
51
 
52
- info_html = await self.fetch(info_url, **kwargs)
53
- catalog_html = await self.fetch(catalog_url, **kwargs)
54
-
52
+ info_html, catalog_html = await asyncio.gather(
53
+ self.fetch(info_url, **kwargs),
54
+ self.fetch(catalog_url, **kwargs),
55
+ )
55
56
  return [info_html, catalog_html]
56
57
 
57
58
  async def get_book_chapter(
@@ -65,11 +66,10 @@ class QianbiBrowser(BaseBrowser):
65
66
 
66
67
  :param book_id: The book identifier.
67
68
  :param chapter_id: The chapter identifier.
68
- :return: The chapter content as a string.
69
+ :return: The page content as string list.
69
70
  """
70
- catalog_url = self.book_catalog_url(book_id=book_id)
71
71
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
72
- return [await self.fetch(url, referer=catalog_url, **kwargs)]
72
+ return [await self.fetch(url, **kwargs)]
73
73
 
74
74
  @classmethod
75
75
  def book_info_url(cls, book_id: str) -> str:
@@ -101,7 +101,3 @@ class QianbiBrowser(BaseBrowser):
101
101
  :return: Fully qualified chapter URL.
102
102
  """
103
103
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
104
-
105
- @property
106
- def hostname(self) -> str:
107
- return "www.23qb.com"
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.ixdzs8
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ import asyncio
9
+ import re
10
+ from typing import Any
11
+
12
+ from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
14
+ from novel_downloader.models import FetcherConfig
15
+
16
+
17
+ @register_fetcher(
18
+ site_keys=["ixdzs8"],
19
+ )
20
+ class Ixdzs8Session(BaseSession):
21
+ """
22
+ A session class for interacting with the 爱下电子书 (ixdzs8.com) novel website.
23
+ """
24
+
25
+ BOOK_INFO_URL = "https://ixdzs8.com/read/{book_id}/"
26
+ BOOK_CATALOG_URL = "https://ixdzs8.com/novel/clist/"
27
+ CHAPTER_URL = "https://ixdzs8.com/read/{book_id}/{chapter_id}.html"
28
+ _TOKEN_PATTERN = re.compile(r'let\s+token\s*=\s*"([^"]+)"')
29
+
30
+ def __init__(
31
+ self,
32
+ config: FetcherConfig,
33
+ cookies: dict[str, str] | None = None,
34
+ **kwargs: Any,
35
+ ) -> None:
36
+ super().__init__("ixdzs8", config, cookies, **kwargs)
37
+
38
+ async def get_book_info(
39
+ self,
40
+ book_id: str,
41
+ **kwargs: Any,
42
+ ) -> list[str]:
43
+ """
44
+ Fetch the raw HTML of the book info page asynchronously.
45
+
46
+ Order: [info, catalog]
47
+
48
+ :param book_id: The book identifier.
49
+ :return: The page content as string list.
50
+ """
51
+ url = self.book_info_url(book_id=book_id)
52
+ data = {"bid": book_id}
53
+ info_html, clist_response = await asyncio.gather(
54
+ self.fetch_verified_html(url, **kwargs),
55
+ self.post(self.BOOK_CATALOG_URL, data),
56
+ )
57
+ catalog_html = await clist_response.text()
58
+ return [info_html, catalog_html]
59
+
60
+ async def get_book_chapter(
61
+ self,
62
+ book_id: str,
63
+ chapter_id: str,
64
+ **kwargs: Any,
65
+ ) -> list[str]:
66
+ """
67
+ Fetch the raw HTML of a single chapter asynchronously.
68
+
69
+ :param book_id: The book identifier.
70
+ :param chapter_id: The chapter identifier.
71
+ :return: The page content as string list.
72
+ """
73
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
74
+ return [await self.fetch_verified_html(url, **kwargs)]
75
+
76
+ @classmethod
77
+ def book_info_url(cls, book_id: str) -> str:
78
+ """
79
+ Construct the URL for fetching a book's info page.
80
+
81
+ :param book_id: The identifier of the book.
82
+ :return: Fully qualified URL for the book info page.
83
+ """
84
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
85
+
86
+ @classmethod
87
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
88
+ """
89
+ Construct the URL for fetching a specific chapter.
90
+
91
+ :param book_id: The identifier of the book.
92
+ :param chapter_id: The identifier of the chapter.
93
+ :return: Fully qualified chapter URL.
94
+ """
95
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
96
+
97
+ async def fetch_verified_html(self, url: str, **kwargs: Any) -> str:
98
+ """
99
+ Automatically solving the browser verification challenge if required.
100
+ """
101
+ resp = await self.fetch(url, **kwargs)
102
+
103
+ if "正在验证浏览器" not in resp:
104
+ return resp
105
+
106
+ token_match = self._TOKEN_PATTERN.search(resp)
107
+ if not token_match:
108
+ raise ValueError("Token not found in page HTML.")
109
+ token_value = token_match.group(1)
110
+
111
+ challenge_url = f"{url}?challenge={token_value}"
112
+ _ = await self.fetch(challenge_url, **kwargs)
113
+ return await self.fetch(url, **kwargs)
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.jpxs123
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
14
+ from novel_downloader.models import FetcherConfig
15
+
16
+
17
+ @register_fetcher(
18
+ site_keys=["jpxs123"],
19
+ )
20
+ class Jpxs123Session(BaseSession):
21
+ """
22
+ A session class for interacting with the 精品小说网 (www.jpxs123.com) novel website.
23
+ """
24
+
25
+ BASE_URL = "https://www.jpxs123.com"
26
+ BOOK_INFO_URL = "https://www.jpxs123.com/{book_id}.html"
27
+ CHAPTER_URL = "https://www.jpxs123.com/{book_id}/{chapter_id}.html"
28
+
29
+ def __init__(
30
+ self,
31
+ config: FetcherConfig,
32
+ cookies: dict[str, str] | None = None,
33
+ **kwargs: Any,
34
+ ) -> None:
35
+ super().__init__("jpxs123", config, cookies, **kwargs)
36
+
37
+ async def get_book_info(
38
+ self,
39
+ book_id: str,
40
+ **kwargs: Any,
41
+ ) -> list[str]:
42
+ """
43
+ Fetch the raw HTML of the book info page asynchronously.
44
+
45
+ Order: [info, download]
46
+
47
+ :param book_id: The book identifier.
48
+ :return: The page content as string list.
49
+ """
50
+ book_id = book_id.replace("-", "/")
51
+ url = self.book_info_url(book_id=book_id)
52
+ info_html = await self.fetch(url, **kwargs)
53
+ try:
54
+ info_tree = html.fromstring(info_html)
55
+ txt_link = info_tree.xpath(
56
+ '//div[@class="booktips"]//a[contains(text(), "txt下载")]/@href'
57
+ )
58
+ download_url = f"{self.BASE_URL}{txt_link[0]}" if txt_link else None
59
+ except Exception:
60
+ download_url = None
61
+
62
+ download_html = await self.fetch(download_url, **kwargs) if download_url else ""
63
+ return [info_html, download_html]
64
+
65
+ async def get_book_chapter(
66
+ self,
67
+ book_id: str,
68
+ chapter_id: str,
69
+ **kwargs: Any,
70
+ ) -> list[str]:
71
+ """
72
+ Fetch the raw HTML of a single chapter asynchronously.
73
+
74
+ :param book_id: The book identifier.
75
+ :param chapter_id: The chapter identifier.
76
+ :return: The page content as string list.
77
+ """
78
+ book_id = book_id.replace("-", "/")
79
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
80
+ return [await self.fetch(url, **kwargs)]
81
+
82
+ @classmethod
83
+ def book_info_url(cls, book_id: str) -> str:
84
+ """
85
+ Construct the URL for fetching a book's info page.
86
+
87
+ :param book_id: The identifier of the book.
88
+ :return: Fully qualified URL for the book info page.
89
+ """
90
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
91
+
92
+ @classmethod
93
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
94
+ """
95
+ Construct the URL for fetching a specific chapter.
96
+
97
+ :param book_id: The identifier of the book.
98
+ :param chapter_id: The identifier of the chapter.
99
+ :return: Fully qualified chapter URL.
100
+ """
101
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -1,31 +1,35 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.biquge.browser
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.lewenn
4
+ -------------------------------------
5
5
 
6
6
  """
7
7
 
8
8
  from typing import Any
9
9
 
10
- from novel_downloader.core.fetchers.base import BaseBrowser
10
+ from novel_downloader.core.fetchers.base import BaseSession
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
11
12
  from novel_downloader.models import FetcherConfig
12
13
 
13
14
 
14
- class BiqugeBrowser(BaseBrowser):
15
+ @register_fetcher(
16
+ site_keys=["lewenn", "lewen"],
17
+ )
18
+ class LewennSession(BaseSession):
15
19
  """
16
- A browser class for interacting with the Biquge (www.b520.cc) novel website.
20
+ A session class for interacting with the 乐文小说网 (www.lewenn.net) novel website.
17
21
  """
18
22
 
19
- BOOK_INFO_URL = "http://www.b520.cc/{book_id}/"
20
- CHAPTER_URL = "http://www.b520.cc/{book_id}/{chapter_id}.html"
23
+ BOOK_INFO_URL = "https://www.lewenn.net/{book_id}/"
24
+ CHAPTER_URL = "https://www.lewenn.net/{book_id}/{chapter_id}.html"
21
25
 
22
26
  def __init__(
23
27
  self,
24
28
  config: FetcherConfig,
25
- reuse_page: bool = False,
29
+ cookies: dict[str, str] | None = None,
26
30
  **kwargs: Any,
27
31
  ) -> None:
28
- super().__init__("biquge", config, reuse_page, **kwargs)
32
+ super().__init__("lewenn", config, cookies, **kwargs)
29
33
 
30
34
  async def get_book_info(
31
35
  self,
@@ -36,7 +40,7 @@ class BiqugeBrowser(BaseBrowser):
36
40
  Fetch the raw HTML of the book info page asynchronously.
37
41
 
38
42
  :param book_id: The book identifier.
39
- :return: The page content as a string.
43
+ :return: The page content as string list.
40
44
  """
41
45
  url = self.book_info_url(book_id=book_id)
42
46
  return [await self.fetch(url, **kwargs)]
@@ -52,7 +56,7 @@ class BiqugeBrowser(BaseBrowser):
52
56
 
53
57
  :param book_id: The book identifier.
54
58
  :param chapter_id: The chapter identifier.
55
- :return: The chapter content as a string.
59
+ :return: The page content as string list.
56
60
  """
57
61
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
58
62
  return [await self.fetch(url, **kwargs)]
@@ -77,7 +81,3 @@ class BiqugeBrowser(BaseBrowser):
77
81
  :return: Fully qualified chapter URL.
78
82
  """
79
83
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
80
-
81
- @property
82
- def hostname(self) -> str:
83
- return "www.b520.cc"