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,84 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.shuhaige
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=["shuhaige"],
17
+ )
18
+ class ShuhaigeSession(BaseSession):
19
+ """
20
+ A session class for interacting with the
21
+ 书海阁小说网 (www.shuhaige.net) novel website.
22
+ """
23
+
24
+ BOOK_INFO_URL = "https://www.shuhaige.net/{book_id}/"
25
+ CHAPTER_URL = "https://www.shuhaige.net/{book_id}/{chapter_id}.html"
26
+
27
+ def __init__(
28
+ self,
29
+ config: FetcherConfig,
30
+ cookies: dict[str, str] | None = None,
31
+ **kwargs: Any,
32
+ ) -> None:
33
+ super().__init__("shuhaige", config, cookies, **kwargs)
34
+
35
+ async def get_book_info(
36
+ self,
37
+ book_id: str,
38
+ **kwargs: Any,
39
+ ) -> list[str]:
40
+ """
41
+ Fetch the raw HTML of the book info page asynchronously.
42
+
43
+ :param book_id: The book identifier.
44
+ :return: The page content as string list.
45
+ """
46
+ url = self.book_info_url(book_id=book_id)
47
+ return [await self.fetch(url, **kwargs)]
48
+
49
+ async def get_book_chapter(
50
+ self,
51
+ book_id: str,
52
+ chapter_id: str,
53
+ **kwargs: Any,
54
+ ) -> list[str]:
55
+ """
56
+ Fetch the raw HTML of a single chapter asynchronously.
57
+
58
+ :param book_id: The book identifier.
59
+ :param chapter_id: The chapter identifier.
60
+ :return: The page content as string list.
61
+ """
62
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
63
+ return [await self.fetch(url, **kwargs)]
64
+
65
+ @classmethod
66
+ def book_info_url(cls, book_id: str) -> str:
67
+ """
68
+ Construct the URL for fetching a book's info page.
69
+
70
+ :param book_id: The identifier of the book.
71
+ :return: Fully qualified URL for the book info page.
72
+ """
73
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
74
+
75
+ @classmethod
76
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
77
+ """
78
+ Construct the URL for fetching a specific chapter.
79
+
80
+ :param book_id: The identifier of the book.
81
+ :param chapter_id: The identifier of the chapter.
82
+ :return: Fully qualified chapter URL.
83
+ """
84
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.tongrenquan
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=["tongrenquan"],
17
+ )
18
+ class TongrenquanSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 同人圈 (www.tongrenquan.org) novel website.
21
+ """
22
+
23
+ BASE_URL = "https://www.tongrenquan.org"
24
+ BOOK_INFO_URL = "https://www.tongrenquan.org/tongren/{book_id}.html"
25
+ CHAPTER_URL = "https://www.tongrenquan.org/tongren/{book_id}/{chapter_id}.html"
26
+
27
+ def __init__(
28
+ self,
29
+ config: FetcherConfig,
30
+ cookies: dict[str, str] | None = None,
31
+ **kwargs: Any,
32
+ ) -> None:
33
+ super().__init__("tongrenquan", config, cookies, **kwargs)
34
+
35
+ async def get_book_info(
36
+ self,
37
+ book_id: str,
38
+ **kwargs: Any,
39
+ ) -> list[str]:
40
+ """
41
+ Fetch the raw HTML of the book info page asynchronously.
42
+
43
+ :param book_id: The book identifier.
44
+ :return: The page content as string list.
45
+ """
46
+ url = self.book_info_url(book_id=book_id)
47
+ return [await self.fetch(url, **kwargs)]
48
+
49
+ async def get_book_chapter(
50
+ self,
51
+ book_id: str,
52
+ chapter_id: str,
53
+ **kwargs: Any,
54
+ ) -> list[str]:
55
+ """
56
+ Fetch the raw HTML of a single chapter asynchronously.
57
+
58
+ :param book_id: The book identifier.
59
+ :param chapter_id: The chapter identifier.
60
+ :return: The page content as string list.
61
+ """
62
+ url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
63
+ return [await self.fetch(url, **kwargs)]
64
+
65
+ @classmethod
66
+ def book_info_url(cls, book_id: str) -> str:
67
+ """
68
+ Construct the URL for fetching a book's info page.
69
+
70
+ :param book_id: The identifier of the book.
71
+ :return: Fully qualified URL for the book info page.
72
+ """
73
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
74
+
75
+ @classmethod
76
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
77
+ """
78
+ Construct the URL for fetching a specific chapter.
79
+
80
+ :param book_id: The identifier of the book.
81
+ :param chapter_id: The identifier of the chapter.
82
+ :return: Fully qualified chapter URL.
83
+ """
84
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.ttkan
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=["ttkan"],
17
+ )
18
+ class TtkanSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 天天看小说 (www.ttkan.co) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://{lang}.ttkan.co/novel/chapters/{book_id}"
24
+ CHAPTER_URL = "https://{lang}.wa01.com/novel/pagea/{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__("ttkan", config, cookies, **kwargs)
33
+ self._lang = "cn" if config.locale_style == "simplified" else "tw"
34
+
35
+ async def get_book_info(
36
+ self,
37
+ book_id: str,
38
+ **kwargs: Any,
39
+ ) -> list[str]:
40
+ """
41
+ Fetch the raw HTML of the book info page asynchronously.
42
+
43
+ :param book_id: The book identifier.
44
+ :return: The page content as string list.
45
+ """
46
+ url = self.book_info_url(book_id=book_id)
47
+ return [await self.fetch(url, **kwargs)]
48
+
49
+ async def get_book_chapter(
50
+ self,
51
+ book_id: str,
52
+ chapter_id: str,
53
+ **kwargs: Any,
54
+ ) -> list[str]:
55
+ """
56
+ Fetch the raw HTML of a single chapter asynchronously.
57
+
58
+ :param book_id: The book identifier.
59
+ :param chapter_id: The chapter identifier.
60
+ :return: The page content as string list.
61
+ """
62
+ url = self.chapter_url(
63
+ book_id=book_id,
64
+ chapter_id=chapter_id,
65
+ lang=self._lang,
66
+ )
67
+ return [await self.fetch(url, **kwargs)]
68
+
69
+ @classmethod
70
+ def book_info_url(cls, book_id: str, lang: str = "cn") -> str:
71
+ """
72
+ Construct the URL for fetching a book's info page.
73
+
74
+ :param book_id: The identifier of the book.
75
+ :param lang: The language of the book. (cn / tw)
76
+ :return: Fully qualified URL for the book info page.
77
+ """
78
+ return cls.BOOK_INFO_URL.format(book_id=book_id, lang=lang)
79
+
80
+ @classmethod
81
+ def chapter_url(
82
+ cls,
83
+ book_id: str,
84
+ chapter_id: str,
85
+ lang: str = "cn",
86
+ ) -> str:
87
+ """
88
+ Construct the URL for fetching a specific chapter.
89
+
90
+ :param book_id: The identifier of the book.
91
+ :param chapter_id: The chapter identifier.
92
+ :param lang: The language of the book. (cn / tw)
93
+ :return: Fully qualified chapter URL.
94
+ """
95
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id, lang=lang)
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.wanbengo
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=["wanbengo"],
17
+ )
18
+ class WanbengoSession(BaseSession):
19
+ """
20
+ A session class for interacting with the 完本神站 (www.wanbengo.com) novel website.
21
+ """
22
+
23
+ BOOK_INFO_URL = "https://www.wanbengo.com/{book_id}/"
24
+ CHAPTER_URL = "https://www.wanbengo.com/{book_id}/{chapter_id}.html"
25
+
26
+ def __init__(
27
+ self,
28
+ config: FetcherConfig,
29
+ cookies: dict[str, str] | None = None,
30
+ **kwargs: Any,
31
+ ) -> None:
32
+ super().__init__("wanbengo", 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)
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.xiaoshuowu
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=["xiaoshuowu", "xiaoshuoge"],
18
+ )
19
+ class XiaoshuowuSession(BaseSession):
20
+ """
21
+ A session class for interacting with the 小说屋 (www.xiaoshuoge.info) novel.
22
+ """
23
+
24
+ BOOK_INFO_URL = "http://www.xiaoshuoge.info/book/{book_id}/"
25
+ BOOK_CATALOG_URL = "http://www.xiaoshuoge.info/html/{book_id}/"
26
+ CHAPTER_URL = "http://www.xiaoshuoge.info/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__("xiaoshuowu", 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, ssl=False, **kwargs),
55
+ self.fetch(catalog_url, ssl=False, **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, ssl=False, **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)
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.fetchers.xiguashuwu
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
+ from novel_downloader.utils import async_jitter_sleep
14
+
15
+
16
+ @register_fetcher(
17
+ site_keys=["xiguashuwu"],
18
+ )
19
+ class XiguashuwuSession(BaseSession):
20
+ """
21
+ A session class for interacting with the 西瓜书屋 (www.xiguashuwu.com) novel.
22
+ """
23
+
24
+ BASE_URL = "https://www.xiguashuwu.com"
25
+ BOOK_INFO_URL = "https://www.xiguashuwu.com/book/{book_id}/iszip/0/"
26
+ BOOK_CATALOG_URL = "https://www.xiguashuwu.com/book/{book_id}/catalog/"
27
+ CHAPTER_URL = "https://www.xiguashuwu.com/book/{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__("xiguashuwu", 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, catalogs1, ..., catalogsN]
46
+
47
+ :param book_id: The book identifier.
48
+ :return: The page content as string list.
49
+ """
50
+ info_url = self.book_info_url(book_id=book_id)
51
+ info_html = await self.fetch(info_url, **kwargs)
52
+
53
+ catalog_url = self.book_catalog_url(book_id=book_id)
54
+ catalog_pages: list[str] = []
55
+ idx = 1
56
+ while True:
57
+ suffix = "" if idx == 1 else f"{idx}.html"
58
+ full_url = catalog_url + suffix
59
+
60
+ try:
61
+ html = await self.fetch(full_url, **kwargs)
62
+ except Exception as exc:
63
+ self.logger.warning(
64
+ "[async] get_book_catalog(%s page %d) failed: %s",
65
+ book_id,
66
+ idx,
67
+ exc,
68
+ )
69
+ break
70
+
71
+ catalog_pages.append(html)
72
+ idx += 1
73
+ next_patterns = [
74
+ # f"javascript:readbook('{book_id}','{idx}');",
75
+ # f"javascript:gobook('{book_id}','{idx}');",
76
+ # f"javascript:runbook('{book_id}','{idx}');",
77
+ # f"javascript:gotochapter('{book_id}','{idx}');",
78
+ f"javascript:readbookjump('{book_id}','{idx}');",
79
+ f"javascript:gobookjump('{book_id}','{idx}');",
80
+ f"javascript:runbookjump('{book_id}','{idx}');",
81
+ f"javascript:gotojump('{book_id}','{idx}');",
82
+ f"javascript:gotochapterjump('{book_id}','{idx}');",
83
+ f"/book/{book_id}/catalog/{idx}.html",
84
+ ]
85
+ if not any(pat in html for pat in next_patterns):
86
+ break
87
+
88
+ await async_jitter_sleep(
89
+ self.request_interval,
90
+ mul_spread=1.1,
91
+ max_sleep=self.request_interval + 2,
92
+ )
93
+ return [info_html, *catalog_pages]
94
+
95
+ async def get_book_chapter(
96
+ self,
97
+ book_id: str,
98
+ chapter_id: str,
99
+ **kwargs: Any,
100
+ ) -> list[str]:
101
+ """
102
+ Fetch the raw HTML of a single chapter asynchronously.
103
+
104
+ Order: [page1, ..., pageN]
105
+
106
+ :param book_id: The book identifier.
107
+ :param chapter_id: The chapter identifier.
108
+ :return: The page content as string list.
109
+ """
110
+ html_pages: list[str] = []
111
+ idx = 1
112
+
113
+ while True:
114
+ chapter_suffix = chapter_id if idx == 1 else f"{chapter_id}_{idx}"
115
+ relative_path = self.relative_chapter_url(book_id, chapter_suffix)
116
+ if idx > 1 and relative_path not in html_pages[-1]:
117
+ break
118
+ full_url = self.BASE_URL + relative_path
119
+
120
+ try:
121
+ html = await self.fetch(full_url, **kwargs)
122
+ except Exception as exc:
123
+ self.logger.warning(
124
+ "[async] get_book_chapter(%s page %d) failed: %s",
125
+ chapter_id,
126
+ idx,
127
+ exc,
128
+ )
129
+ break
130
+
131
+ html_pages.append(html)
132
+ idx += 1
133
+ await async_jitter_sleep(
134
+ self.request_interval,
135
+ mul_spread=1.1,
136
+ max_sleep=self.request_interval + 2,
137
+ )
138
+
139
+ return html_pages
140
+
141
+ @classmethod
142
+ def book_info_url(cls, book_id: str) -> str:
143
+ """
144
+ Construct the URL for fetching a book's info page.
145
+
146
+ :param book_id: The identifier of the book.
147
+ :return: Fully qualified URL for the book info page.
148
+ """
149
+ return cls.BOOK_INFO_URL.format(book_id=book_id)
150
+
151
+ @classmethod
152
+ def book_catalog_url(cls, book_id: str) -> str:
153
+ """
154
+ Construct the URL for fetching a book's catalog page.
155
+
156
+ :param book_id: The identifier of the book.
157
+ :return: Fully qualified catalog page URL.
158
+ """
159
+ return cls.BOOK_CATALOG_URL.format(book_id=book_id)
160
+
161
+ @classmethod
162
+ def chapter_url(cls, book_id: str, chapter_id: str) -> str:
163
+ """
164
+ Construct the URL for fetching a specific chapter.
165
+
166
+ :param book_id: The identifier of the book.
167
+ :param chapter_id: The identifier of the chapter.
168
+ :return: Fully qualified chapter URL.
169
+ """
170
+ return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
171
+
172
+ @classmethod
173
+ def relative_chapter_url(cls, book_id: str, chapter_id: str) -> str:
174
+ """
175
+ Return the relative URL path for a given chapter.
176
+ """
177
+ return f"/book/{book_id}/{chapter_id}.html"