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
@@ -21,63 +21,81 @@ logger = logging.getLogger(__name__)
21
21
  )
22
22
  class EsjzoneSearcher(BaseSearcher):
23
23
  site_name = "esjzone"
24
- priority = 3
24
+ priority = 30
25
+ BASE_URL = "https://www.esjzone.cc"
25
26
  SEARCH_URL = "https://www.esjzone.cc/tags/{query}/"
26
27
 
27
28
  @classmethod
28
- def _fetch_html(cls, keyword: str) -> str:
29
- """
30
- Fetch raw HTML from Esjzone's search page.
31
-
32
- :param keyword: The search term to query on Esjzone.
33
- :return: HTML text of the search results page, or an empty string on fail.
34
- """
29
+ async def _fetch_html(cls, keyword: str) -> str:
35
30
  url = cls.SEARCH_URL.format(query=cls._quote(keyword))
36
31
  try:
37
- response = cls._http_get(url)
38
- return response.text
32
+ async with (await cls._http_get(url)) as resp:
33
+ return await cls._response_to_str(resp)
39
34
  except Exception:
40
35
  logger.error(
41
36
  "Failed to fetch HTML for keyword '%s' from '%s'",
42
37
  keyword,
43
38
  url,
44
- exc_info=True,
45
39
  )
46
40
  return ""
47
41
 
48
42
  @classmethod
49
43
  def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
50
- """
51
- Parse raw HTML from Esjzone search results into list of SearchResult.
52
-
53
- :param html_str: Raw HTML string from Esjzone search results page.
54
- :param limit: Maximum number of results to return, or None for all.
55
- :return: List of SearchResult dicts.
56
- """
57
44
  doc = html.fromstring(html_str)
58
45
  cards = doc.xpath('//div[contains(@class,"card-body")]')
59
46
  results: list[SearchResult] = []
60
47
 
61
48
  for idx, card in enumerate(cards):
49
+ href = cls._first_str(
50
+ card.xpath(".//h5[contains(@class,'card-title')]/a[1]/@href")
51
+ )
52
+ if not href:
53
+ continue
54
+
62
55
  if limit is not None and idx >= limit:
63
56
  break
64
- # Title and book_id
65
- link = card.xpath('.//h5[@class="card-title"]/a')[0]
66
- title = link.text_content().strip()
67
- href = link.get("href", "")
57
+
68
58
  # href format: /detail/<book_id>.html
69
- book_id = href.strip("/").replace("detail/", "").replace(".html", "")
59
+ book_id = href.split("/")[-1].split(".")[0]
60
+ book_url = cls._abs_url(href)
61
+
62
+ title = cls._first_str(
63
+ card.xpath(".//h5[contains(@class,'card-title')]/a[1]//text()")
64
+ )
65
+
66
+ latest_chapter = (
67
+ cls._first_str(
68
+ card.xpath(".//div[contains(@class,'card-ep')]//a[1]//text()")
69
+ )
70
+ or "-"
71
+ )
72
+
70
73
  # Author
71
- author_link = card.xpath('.//div[@class="card-author"]/a')[0]
72
- author = author_link.text_content().strip()
74
+ author = cls._first_str(
75
+ card.xpath(".//div[contains(@class,'card-author')]//a[1]//text()")
76
+ ) or cls._first_str(
77
+ card.xpath(".//div[contains(@class,'card-author')]//text()")
78
+ )
79
+
80
+ cover_data = card.xpath(
81
+ './preceding-sibling::a[contains(@class,"card-img-tiles")]'
82
+ '//div[contains(@class,"lazyload")]/@data-src'
83
+ )
84
+ cover_url = cover_data[0].strip() if cover_data else ""
85
+
73
86
  # Compute priority incrementally
74
87
  prio = cls.priority + idx
