novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +79 -66
  6. novel_downloader/cli/export.py +17 -21
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +17 -12
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +20 -14
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +64 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +64 -69
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,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
 
@@ -15,11 +15,10 @@ from novel_downloader.models import FetcherConfig
15
15
 
16
16
  @register_fetcher(
17
17
  site_keys=["qianbi"],
18
- backends=["session"],
19
18
  )
20
19
  class QianbiSession(BaseSession):
21
20
  """
22
- 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.
23
22
  """
24
23
 
25
24
  BASE_URLS = [
@@ -50,7 +49,7 @@ class QianbiSession(BaseSession):
50
49
  Order: [info, catalog]
51
50
 
52
51
  :param book_id: The book identifier.
53
- :return: The page content as a string.
52
+ :return: The page content as string list.
54
53
  """
55
54
  info_url = self.book_info_url(book_id=book_id)
56
55
  catalog_url = self.book_catalog_url(book_id=book_id)
@@ -72,7 +71,7 @@ class QianbiSession(BaseSession):
72
71
 
73
72
  :param book_id: The book identifier.
74
73
  :param chapter_id: The chapter identifier.
75
- :return: The chapter content as a string.
74
+ :return: The page content as string list.
76
75
  """
77
76
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
78
77
  return [await self.fetch(url, **kwargs)]
@@ -107,7 +106,3 @@ class QianbiSession(BaseSession):
107
106
  :return: Fully qualified chapter URL.
108
107
  """
109
108
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
110
-
111
- @property
112
- def hostname(self) -> str:
113
- 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,6 +10,7 @@ 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
@@ -17,24 +18,20 @@ import aiohttp
17
18
  from novel_downloader.core.fetchers.base import BaseSession
18
19
  from novel_downloader.core.fetchers.registry import register_fetcher
19
20
  from novel_downloader.models import FetcherConfig, LoginField
20
- from novel_downloader.utils import (
21
- async_sleep_with_random_delay,
22
- rc4_crypt,
23
- )
21
+ from novel_downloader.utils import async_jitter_sleep
22
+ from novel_downloader.utils.crypto_utils.rc4 import rc4_init, rc4_stream
24
23
 
25
24
 
26
25
  @register_fetcher(
27
26
  site_keys=["qidian", "qd"],
28
- backends=["session"],
29
27
  )
30
28
  class QidianSession(BaseSession):
31
29
  """
32
- A session class for interacting with the Qidian (www.qidian.com) novel website.
30
+ A session class for interacting with the 起点中文网 (www.qidian.com) novel website.
33
31
  """
34
32
 
35
33
  HOMEPAGE_URL = "https://www.qidian.com/"
36
34
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
37
- # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
38
35
  BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
39
36
  CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
40
37
 
@@ -55,11 +52,13 @@ class QidianSession(BaseSession):
55
52
  **kwargs: Any,
56
53
  ) -> None:
57
54
  super().__init__("qidian", config, cookies, **kwargs)
58
- self._fp_key = _d("ZmluZ2VycHJpbnQ=")
59
- self._ab_key = _d("YWJub3JtYWw=")
60
- self._ck_key = _d("Y2hlY2tzdW0=")
61
- self._lt_key = _d("bG9hZHRz")
62
- self._ts_key = _d("dGltZXN0YW1w")
55
+ self._s_init = rc4_init(self._d2("dGcwOUl0Myo5aA=="))
56
+ self._cookie_key = self._d("d190c2Zw")
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")
63
62
  self._fp_val: str = ""
64
63
  self._ab_val: str = ""
65
64
 
@@ -90,7 +89,7 @@ class QidianSession(BaseSession):
90
89
  Fetch the raw HTML of the book info page asynchronously.
91
90
 
92
91
  :param book_id: The book identifier.
93
- :return: The page content as a string.
92
+ :return: The page content as string list.
94
93
  """
95
94
  url = self.book_info_url(book_id=book_id)
96
95
  return [await self.fetch(url, **kwargs)]
@@ -106,7 +105,7 @@ class QidianSession(BaseSession):
106
105
 
107
106
  :param book_id: The book identifier.
108
107
  :param chapter_id: The chapter identifier.
109
- :return: The chapter content as a string.
108
+ :return: The page content as string list.
110
109
  """
111
110
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
112
111
  return [await self.fetch(url, **kwargs)]
@@ -159,19 +158,17 @@ class QidianSession(BaseSession):
159
158
  a cookie-based token used for request validation.
160
159
 
161
160
  The method:
162
- 1. Reads the existing cookie (if any);
163
- 2. Generates a new value tied to *url*;
164
- 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``;
165
164
  """
166
165
  if self._rate_limiter:
167
166
  await self._rate_limiter.wait()
168
167
 
169
- cookie_key = _d("d190c2Zw")
170
-
171
168
  for attempt in range(self.retry_times + 1):
172
169
  try:
173
170
  refreshed_token = self._build_payload_token(url)
174
- self.update_cookies({cookie_key: refreshed_token})
171
+ self.update_cookies({self._cookie_key: refreshed_token})
175
172
 
176
173
  async with self.session.get(url, **kwargs) as resp:
177
174
  resp.raise_for_status()
@@ -179,7 +176,7 @@ class QidianSession(BaseSession):
179
176
  return text
180
177
  except aiohttp.ClientError:
181
178
  if attempt < self.retry_times:
182
- await async_sleep_with_random_delay(
179
+ await async_jitter_sleep(
183
180
  self.backoff_factor,
184
181
  mul_spread=1.1,
185
182
  max_sleep=self.backoff_factor + 2,
@@ -228,47 +225,30 @@ class QidianSession(BaseSession):
228
225
  """
229
226
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
230
227
 
231
- @property
232
- def hostname(self) -> str:
233
- return "www.qidian.com"
234
-
235
- def _update_fp_val(
236
- self,
237
- *,
238
- key: str = "",
239
- ) -> None:
240
- """"""
241
- enc_token = self.get_cookie_value(_d("d190c2Zw"))
228
+ def _update_fp_val(self) -> None:
229
+ """
230
+ Decrypt the payload from cookie and update `_fp_val` and `_ab_val`.
231
+ """
232
+ enc_token = self._get_cookie_value(self._cookie_key)
242
233
  if not enc_token:
243
234
  return
244
- if not key:
245
- key = _get_key()
246
- decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
235
+
236
+ cipher_bytes = base64.b64decode(enc_token)
237
+ plain_bytes = rc4_stream(self._s_init, cipher_bytes)
238
+ decrypted_json = plain_bytes.decode("utf-8", errors="replace")
247
239
  payload: dict[str, Any] = json.loads(decrypted_json)
248
240
  self._fp_val = payload.get(self._fp_key, "")
249
241
  self._ab_val = payload.get(self._ab_key, "0" * 32)
250
242
 
251
- def _build_payload_token(
252
- self,
253
- new_uri: str,
254
- *,
255
- key: str = "",
256
- ) -> str:
243
+ def _build_payload_token(self, new_uri: str) -> str:
257
244
  """
258
245
  Patch a timestamp-bearing token with fresh timing and checksum info.
259
246
 
260
247
  :param new_uri: URI used in checksum generation.
261
- :type new_uri: str
262
- :param key: RC4 key extracted from front-end JavaScript (optional).
263
- :type key: str, optional
264
-
265
248
  :return: Updated token with new timing and checksum values.
266
- :rtype: str
267
249
  """
268
250
  if not self._fp_val or not self._ab_val:
269
251
  self._update_fp_val()
270
- if not key:
271
- key = _get_key()
272
252
 
273
253
  # rebuild timing fields
274
254
  loadts = int(time.time() * 1000) # ms since epoch
@@ -286,9 +266,9 @@ class QidianSession(BaseSession):
286
266
  self._ab_key: self._ab_val,
287
267
  self._ck_key: ck_val,
288
268
  }
289
- return rc4_crypt(
290
- key, json.dumps(new_payload, separators=(",", ":")), mode="encrypt"
291
- )
269
+ plain_bytes = json.dumps(new_payload, separators=(",", ":")).encode("utf-8")
270
+ cipher_bytes = rc4_stream(self._s_init, plain_bytes)
271
+ return base64.b64encode(cipher_bytes).decode("utf-8")
292
272
 
293
273
  async def _check_login_status(self) -> bool:
294
274
  """
@@ -311,25 +291,37 @@ class QidianSession(BaseSession):
311
291
  """
312
292
  Check if the provided cookies contain all required keys.
313
293
 
314
- Logs any missing keys as warnings.
315
-
316
294
  :param cookies: The cookie dictionary to validate.
317
295
  :return: True if all required keys are present, False otherwise.
318
296
  """
319
- required = {_d(k) for k in self._cookie_keys}
297
+ required = {self._d(k) for k in self._cookie_keys}
320
298
  actual = set(cookies)
321
299
  missing = required - actual
322
300
  if missing:
323
301
  self.logger.warning("Missing required cookies: %s", ", ".join(missing))
324
302
  return not missing
325
303
 
304
+ def _get_cookie_value(self, key: str) -> str | None:
305
+ for cookie in self.session.cookie_jar:
306
+ if cookie.key == key:
307
+ return str(cookie.value)
308
+ return None
309
+
310
+ @staticmethod
311
+ def _filter_cookies(
312
+ raw_cookies: list[Mapping[str, Any]],
313
+ ) -> dict[str, str]:
314
+ ALLOWED_DOMAINS = {".qidian.com", "www.qidian.com", ""}
315
+ return {
316
+ c["name"]: c["value"]
317
+ for c in raw_cookies
318
+ if c.get("domain", "") in ALLOWED_DOMAINS
319
+ }
326
320
 
327
- def _d(b: str) -> str:
328
- return base64.b64decode(b).decode()
329
-
321
+ @staticmethod
322
+ def _d(b: str) -> str:
323
+ return base64.b64decode(b).decode()
330
324
 
331
- def _get_key() -> str:
332
- encoded = "Lj1qYxMuaXBjMg=="
333
- decoded = base64.b64decode(encoded)
334
- key = "".join([chr(b ^ 0x5A) for b in decoded])
335
- return key
325
+ @staticmethod
326
+ def _d2(b: str) -> bytes:
327
+ return base64.b64decode(b)
@@ -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
 
@@ -3,6 +3,7 @@
3
3
  novel_downloader.core.fetchers.registry
4
4
  ---------------------------------------
5
5
 
6
+ Registry and factory helpers for creating site-specific fetchers.
6
7
  """
7
8
 
8
9
  __all__ = ["register_fetcher", "get_fetcher"]
@@ -16,27 +17,24 @@ from novel_downloader.models import FetcherConfig
16
17
  FetcherBuilder = Callable[[FetcherConfig], FetcherProtocol]
17
18
 
18
19
  F = TypeVar("F", bound=FetcherProtocol)
19
- _FETCHER_MAP: dict[str, dict[str, FetcherBuilder]] = {}
20
+ _FETCHER_MAP: dict[str, FetcherBuilder] = {}
20
21
 
21
22
 
22
23
  def register_fetcher(
23
24
  site_keys: Sequence[str],
24
- backends: Sequence[str],
25
25
  ) -> Callable[[type[F]], type[F]]:
26
26
  """
27
27
  Decorator to register a fetcher class under given keys.
28
28
 
29
29
  :param site_keys: Sequence of site identifiers
30
- :param backends: Sequence of backend types
30
+ :param backends: Sequence of backend types
31
31
  :return: A class decorator that populates _FETCHER_MAP.
32
32
  """
33
33
 
34
34
  def decorator(cls: type[F]) -> type[F]:
35
35
  for site in site_keys:
36
36
  site_lower = site.lower()
37
- bucket = _FETCHER_MAP.setdefault(site_lower, {})
38
- for backend in backends:
39
- bucket[backend] = cls
37
+ _FETCHER_MAP[site_lower] = cls
40
38
  return cls
41
39
 
42
40
  return decorator
@@ -55,17 +53,8 @@ def get_fetcher(
55
53
  """
56
54
  site_key = site.lower()
57
55
  try:
58
- backend_map = _FETCHER_MAP[site_key]
56
+ fetcher_cls = _FETCHER_MAP[site_key]
59
57
  except KeyError as err:
60
58
  raise ValueError(f"Unsupported site: {site!r}") from err
61
59
 
62
- mode = config.mode
63
- try:
64
- fetcher_cls = backend_map[mode]
65
- except KeyError as err:
66
- raise ValueError(
67
- f"Unsupported fetcher mode {mode!r} for site {site!r}. "
68
- f"Available modes: {list(backend_map)}"
69
- ) from err
70
-
71
60
  return fetcher_cls(config)
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.sfacg.session
4
- --------------------------------------------
3
+ novel_downloader.core.fetchers.sfacg
4
+ ------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -14,11 +14,10 @@ from novel_downloader.models import FetcherConfig, LoginField
14
14
 
15
15
  @register_fetcher(
16
16
  site_keys=["sfacg"],
17
- backends=["session"],
18
17
  )
19
18
  class SfacgSession(BaseSession):
20
19
  """
21
- A session class for interacting with the Sfacg (m.sfacg.com) novel website.
20
+ A session class for interacting with the SF轻小说 (m.sfacg.com) novel website.
22
21
  """
23
22
 
24
23
  LOGIN_URL = "https://m.sfacg.com/login"
@@ -65,8 +64,10 @@ class SfacgSession(BaseSession):
65
64
  """
66
65
  Fetch the raw HTML of the book info page asynchronously.
67
66
 
67
+ Order: [info, catalog]
68
+
68
69
  :param book_id: The book identifier.
69
- :return: The page content as a string.
70
+ :return: The page content as string list.
70
71
  """
71
72
  info_url = self.book_info_url(book_id=book_id)
72
73
  catalog_url = self.book_catalog_url(book_id=book_id)
@@ -87,7 +88,7 @@ class SfacgSession(BaseSession):
87
88
 
88
89
  :param book_id: The book identifier.
89
90
  :param chapter_id: The chapter identifier.
90
- :return: The chapter content as a string.
91
+ :return: The page content as string list.
91
92
  """
92
93
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
93
94
  return [await self.fetch(url, **kwargs)]
@@ -157,10 +158,6 @@ class SfacgSession(BaseSession):
157
158
  """
158
159
  return cls.CHAPTER_URL.format(chapter_id=chapter_id)
159
160
 
160
- @property
161
- def hostname(self) -> str:
162
- return "m.sfacg.com"
163
-
164
161
  async def _check_login_status(self) -> bool:
165
162
  """
166
163
  Check whether the user is currently logged in by