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,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.linovelib.session
4
- ------------------------------------------------
3
+ novel_downloader.core.fetchers.linovelib
4
+ ----------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -9,13 +9,17 @@ import re
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
14
+ from novel_downloader.utils import async_jitter_sleep
14
15
 
15
16
 
17
+ @register_fetcher(
18
+ site_keys=["linovelib"],
19
+ )
16
20
  class LinovelibSession(BaseSession):
17
21
  """
18
- A session class for interacting with Linovelib (www.linovelib.com) novel website.
22
+ A session class for interacting with 哔哩轻小说 (www.linovelib.com) novel website.
19
23
  """
20
24
 
21
25
  BASE_URL = "https://www.linovelib.com"
@@ -42,8 +46,10 @@ class LinovelibSession(BaseSession):
42
46
  """
43
47
  Fetch the raw HTML of the book info page.
44
48
 
49
+ Order: [info, vol1_html, ..., volN_html]
50
+
45
51
  :param book_id: The book identifier.
46
- :return: A list of HTML strings: [info_html, vol1_html, ..., volN_html]
52
+ :return: The page content as string list.
47
53
  """
48
54
  url = self.book_info_url(book_id=book_id)
49
55
  info_html = await self.fetch(url, **kwargs)
@@ -57,7 +63,7 @@ class LinovelibSession(BaseSession):
57
63
 
58
64
  vol_htmls = []
59
65
  for vol_id in vol_ids:
