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,92 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.hetushu
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from novel_downloader.core.fetchers.base import BaseSession
11
+ from novel_downloader.core.fetchers.registry import register_fetcher
12
+ from novel_downloader.models import FetcherConfig
13
+
14
+
15
+ @register_fetcher(
16
+ site_keys=["hetushu"],
17
+ )
18
+ class HetushuSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 和图书 (www.hetushu.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://{base_url}/book/{book_id}/index.html"
24
+ CHAPTER_URL = "https://{base_url}/book/{book_id}/{chapter_id}.html"
25
+
26
+ def __init__(
27
+ self,
28
+ config: FetcherConfig,
29
+ cookies: dict[str, str] | None = None,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ super().__init__("hetushu", config, cookies, **kwargs)
33
+ self.base_url = (
34
+ "www.hetushu.com"
35
+ if config.locale_style == "simplified"
36
+ else "www.hetubook.com"
37
+ )
38
+
39
+ async def get_book_info(
40
+ self,
41
+ book_id: str,
42
+ **kwargs: Any,
43
+ ) -> list[str]:
44
+ """
45
+ Fetch the raw HTML of the book info page asynchronously.
46
+
47
+ :param book_id: The book identifier.
48
+ :return: The page content as string list.
49
+ """
50
+ url = self.book_info_url(base_url=self.base_url, book_id=book_id)
51
+ return [await self.fetch(url, **kwargs)]
52
+
53
+ async def get_book_chapter(
54
+ self,
55
+ book_id: str,
56
+ chapter_id: str,
57
+ **kwargs: Any,
58
+ ) -> list[str]:
59
+ """
60
+ Fetch the raw HTML of a single chapter asynchronously.
61
+
62
+ :param book_id: The book identifier.
63
+ :param chapter_id: The chapter identifier.
64
+ :return: The page content as string list.
65
+ """
66
+ url = self.chapter_url(
67
+ base_url=self.base_url, book_id=book_id, chapter_id=chapter_id
68
+ )
69
+ return [await self.fetch(url, **kwargs)]
70
+
71
+ @classmethod
72
+ def book_info_url(cls, base_url: str, book_id: str) -> str:
73
+ """
74
+ Construct the URL for fetching a book's info page.
75
+
76
+ :param book_id: The identifier of the book.
77
+ :return: Fully qualified URL for the book info page.
78
+ """
79
+ return cls.BOOK_INFO_URL.format(base_url=base_url, book_id=book_id)
80
+
81
+ @classmethod
82
+ def chapter_url(cls, base_url: str, book_id: str, chapter_id: str) -> str:
83
+ """
84
+ Construct the URL for fetching a specific chapter.
85
+
86
+ :param book_id: The identifier of the book.
87
+ :param chapter_id: The identifier of the chapter.
88
+ :return: Fully qualified chapter URL.
89
+ """
90
+ return cls.CHAPTER_URL.format(
91
+ base_url=base_url, book_id=book_id, chapter_id=chapter_id
92
+ )
@@ -1,42 +1,37 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.fetchers.qianbi.browser
4
- ---------------------------------------------
3
+ novel_downloader.core.fetchers.i25zw
4
+ ------------------------------------
5
5
 
6
6
  """
7
7
 
8
+ import asyncio
8
9
  from typing import Any
9
10
 
10
- from novel_downloader.core.fetchers.base import BaseBrowser
11
+ from novel_downloader.core.fetchers.base import BaseSession
11
12
  from novel_downloader.core.fetchers.registry import register_fetcher
12
13
  from novel_downloader.models import FetcherConfig
13
14
 
14
15
 
15
16
  @register_fetcher(
16
- site_keys=["qianbi"],
17
- backends=["browser"],
17
+ site_keys=["i25zw"],
18
18
  )
19
- class QianbiBrowser(BaseBrowser):
19
+ class I25zwSession(BaseSession):
20
20
  """
21
- A browser class for interacting with the Qianbi (www.23qb.com) novel website.
21
+ A session class for interacting with the 25中文网 (www.i25zw.com) novel website.
22
22
  """
23
23
 
24
- BASE_URLS = [
25
- "www.23qb.com",
26
- "www.23qb.net",
27
- ]
28
-
29
- BOOK_INFO_URL = "https://www.23qb.com/book/{book_id}/"
30
- BOOK_CATALOG_URL = "https://www.23qb.com/book/{book_id}/catalog"
31
- CHAPTER_URL = "https://www.23qb.com/book/{book_id}/{chapter_id}.html"
24
+ BOOK_INFO_URL = "https://www.i25zw.com/book/{book_id}.html"
25
+ BOOK_CATALOG_URL = "https://www.i25zw.com/{book_id}/"
26
+ CHAPTER_URL = "https://www.i25zw.com/{book_id}/{chapter_id}.html"
32
27
 
33
28
  def __init__(
34
29
  self,
35
30
  config: FetcherConfig,
36
- reuse_page: bool = False,
31
+ cookies: dict[str, str] | None = None,
37
32
  **kwargs: Any,
38
33
  ) -> None:
39
- super().__init__("qianbi", config, reuse_page, **kwargs)
34
+ super().__init__("i25zw", config, cookies, **kwargs)
40
35
 
41
36
  async def get_book_info(
42
37
  self,
@@ -49,14 +44,15 @@ class QianbiBrowser(BaseBrowser):
49
44
  Order: [info, catalog]
50
45
 
51
46
  :param book_id: The book identifier.
52
- :return: The page content as a string.
47
+ :return: The page content as string list.
53
48
  """
54
49
  info_url = self.book_info_url(book_id=book_id)
55
50
  catalog_url = self.book_catalog_url(book_id=book_id)
56
51
 
57
- info_html = await self.fetch(info_url, **kwargs)
58
- catalog_html = await self.fetch(catalog_url, **kwargs)
59
-
52
+ info_html, catalog_html = await asyncio.gather(
53
+ self.fetch(info_url, **kwargs),
54
+ self.fetch(catalog_url, **kwargs),
55
+ )
60
56
  return [info_html, catalog_html]
61
57
 
62
58
  async def get_book_chapter(
@@ -70,11 +66,10 @@ class QianbiBrowser(BaseBrowser):
70
66
 
71
67
  :param book_id: The book identifier.
72
68
  :param chapter_id: The chapter identifier.
73
- :return: The chapter content as a string.
69
+ :return: The page content as string list.
74
70
  """
75
- catalog_url = self.book_catalog_url(book_id=book_id)
76
71
  url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
77
- return [await self.fetch(url, referer=catalog_url, **kwargs)]
72
+ return [await self.fetch(url, **kwargs)]
78
73
 
79
74
  @classmethod
80
75
  def book_info_url(cls, book_id: str) -> str:
@@ -106,7 +101,3 @@ class QianbiBrowser(BaseBrowser):
106
101
  :return: Fully qualified chapter URL.
107
102
  """
108
103
  return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
109
-
110
- @property
111
- def hostname(self) -> str:
112
- return "www.23qb.com"
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.ixdzs8
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ import asyncio
9
+ import re
10
+ from typing import Any
11
+
12
+ from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
14
+ from novel_downloader.models import FetcherConfig
15
+
16
+
17
+ @register_fetcher(
18
+ site_keys=["ixdzs8"],
19
+ )
20
+ class Ixdzs8Session(BaseSession):
21
+ """
22
+ A session class for interacting with the 爱下电子书 (ixdzs8.com) novel website.
23
+ """
24
+
25
+ BOOK_INFO_URL = "https://ixdzs8.com/read/{book_id}/"
26
+ BOOK_CATALOG_URL = "https://ixdzs8.com/novel/clist/"
27
+ CHAPTER_URL = "https://ixdzs8.com/read/{book_id}/{chapter_id}.html"
28
+ _TOKEN_PATTERN = re.compile(r'let\s+token\s*=\s*"([^"]+)"')
29
+
30
+ def __init__(
31
+ self,
32
+ config: FetcherConfig,
33
+ cookies: dict[str, str] | None = None,
34
+ **kwargs: Any,
35
+ ) -> None:
36
+ super().__init__("ixdzs8", config, cookies, **kwargs)
37
+
38
+ async def get_book_info(
39
+ self,
40
+ book_id: str,
41
+ **kwargs: Any,
42
+ ) -> list[str]:
43
+ """
44
+ Fetch the raw HTML of the book info page asynchronously.
45
+
46
+ Order: [info, catalog]
47
+
48
+ :param book_id: The book identifier.
49
+ :return: The page content as string list.
50
+ """
51
+ url = self.book_info_url(book_id=book_id)
52
+ data = {"bid": book_id}
53
+ info_html, clist_response = await asyncio.gather(
54
+ self.fetch_verified_html(url, **kwargs),
55
+ self.post(self.BOOK_CATALOG_URL, data),
56
+ )
57
+ catalog_html = await clist_response.text()
58
+ return [info_html, catalog_html]
59
+
60
+ async def get_book_chapter(
61
+ self,
62
+ book_id: str,
63
+ chapter_id: str,
64
+ **kwargs: Any,
65
+ ) -> list[str]:
66
+ """
67
+ Fetch the raw HTML of a single chapter asynchronously.
68
+
69
+ :param book_id: The book identifier.
70
+ :param chapter_id: The chapter identifier.
71
+ :return: The page content as string list.
72
+ """
73
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
74
+ return [await self.fetch_verified_html(url, **kwargs)]
75
+
76
+ @classmethod
77
+ def book_info_url(cls, book_id: str) -> str:
78
+ """
79
+ Construct the URL for fetching a book's info page.
80
+
81
+ :param book_id: The identifier of the book.
82
+ :return: Fully qualified URL for the book info page.
83
+ """
84
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
85
+
86
+ @classmethod
87
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
88
+ """
89
+ Construct the URL for fetching a specific chapter.
90
+
91
+ :param book_id: The identifier of the book.
92
+ :param chapter_id: The identifier of the chapter.
93
+ :return: Fully qualified chapter URL.
94
+ """
95
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
96
+
97
+ async def fetch_verified_html(self, url: str, **kwargs: Any) -> str:
98
+ """
99
+ Automatically solving the browser verification challenge if required.
100
+ """
101
+ resp = await self.fetch(url, **kwargs)
102
+
103
+ if "正在验证浏览器" not in resp:
104
+ return resp
105
+
106
+ token_match = self._TOKEN_PATTERN.search(resp)
107
+ if not token_match:
108
+ raise ValueError("Token not found in page HTML.")
109
+ token_value = token_match.group(1)
110
+
111
+ challenge_url = f"{url}?challenge={token_value}"
112
+ _ = await self.fetch(challenge_url, **kwargs)
113
+ return await self.fetch(url, **kwargs)
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.jpxs123
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.fetchers.base import BaseSession
13
+ from novel_downloader.core.fetchers.registry import register_fetcher
14
+ from novel_downloader.models import FetcherConfig
15
+
16
+
17
+ @register_fetcher(
18
+ site_keys=["jpxs123"],
19
+ )
20
+ class Jpxs123Session(BaseSession):
21
+ """
22
+ A session class for interacting with the 精品小说网 (www.jpxs123.com) novel website.
23
+ """
24
+
25
+ BASE_URL = "https://www.jpxs123.com"
26
+ BOOK_INFO_URL = "https://www.jpxs123.com/{book_id}.html"
27
+ CHAPTER_URL = "https://www.jpxs123.com/{book_id}/{chapter_id}.html"
28
+
29
+ def __init__(
30
+ self,
31
+ config: FetcherConfig,
32
+ cookies: dict[str, str] | None = None,
33
+ **kwargs: Any,
34
+ ) -> None:
35
+ super().__init__("jpxs123", config, cookies, **kwargs)
36
+
37
+ async def get_book_info(
38
+ self,
39
+ book_id: str,
40
+ **kwargs: Any,
41
+ ) -> list[str]:
42
+ """
43
+ Fetch the raw HTML of the book info page asynchronously.
44
+
45
+ Order: [info, download]
46
+
47
+ :param book_id: The book identifier.
48
+ :return: The page content as string list.
49
+ """
50
+ book_id = book_id.replace("-", "/")
51
+ url = self.book_info_url(book_id=book_id)
52
+ info_html = await self.fetch(url, **kwargs)
53
+ try:
54
+ info_tree = html.fromstring(info_html)
55
+ txt_link = info_tree.xpath(
56
+ '//div[@class="booktips"]//a[contains(text(), "txt下载")]/@href'
57
+ )
58
+ download_url = f"{self.BASE_URL}{txt_link[0]}" if txt_link else None
59
+ except Exception:
60
+ download_url = None
61
+
62
+ download_html = await self.fetch(download_url, **kwargs) if download_url else ""
63
+ return [info_html, download_html]
64
+
65
+ async def get_book_chapter(
66
+ self,
67
+ book_id: str,
68
+ chapter_id: str,
69
+ **kwargs: Any,
70
+ ) -> list[str]:
71
+ """
72
+ Fetch the raw HTML of a single chapter asynchronously.
73
+
74
+ :param book_id: The book identifier.
75
+ :param chapter_id: The chapter identifier.
76
+ :return: The page content as string list.
77
+ """
78
+ book_id = book_id.replace("-", "/")
79
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
80
+ return [await self.fetch(url, **kwargs)]
81
+
82
+ @classmethod
83
+ def book_info_url(cls, book_id: str) -> str:
84
+ """
85
+ Construct the URL for fetching a book's info page.
86
+
87
+ :param book_id: The identifier of the book.
88
+ :return: Fully qualified URL for the book info page.
89
+ """
90
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
91
+
92
+ @classmethod
93
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
94
+ """
95
+ Construct the URL for fetching a specific chapter.
96
+
97
+ :param book_id: The identifier of the book.
98
+ :param chapter_id: The identifier of the chapter.
99
+ :return: Fully qualified chapter URL.
100
+ """
101
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.lewenn
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=["lewenn", "lewen"],
17
+ )
18
+ class LewennSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 乐文小说网 (www.lewenn.net) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://www.lewenn.net/{book_id}/"
24
+ CHAPTER_URL = "https://www.lewenn.net/{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__("lewenn", config, cookies, **kwargs)
33
+
34
+ async def get_book_info(
35
+ self,
36
+ book_id: str,
37
+ **kwargs: Any,
38
+ ) -> list[str]:
39
+ """
40
+ Fetch the raw HTML of the book info page asynchronously.
41
+
42
+ :param book_id: The book identifier.
43
+ :return: The page content as string list.
44
+ """
45
+ url = self.book_info_url(book_id=book_id)
46
+ return [await self.fetch(url, **kwargs)]
47
+
48
+ async def get_book_chapter(
49
+ self,
50
+ book_id: str,
51
+ chapter_id: str,
52
+ **kwargs: Any,
53
+ ) -> list[str]:
54
+ """
55
+ Fetch the raw HTML of a single chapter asynchronously.
56
+
57
+ :param book_id: The book identifier.
58
+ :param chapter_id: The chapter identifier.
59
+ :return: The page content as string list.
60
+ """
61
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
62
+ return [await self.fetch(url, **kwargs)]
63
+
64
+ @classmethod
65
+ def book_info_url(cls, book_id: str) -> str:
66
+ """
67
+ Construct the URL for fetching a book's info page.
68
+
69
+ :param book_id: The identifier of the book.
70
+ :return: Fully qualified URL for the book info page.
71
+ """
72
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
73
+
74
+ @classmethod
75
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
76
+ """
77
+ Construct the URL for fetching a specific chapter.
78
+
79
+ :param book_id: The identifier of the book.
80
+ :param chapter_id: The identifier of the chapter.
81
+ :return: Fully qualified chapter URL.
82
+ """
83
+ return cls.CHAPTER_URL.format(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.linovelib.session
4
- ------------------------------------------------
3
+ novel_downloader.core.fetchers.linovelib
4
+ ----------------------------------------
5
5
 
6
6
  """
7
7
 
@@ -11,16 +11,15 @@ from typing import Any
11
11
  from novel_downloader.core.fetchers.base import BaseSession
12
12
  from novel_downloader.core.fetchers.registry import register_fetcher
13
13
  from novel_downloader.models import FetcherConfig
14
- from novel_downloader.utils import async_sleep_with_random_delay
14
+ from novel_downloader.utils import async_jitter_sleep
15
15
 
16
16
 
17
17
  @register_fetcher(
18
18
  site_keys=["linovelib"],
19
- backends=["session"],
20
19
  )
21
20
  class LinovelibSession(BaseSession):
22
21
  """
23
- A session class for interacting with Linovelib (www.linovelib.com) novel website.
22
+ A session class for interacting with 哔哩轻小说 (www.linovelib.com) novel website.
24
23
  """
25
24
 
26
25
  BASE_URL = "https://www.linovelib.com"
@@ -47,8 +46,10 @@ class LinovelibSession(BaseSession):
47
46
  """
48
47
  Fetch the raw HTML of the book info page.
49
48
 
49
+ Order: [info, vol1_html, ..., volN_html]
50
+
50
51
  :param book_id: The book identifier.
51
- :return: A list of HTML strings: [info_html, vol1_html, ..., volN_html]
52
+ :return: The page content as string list.
52
53
  """
53
54
  url = self.book_info_url(book_id=book_id)
54
55
  info_html = await self.fetch(url, **kwargs)
@@ -62,7 +63,7 @@ class LinovelibSession(BaseSession):
62
63
 
63
64
  vol_htmls = []
64
65
  for vol_id in vol_ids:
65
- await async_sleep_with_random_delay(
66
+ await async_jitter_sleep(
66
67
  self.request_interval,
67
68
  mul_spread=1.1,
68
69
  max_sleep=self.request_interval + 2,
@@ -98,9 +99,11 @@ class LinovelibSession(BaseSession):
98
99
  """
99
100
  Fetch the raw HTML of a single chapter asynchronously.
100
101
 
102
+ Order: [page1, ..., pageN]
103
+
101
104
  :param book_id: The book identifier.
102
105
  :param chapter_id: The chapter identifier.
103
- :return: The chapter content as a string.
106
+ :return: The page content as string list.
104
107
  """
105
108
  html_pages: list[str] = []
106
109
  idx = 1
@@ -126,7 +129,7 @@ class LinovelibSession(BaseSession):
126
129
 
127
130
  html_pages.append(html)
128
131
  idx += 1
129
- await async_sleep_with_random_delay(
132
+ await async_jitter_sleep(
130
133
  self.request_interval,
131
134
  mul_spread=1.1,
132
135
  max_sleep=self.request_interval + 2,
@@ -183,10 +186,6 @@ class LinovelibSession(BaseSession):
183
186
  """
184
187
  return f"/novel/{book_id}/{chapter_id}.html"
185
188
 
186
- @property
187
- def hostname(self) -> str:
188
- return "www.linovelib.com"
189
-
190
189
  def _extract_vol_ids(self, html_str: str) -> list[str]:
191
190
  """
192
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)