novel-downloader 1.4.5__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 (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  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/archived/qidian/searcher.py +79 -0
  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 +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  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} +23 -11
  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} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  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} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  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 +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  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} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  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 +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  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/main_parser.py → esjzone.py} +67 -67
  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/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.ixdzs8
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=["ixdzs8"],
21
+ )
22
+ class Ixdzs8Searcher(BaseSearcher):
23
+ site_name = "ixdzs8"
24
+ priority = 30
25
+ BASE_URL = "https://ixdzs8.com"
26
+ SEARCH_URL = "https://ixdzs8.com/bsearch"
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
+ rows = doc.xpath("//ul[contains(@class,'u-list')]/li[contains(@class,'burl')]")
46
+ results: list[SearchResult] = []
47
+
48
+ for idx, row in enumerate(rows):
49
+ book_path = cls._first_str(row.xpath("./@data-url"))
50
+ if not book_path:
51
+ book_path = cls._first_str(
52
+ row.xpath(".//h3[contains(@class,'bname')]/a/@href")
53
+ )
54
+ if not book_path:
55
+ continue
56
+
57
+ if limit is not None and idx >= limit:
58
+ break
59
+
60
+ book_id = book_path.strip("/").split("/")[-1]
61
+ book_url = cls._abs_url(book_path)
62
+
63
+ cover_rel = cls._first_str(
64
+ row.xpath(".//div[contains(@class,'l-img')]//img/@src")
65
+ )
66
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
67
+
68
+ title = cls._first_str(
69
+ row.xpath(".//h3[contains(@class,'bname')]/a/@title")
70
+ ) or cls._first_str(row.xpath(".//h3[contains(@class,'bname')]/a/text()"))
71
+
72
+ author = cls._first_str(
73
+ row.xpath(".//span[contains(@class,'bauthor')]//a/text()")
74
+ )
75
+ word_count = cls._first_str(
76
+ row.xpath(".//span[contains(@class,'size')]/text()")
77
+ )
78
+
79
+ latest_chapter = cls._first_str(
80
+ row.xpath(
81
+ ".//p[contains(@class,'l-last')]//span[contains(@class,'l-chapter')]/text()"
82
+ )
83
+ )
84
+ update_date = cls._first_str(
85
+ row.xpath(
86
+ ".//p[contains(@class,'l-last')]//span[contains(@class,'l-time')]/text()"
87
+ )
88
+ )
89
+
90
+ # Compute priority
91
+ prio = cls.priority + idx
92
+
93
+ results.append(
94
+ SearchResult(
95
+ site=cls.site_name,
96
+ book_id=book_id,
97
+ book_url=book_url,
98
+ cover_url=cover_url,
99
+ title=title,
100
+ author=author,
101
+ latest_chapter=latest_chapter,
102
+ update_date=update_date,
103
+ word_count=word_count,
104
+ priority=prio,
105
+ )
106
+ )
107
+ return results
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.jpxs123
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=["jpxs123"],
21
+ )
22
+ class Jpxs123Searcher(BaseSearcher):
23
+ site_name = "jpxs123"
24
+ priority = 30
25
+ BASE_URL = "https://www.jpxs123.com"
26
+ SEARCH_URL = "https://www.jpxs123.com/e/search/indexsearch.php"
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.jpxs123.com",
36
+ "Referer": "https://www.jpxs123.com/",
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/@href"))
60
+ if not href:
61
+ continue
62
+
63
+ if limit is not None and idx >= limit:
64
+ break
65
+
66
+ book_id = href.strip("/").split(".", 1)[0].replace("/", "-")
67
+ book_url = cls._abs_url(href)
68
+
69
+ title = cls._first_str(row.xpath(".//h3/a//text()"))
70
+
71
+ cover_rel = cls._first_str(
72
+ row.xpath(".//div[contains(@class,'pic')]//a//img/@src")
73
+ )
74
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
75
+
76
+ author = (
77
+ cls._first_str(
78
+ row.xpath(".//div[contains(@class,'booknews')]/text()"),
79
+ replaces=[("作者:", "")],
80
+ )
81
+ or "-"
82
+ )
83
+
84
+ update_date = cls._first_str(
85
+ row.xpath(
86
+ ".//div[contains(@class,'booknews')]/label[contains(@class,'date')]/text()"
87
+ )
88
+ )
89
+
90
+ # Compute priority
91
+ prio = cls.priority + idx
92
+
93
+ results.append(
94
+ SearchResult(
95
+ site=cls.site_name,
96
+ book_id=book_id,
97
+ book_url=book_url,
98
+ cover_url=cover_url,
99
+ title=title,
100
+ author=author,
101
+ latest_chapter="-",
102
+ update_date=update_date,
103
+ word_count="-",
104
+ priority=prio,
105
+ )
106
+ )
107
+ return results
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.piaotia
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=["piaotia"],
21
+ )
22
+ class PiaotiaSearcher(BaseSearcher):
23
+ site_name = "piaotia"
24
+ priority = 30
25
+ SEARCH_URL = "https://www.piaotia.com/modules/article/search.php"
26
+
27
+ @classmethod
28
+ async def _fetch_html(cls, keyword: str) -> str:
29
+ # data = {
30
+ # "searchtype": "articlename",
31
+ # # "searchtype": "author",
32
+ # # "searchtype": "keywords",
33
+ # "searchkey": cls._quote(keyword, encoding="gbk", errors='replace'),
34
+ # "Submit": cls._quote(" 搜 索 ", encoding="gbk", errors='replace'),
35
+ # }
36
+ searchtype = "articlename"
37
+ searchkey = cls._quote(keyword, encoding="gbk", errors="replace")
38
+ submit = cls._quote(" 搜 索 ", encoding="gbk", errors="replace")
39
+ body = f"searchtype={searchtype}&searchkey={searchkey}&Submit={submit}"
40
+ headers = {
41
+ "Origin": "https://www.piaotia.com",
42
+ "Referer": "https://www.piaotia.com",
43
+ "Content-Type": "application/x-www-form-urlencoded",
44
+ }
45
+ try:
46
+ async with (
47
+ await cls._http_post(cls.SEARCH_URL, data=body, headers=headers)
48
+ ) as resp:
49
+ return await cls._response_to_str(resp, encoding="gbk")
50
+ except Exception:
51
+ logger.error(
52
+ "Failed to fetch HTML for keyword '%s' from '%s'",
53
+ keyword,
54
+ cls.SEARCH_URL,
55
+ )
56
+ return ""
57
+
58
+ @classmethod
59
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
60
+ doc = html.fromstring(html_str)
61
+ rows = doc.xpath('//table[@class="grid"]//tr[td]')
62
+ results: list[SearchResult] = []
63
+
64
+ for idx, row in enumerate(rows):
65
+ href = cls._first_str(row.xpath("./td[1]/a[1]/@href"))
66
+ if not href:
67
+ continue
68
+
69
+ if limit is not None and idx >= limit:
70
+ break
71
+
72
+ # "https://www.piaotia.com/bookinfo/14/14767.html" -> "14-14767"
73
+ book_id = href.rstrip(".html").split("bookinfo/")[-1].replace("/", "-")
74
+ book_url = cls._abs_url(href)
75
+
76
+ title = cls._first_str(row.xpath("./td[1]/a[1]//text()"))
77
+
78
+ latest_chapter = cls._first_str(row.xpath("./td[2]/a[1]//text()")) or "-"
79
+
80
+ author = cls._first_str(row.xpath("./td[3]//text()")) or "-"
81
+ word_count = cls._first_str(row.xpath("./td[4]//text()")) or "-"
82
+ update_date = cls._first_str(row.xpath("./td[5]//text()")) or "-"
83
+
84
+ # Compute priority incrementally
85
+ prio = cls.priority + idx
86
+ results.append(
87
+ SearchResult(
88
+ site=cls.site_name,
89
+ book_id=book_id,
90
+ book_url=book_url,
91
+ cover_url="",
92
+ title=title,
93
+ author=author,
94
+ latest_chapter=latest_chapter,
95
+ update_date=update_date,
96
+ word_count=word_count,
97
+ priority=prio,
98
+ )
99
+ )
100
+ return results
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.qbtr
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=["qbtr"],
21
+ )
22
+ class QbtrSearcher(BaseSearcher):
23
+ site_name = "qbtr"
24
+ priority = 30
25
+ BASE_URL = "https://www.qbtr.cc"
26
+ SEARCH_URL = "https://www.qbtr.cc/e/search/index.php"
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.qbtr.cc",
36
+ "Referer": "https://www.qbtr.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/8850.html' -> "tongren-8850"
67
+ book_id = href.strip("/").split(".")[0].replace("/", "-")
68
+ book_url = cls._abs_url(href)
69
+
70
+ title = cls._first_str(row.xpath(".//h3/a[1]//text()"))
71
+
72
+ author = (
73
+ cls._first_str(
74
+ row.xpath(".//div[contains(@class,'booknews')]/text()"),
75
+ replaces=[("作者:", "")],
76
+ )
77
+ or "-"
78
+ )
79
+
80
+ update_date = (
81
+ cls._first_str(
82
+ row.xpath(
83
+ ".//div[contains(@class,'booknews')]/label[contains(@class,'date')]/text()"
84
+ )
85
+ )
86
+ or "-"
87
+ )
88
+
89
+ # Compute priority
90
+ prio = cls.priority + idx
91
+
92
+ results.append(
93
+ SearchResult(
94
+ site=cls.site_name,
95
+ book_id=book_id,
96
+ book_url=book_url,
97
+ cover_url="",
98
+ title=title,
99
+ author=author,
100
+ latest_chapter="-",
101
+ update_date=update_date,
102
+ word_count="-",
103
+ priority=prio,
104
+ )
105
+ )
106
+ return results
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.qianbi
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=["qianbi"],
21
+ )
22
+ class QianbiSearcher(BaseSearcher):
23
+ site_name = "qianbi"
24
+ priority = 10
25
+ BASE_URL = "https://www.23qb.com/"
26
+ SEARCH_URL = "https://www.23qb.com/search.html"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ params = {"searchkey": 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
+ if html_str.find('<meta property="og:url"') != -1:
45
+ return cls._parse_detail_html(html_str)
46
+ return cls._parse_search_list_html(html_str, limit)
47
+
48
+ @classmethod
49
+ def _parse_detail_html(cls, html_str: str) -> list[SearchResult]:
50
+ """
51
+ Parse a single-book detail page, detected via <meta property="og:url">.
52
+
53
+ :param html_str: Raw HTML of the book detail page.
54
+ :return: A single-element list with the book's SearchResult.
55
+ """
56
+ doc = html.fromstring(html_str)
57
+
58
+ book_url = cls._first_str(doc.xpath("//meta[@property='og:url']/@content"))
59
+ if not book_url:
60
+ return []
61
+
62
+ # 'https://www.23qb.com/book/9268/' -> "9268"
63
+ book_id = book_url.split("book/", 1)[-1].strip("/")
64
+
65
+ cover_rel = cls._first_str(
66
+ doc.xpath("//div[contains(@class,'novel-cover')]//img/@data-src")
67
+ ) or cls._first_str(
68
+ doc.xpath("//div[contains(@class,'novel-cover')]//img/@src")
69
+ )
70
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
71
+
72
+ title = cls._first_str(doc.xpath("//h1[@class='page-title']/text()"))
73
+ author = cls._first_str(doc.xpath("//a[contains(@href, '/author/')]/@title"))
74
+
75
+ latest_chapter = (
76
+ cls._first_str(
77
+ doc.xpath(
78
+ "//div[@class='module-row-info']//a[@class='module-row-text']/@title"
79
+ )
80
+ )
81
+ or "-"
82
+ )
83
+ update_date = (
84
+ cls._first_str(
85
+ doc.xpath("//div[@class='module-heading newchapter']/time/text()"),
86
+ replaces=[("更新时间:", "")],
87
+ )
88
+ or "-"
89
+ )
90
+
91
+ word_count = cls._first_str(doc.xpath("//span[contains(text(), '字')]/text()"))
92
+
93
+ return [
94
+ SearchResult(
95
+ site=cls.site_name,
96
+ book_id=book_id,
97
+ book_url=book_url,
98
+ cover_url=cover_url,
99
+ title=title,
100
+ author=author,
101
+ latest_chapter=latest_chapter,
102
+ update_date=update_date,
103
+ word_count=word_count,
104
+ priority=cls.priority,
105
+ )
106
+ ]
107
+
108
+ @classmethod
109
+ def _parse_search_list_html(
110
+ cls, html_str: str, limit: int | None
111
+ ) -> list[SearchResult]:
112
+ """
113
+ Parse a multi-item search result page.
114
+
115
+ :param html_str: Raw HTML of the search-results page.
116
+ :param limit: Maximum number of items to return, or None for all.
117
+ :return: List of SearchResult.
118
+ """
119
+ doc = html.fromstring(html_str)
120
+ items = doc.xpath('//div[contains(@class,"module-search-item")]')
121
+ results: list[SearchResult] = []
122
+
123
+ for idx, item in enumerate(items):
124
+ href = cls._first_str(
125
+ item.xpath(".//div[@class='novel-info-header']/h3/a/@href")
126
+ )
127
+ if not href:
128
+ continue
129
+
130
+ if limit is not None and idx >= limit:
131
+ break
132
+
133
+ # '/book/9138/' -> "9138"
134
+ book_id = href.rstrip("/").split("/")[-1]
135
+ book_url = cls._abs_url(href)
136
+
137
+ title = cls._first_str(
138
+ item.xpath(".//div[@class='novel-info-header']/h3/a//text()")
139
+ )
140
+
141
+ cover_rel = cls._first_str(
142
+ item.xpath(".//div[contains(@class,'module-item-pic')]//img/@data-src")
143
+ ) or cls._first_str(
144
+ item.xpath(".//div[contains(@class,'module-item-pic')]//img/@src")
145
+ )
146
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
147
+
148
+ # Compute priority
149
+ prio = cls.priority + idx
150
+
151
+ results.append(
152
+ SearchResult(
153
+ site=cls.site_name,
154
+ book_id=book_id,
155
+ book_url=book_url,
156
+ cover_url=cover_url,
157
+ title=title,
158
+ author="-", # Author is not present on the page
159
+ latest_chapter="-",
160
+ update_date="-",
161
+ word_count="-",
162
+ priority=prio,
163
+ )
164
+ )
165
+ return results