novel-downloader 1.5.0__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 (241) 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 +77 -64
  6. novel_downloader/cli/export.py +16 -20
  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 +65 -105
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +1 -0
  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 +14 -9
  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 +17 -11
  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} +46 -39
  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 +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  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 +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.py +61 -66
  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/book_info_parser.py +58 -59
  100. novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
  101. novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
  102. novel_downloader/core/parsers/qidian/main_parser.py +11 -38
  103. novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
  104. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  105. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  106. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  107. novel_downloader/core/parsers/quanben5.py +103 -0
  108. novel_downloader/core/parsers/registry.py +5 -16
  109. novel_downloader/core/parsers/sfacg.py +38 -45
  110. novel_downloader/core/parsers/shencou.py +215 -0
  111. novel_downloader/core/parsers/shuhaige.py +111 -0
  112. novel_downloader/core/parsers/tongrenquan.py +116 -0
  113. novel_downloader/core/parsers/ttkan.py +132 -0
  114. novel_downloader/core/parsers/wanbengo.py +191 -0
  115. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  116. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  117. novel_downloader/core/parsers/xs63b.py +161 -0
  118. novel_downloader/core/parsers/xshbook.py +134 -0
  119. novel_downloader/core/parsers/yamibo.py +87 -131
  120. novel_downloader/core/parsers/yibige.py +166 -0
  121. novel_downloader/core/searchers/__init__.py +34 -3
  122. novel_downloader/core/searchers/aaatxt.py +107 -0
  123. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  124. novel_downloader/core/searchers/base.py +112 -36
  125. novel_downloader/core/searchers/dxmwx.py +105 -0
  126. novel_downloader/core/searchers/eightnovel.py +84 -0
  127. novel_downloader/core/searchers/esjzone.py +43 -25
  128. novel_downloader/core/searchers/hetushu.py +92 -0
  129. novel_downloader/core/searchers/i25zw.py +93 -0
  130. novel_downloader/core/searchers/ixdzs8.py +107 -0
  131. novel_downloader/core/searchers/jpxs123.py +107 -0
  132. novel_downloader/core/searchers/piaotia.py +100 -0
  133. novel_downloader/core/searchers/qbtr.py +106 -0
  134. novel_downloader/core/searchers/qianbi.py +74 -40
  135. novel_downloader/core/searchers/quanben5.py +144 -0
  136. novel_downloader/core/searchers/registry.py +24 -8
  137. novel_downloader/core/searchers/shuhaige.py +124 -0
  138. novel_downloader/core/searchers/tongrenquan.py +110 -0
  139. novel_downloader/core/searchers/ttkan.py +92 -0
  140. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  141. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  142. novel_downloader/core/searchers/xs63b.py +104 -0
  143. novel_downloader/locales/en.json +31 -82
  144. novel_downloader/locales/zh.json +32 -83
  145. novel_downloader/models/__init__.py +21 -22
  146. novel_downloader/models/book.py +44 -0
  147. novel_downloader/models/config.py +4 -37
  148. novel_downloader/models/login.py +1 -1
  149. novel_downloader/models/search.py +5 -0
  150. novel_downloader/resources/config/settings.toml +8 -70
  151. novel_downloader/resources/json/xiguashuwu.json +718 -0
  152. novel_downloader/utils/__init__.py +13 -22
  153. novel_downloader/utils/chapter_storage.py +3 -2
  154. novel_downloader/utils/constants.py +4 -29
  155. novel_downloader/utils/cookies.py +6 -18
  156. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  157. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  158. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  159. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  160. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  161. novel_downloader/utils/epub/__init__.py +1 -1
  162. novel_downloader/utils/epub/constants.py +57 -16
  163. novel_downloader/utils/epub/documents.py +88 -194
  164. novel_downloader/utils/epub/models.py +0 -14
  165. novel_downloader/utils/epub/utils.py +63 -96
  166. novel_downloader/utils/file_utils/__init__.py +2 -23
  167. novel_downloader/utils/file_utils/io.py +3 -113
  168. novel_downloader/utils/file_utils/sanitize.py +0 -4
  169. novel_downloader/utils/fontocr.py +207 -0
  170. novel_downloader/utils/logger.py +8 -16
  171. novel_downloader/utils/network.py +2 -2
  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/time_utils/__init__.py +5 -11
  176. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  177. novel_downloader/utils/time_utils/sleep_utils.py +4 -8
  178. novel_downloader/web/__init__.py +13 -0
  179. novel_downloader/web/components/__init__.py +11 -0
  180. novel_downloader/web/components/navigation.py +35 -0
  181. novel_downloader/web/main.py +66 -0
  182. novel_downloader/web/pages/__init__.py +17 -0
  183. novel_downloader/web/pages/download.py +78 -0
  184. novel_downloader/web/pages/progress.py +147 -0
  185. novel_downloader/web/pages/search.py +329 -0
  186. novel_downloader/web/services/__init__.py +17 -0
  187. novel_downloader/web/services/client_dialog.py +164 -0
  188. novel_downloader/web/services/cred_broker.py +113 -0
  189. novel_downloader/web/services/cred_models.py +35 -0
  190. novel_downloader/web/services/task_manager.py +264 -0
  191. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  192. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  193. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  194. novel_downloader/core/downloaders/biquge.py +0 -29
  195. novel_downloader/core/downloaders/esjzone.py +0 -29
  196. novel_downloader/core/downloaders/linovelib.py +0 -29
  197. novel_downloader/core/downloaders/sfacg.py +0 -29
  198. novel_downloader/core/downloaders/yamibo.py +0 -29
  199. novel_downloader/core/exporters/biquge.py +0 -22
  200. novel_downloader/core/exporters/esjzone.py +0 -22
  201. novel_downloader/core/exporters/qianbi.py +0 -22
  202. novel_downloader/core/exporters/sfacg.py +0 -22
  203. novel_downloader/core/exporters/yamibo.py +0 -22
  204. novel_downloader/core/fetchers/base/__init__.py +0 -14
  205. novel_downloader/core/fetchers/base/browser.py +0 -422
  206. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  207. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  208. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  209. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  210. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  211. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  212. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  213. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  214. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  215. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  216. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  217. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  218. novel_downloader/core/parsers/biquge.py +0 -139
  219. novel_downloader/models/chapter.py +0 -25
  220. novel_downloader/models/types.py +0 -13
  221. novel_downloader/tui/__init__.py +0 -7
  222. novel_downloader/tui/app.py +0 -32
  223. novel_downloader/tui/main.py +0 -17
  224. novel_downloader/tui/screens/__init__.py +0 -14
  225. novel_downloader/tui/screens/home.py +0 -198
  226. novel_downloader/tui/screens/login.py +0 -74
  227. novel_downloader/tui/styles/home_layout.tcss +0 -79
  228. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  229. novel_downloader/utils/cache.py +0 -24
  230. novel_downloader/utils/fontocr/__init__.py +0 -22
  231. novel_downloader/utils/fontocr/hash_store.py +0 -280
  232. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  233. novel_downloader/utils/fontocr/model_loader.py +0 -69
  234. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  235. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  236. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  237. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  238. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  239. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  240. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  241. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.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
