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
@@ -1,193 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.linovelib.browser
4
- ------------------------------------------------
5
-
6
- """
7
-
8
- import re
9
- from typing import Any
10
-
11
- from novel_downloader.core.fetchers.base import BaseBrowser
12
- from novel_downloader.models import FetcherConfig
13
- from novel_downloader.utils.time_utils import async_sleep_with_random_delay
14
-
15
-
16
- class LinovelibBrowser(BaseBrowser):
17
- """
18
- A browser class for interacting with Linovelib (www.linovelib.com) novel website.
19
- """
20
-
21
- BASE_URL = "https://www.linovelib.com"
22
- BOOK_INFO_URL = "https://www.linovelib.com/novel/{book_id}.html"
23
- BOOK_CATALOG_UTL = "https://www.linovelib.com/novel/{book_id}/catalog"
24
- BOOK_VOL_URL = "https://www.linovelib.com/novel/{book_id}/{vol_id}.html"
25
- CHAPTER_URL = "https://www.linovelib.com/novel/{book_id}/{chapter_id}.html"
26
-
27
- _VOL_ID_PATTERN: re.Pattern[str] = re.compile(r"/novel/\d+/(vol_\d+)\.html")
28
-
29
- def __init__(
30
- self,
31
- config: FetcherConfig,
32
- reuse_page: bool = False,
33
- **kwargs: Any,
34
- ) -> None:
35
- super().__init__("linovelib", config, reuse_page, **kwargs)
36
-
37
- async def get_book_info(
38
- self,
39
- book_id: str,
40
- **kwargs: Any,
41
- ) -> list[str]:
42
- """
43
- Fetch the raw HTML of the book info page.
44
-
45
- :param book_id: The book identifier.
46
- :return: A list of HTML strings: [info_html, vol1_html, ..., volN_html]
47
- """
48
- url = self.book_info_url(book_id=book_id)
49
- info_html = await self.fetch(url, **kwargs)
50
-
51
- vol_ids = self._extract_vol_ids(info_html)
52
- vol_ids.reverse()
53
- if not vol_ids:
54
- url = self.catalog_url(book_id=book_id)
55
- catalog_html = await self.fetch(url, **kwargs)
56
- vol_ids = self._extract_vol_ids(catalog_html)
57
-
58
- vol_htmls = []
59
- for vol_id in vol_ids:
60
- await async_sleep_with_random_delay(
61
- self.request_interval,
62
- mul_spread=1.1,
63
- max_sleep=self.request_interval + 2,
64
- )
65
- html = await self.get_book_volume(book_id, vol_id, **kwargs)
66
- if html:
67
- vol_htmls.append(html)
68
-
69
- return [info_html] + vol_htmls
70
-
71
- async def get_book_volume(
72
- self,
73
- book_id: str,
74
- vol_id: str,
75
- **kwargs: Any,
76
- ) -> str:
77
- """
78
- Fetch the HTML content of a specific volume.
79
-
80
- :param book_id: The book identifier.
81
- :param vol_id: The volume identifier.
82
- :return: The volume content as a string.
83
- """
84
- url = self.volume_url(book_id=book_id, vol_id=vol_id)
85
- return await self.fetch(url, **kwargs)
86
-
87
- async def get_book_chapter(
88
- self,
89
- book_id: str,
90
- chapter_id: str,
91
- **kwargs: Any,
92
- ) -> list[str]:
93
- """
94
- Fetch the raw HTML of a single chapter asynchronously.
95
-
96
- :param book_id: The book identifier.
97
- :param chapter_id: The chapter identifier.
98
- :return: The chapter content as a string.
99
- """
100
- html_pages: list[str] = []
101
- idx = 1
102
-
103
- while True:
104
- chapter_suffix = chapter_id if idx == 1 else f"{chapter_id}_{idx}"
105
- relative_path = self.relative_chapter_url(book_id, chapter_suffix)
106
- full_url = self.BASE_URL + relative_path
107
-
108
- if idx > 1 and relative_path not in html_pages[-1]:
109
- break
110
-
111
- try:
112
- html = await self.fetch(full_url, **kwargs)
113
- except Exception as exc:
114
- self.logger.warning(
115
- "[async] get_book_chapter(%s page %d) failed: %s",
116
- chapter_id,
117
- idx,
118
- exc,
119
- )
120
- break
121
-
122
- html_pages.append(html)
123
- idx += 1
124
- await async_sleep_with_random_delay(
125
- self.request_interval,
126
- mul_spread=1.1,
127
- max_sleep=self.request_interval + 2,
128
- )
129
-
130
- return html_pages
131
-
132
- @classmethod
133
- def book_info_url(cls, book_id: str) -> str:
134
- """
135
- Construct the URL for fetching a book's info page.
136
-
137
- :param book_id: The identifier of the book.
138
- :return: Fully qualified URL for the book info page.
139
- """
140
- return cls.BOOK_INFO_URL.format(book_id=book_id)
141
-
142
- @classmethod
143
- def catalog_url(cls, book_id: str) -> str:
144
- """
145
- Construct the URL for fetching a catalog page.
146
-
147
- :param book_id: The identifier of the book.
148
- :return: Fully qualified catalog URL.
149
- """
150
- return cls.BOOK_CATALOG_UTL.format(book_id=book_id)
151
-
152
- @classmethod
153
- def volume_url(cls, book_id: str, vol_id: str) -> str:
154
- """
155
- Construct the URL for fetching a specific volume.
156
-
157
- :param book_id: The identifier of the book.
158
- :param vol_id: The identifier of the volume.
159
- :return: Fully qualified volume URL.
160
- """
161
- return cls.BOOK_VOL_URL.format(book_id=book_id, vol_id=vol_id)
162
-
163
- @classmethod
164
- def chapter_url(cls, book_id: str, chapter_id: str) -> str:
165
- """
166
- Construct the URL for fetching a specific chapter.
167
-
168
- :param book_id: The identifier of the book.
169
- :param chapter_id: The identifier of the chapter.
170
- :return: Fully qualified chapter URL.
171
- """
172
- return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
173
-
174
- @property
175
- def hostname(self) -> str:
176
- return "www.linovelib.com"
177
-
178
- @classmethod
179
- def relative_chapter_url(cls, book_id: str, chapter_id: str) -> str:
180
- """
181
- Return the relative URL path for a given chapter.
182
- """
183
- return f"/novel/{book_id}/{chapter_id}.html"
184
-
185
- def _extract_vol_ids(self, html_str: str) -> list[str]:
186
- """
187
- Extract volume IDs (like 'vol_12345') from the info HTML.
188
-
189
- :param html_str: Raw HTML of the info page.
190
- :return: List of volume ID strings.
191
- """
192
- # /novel/{book_id}/{vol_id}.html
193
- return self._VOL_ID_PATTERN.findall(html_str)
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.qianbi
4
- -------------------------------------
5
-
6
- """
7
-
8
- from .browser import QianbiBrowser
9
- from .session import QianbiSession
10
-
11
- __all__ = [
12
- "QianbiBrowser",
13
- "QianbiSession",
14
- ]
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.qidian
4
- -------------------------------------
5
-
6
- """
7
-
8
- from .browser import QidianBrowser
9
- from .session import QidianSession
10
-
11
- __all__ = [
12
- "QidianBrowser",
13
- "QidianSession",
14
- ]
@@ -1,318 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.qidian.browser
4
- ---------------------------------------------
5
-
6
- """
7
-
8
- import asyncio
9
- from typing import Any
10
-
11
- from playwright.async_api import Page
12
-
13
- from novel_downloader.core.fetchers.base import BaseBrowser
14
- from novel_downloader.models import FetcherConfig, LoginField
15
- from novel_downloader.utils.i18n import t
16
-
17
-
18
- class QidianBrowser(BaseBrowser):
19
- """
20
- A browser class for interacting with the Qidian (www.qidian.com) novel website.
21
- """
22
-
23
- HOMEPAGE_URL = "https://www.qidian.com/"
24
- BOOKCASE_URL = "https://my.qidian.com/bookcase/"
25
- # BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
26
- BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
27
- CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
28
-
29
- LOGIN_URL = "https://passport.qidian.com/"
30
-
31
- def __init__(
32
- self,
33
- config: FetcherConfig,
34
- reuse_page: bool = False,
35
- **kwargs: Any,
36
- ) -> None:
37
- super().__init__("qidian", config, reuse_page, **kwargs)
38
-
39
- async def login(
40
- self,
41
- username: str = "",
42
- password: str = "",
43
- cookies: dict[str, str] | None = None,
44
- attempt: int = 1,
45
- **kwargs: Any,
46
- ) -> bool:
47
- self._is_logged_in = await self._check_login_status()
48
- return self._is_logged_in
49
-
50
- async def get_book_info(
51
- self,
52
- book_id: str,
53
- **kwargs: Any,
54
- ) -> list[str]:
55
- """
56
- Fetch the raw HTML of the book info page asynchronously.
57
-
58
- :param book_id: The book identifier.
59
- :return: The page content as a string.
60
- """
61
- url = self.book_info_url(book_id=book_id)
62
- return [await self.fetch(url, **kwargs)]
63
-
64
- async def get_book_chapter(
65
- self,
66
- book_id: str,
67
- chapter_id: str,
68
- **kwargs: Any,
69
- ) -> list[str]:
70
- """
71
- Fetch the raw HTML of a single chapter asynchronously.
72
-
73
- :param book_id: The book identifier.
74
- :param chapter_id: The chapter identifier.
75
- :return: The chapter content as a string.
76
- """
77
- catalog_url = self.book_info_url(book_id=book_id)
78
- url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
79
- return [await self.fetch(url, referer=catalog_url, **kwargs)]
80
-
81
- async def get_bookcase(
82
- self,
83
- **kwargs: Any,
84
- ) -> list[str]:
85
- """
86
- Retrieve the user's *bookcase* page.
87
-
88
- :return: The HTML markup of the bookcase page.
89
- """
90
- url = self.bookcase_url()
91
- return [await self.fetch(url, **kwargs)]
92
-
93
- async def get_homepage(
94
- self,
95
- **kwargs: Any,
96
- ) -> list[str]:
97
- """
98
- Retrieve the site home page.
99
-
100
- :return: The HTML markup of the home page.
101
- """
102
- url = self.homepage_url()
103
- return [await self.fetch(url, **kwargs)]
104
-
105
- async def set_interactive_mode(self, enable: bool) -> bool:
106
- """
107
- Enable or disable interactive mode for manual login.
108
-
109
- :param enable: True to enable, False to disable interactive mode.
110
- :return: True if operation or login check succeeded, False otherwise.
111
- """
112
- if enable:
113
- if self.headless:
114
- await self._restart_browser(headless=False)
115
- if self._manual_page is None:
116
- self._manual_page = await self.context.new_page()
117
- await self._manual_page.goto(self.LOGIN_URL)
118
- return True
119
-
120
- # restore
121
- if self._manual_page:
122
- await self._manual_page.close()
123
- self._manual_page = None
124
- if self.headless:
125
- await self._restart_browser(headless=True)
126
- self._is_logged_in = await self._check_login_status()
127
- return self.is_logged_in
128
-
129
- @property
130
- def login_fields(self) -> list[LoginField]:
131
- return [
132
- LoginField(
133
- name="manual_login",
134
- label="手动登录",
135
- type="manual_login",
136
- required=True,
137
- description=t("login_prompt_intro"),
138
- )
139
- ]
140
-
141
- @classmethod
142
- def homepage_url(cls) -> str:
143
- """
144
- Construct the URL for the site home page.
145
-
146
- :return: Fully qualified URL of the home page.
147
- """
148
- return cls.HOMEPAGE_URL
149
-
150
- @classmethod
151
- def bookcase_url(cls) -> str:
152
- """
153
- Construct the URL for the user's bookcase page.
154
-
155
- :return: Fully qualified URL of the bookcase.
156
- """
157
- return cls.BOOKCASE_URL
158
-
159
- @classmethod
160
- def book_info_url(cls, book_id: str) -> str:
161
- """
162
- Construct the URL for fetching a book's info page.
163
-
164
- :param book_id: The identifier of the book.
165
- :return: Fully qualified URL for the book info page.
166
- """
167
- return cls.BOOK_INFO_URL.format(book_id=book_id)
168
-
169
- @classmethod
170
- def chapter_url(cls, book_id: str, chapter_id: str) -> str:
171
- """
172
- Construct the URL for fetching a specific chapter.
173
-
174
- :param book_id: The identifier of the book.
175
- :param chapter_id: The identifier of the chapter.
176
- :return: Fully qualified chapter URL.
177
- """
178
- return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
179
-
180
- @property
181
- def hostname(self) -> str:
182
- return "www.qidian.com"
183
-
184
- async def _check_login_status(self) -> bool:
185
- """
186
- Check whether the user is currently logged in by inspecting
187
- the visibility of the 'sign-in' element on the homepage.
188
-
189
- :return: True if the user appears to be logged in, False otherwise.
190
- """
191
- try:
192
- page = await self.context.new_page()
193
- await self._login_auto(page)
194
- await self._dismiss_overlay(page)
195
- await page.goto(self.HOMEPAGE_URL, wait_until="networkidle")
196
- sign_in_elem = await page.query_selector("#login-box .sign-in")
197
- sign_out_elem = await page.query_selector("#login-box .sign-out")
198
-
199
- sign_in_class = (
200
- (await sign_in_elem.get_attribute("class") or "")
201
- if sign_in_elem
202
- else ""
203
- )
204
- sign_out_class = (
205
- (await sign_out_elem.get_attribute("class") or "")
206
- if sign_out_elem
207
- else ""
208
- )
209
-
210
- sign_in_hidden = "hidden" in sign_in_class
211
- sign_out_hidden = "hidden" in sign_out_class
212
-
213
- await page.close()
214
-
215
- # if sign_in_visible and not sign_out_visible:
216
- if not sign_in_hidden and sign_out_hidden:
217
- self.logger.debug("[auth] Detected as logged in.")
218
- return True
219
- else:
220
- self.logger.debug("[auth] Detected as not logged in.")
221
- return False
222
- except Exception as e:
223
- self.logger.warning("[auth] Error while checking login status: %s", e)
224
- return False
225
-
226
- async def _dismiss_overlay(
227
- self,
228
- page: Page,
229
- timeout: float = 2.0,
230
- ) -> None:
231
- """
232
- Detect and close any full-page overlay mask that might block the login UI.
233
- """
234
- try:
235
- mask = await page.wait_for_selector("div.mask", timeout=timeout * 1000)
236
- if not mask or not await mask.is_visible():
237
- return
238
-
239
- self.logger.debug("[auth] Overlay mask detected; attempting to close.")
240
-
241
- iframe_element = await page.wait_for_selector(
242
- "#loginIfr",
243
- timeout=timeout * 1000,
244
- )
245
- if iframe_element is None:
246
- self.logger.debug("[auth] Login iframe not found.")
247
- return
248
-
249
- iframe = await iframe_element.content_frame()
250
- if iframe is None:
251
- self.logger.debug("[auth] Unable to access iframe content.")
252
- return
253
-
254
- # 点击关闭按钮
255
- await iframe.click("#close", timeout=2000)
256
- self.logger.debug("[auth] Overlay mask closed.")
257
-
258
- except Exception as e:
259
- self.logger.debug("[auth] Error handling overlay mask: %s", e)
260
-
261
- async def _login_auto(
262
- self,
263
- page: Page,
264
- timeout: float = 5.0,
265
- ) -> None:
266
- """
267
- Attempt one automatic login interaction (click once and check).
268
-
269
- :param page: Playwright Page object to interact with.
270
- :param timeout: Seconds to wait for login box to appear.
271
- :return: True if login successful or already logged in; False otherwise.
272
- """
273
- try:
274
- await page.goto("https://www.qidian.com/", wait_until="networkidle")
275
- await page.wait_for_selector("#login-box", timeout=timeout * 1000)
276
- except Exception as e:
277
- self.logger.warning("[auth] Failed to load login box: %s", e)
278
- return
279
-
280
- self.logger.debug("[auth] Clicking login button once.")
281
- try:
282
- btn = await page.query_selector("#login-btn")
283
- if btn and await btn.is_visible():
284
- await btn.click()
285
- tasks = [
286
- asyncio.create_task(
287
- page.wait_for_selector(
288
- "div.mask",
289
- timeout=timeout * 1000,
290
- )
291
- ),
292
- asyncio.create_task(
293
- page.wait_for_selector(
294
- "div.qdlogin-wrap",
295
- timeout=timeout * 1000,
296
- )
297
- ),
298
- asyncio.create_task(
299
- page.wait_for_url(
300
- lambda url: "login" not in url,
301
- timeout=timeout * 1000,
302
- )
303
- ),
304
- ]
305
- done, pending = await asyncio.wait(
306
- tasks,
307
- timeout=timeout + 1,
308
- return_when=asyncio.FIRST_COMPLETED,
309
- )
310
- for task in pending:
311
- task.cancel()
312
- if done:
313
- self.logger.debug("[auth] Login flow proceeded after button click.")
314
- else:
315
- self.logger.warning("[auth] Timeout waiting for login to proceed.")
316
- except Exception as e:
317
- self.logger.debug("[auth] Failed to click login button: %s", e)
318
- return
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.sfacg
4
- ------------------------------------
5
-
6
- """
7
-
8
- from .browser import SfacgBrowser
9
- from .session import SfacgSession
10
-
11
- __all__ = [
12
- "SfacgBrowser",
13
- "SfacgSession",
14
- ]