novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +79 -66
  6. novel_downloader/cli/export.py +17 -21
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +17 -12
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +20 -14
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +64 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +64 -69
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.base
4
- -----------------------------------
5
-
6
- """
7
-
8
- __all__ = [
9
- "BaseBrowser",
10
- "BaseSession",
11
- ]
12
-
13
- from .browser import BaseBrowser
14
- from .session import BaseSession
@@ -1,422 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.base.browser
4
- -------------------------------------------
5
-
6
- """
7
-
8
- import abc
9
- import asyncio
10
- import logging
11
- import types
12
- from pathlib import Path
13
- from typing import Any, Literal, Self, TypedDict
14
-
15
- from playwright.async_api import (
16
- Browser,
17
- BrowserContext,
18
- BrowserType,
19
- Page,
20
- Playwright,
21
- ViewportSize,
22
- async_playwright,
23
- )
24
-
25
- from novel_downloader.core.interfaces import FetcherProtocol
26
- from novel_downloader.models import FetcherConfig, LoginField
27
- from novel_downloader.utils.constants import (
28
- DATA_DIR,
29
- DEFAULT_USER_AGENT,
30
- )
31
-
32
- from .rate_limiter import TokenBucketRateLimiter
33
-
34
- _STEALTH_SCRIPT = """
35
- Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
36
- Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
37
- Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });
38
- window.chrome = { runtime: {} };
39
- """.strip()
40
-
41
-
42
- class NewContextOptions(TypedDict, total=False):
43
- user_agent: str
44
- locale: str
45
- storage_state: Path
46
- viewport: ViewportSize
47
- java_script_enabled: bool
48
- ignore_https_errors: bool
49
- extra_http_headers: dict[str, str]
50
-
51
-
52
- class BaseBrowser(FetcherProtocol, abc.ABC):
53
- """
54
- BaseBrowser wraps basic browser operations using playwright
55
- """
56
-
57
- def __init__(
58
- self,
59
- site: str,
60
- config: FetcherConfig,
61
- reuse_page: bool = False,
62
- **kwargs: Any,
63
- ) -> None:
64
- """
65
- Initialize the async browser with configuration.
66
-
67
- :param config: Configuration object for session behavior
68
- """
69
- self._site = site
70
- self._config = config
71
-
72
- self._state_file = DATA_DIR / site / "browser_state.cookies"
73
- self._state_file.parent.mkdir(parents=True, exist_ok=True)
74
-
75
- self._is_logged_in = False
76
- self._reuse_page = reuse_page
77
- self._pw: Playwright | None = None
78
- self._browser: Browser | None = None
79
- self._context: BrowserContext | None = None
80
- self._page: Page | None = None
81
- self._manual_page: Page | None = None
82
- self._rate_limiter: TokenBucketRateLimiter | None = None
83
-
84
- if config.max_rps is not None and config.max_rps > 0:
85
- self._rate_limiter = TokenBucketRateLimiter(config.max_rps)
86
-
87
- self.logger = logging.getLogger(f"{self.__class__.__name__}")
88
-
89
- async def login(
90
- self,
91
- username: str = "",
92
- password: str = "",
93
- cookies: dict[str, str] | None = None,
94
- attempt: int = 1,
95
- **kwargs: Any,
96
- ) -> bool:
97
- """
98
- Attempt to log in asynchronously.
99
-
100
- :returns: True if login succeeded.
101
- """
102
- return False
103
-
104
- @abc.abstractmethod
105
- async def get_book_info(
106
- self,
107
- book_id: str,
108
- **kwargs: Any,
109
- ) -> list[str]:
110
- """
111
- Fetch the raw HTML (or JSON) of the book info page asynchronously.
112
-
113
- :param book_id: The book identifier.
114
- :return: The page content as a string.
115
- """
116
- ...
117
-
118
- @abc.abstractmethod
119
- async def get_book_chapter(
120
- self,
121
- book_id: str,
122
- chapter_id: str,
123
- **kwargs: Any,
124
- ) -> list[str]:
125
- """
126
- Fetch the raw HTML (or JSON) of a single chapter asynchronously.
127
-
128
- :param book_id: The book identifier.
129
- :param chapter_id: The chapter identifier.
130
- :return: The chapter content as a string.
131
- """
132
- ...
133
-
134
- async def get_bookcase(
135
- self,
136
- **kwargs: Any,
137
- ) -> list[str]:
138
- """
139
- Optional: Retrieve the HTML content of the authenticated user's bookcase page.
140
- Subclasses that support user login/bookcase should override this.
141
-
142
- :return: The HTML of the bookcase page.
143
- """
144
- raise NotImplementedError(
145
- "Bookcase fetching is not supported by this session type. "
146
- "Override get_bookcase() in your subclass to enable it."
147
- )
148
-
149
- async def init(
150
- self,
151
- headless: bool = True,
152
- **kwargs: Any,
153
- ) -> None:
154
- """
155
- Set up the playwright.
156
- """
157
- if self._pw is None:
158
- self._pw = await async_playwright().start()
159
-
160
- if self._browser is None or not self._browser.is_connected():
161
- browser_cls: BrowserType = getattr(self._pw, self.browser_type)
162
-
163
- launch_args: dict[str, Any] = {
164
- "headless": headless and self.headless,
165
- }
166
- if self._config.proxy:
167
- launch_args["proxy"] = {"server": self._config.proxy}
168
-
169
- self._browser = await browser_cls.launch(**launch_args)
170
-
171
- if self._context is None:
172
- context_args: NewContextOptions = {
173
- "user_agent": self.user_agent,
174
- "locale": "zh-CN",
175
- "viewport": ViewportSize(width=1280, height=800),
176
- "java_script_enabled": True,
177
- "ignore_https_errors": not self._config.verify_ssl,
178
- }
179
-
180
- if self._config.headers:
181
- context_args["extra_http_headers"] = self._config.headers
182
-
183
- self._context = await self._browser.new_context(**context_args)
184
- await self._context.add_init_script(_STEALTH_SCRIPT)
185
- self._context.set_default_timeout(self.timeout * 1000)
186
-
187
- async def close(self) -> None:
188
- """
189
- Shutdown and clean up the broswer.
190
- """
191
- if self._page:
192
- await self._page.close()
193
- self._page = None
194
- if self._manual_page:
195
- await self._manual_page.close()
196
- self._manual_page = None
197
- if self._context:
198
- await self._context.close()
199
- self._context = None
200
- if self._browser:
201
- await self._browser.close()
202
- self._browser = None
203
- if self._pw:
204
- await self._pw.stop()
205
- self._pw = None
206
-
207
- async def fetch(
208
- self,
209
- url: str,
210
- wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"]
211
- | None = "load",
212
- referer: str | None = None,
213
- delay: float = 0.0,
214
- **kwargs: Any,
215
- ) -> str:
216
- if self._reuse_page:
217
- return await self._fetch_with_reuse(
218
- url, wait_until, referer, delay, **kwargs
219
- )
220
- else:
221
- return await self._fetch_with_new(url, wait_until, referer, delay, **kwargs)
222
-
223
- async def load_state(self) -> bool:
224
- """ """
225
- if not self._state_file.exists() or self._context is None:
226
- return False
227
- try:
228
- if self._context is not None:
229
- await self._context.close()
230
- context_args: NewContextOptions = {
231
- "user_agent": self.user_agent,
232
- "locale": "zh-CN",
233
- "viewport": ViewportSize(width=1280, height=800),
234
- "java_script_enabled": True,
235
- "ignore_https_errors": not self._config.verify_ssl,
236
- "storage_state": self._state_file,
237
- }
238
-
239
- if self._config.headers:
240
- context_args["extra_http_headers"] = self._config.headers
241
-
242
- self._context = await self.browser.new_context(**context_args)
243
- self._context.set_default_timeout(self.timeout * 1000)
244
- await self._context.add_init_script(_STEALTH_SCRIPT)
245
- self._is_logged_in = await self._check_login_status()
246
- return self._is_logged_in
247
- except Exception as e:
248
- self.logger.warning("Failed to load state: %s", e)
249
- return False
250
-
251
- async def save_state(self) -> bool:
252
- """ """
253
- if self._context is None:
254
- return False
255
- try:
256
- await self._context.storage_state(path=self._state_file)
257
- return True
258
- except Exception as e:
259
- self.logger.warning("Failed to save state: %s", e)
260
- return False
261
-
262
- async def set_interactive_mode(self, enable: bool) -> bool:
263
- """
264
- Enable or disable interactive mode for manual login.
265
-
266
- :param enable: True to enable, False to disable interactive mode.
267
- :return: True if operation or login check succeeded, False otherwise.
268
- """
269
- return False
270
-
271
- async def _check_login_status(self) -> bool:
272
- """
273
- Check whether the user is currently logged in
274
-
275
- :return: True if the user is logged in, False otherwise.
276
- """
277
- return False
278
-
279
- async def _restart_browser(
280
- self,
281
- headless: bool = True,
282
- ) -> None:
283
- """
284
- Shutdown the current browser and restart it with the given headless setting.
285
-
286
- :param headless: Whether to run the browser in headless mode.
287
- """
288
- await self.close()
289
-
290
- # Apply new headless setting and reinitialize
291
- await self.init(headless=headless)
292
- self.logger.debug("[browser] Browser restarted (headless=%s).", headless)
293
-
294
- async def _fetch_with_new(
295
- self,
296
- url: str,
297
- wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"]
298
- | None = "load",
299
- referer: str | None = None,
300
- delay: float = 0.0,
301
- **kwargs: Any,
302
- ) -> str:
303
- page = await self.context.new_page()
304
- try:
305
- await page.goto(url, wait_until=wait_until, referer=referer, **kwargs)
306
- await asyncio.sleep(delay)
307
- html: str = await page.content()
308
- return html
309
- finally:
310
- await page.close()
311
-
312
- async def _fetch_with_reuse(
313
- self,
314
- url: str,
315
- wait_until: Literal["commit", "domcontentloaded", "load", "networkidle"]
316
- | None = "load",
317
- referer: str | None = None,
318
- delay: float = 0.0,
319
- **kwargs: Any,
320
- ) -> str:
321
- if not self._page:
322
- self._page = await self.context.new_page()
323
- await self._page.goto(url, wait_until=wait_until, referer=referer, **kwargs)
324
- await asyncio.sleep(delay)
325
- html: str = await self._page.content()
326
- return html
327
-
328
- @property
329
- def hostname(self) -> str:
330
- return ""
331
-
332
- @property
333
- def site(self) -> str:
334
- return self._site
335
-
336
- @property
337
- def requester_type(self) -> str:
338
- return "browser"
339
-
340
- @property
341
- def is_logged_in(self) -> bool:
342
- """
343
- Indicates whether the requester is currently authenticated.
344
- """
345
- return self._is_logged_in
346
-
347
- @property
348
- def login_fields(self) -> list[LoginField]:
349
- return []
350
-
351
- @property
352
- def browser(self) -> Browser:
353
- """
354
- Return the active playwright.Browser.
355
-
356
- :raises RuntimeError: If the browser is uninitialized.
357
- """
358
- if self._browser is None:
359
- raise RuntimeError("Browser is not initialized or has been shut down.")
360
- return self._browser
361
-
362
- @property
363
- def context(self) -> BrowserContext:
364
- """
365
- Return the active playwright.BrowserContext.
366
-
367
- :raises RuntimeError: If the context is uninitialized.
368
- """
369
- if self._context is None:
370
- raise RuntimeError(
371
- "BrowserContext is not initialized or has been shut down."
372
- )
373
- return self._context
374
-
375
- @property
376
- def headless(self) -> bool:
377
- return self._config.headless
378
-
379
- @property
380
- def user_agent(self) -> str:
381
- ua = self._config.user_agent or ""
382
- return ua.strip() or DEFAULT_USER_AGENT
383
-
384
- @property
385
- def browser_type(self) -> str:
386
- return self._config.browser_type
387
-
388
- @property
389
- def disable_images(self) -> bool:
390
- return self._config.disable_images
391
-
392
- @property
393
- def retry_times(self) -> int:
394
- return self._config.retry_times
395
-
396
- @property
397
- def request_interval(self) -> float:
398
- return self._config.request_interval
399
-
400
- @property
401
- def backoff_factor(self) -> float:
402
- return self._config.backoff_factor
403
-
404
- @property
405
- def timeout(self) -> float:
406
- return self._config.timeout
407
-
408
- @property
409
- def max_connections(self) -> int:
410
- return self._config.max_connections
411
-
412
- async def __aenter__(self) -> Self:
413
- await self.init()
414
- return self
415
-
416
- async def __aexit__(
417
- self,
418
- exc_type: type[BaseException] | None,
419
- exc_val: BaseException | None,
420
- tb: types.TracebackType | None,
421
- ) -> None:
422
- await self.close()
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.biquge
4
- -------------------------------------
5
-
6
- """
7
-
8
- __all__ = [
9
- "BiqugeBrowser",
10
- "BiqugeSession",
11
- ]
12
-
13
- from .browser import BiqugeBrowser
14
- from .session import BiqugeSession
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.esjzone
4
- --------------------------------------
5
-
6
- """
7
-
8
- __all__ = [
9
- "EsjzoneBrowser",
10
- "EsjzoneSession",
11
- ]
12
-
13
- from .browser import EsjzoneBrowser
14
- from .session import EsjzoneSession
@@ -1,209 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.esjzone.browser
4
- ----------------------------------------------
5
-
6
- """
7
-
8
- from typing import Any
9
-
10
- from novel_downloader.core.fetchers.base import BaseBrowser
11
- from novel_downloader.core.fetchers.registry import register_fetcher
12
- from novel_downloader.models import FetcherConfig, LoginField
13
-
14
-
15
- @register_fetcher(
16
- site_keys=["esjzone"],
17
- backends=["browser"],
18
- )
19
- class EsjzoneBrowser(BaseBrowser):
20
- """
21
- A browser class for interacting with the Esjzone (www.esjzone.cc) novel website.
22
- """
23
-
24
- BOOKCASE_URL = "https://www.esjzone.cc/my/favorite"
25
- BOOK_INFO_URL = "https://www.esjzone.cc/detail/{book_id}.html"
26
- CHAPTER_URL = "https://www.esjzone.cc/forum/{book_id}/{chapter_id}.html"
27
-
28
- API_LOGIN_URL_1 = "https://www.esjzone.cc/my/login"
29
- API_LOGIN_URL_2 = "https://www.esjzone.cc/inc/mem_login.php"
30
-
31
- def __init__(
32
- self,
33
- config: FetcherConfig,
34
- reuse_page: bool = False,
35
- **kwargs: Any,
36
- ) -> None:
37
- super().__init__("esjzone", 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
- if self._is_logged_in:
49
- return True
50
-
51
- if not (username and password):
52
- self.logger.warning("[auth] No credentials provided.")
53
- return False
54
-
55
- login_page = await self.context.new_page()
56
-
57
- try:
58
- await login_page.goto(self.API_LOGIN_URL_1, wait_until="networkidle")
59
-
60
- await login_page.fill('input[name="email"]', username)
61
- await login_page.fill('input[name="pwd"]', password)
62
-
63
- await login_page.click('a.btn-send[data-send="mem_login"]')
64
-
65
- await login_page.wait_for_load_state("networkidle")
66
- finally:
67
- await login_page.close()
68
-
69
- self._is_logged_in = await self._check_login_status()
70
-
71
- return self._is_logged_in
72
-
73
- async def get_book_info(
74
- self,
75
- book_id: str,
76
- **kwargs: Any,
77
- ) -> list[str]:
78
- """
79
- Fetch the raw HTML of the book info page asynchronously.
80
-
81
- :param book_id: The book identifier.
82
- :return: The page content as a string.
83
- """
84
- url = self.book_info_url(book_id=book_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
- url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
101
- return [await self.fetch(url, **kwargs)]
102
-
103
- async def get_bookcase(
104
- self,
105
- **kwargs: Any,
106
- ) -> list[str]:
107
- """
108
- Retrieve the user's *bookcase* page.
109
-
110
- :return: The HTML markup of the bookcase page.
111
- """
112
- url = self.bookcase_url()
113
- return [await self.fetch(url, **kwargs)]
114
-
115
- async def set_interactive_mode(self, enable: bool) -> bool:
116
- """
117
- Enable or disable interactive mode for manual login.
118
-
119
- :param enable: True to enable, False to disable interactive mode.
120
- :return: True if operation or login check succeeded, False otherwise.
121
- """
122
- if enable:
123
- if self.headless:
124
- await self._restart_browser(headless=False)
125
- if self._manual_page is None:
126
- self._manual_page = await self.context.new_page()
127
- await self._manual_page.goto(self.API_LOGIN_URL_1)
128
- return True
129
-
130
- # restore
131
- if self._manual_page:
132
- await self._manual_page.close()
133
- self._manual_page = None
134
- if self.headless:
135
- await self._restart_browser(headless=True)
136
- self._is_logged_in = await self._check_login_status()
137
- return self.is_logged_in
138
-
139
- @property
140
- def login_fields(self) -> list[LoginField]:
141
- return [
142
- LoginField(
143
- name="username",
144
- label="用户名",
145
- type="text",
146
- required=True,
147
- placeholder="请输入你的用户名",
148
- description="用于登录 esjzone.cc 的用户名",
149
- ),
150
- LoginField(
151
- name="password",
152
- label="密码",
153
- type="password",
154
- required=True,
155
- placeholder="请输入你的密码",
156
- description="用于登录 esjzone.cc 的密码",
157
- ),
158
- ]
159
-
160
- @classmethod
161
- def bookcase_url(cls) -> str:
162
- """
163
- Construct the URL for the user's bookcase page.
164
-
165
- :return: Fully qualified URL of the bookcase.
166
- """
167
- return cls.BOOKCASE_URL
168
-
169
- @classmethod
170
- def book_info_url(cls, book_id: str) -> str:
171
- """
172
- Construct the URL for fetching a book's info page.
173
-
174
- :param book_id: The identifier of the book.
175
- :return: Fully qualified URL for the book info page.
176
- """
177
- return cls.BOOK_INFO_URL.format(book_id=book_id)
178
-
179
- @classmethod
180
- def chapter_url(cls, book_id: str, chapter_id: str) -> str:
181
- """
182
- Construct the URL for fetching a specific chapter.
183
-
184
- :param book_id: The identifier of the book.
185
- :param chapter_id: The identifier of the chapter.
186
- :return: Fully qualified chapter URL.
187
- """
188
- return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
189
-
190
- async def _check_login_status(self) -> bool:
191
- """
192
- Check whether the user is currently logged in by
193
- inspecting the bookcase page content.
194
-
195
- :return: True if the user is logged in, False otherwise.
196
- """
197
- keywords = [
198
- "window.location.href='/my/login'",
199
- "會員登入",
200
- "會員註冊 SIGN UP",
201
- ]
202
- resp_text = await self.get_bookcase()
203
- if not resp_text:
204
- return False
205
- return not any(kw in resp_text[0] for kw in keywords)
206
-
207
- @property
208
- def hostname(self) -> str:
209
- return "www.esjzone.cc"
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.fetchers.linovelib
4
- ----------------------------------------
5
-
6
- """
7
-
8
- __all__ = [
9
- "LinovelibBrowser",
10
- "LinovelibSession",
11
- ]
12
-
13
- from .browser import LinovelibBrowser
14
- from .session import LinovelibSession