60
- await async_sleep_with_random_delay(
66
+ await async_jitter_sleep(
61
67
  self.request_interval,
62
68
  mul_spread=1.1,
63
69
  max_sleep=self.request_interval + 2,
@@ -93,9 +99,11 @@ class LinovelibSession(BaseSession):
93
99
  """
94
100
  Fetch the raw HTML of a single chapter asynchronously.
95
101
 
102
+ Order: [page1, ..., pageN]
103
+
96
104
  :param book_id: The book identifier.
97
105
  :param chapter_id: The chapter identifier.
98
- :return: The chapter content as a string.
106
+ :return: The page content as string list.
99
107
  """
100
108
  html_pages: list[str] = []
101
109
  idx = 1
@@ -121,7 +129,7 @@ class LinovelibSession(BaseSession):
121
129
 
122
130
  html_pages.append(html)
123
131
  idx += 1
124
- await async_sleep_with_random_delay(
132
+ await async_jitter_sleep(
125
133
  self.request_interval,
126
134
  mul_spread=1.1,
127
135
  max_sleep=self.request_interval + 2,
@@ -178,10 +186,6 @@ class LinovelibSession(BaseSession):
178
186
  """
179
187
  return f"/novel/{book_id}/{chapter_id}.html"
180
188
 
181
- @property
182
- def hostname(self) -> str:
183
- return "www.linovelib.com"
184
-
185
189
  def _extract_vol_ids(self, html_str: str) -> list[str]:
186
190
  """
187
191
  Extract volume IDs (like 'vol_12345') from the info HTML.
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.piaotia
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ import asyncio
9
+ from typing import Any
10
+
11
+ from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
13
+ from novel_downloader.models import FetcherConfig
14
+
15
+
16
+ @register_fetcher(
17
+ site_keys=["piaotia"],
18
+ )
19
+ class PiaotiaSession(BaseSession):
20
+ """
21
+ A session class for interacting with the 飘天文学网 (www.piaotia.com) novel website.
22
+ """
23
+
24
+ BOOK_INFO_URL = "https://www.piaotia.com/bookinfo/{book_id}.html"
25
+ BOOK_CATALOG_URL = "https://www.piaotia.com/html/{book_id}/index.html"
26
+ CHAPTER_URL = "https://www.piaotia.com/html/{book_id}/{chapter_id}.html"
27
+
28
+ def __init__(
29
+ self,
30
+ config: FetcherConfig,
31
+ cookies: dict[str, str] | None = None,
32
+ **kwargs: Any,
33
+ ) -> None:
34
+ super().__init__("piaotia", config, cookies, **kwargs)
35
+
36
+ async def get_book_info(
37
+ self,
38
+ book_id: str,
39
+ **kwargs: Any,
40
+ ) -> list[str]:
41
+ """
42
+ Fetch the raw HTML of the book info page asynchronously.
43
+
44
+ Order: [info, catalog]
45
+
46
+ :param book_id: The book identifier.
47
+ :return: The page content as string list.
48
+ """
49
+ book_id = book_id.replace("-", "/")
50
+ info_url = self.book_info_url(book_id=book_id)
51
+ catalog_url = self.book_catalog_url(book_id=book_id)
52
+
53
+ info_html, catalog_html = await asyncio.gather(
54
+ self.fetch(info_url, **kwargs),
55
+ self.fetch(catalog_url, **kwargs),
56
+ )
57
+ return [info_html, catalog_html]
58
+
59
+ async def get_book_chapter(
60
+ self,
61
+ book_id: str,
62
+ chapter_id: str,
63
+ **kwargs: Any,
64
+ ) -> list[str]:
65
+ """
66
+ Fetch the raw HTML of a single chapter asynchronously.
67
+
68
+ :param book_id: The book identifier.
69
+ :param chapter_id: The chapter identifier.
70
+ :return: The page content as string list.
71
+ """
72
+ book_id = book_id.replace("-", "/")
73
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
74
+ return [await self.fetch(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 book_catalog_url(cls, book_id: str) -> str:
88
+ """
89
+ Construct the URL for fetching a book's catalog page.
90
+
91
+ :param book_id: The identifier of the book.
92
+ :return: Fully qualified catalog page URL.
93
+ """
94
+ return cls.BOOK_CATALOG_URL.format(book_id=book_id)
95
+
96
+ @classmethod
97
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
98
+ """
99
+ Construct the URL for fetching a specific chapter.
100
+
101
+ :param book_id: The identifier of the book.
102
+ :param chapter_id: The identifier of the chapter.
103
+ :return: Fully qualified chapter URL.
104
+ """
105
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.qbtr
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=["qbtr"],
19
+ )
20
+ class QbtrSession(BaseSession):
21
+ """
22
+ A session class for interacting with the 全本同人小说 (www.qbtr.cc) novel website.
23
+ """
24
+
25
+ BASE_URL = "https://www.qbtr.cc"
26
+ BOOK_INFO_URL = "https://www.qbtr.cc/{book_id}.html"
27
+ CHAPTER_URL = "https://www.qbtr.cc/{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__("qbtr", 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"]/h3/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,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.qianbi.session
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.qianbi
4
+ -------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -9,12 +9,16 @@ import asyncio
9
9
  from typing import Any
10
10
 
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
14
 
14
15
 
16
+ @register_fetcher(
17
+ site_keys=["qianbi"],
18
+ )
15
19
  class QianbiSession(BaseSession):
16
20
  """
17
- A session class for interacting with the Qianbi (www.23qb.com) novel website.
21
+ A session class for interacting with the 铅笔小说 (www.23qb.com) novel website.
18
22
  """
19
23
 
20
24
  BASE_URLS = [
@@ -45,7 +49,7 @@ class QianbiSession(BaseSession):
45
49
  Order: [info, catalog]
46
50
 
47
51
  :param book_id: The book identifier.
48
- :return: The page content as a string.
52
+ :return: The page content as string list.
49
53
  """
50
54
  info_url = self.book_info_url(book_id=book_id)
51
55
  catalog_url = self.book_catalog_url(book_id=book_id)
@@ -67,7 +71,7 @@ class QianbiSession(BaseSession):
67
71
 
68
72
  :param book_id: The book identifier.
69
73
  :param chapter_id: The chapter identifier.
70
- :return: The chapter content as a string.
74
+ :return: The page content as string list.
71
75
  """
72
76
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
73
77
  return [await self.fetch(url, **kwargs)]
@@ -102,7 +106,3 @@ class QianbiSession(BaseSession):
102
106
  :return: Fully qualified chapter URL.
103
107
  """
104
108
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
105
-
106
- @property
107
- def hostname(self) -> str:
108
- return "www.23qb.com"
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.qidian.session
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.qidian
4
+ -------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -10,24 +10,30 @@ import hashlib
10
10
  import json
11
11
  import random
12
12
  import time
13
+ from collections.abc import Mapping
13
14
  from typing import Any, ClassVar
14
15
 
15
16
  import aiohttp
16
17
 
17
18
  from novel_downloader.core.fetchers.base import BaseSession
19
+ from novel_downloader.core.fetchers.registry import register_fetcher
18
20
  from novel_downloader.models import FetcherConfig, LoginField
19
- from novel_downloader.utils.crypto_utils import rc4_crypt
20
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
21
+ from novel_downloader.utils import (
22
+ async_jitter_sleep,
23
+ rc4_crypt,
24
+ )
21
25
 
22
26
 
27
+ @register_fetcher(
28
+ site_keys=["qidian", "qd"],
29
+ )
23
30
  class QidianSession(BaseSession):
24
31
  """
25
- A session class for interacting with the Qidian (www.qidian.com) novel website.
32
+ A session class for interacting with the 起点中文网 (www.qidian.com) novel website.
26
33
  """
27
34
 
28
35
  HOMEPAGE_URL = "https://www.qidian.com/"
29
36
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
30
- # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
31
37
  BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
32
38
  CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
33
39
 
@@ -48,11 +54,11 @@ class QidianSession(BaseSession):
48
54
  **kwargs: Any,
49
55
  ) -> None:
50
56
  super().__init__("qidian", config, cookies, **kwargs)
51
- self._fp_key = _d("ZmluZ2VycHJpbnQ=")
52
- self._ab_key = _d("YWJub3JtYWw=")
53
- self._ck_key = _d("Y2hlY2tzdW0=")
54
- self._lt_key = _d("bG9hZHRz")
55
- self._ts_key = _d("dGltZXN0YW1w")
57
+ self._fp_key = self._d("ZmluZ2VycHJpbnQ=")
58
+ self._ab_key = self._d("YWJub3JtYWw=")
59
+ self._ck_key = self._d("Y2hlY2tzdW0=")
60
+ self._lt_key = self._d("bG9hZHRz")
61
+ self._ts_key = self._d("dGltZXN0YW1w")
56
62
  self._fp_val: str = ""
57
63
  self._ab_val: str = ""
58
64
 
@@ -83,7 +89,7 @@ class QidianSession(BaseSession):
83
89
  Fetch the raw HTML of the book info page asynchronously.
84
90
 
85
91
  :param book_id: The book identifier.
86
- :return: The page content as a string.
92
+ :return: The page content as string list.
87
93
  """
88
94
  url = self.book_info_url(book_id=book_id)
89
95
  return [await self.fetch(url, **kwargs)]
@@ -99,7 +105,7 @@ class QidianSession(BaseSession):
99
105
 
100
106
  :param book_id: The book identifier.
101
107
  :param chapter_id: The chapter identifier.
102
- :return: The chapter content as a string.
108
+ :return: The page content as string list.
103
109
  """
104
110
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
105
111
  return [await self.fetch(url, **kwargs)]
@@ -144,6 +150,7 @@ class QidianSession(BaseSession):
144
150
  async def fetch(
145
151
  self,
146
152
  url: str,
153
+ encoding: str | None = None,
147
154
  **kwargs: Any,
148
155
  ) -> str:
149
156
  """
@@ -151,14 +158,14 @@ class QidianSession(BaseSession):
151
158
  a cookie-based token used for request validation.
152
159
 
153
160
  The method:
154
- 1. Reads the existing cookie (if any);
155
- 2. Generates a new value tied to *url*;
156
- 3. Updates the live ``requests.Session``;
161
+ 1. Reads the existing cookie (if any);
162
+ 2. Generates a new value tied to *url*;
163
+ 3. Updates the live ``requests.Session``;
157
164
  """
158
165
  if self._rate_limiter:
159
166
  await self._rate_limiter.wait()
160
167
 
161
- cookie_key = _d("d190c2Zw")
168
+ cookie_key = self._d("d190c2Zw")
162
169
 
163
170
  for attempt in range(self.retry_times + 1):
164
171
  try:
@@ -167,11 +174,11 @@ class QidianSession(BaseSession):
167
174
 
168
175
  async with self.session.get(url, **kwargs) as resp:
169
176
  resp.raise_for_status()
170
- text: str = await resp.text()
177
+ text: str = await resp.text(encoding=encoding)
171
178
  return text
172
179
  except aiohttp.ClientError:
173
180
  if attempt < self.retry_times:
174
- await async_sleep_with_random_delay(
181
+ await async_jitter_sleep(
175
182
  self.backoff_factor,
176
183
  mul_spread=1.1,
177
184
  max_sleep=self.backoff_factor + 2,
@@ -220,21 +227,17 @@ class QidianSession(BaseSession):
220
227
  """
221
228
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
222
229
 
223
- @property
224
- def hostname(self) -> str:
225
- return "www.qidian.com"
226
-
227
230
  def _update_fp_val(
228
231
  self,
229
232
  *,
230
233
  key: str = "",
231
234
  ) -> None:
232
235
  """"""
233
- enc_token = self.get_cookie_value(_d("d190c2Zw"))
236
+ enc_token = self._get_cookie_value(self._d("d190c2Zw"))
234
237
  if not enc_token:
235
238
  return
236
239
  if not key:
237
- key = _get_key()
240
+ key = self._get_key()
238
241
  decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
239
242
  payload: dict[str, Any] = json.loads(decrypted_json)
240
243
  self._fp_val = payload.get(self._fp_key, "")
@@ -250,17 +253,14 @@ class QidianSession(BaseSession):
250
253
  Patch a timestamp-bearing token with fresh timing and checksum info.
251
254
 
252
255
  :param new_uri: URI used in checksum generation.
253
- :type new_uri: str
254
256
  :param key: RC4 key extracted from front-end JavaScript (optional).
255
- :type key: str, optional
256
257
 
257
258
  :return: Updated token with new timing and checksum values.
258
- :rtype: str
259
259
  """
260
260
  if not self._fp_val or not self._ab_val:
261
261
  self._update_fp_val()
262
262
  if not key:
263
- key = _get_key()
263
+ key = self._get_key()
264
264
 
265
265
  # rebuild timing fields
266
266
  loadts = int(time.time() * 1000) # ms since epoch
@@ -303,25 +303,40 @@ class QidianSession(BaseSession):
303
303
  """
304
304
  Check if the provided cookies contain all required keys.
305
305
 
306
- Logs any missing keys as warnings.
307
-
308
306
  :param cookies: The cookie dictionary to validate.
309
307
  :return: True if all required keys are present, False otherwise.
310
308
  """
311
- required = {_d(k) for k in self._cookie_keys}
309
+ required = {self._d(k) for k in self._cookie_keys}
312
310
  actual = set(cookies)
313
311
  missing = required - actual
314
312
  if missing:
315
313
  self.logger.warning("Missing required cookies: %s", ", ".join(missing))
316
314
  return not missing
317
315
 
316
+ def _get_cookie_value(self, key: str) -> str | None:
317
+ for cookie in self.session.cookie_jar:
318
+ if cookie.key == key:
319
+ return str(cookie.value)
320
+ return None
321
+
322
+ @staticmethod
323
+ def _filter_cookies(
324
+ raw_cookies: list[Mapping[str, Any]],
325
+ ) -> dict[str, str]:
326
+ ALLOWED_DOMAINS = {".qidian.com", "www.qidian.com", ""}
327
+ return {
328
+ c["name"]: c["value"]
329
+ for c in raw_cookies
330
+ if c.get("domain", "") in ALLOWED_DOMAINS
331
+ }
318
332
 
319
- def _d(b: str) -> str:
320
- return base64.b64decode(b).decode()
321
-
333
+ @staticmethod
334
+ def _d(b: str) -> str:
335
+ return base64.b64decode(b).decode()
322
336
 
323
- def _get_key() -> str:
324
- encoded = "Lj1qYxMuaXBjMg=="
325
- decoded = base64.b64decode(encoded)
326
- key = "".join([chr(b ^ 0x5A) for b in decoded])
327
- return key
337
+ @staticmethod
338
+ def _get_key() -> str:
339
+ encoded = "Lj1qYxMuaXBjMg=="
340
+ decoded = base64.b64decode(encoded)
341
+ key = "".join([chr(b ^ 0x5A) for b in decoded])
342
+ return key
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.quanben5
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=["quanben5"],
17
+ )
18
+ class Quanben5Session(BaseSession):
19
+ """
20
+ A session class for interacting with the 全本小说网 (quanben5.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://{base_url}/n/{book_id}/xiaoshuo.html"
24
+ CHAPTER_URL = "https://{base_url}/n/{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__("quanben5", config, cookies, **kwargs)
33
+ self.base_url = (
34
+ "quanben5.com"
35
+ if config.locale_style == "simplified"
36
+ else "big5.quanben5.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,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.base.rate_limiter
4
- ------------------------------------------------
3
+ novel_downloader.core.fetchers.rate_limiter
4
+ -------------------------------------------
5
5
 
6
6
  """
7
7