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,110 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.tongrenquan
4
+ -------------------------------------------
5
+
6
+ """
7
+
8
+ import logging
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.searchers.base import BaseSearcher
13
+ from novel_downloader.core.searchers.registry import register_searcher
14
+ from novel_downloader.models import SearchResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @register_searcher(
20
+ site_keys=["tongrenquan"],
21
+ )
22
+ class TongrenquanSearcher(BaseSearcher):
23
+ site_name = "tongrenquan"
24
+ priority = 30
25
+ SEARCH_URL = "https://www.tongrenquan.org/e/search/indexstart.php"
26
+ BASE_URL = "https://www.tongrenquan.org"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ keyboard = cls._quote(keyword, encoding="gbk", errors="replace")
31
+ show = "title"
32
+ classid = "0"
33
+ body = f"keyboard={keyboard}&show={show}&classid={classid}"
34
+ headers = {
35
+ "Origin": "https://www.tongrenquan.cc",
36
+ "Referer": "https://www.tongrenquan.cc/",
37
+ "Content-Type": "application/x-www-form-urlencoded",
38
+ }
39
+ try:
40
+ async with (
41
+ await cls._http_post(cls.SEARCH_URL, data=body, headers=headers)
42
+ ) as resp:
43
+ return await cls._response_to_str(resp)
44
+ except Exception:
45
+ logger.error(
46
+ "Failed to fetch HTML for keyword '%s' from '%s'",
47
+ keyword,
48
+ cls.SEARCH_URL,
49
+ )
50
+ return ""
51
+
52
+ @classmethod
53
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
54
+ doc = html.fromstring(html_str)
55
+ rows = doc.xpath('//div[@class="books m-cols"]/div[@class="bk"]')
56
+ results: list[SearchResult] = []
57
+
58
+ for idx, row in enumerate(rows):
59
+ href = cls._first_str(row.xpath(".//h3/a[1]/@href"))
60
+ if not href:
61
+ continue
62
+
63
+ if limit is not None and idx >= limit:
64
+ break
65
+
66
+ # '/tongren/9302.html' -> "9302"
67
+ book_id = href.split("/")[-1].split(".")[0]
68
+ book_url = cls._abs_url(href)
69
+
70
+ cover_rel = cls._first_str(
71
+ row.xpath("./div[@class='pic']/a[1]/img[1]/@src")
72
+ )
73
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
74
+
75
+ title = cls._first_str(
76
+ row.xpath("./div[@class='bk_right']/h3/a[1]//text()")
77
+ )
78
+
79
+ author = (
80
+ cls._first_str(
81
+ row.xpath("./div[@class='bk_right']/div[@class='booknews']/text()"),
82
+ replaces=[("作者:", "")],
83
+ )
84
+ or "-"
85
+ )
86
+
87
+ update_date = cls._first_str(
88
+ row.xpath(
89
+ "./div[@class='bk_right']/div[@class='booknews']/label[@class='date']/text()"
90
+ )
91
+ )
92
+
93
+ # Compute priority
94
+ prio = cls.priority + idx
95
+
96
+ results.append(
97
+ SearchResult(
98
+ site=cls.site_name,
99
+ book_id=book_id,
100
+ book_url=book_url,
101
+ cover_url=cover_url,
102
+ title=title,
103
+ author=author,
104
+ latest_chapter="-",
105
+ update_date=update_date,
106
+ word_count="-",
107
+ priority=prio,
108
+ )
109
+ )
110
+ return results
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.ttkan
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ import logging
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.searchers.base import BaseSearcher
13
+ from novel_downloader.core.searchers.registry import register_searcher
14
+ from novel_downloader.models import SearchResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @register_searcher(
20
+ site_keys=["ttkan"],
21
+ )
22
+ class TtkanSearcher(BaseSearcher):
23
+ site_name = "ttkan"
24
+ priority = 100
25
+ BASE_URL = "https://www.ttkan.co"
26
+ SEARCH_URL = "https://www.ttkan.co/novel/search"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ params = {"q": keyword}
31
+ try:
32
+ async with (await cls._http_get(cls.SEARCH_URL, params=params)) as resp:
33
+ return await cls._response_to_str(resp)
34
+ except Exception:
35
+ logger.error(
36
+ "Failed to fetch HTML for keyword '%s' from '%s'",
37
+ keyword,
38
+ cls.SEARCH_URL,
39
+ )
40
+ return ""
41
+
42
+ @classmethod
43
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
44
+ doc = html.fromstring(html_str)
45
+ items = doc.xpath(
46
+ '//div[contains(@class,"frame_body")]//div[@class="pure-g"]/div[contains(@class,"novel_cell")]'
47
+ )
48
+ if not items:
49
+ items = doc.xpath('//div[contains(@class,"novel_cell")]')
50
+ results: list[SearchResult] = []
51
+
52
+ for idx, item in enumerate(items):
53
+ href = cls._first_str(item.xpath(".//a[@href][1]/@href"))
54
+ if not href:
55
+ continue
56
+
57
+ if limit is not None and len(results) >= limit:
58
+ break
59
+
60
+ # link -> /novel/chapters/<book_id>
61
+ book_id = href.strip("/").split("/")[-1]
62
+ book_url = cls._abs_url(href)
63
+
64
+ cover_rel = cls._first_str(item.xpath(".//amp-img/@src"))
65
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
66
+
67
+ title = cls._first_str(item.xpath(".//h3/text()"))
68
+
69
+ author = (
70
+ cls._first_str(
71
+ item.xpath(".//li[starts-with(normalize-space(.),'作者')]/text()"),
72
+ replaces=[("作者:", "")],
73
+ )
74
+ or "-"
75
+ )
76
+
77
+ prio = cls.priority + idx
78
+ results.append(
79
+ SearchResult(
80
+ site=cls.site_name,
81
+ book_id=book_id,
82
+ book_url=book_url,
83
+ cover_url=cover_url,
84
+ title=title,
85
+ author=author,
86
+ latest_chapter="-",
87
+ update_date="-",
88
+ word_count="-",
89
+ priority=prio,
90
+ )
91
+ )
92
+ return results
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.xiaoshuowu
4
+ ------------------------------------------
5
+
6
+ """
7
+
8
+ import logging
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.searchers.base import BaseSearcher
13
+ from novel_downloader.core.searchers.registry import register_searcher
14
+ from novel_downloader.models import SearchResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @register_searcher(
20
+ site_keys=["xiaoshuowu", "xiaoshuoge"],
21
+ )
22
+ class XiaoshuowuSearcher(BaseSearcher):
23
+ site_name = "xiaoshuowu"
24
+ priority = 30
25
+ SEARCH_URL = "http://www.xiaoshuoge.info/modules/article/search.php"
26
+
27
+ @classmethod
28
+ async def _fetch_html(cls, keyword: str) -> str:
29
+ params = {"q": keyword}
30
+ try:
31
+ async with (await cls._http_get(cls.SEARCH_URL, params=params)) as resp:
32
+ return await cls._response_to_str(resp)
33
+ except Exception:
34
+ logger.error(
35
+ "Failed to fetch HTML for keyword '%s' from '%s'",
36
+ keyword,
37
+ cls.SEARCH_URL,
38
+ )
39
+ return ""
40
+
41
+ @classmethod
42
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
43
+ """
44
+ Parse raw HTML from Xiaoshuowu search results into list of SearchResult.
45
+
46
+ :param html_str: Raw HTML string from Xiaoshuowu search results page.
47
+ :param limit: Maximum number of results to return, or None for all.
48
+ :return: List of SearchResult dicts.
49
+ """
50
+ doc = html.fromstring(html_str)
51
+ rows = doc.xpath('//div[@class="c_row"]')
52
+ results: list[SearchResult] = []
53
+
54
+ for idx, row in enumerate(rows):
55
+ href = cls._first_str(row.xpath(".//span[@class='c_subject']/a/@href"))
56
+ if not href:
57
+ continue
58
+
59
+ if limit is not None and idx >= limit:
60
+ break
61
+
62
+ # 'http://www.xiaoshuoge.info/book/374339/' -> "374339"
63
+ book_id = href.split("book/")[-1].strip("/")
64
+ book_url = cls._abs_url(href)
65
+
66
+ cover_rel = cls._first_str(row.xpath(".//div[@class='fl']//img/@src"))
67
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
68
+
69
+ title = cls._first_str(row.xpath(".//span[@class='c_subject']/a/text()"))
70
+
71
+ author = (
72
+ cls._first_str(
73
+ row.xpath(
74
+ ".//div[@class='c_tag'][1]/span[@class='c_label'][contains(.,'作者')]/following-sibling::span[@class='c_value'][1]/text()"
75
+ )
76
+ )
77
+ or "-"
78
+ )
79
+ word_count = (
80
+ cls._first_str(
81
+ row.xpath(
82
+ ".//div[@class='c_tag'][1]/span[@class='c_label'][contains(.,'字数')]/following-sibling::span[@class='c_value'][1]/text()"
83
+ )
84
+ )
85
+ or "-"
86
+ )
87
+
88
+ latest_chapter = (
89
+ cls._first_str(
90
+ row.xpath(
91
+ ".//div[@class='c_tag'][last()]/span[@class='c_label'][contains(.,'最新')]/following-sibling::span[@class='c_value'][1]//a//text()"
92
+ )
93
+ )
94
+ or "-"
95
+ )
96
+ update_date = (
97
+ cls._first_str(
98
+ row.xpath(
99
+ ".//div[@class='c_tag'][last()]/span[@class='c_label'][contains(.,'更新')]/following-sibling::span[@class='c_value'][1]/text()"
100
+ )
101
+ )
102
+ or "-"
103
+ )
104
+
105
+ # Priority
106
+ prio = cls.priority + idx
107
+
108
+ results.append(
109
+ SearchResult(
110
+ site=cls.site_name,
111
+ book_id=book_id,
112
+ book_url=book_url,
113
+ cover_url=cover_url,
114
+ title=title,
115
+ author=author,
116
+ latest_chapter=latest_chapter,
117
+ update_date=update_date,
118
+ word_count=word_count,
119
+ priority=prio,
120
+ )
121
+ )
122
+ return results
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.xiguashuwu
4
+ ------------------------------------------
5
+
6
+ """
7
+
8
+ import logging
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.searchers.base import BaseSearcher
13
+ from novel_downloader.core.searchers.registry import register_searcher
14
+ from novel_downloader.models import SearchResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @register_searcher(
20
+ site_keys=["xiguashuwu"],
21
+ )
22
+ class XiguashuwuSearcher(BaseSearcher):
23
+ site_name = "xiguashuwu"
24
+ priority = 500
25
+ BASE_URL = "https://www.xiguashuwu.com"
26
+ SEARCH_URL = "https://www.xiguashuwu.com/search/{query}"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ url = cls.SEARCH_URL.format(query=cls._quote(keyword))
31
+ headers = {
32
+ "Referer": "https://www.xiguashuwu.com/search/",
33
+ }
34
+ try:
35
+ async with (await cls._http_get(url, headers=headers)) as resp:
36
+ return await cls._response_to_str(resp)
37
+ except Exception:
38
+ logger.error(
39
+ "Failed to fetch HTML for keyword '%s' from '%s'",
40
+ keyword,
41
+ cls.SEARCH_URL,
42
+ )
43
+ return ""
44
+
45
+ @classmethod
46
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
47
+ doc = html.fromstring(html_str)
48
+ rows = doc.xpath('//div[@class="SHsectionThree-middle"]/p')
49
+ results: list[SearchResult] = []
50
+
51
+ for idx, row in enumerate(rows):
52
+ href = cls._first_str(
53
+ row.xpath(".//a[starts-with(@href,'/book/')][1]/@href")
54
+ ) or cls._first_str(row.xpath(".//a[1]/@href"))
55
+ if not href:
56
+ continue
57
+
58
+ if limit is not None and idx >= limit:
59
+ break
60
+
61
+ # '/book/184974/iszip/0/' -> "184974"
62
+ book_id = href.split("/book/")[-1].split("/")[0]
63
+ book_url = cls._abs_url(href)
64
+
65
+ title = (
66
+ cls._first_str(
67
+ row.xpath(".//a[starts-with(@href,'/book/')][1]//text()")
68
+ )
69
+ or cls._first_str(row.xpath(".//a[1]//text()"))
70
+ or "-"
71
+ )
72
+
73
+ author = (
74
+ cls._first_str(
75
+ row.xpath(".//a[starts-with(@href,'/writer/')][1]//text()")
76
+ )
77
+ or cls._first_str(row.xpath(".//a[2]//text()"))
78
+ or "-"
79
+ )
80
+
81
+ results.append(
82
+ SearchResult(
83
+ site=cls.site_name,
84
+ book_id=book_id,
85
+ book_url=book_url,
86
+ cover_url="-",
87
+ title=title,
88
+ author=author,
89
+ latest_chapter="-",
90
+ update_date="-",
91
+ word_count="-",
92
+ priority=cls.priority + idx,
93
+ )
94
+ )
95
+ return results
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.xs63b
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ import logging
9
+
10
+ from lxml import html
11
+
12
+ from novel_downloader.core.searchers.base import BaseSearcher
13
+ from novel_downloader.core.searchers.registry import register_searcher
14
+ from novel_downloader.models import SearchResult
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @register_searcher(
20
+ site_keys=["xs63b"],
21
+ )
22
+ class Xs63bSearcher(BaseSearcher):
23
+ site_name = "xs63b"
24
+ priority = 30
25
+ BASE_URL = "https://www.xs63b.com"
26
+ SEARCH_URL = "https://www.xs63b.com/search/"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ headers = {
31
+ "Host": "www.xs63b.com",
32
+ "Origin": "https://www.xs63b.com",
33
+ "Referer": "https://www.xs63b.com/",
34
+ }
35
+ try:
36
+ async with (await cls._http_get(cls.BASE_URL, headers=headers)) as resp:
37
+ base_html = await cls._response_to_str(resp)
38
+ data = {
39
+ "_token": cls._parse_token(base_html),
40
+ "kw": keyword,
41
+ }
42
+ async with (
43
+ await cls._http_post(cls.SEARCH_URL, data=data, headers=headers)
44
+ ) as resp:
45
+ return await cls._response_to_str(resp)
46
+ except Exception:
47
+ logger.error(
48
+ "Failed to fetch HTML for keyword '%s' from '%s'",
49
+ keyword,
50
+ cls.SEARCH_URL,
51
+ )
52
+ return ""
53
+
54
+ @classmethod
55
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
56
+ doc = html.fromstring(html_str)
57
+ rows = doc.xpath("//div[@class='toplist']/ul/li")
58
+ results: list[SearchResult] = []
59
+
60
+ for idx, row in enumerate(rows):
61
+ href = cls._first_str(row.xpath(".//p[@class='s1']/a[1]/@href"))
62
+ if not href:
63
+ continue
64
+
65
+ if limit is not None and idx >= limit:
66
+ break
67
+
68
+ # 'https://www.xs63b.com/{catalog}/{name}/' -> "{catalog}-{name}"
69
+ book_id = href.split("xs63b.com", 1)[-1].strip(" /").replace("/", "-")
70
+ book_url = cls._abs_url(href)
71
+
72
+ title = "".join(row.xpath(".//p[@class='s1']//a//text()"))
73
+
74
+ latest_chapter = (
75
+ cls._first_str(row.xpath(".//p[@class='s2']//a/text()")) or "-"
76
+ )
77
+ author = cls._first_str(row.xpath(".//p[@class='s3']/text()")) or "-"
78
+ word_count = cls._first_str(row.xpath(".//p[@class='s4']/text()")) or "-"
79
+ update_date = cls._first_str(row.xpath(".//p[@class='s6']/text()")) or "-"
80
+
81
+ # Compute priority
82
+ prio = cls.priority + idx
83
+
84
+ results.append(
85
+ SearchResult(
86
+ site=cls.site_name,
87
+ book_id=book_id,
88
+ book_url=book_url,
89
+ cover_url="",
90
+ title=title,
91
+ author=author,
92
+ latest_chapter=latest_chapter,
93
+ update_date=update_date,
94
+ word_count=word_count,
95
+ priority=prio,
96
+ )
97
+ )
98
+ return results
99
+
100
+ @staticmethod
101
+ def _parse_token(html_str: str) -> str:
102
+ doc = html.fromstring(html_str)
103
+ vals = doc.xpath("//div[@id='search']//input[@name='_token']/@value")
104
+ return vals[0].strip() if vals else ""