@@ -18,23 +19,21 @@ 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
21
  from novel_downloader.utils import (
21
- async_sleep_with_random_delay,
22
+ async_jitter_sleep,
22
23
  rc4_crypt,
23
24
  )
24
25
 
25
26
 
26
27
  @register_fetcher(
27
28
  site_keys=["qidian", "qd"],
28
- backends=["session"],
29
29
  )
30
30
  class QidianSession(BaseSession):
31
31
  """
32
- 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.
33
33
  """
34
34
 
35
35
  HOMEPAGE_URL = "https://www.qidian.com/"
36
36
  BOOKCASE_URL = "https://my.qidian.com/bookcase/"
37
- # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
38
37
  BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
39
38
  CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
40
39
 
@@ -55,11 +54,11 @@ class QidianSession(BaseSession):
55
54
  **kwargs: Any,
56
55
  ) -> None:
57
56
  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")
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,14 +158,14 @@ 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")
168
+ cookie_key = self._d("d190c2Zw")
170
169
 
171
170
  for attempt in range(self.retry_times + 1):
172
171
  try:
@@ -179,7 +178,7 @@ class QidianSession(BaseSession):
179
178
  return text
180
179
  except aiohttp.ClientError:
181
180
  if attempt < self.retry_times:
182
- await async_sleep_with_random_delay(
181
+ await async_jitter_sleep(
183
182
  self.backoff_factor,
184
183
  mul_spread=1.1,
185
184
  max_sleep=self.backoff_factor + 2,
@@ -228,21 +227,17 @@ class QidianSession(BaseSession):
228
227
  """
229
228
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
230
229
 
231
- @property
232
- def hostname(self) -> str:
233
- return "www.qidian.com"
234
-
235
230
  def _update_fp_val(
236
231
  self,
237
232
  *,
238
233
  key: str = "",
239
234
  ) -> None:
240
235
  """"""
241
- enc_token = self.get_cookie_value(_d("d190c2Zw"))
236
+ enc_token = self._get_cookie_value(self._d("d190c2Zw"))
242
237
  if not enc_token:
243
238
  return
244
239
  if not key:
245
- key = _get_key()
240
+ key = self._get_key()
246
241
  decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
247
242
  payload: dict[str, Any] = json.loads(decrypted_json)
248
243
  self._fp_val = payload.get(self._fp_key, "")
@@ -258,17 +253,14 @@ class QidianSession(BaseSession):
258
253
  Patch a timestamp-bearing token with fresh timing and checksum info.
259
254
 
260
255
  :param new_uri: URI used in checksum generation.
261
- :type new_uri: str
262
256
  :param key: RC4 key extracted from front-end JavaScript (optional).
263
- :type key: str, optional
264
257
 
265
258
  :return: Updated token with new timing and checksum values.
266
- :rtype: str
267
259
  """
268
260
  if not self._fp_val or not self._ab_val:
269
261
  self._update_fp_val()
270
262
  if not key:
271
- key = _get_key()
263
+ key = self._get_key()
272
264
 
273
265
  # rebuild timing fields
274
266
  loadts = int(time.time() * 1000) # ms since epoch
@@ -311,25 +303,40 @@ class QidianSession(BaseSession):
311
303
  """
312
304
  Check if the provided cookies contain all required keys.
313
305
 
314
- Logs any missing keys as warnings.
315
-
316
306
  :param cookies: The cookie dictionary to validate.
317
307
  :return: True if all required keys are present, False otherwise.
318
308
  """
319
- required = {_d(k) for k in self._cookie_keys}
309
+ required = {self._d(k) for k in self._cookie_keys}
320
310
  actual = set(cookies)
321
311
  missing = required - actual
322
312
  if missing:
323
313
  self.logger.warning("Missing required cookies: %s", ", ".join(missing))
324
314
  return not missing
325
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
+ }
326
332
 
327
- def _d(b: str) -> str:
328
- return base64.b64decode(b).decode()
329
-
333
+ @staticmethod
334
+ def _d(b: str) -> str:
335
+ return base64.b64decode(b).decode()
330
336
 
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
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
 
@@ -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
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.shencou
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=["shencou"],
18
+ )
19
+ class ShencouSession(BaseSession):
20
+ """
21
+ A session class for interacting with the 神凑轻小说 (www.shencou.com) novel website.
22
+ """
23
+
24
+ BOOK_INFO_URL = "https://www.shencou.com/books/read_{book_id}.html"
25
+ BOOK_CATALOG_URL = "https://www.shencou.com/read/{book_id}/index.html"
26
+ CHAPTER_URL = "https://www.shencou.com/read/{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__("shencou", 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
+ clean_id = book_id.rsplit("/", 1)[-1]
85
+ return cls.BOOK_INFO_URL.format(book_id=clean_id)
86
+
87
+ @classmethod
88
+ def book_catalog_url(cls, book_id: str) -> str:
89
+ """
90
+ Construct the URL for fetching a book's catalog page.
91
+
92
+ :param book_id: The identifier of the book.
93
+ :return: Fully qualified catalog page URL.
94
+ """
95
+ return cls.BOOK_CATALOG_URL.format(book_id=book_id)
96
+
97
+ @classmethod
98
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
99
+ """
100
+ Construct the URL for fetching a specific chapter.
101
+
102
+ :param book_id: The identifier of the book.
103
+ :param chapter_id: The identifier of the chapter.
104
+ :return: Fully qualified chapter URL.
105
+ """
106
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)