75
88
  results.append(
76
89
  SearchResult(
77
90
  site=cls.site_name,
78
91
  book_id=book_id,
92
+ book_url=book_url,
93
+ cover_url=cover_url,
79
94
  title=title,
80
95
  author=author,
96
+ latest_chapter=latest_chapter,
97
+ update_date="-",
98
+ word_count="-",
81
99
  priority=prio,
82
100
  )
83
101
  )
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.hetushu
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=["hetushu"],
21
+ )
22
+ class HetushuSearcher(BaseSearcher):
23
+ site_name = "hetushu"
24
+ priority = 5
25
+ SEARCH_URL = "https://www.hetushu.com/search/"
26
+ BASE_URL = "https://www.hetushu.com"
27
+
28
+ @classmethod
29
+ async def _fetch_html(cls, keyword: str) -> str:
30
+ params = {"keyword": keyword}
31
+ headers = {
32
+ "Referer": "https://www.hetushu.com/",
33
+ }
34
+ try:
35
+ async with (
36
+ await cls._http_get(cls.SEARCH_URL, params=params, headers=headers)
37
+ ) as resp:
38
+ return await cls._response_to_str(resp)
39
+ except Exception:
40
+ logger.error(
41
+ "Failed to fetch HTML for keyword '%s' from '%s'",
42
+ keyword,
43
+ cls.SEARCH_URL,
44
+ )
45
+ return ""
46
+
47
+ @classmethod
48
+ def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
49
+ doc = html.fromstring(html_str)
50
+ rows = doc.xpath('//dl[@class="list" and @id="body"]/dd')
51
+ results: list[SearchResult] = []
52
+
53
+ for idx, row in enumerate(rows):
54
+ href = cls._first_str(row.xpath(".//h4/a/@href"))
55
+ if not href:
56
+ continue
57
+
58
+ if limit is not None and idx >= limit:
59
+ break
60
+
61
+ # "/book/7631/index.html" -> "7631"
62
+ book_id = href.rstrip("/index.html").split("/")[-1]
63
+ book_url = cls._abs_url(href)
64
+
65
+ title = cls._first_str(row.xpath(".//h4/a/text()"))
66
+
67
+ # Author from the adjacent <span>, strip "/" delimiters
68
+ # e.x. " / 风行云亦行 / "
69
+ author_raw = cls._first_str(row.xpath(".//h4/span/text()"))
70
+ author = author_raw.strip("/").strip()
71
+
72
+ cover_rel = cls._first_str(row.xpath(".//a/img/@src"))
73
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
74
+
75
+ # Compute priority
76
+ prio = cls.priority + idx
77
+
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,93 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.searchers.i25zw
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=["i25zw"],
21
+ )
22
+ class I25zwSearcher(BaseSearcher):
23
+ site_name = "i25zw"
24
+ priority = 30
25
+ SEARCH_URL = "https://www.i25zw.com/search.html"
26
+
27
+ @classmethod
28
+ async def _fetch_html(cls, keyword: str) -> str:
29
+ payload = {
30
+ "searchkey": keyword,
31
+ "searchtype": "all",
32
+ "Submit": "",
33
+ }
34
+ try:
35
+ async with (await cls._http_post(cls.SEARCH_URL, data=payload)) 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[@id='alistbox']")
49
+ results: list[SearchResult] = []
50
+
51
+ for idx, row in enumerate(rows):
52
+ book_url = cls._first_str(row.xpath(".//div[@class='pic']/a/@href"))
53
+ if not book_url:
54
+ continue
55
+
56
+ if limit is not None and idx >= limit:
57
+ break
58
+
59
+ # 'https://www.i25zw.com/book/309209.html' -> "309209"
60
+ book_id = book_url.split("/")[-1].split(".")[0]
61
+
62
+ title = cls._first_str(row.xpath(".//div[@class='title']/h2/a/text()"))
63
+
64
+ author = cls._first_str(
65
+ row.xpath(".//div[@class='title']/span/text()"),
66
+ replaces=[("作者:", "")],
67
+ )
68
+
69
+ cover_rel = cls._first_str(row.xpath(".//div[@class='pic']//img/@src"))
70
+ cover_url = cls._abs_url(cover_rel) if cover_rel else ""
71
+
72
+ # Latest chapter
73
+ latest_chapter = (
74
+ cls._first_str(row.xpath(".//div[@class='sys']//li[1]/a/text()")) or "-"
75
+ )
76
+
77
+ prio = cls.priority + idx
78
+
79
+ results.append(
80
+ SearchResult(
81
+ site=cls.site_name,
82
+ book_id=book_id,
83
+ book_url=book_url,
84
+ cover_url=cover_url,
85
+ title=title,
86
+ author=author,
87
+ latest_chapter=latest_chapter,
88
+ update_date="-",
89
+ word_count="-",
90
+ priority=prio,
91
+ )
92
+ )
93
+ return results
@@ -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