novel-downloader 1.3.3__py3-none-any.whl → 1.4.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 (211) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/clean.py +97 -78
  3. novel_downloader/cli/config.py +177 -0
  4. novel_downloader/cli/download.py +132 -87
  5. novel_downloader/cli/export.py +77 -0
  6. novel_downloader/cli/main.py +21 -28
  7. novel_downloader/config/__init__.py +1 -25
  8. novel_downloader/config/adapter.py +32 -31
  9. novel_downloader/config/loader.py +3 -3
  10. novel_downloader/config/site_rules.py +1 -2
  11. novel_downloader/core/__init__.py +3 -6
  12. novel_downloader/core/downloaders/__init__.py +10 -13
  13. novel_downloader/core/downloaders/base.py +233 -0
  14. novel_downloader/core/downloaders/biquge.py +27 -0
  15. novel_downloader/core/downloaders/common.py +414 -0
  16. novel_downloader/core/downloaders/esjzone.py +27 -0
  17. novel_downloader/core/downloaders/linovelib.py +27 -0
  18. novel_downloader/core/downloaders/qianbi.py +27 -0
  19. novel_downloader/core/downloaders/qidian.py +352 -0
  20. novel_downloader/core/downloaders/sfacg.py +27 -0
  21. novel_downloader/core/downloaders/yamibo.py +27 -0
  22. novel_downloader/core/exporters/__init__.py +37 -0
  23. novel_downloader/core/{savers → exporters}/base.py +73 -39
  24. novel_downloader/core/exporters/biquge.py +25 -0
  25. novel_downloader/core/exporters/common/__init__.py +12 -0
  26. novel_downloader/core/{savers → exporters}/common/epub.py +22 -22
  27. novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
  28. novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
  29. novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
  30. novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
  31. novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
  32. novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
  33. novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
  34. novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
  35. novel_downloader/core/exporters/esjzone.py +25 -0
  36. novel_downloader/core/exporters/linovelib/__init__.py +10 -0
  37. novel_downloader/core/exporters/linovelib/epub.py +449 -0
  38. novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
  39. novel_downloader/core/exporters/linovelib/txt.py +129 -0
  40. novel_downloader/core/exporters/qianbi.py +25 -0
  41. novel_downloader/core/{savers → exporters}/qidian.py +8 -8
  42. novel_downloader/core/exporters/sfacg.py +25 -0
  43. novel_downloader/core/exporters/yamibo.py +25 -0
  44. novel_downloader/core/factory/__init__.py +5 -17
  45. novel_downloader/core/factory/downloader.py +24 -126
  46. novel_downloader/core/factory/exporter.py +58 -0
  47. novel_downloader/core/factory/fetcher.py +96 -0
  48. novel_downloader/core/factory/parser.py +17 -12
  49. novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
  50. novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
  51. novel_downloader/core/fetchers/base/browser.py +383 -0
  52. novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
  53. novel_downloader/core/fetchers/base/session.py +419 -0
  54. novel_downloader/core/fetchers/biquge/__init__.py +14 -0
  55. novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
  56. novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
  57. novel_downloader/core/fetchers/common/__init__.py +14 -0
  58. novel_downloader/core/fetchers/common/browser.py +79 -0
  59. novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
  60. novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
  61. novel_downloader/core/fetchers/esjzone/browser.py +202 -0
  62. novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
  63. novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
  64. novel_downloader/core/fetchers/linovelib/browser.py +178 -0
  65. novel_downloader/core/fetchers/linovelib/session.py +178 -0
  66. novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
  67. novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
  68. novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
  69. novel_downloader/core/fetchers/qidian/__init__.py +14 -0
  70. novel_downloader/core/fetchers/qidian/browser.py +266 -0
  71. novel_downloader/core/fetchers/qidian/session.py +326 -0
  72. novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
  73. novel_downloader/core/fetchers/sfacg/browser.py +189 -0
  74. novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
  75. novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
  76. novel_downloader/core/fetchers/yamibo/browser.py +229 -0
  77. novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
  78. novel_downloader/core/interfaces/__init__.py +8 -12
  79. novel_downloader/core/interfaces/downloader.py +54 -0
  80. novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
  81. novel_downloader/core/interfaces/fetcher.py +162 -0
  82. novel_downloader/core/interfaces/parser.py +6 -7
  83. novel_downloader/core/parsers/__init__.py +5 -6
  84. novel_downloader/core/parsers/base.py +9 -13
  85. novel_downloader/core/parsers/biquge/main_parser.py +12 -13
  86. novel_downloader/core/parsers/common/helper.py +3 -3
  87. novel_downloader/core/parsers/common/main_parser.py +39 -34
  88. novel_downloader/core/parsers/esjzone/main_parser.py +20 -14
  89. novel_downloader/core/parsers/linovelib/__init__.py +10 -0
  90. novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
  91. novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
  92. novel_downloader/core/parsers/qidian/__init__.py +2 -11
  93. novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
  94. novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
  95. novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
  96. novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
  97. novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
  98. novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
  99. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
  100. novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
  101. novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
  102. novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
  103. novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
  104. novel_downloader/locales/en.json +18 -2
  105. novel_downloader/locales/zh.json +18 -2
  106. novel_downloader/models/__init__.py +64 -0
  107. novel_downloader/models/browser.py +21 -0
  108. novel_downloader/models/chapter.py +25 -0
  109. novel_downloader/models/config.py +100 -0
  110. novel_downloader/models/login.py +20 -0
  111. novel_downloader/models/site_rules.py +99 -0
  112. novel_downloader/models/tasks.py +33 -0
  113. novel_downloader/models/types.py +15 -0
  114. novel_downloader/resources/config/settings.toml +31 -25
  115. novel_downloader/resources/json/linovelib_font_map.json +3573 -0
  116. novel_downloader/tui/__init__.py +7 -0
  117. novel_downloader/tui/app.py +32 -0
  118. novel_downloader/tui/main.py +17 -0
  119. novel_downloader/tui/screens/__init__.py +14 -0
  120. novel_downloader/tui/screens/home.py +191 -0
  121. novel_downloader/tui/screens/login.py +74 -0
  122. novel_downloader/tui/styles/home_layout.tcss +79 -0
  123. novel_downloader/tui/widgets/richlog_handler.py +24 -0
  124. novel_downloader/utils/__init__.py +6 -0
  125. novel_downloader/utils/chapter_storage.py +25 -38
  126. novel_downloader/utils/constants.py +11 -5
  127. novel_downloader/utils/cookies.py +66 -0
  128. novel_downloader/utils/crypto_utils.py +1 -74
  129. novel_downloader/utils/fontocr/ocr_v1.py +2 -1
  130. novel_downloader/utils/fontocr/ocr_v2.py +2 -2
  131. novel_downloader/utils/hash_store.py +10 -18
  132. novel_downloader/utils/hash_utils.py +3 -2
  133. novel_downloader/utils/logger.py +2 -3
  134. novel_downloader/utils/network.py +2 -1
  135. novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
  136. novel_downloader/utils/text_utils/font_mapping.py +1 -1
  137. novel_downloader/utils/text_utils/text_cleaning.py +1 -1
  138. novel_downloader/utils/time_utils/datetime_utils.py +3 -3
  139. novel_downloader/utils/time_utils/sleep_utils.py +1 -1
  140. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +69 -35
  141. novel_downloader-1.4.0.dist-info/RECORD +170 -0
  142. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
  143. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
  144. novel_downloader/cli/interactive.py +0 -66
  145. novel_downloader/cli/settings.py +0 -177
  146. novel_downloader/config/models.py +0 -187
  147. novel_downloader/core/downloaders/base/__init__.py +0 -14
  148. novel_downloader/core/downloaders/base/base_async.py +0 -153
  149. novel_downloader/core/downloaders/base/base_sync.py +0 -208
  150. novel_downloader/core/downloaders/biquge/__init__.py +0 -14
  151. novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
  152. novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
  153. novel_downloader/core/downloaders/common/__init__.py +0 -14
  154. novel_downloader/core/downloaders/common/common_async.py +0 -210
  155. novel_downloader/core/downloaders/common/common_sync.py +0 -202
  156. novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
  157. novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
  158. novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
  159. novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
  160. novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
  161. novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
  162. novel_downloader/core/downloaders/qidian/__init__.py +0 -10
  163. novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -219
  164. novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
  165. novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
  166. novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
  167. novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
  168. novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
  169. novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
  170. novel_downloader/core/factory/requester.py +0 -144
  171. novel_downloader/core/factory/saver.py +0 -56
  172. novel_downloader/core/interfaces/async_downloader.py +0 -36
  173. novel_downloader/core/interfaces/async_requester.py +0 -84
  174. novel_downloader/core/interfaces/sync_downloader.py +0 -36
  175. novel_downloader/core/interfaces/sync_requester.py +0 -82
  176. novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
  177. novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
  178. novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
  179. novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
  180. novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
  181. novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
  182. novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
  183. novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
  184. novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
  185. novel_downloader/core/requesters/base/async_session.py +0 -410
  186. novel_downloader/core/requesters/base/browser.py +0 -337
  187. novel_downloader/core/requesters/base/session.py +0 -378
  188. novel_downloader/core/requesters/biquge/__init__.py +0 -14
  189. novel_downloader/core/requesters/common/__init__.py +0 -17
  190. novel_downloader/core/requesters/common/session.py +0 -113
  191. novel_downloader/core/requesters/esjzone/__init__.py +0 -13
  192. novel_downloader/core/requesters/esjzone/session.py +0 -235
  193. novel_downloader/core/requesters/qianbi/__init__.py +0 -13
  194. novel_downloader/core/requesters/qidian/__init__.py +0 -21
  195. novel_downloader/core/requesters/qidian/broswer.py +0 -307
  196. novel_downloader/core/requesters/qidian/session.py +0 -290
  197. novel_downloader/core/requesters/sfacg/__init__.py +0 -13
  198. novel_downloader/core/requesters/sfacg/session.py +0 -242
  199. novel_downloader/core/requesters/yamibo/__init__.py +0 -13
  200. novel_downloader/core/requesters/yamibo/session.py +0 -237
  201. novel_downloader/core/savers/__init__.py +0 -34
  202. novel_downloader/core/savers/biquge.py +0 -25
  203. novel_downloader/core/savers/common/__init__.py +0 -12
  204. novel_downloader/core/savers/esjzone.py +0 -25
  205. novel_downloader/core/savers/qianbi.py +0 -25
  206. novel_downloader/core/savers/sfacg.py +0 -25
  207. novel_downloader/core/savers/yamibo.py +0 -25
  208. novel_downloader/resources/config/rules.toml +0 -196
  209. novel_downloader-1.3.3.dist-info/RECORD +0 -166
  210. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
  211. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,307 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.requesters.qidian.broswer
4
- -----------------------------------------------
5
-
6
- This module defines the QidianRequester class for interacting with
7
- the Qidian website.
8
- It extends the BaseBrowser by adding methods for logging in and
9
- retrieving book information.
10
- """
11
-
12
- import time
13
- from typing import Any
14
-
15
- from novel_downloader.config.models import RequesterConfig
16
- from novel_downloader.core.requesters.base import BaseBrowser
17
- from novel_downloader.utils.i18n import t
18
-
19
-
20
- class QidianBrowser(BaseBrowser):
21
- """
22
- QidianRequester provides methods for interacting with Qidian.com,
23
- including checking login status and preparing book-related URLs.
24
-
25
- Inherits base browser setup from BaseBrowser.
26
- """
27
-
28
- BOOKCASE_URL = "https://my.qidian.com/bookcase/"
29
- BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
30
- CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
31
-
32
- def __init__(
33
- self,
34
- config: RequesterConfig,
35
- ):
36
- """
37
- Initialize the QidianRequester with a browser configuration.
38
-
39
- :param config: The RequesterConfig instance containing browser settings.
40
- """
41
- super().__init__(config)
42
- self._logged_in: bool = False
43
- self._retry_times = config.retry_times
44
- self._retry_interval = config.backoff_factor
45
- self._timeout = config.timeout
46
-
47
- def login(
48
- self,
49
- username: str = "",
50
- password: str = "",
51
- manual_login: bool = False,
52
- **kwargs: Any,
53
- ) -> bool:
54
- """
55
- Attempt to log in to Qidian
56
- """
57
- if manual_login:
58
- return self._login_manual()
59
- else:
60
- return self._login_auto()
61
-
62
- def get_book_info(
63
- self,
64
- book_id: str,
65
- **kwargs: Any,
66
- ) -> list[str]:
67
- """
68
- Retrieve the HTML of a Qidian book info page.
69
-
70
- :param book_id: The identifier of the book to fetch.
71
- :return: The HTML content of the book info page, or an empty string on error.
72
- """
73
- url = self.book_info_url(book_id)
74
- try:
75
- # Navigate and fetch
76
- self.page.get(url)
77
- html = str(self.page.html)
78
- self.logger.debug(
79
- "[fetch] Fetched book info for ID %s from %s", book_id, url
80
- )
81
- return [html]
82
- except Exception as e:
83
- self.logger.warning(
84
- "[fetch] Error fetching book info from '%s': %s", url, e
85
- )
86
- return []
87
-
88
- def get_book_chapter(
89
- self,
90
- book_id: str,
91
- chapter_id: str,
92
- **kwargs: Any,
93
- ) -> list[str]:
94
- """
95
- Retrieve the HTML content of a specific chapter.
96
-
97
- Ensures the user is logged in, navigates to the chapter page
98
-
99
- :param book_id: The identifier of the book.
100
- :param chapter_id: The identifier of the chapter.
101
- :return: The HTML content of the chapter page, or empty string on error.
102
- """
103
- url = self.chapter_url(book_id, chapter_id)
104
- try:
105
- # Navigate to chapter URL
106
- self.page.get(url)
107
- html = str(self.page.html)
108
- self.logger.debug(
109
- "[fetch] Fetched chapter %s for book %s", chapter_id, book_id
110
- )
111
- return [html]
112
- except Exception as e:
113
- self.logger.warning("[fetch] Error fetching chapter from '%s': %s", url, e)
114
- return []
115
-
116
- def get_bookcase(
117
- self,
118
- page: int = 1,
119
- **kwargs: Any,
120
- ) -> list[str]:
121
- """
122
- Retrieve the HTML content of the logged-in user's Qidian bookcase page.
123
-
124
- :return: The HTML markup of the bookcase page, or empty string on error.
125
- :raises RuntimeError: If the user is not logged in.
126
- """
127
- if not self._logged_in:
128
- raise RuntimeError("User not logged in. Please call login() first.")
129
-
130
- url = self.bookcase_url()
131
- try:
132
- # Navigate to the bookcase page
133
- self.page.get(url)
134
- html = str(self.page.html)
135
- self.logger.debug("[fetch] Fetched bookcase HTML from %s", url)
136
- return [html]
137
- except Exception as e:
138
- self.logger.warning("[fetch] Error fetching bookcase from '%s': %s", url, e)
139
- return []
140
-
141
- @classmethod
142
- def book_info_url(cls, book_id: str) -> str:
143
- """
144
- Construct the URL for fetching a book's info page.
145
-
146
- :param book_id: The identifier of the book.
147
- :return: Fully qualified URL for the book info page.
148
- """
149
- return cls.BOOK_INFO_URL.format(book_id=book_id)
150
-
151
- @classmethod
152
- def chapter_url(cls, book_id: str, chapter_id: str) -> str:
153
- """
154
- Construct the URL for fetching a specific chapter.
155
-
156
- :param book_id: The identifier of the book.
157
- :param chapter_id: The identifier of the chapter.
158
- :return: Fully qualified chapter URL.
159
- """
160
- return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
161
-
162
- @classmethod
163
- def bookcase_url(cls) -> str:
164
- """
165
- Construct the URL for the user's bookcase page.
166
-
167
- :return: Fully qualified URL of the bookcase.
168
- """
169
- return cls.BOOKCASE_URL
170
-
171
- def _login_auto(self, timeout: float = 5.0) -> bool:
172
- """
173
- Attempt to log in to Qidian by handling overlays and clicking the login button.
174
-
175
- :return: True if login succeeds or is already in place; False otherwise.
176
- """
177
- try:
178
- self.page.get("https://www.qidian.com/")
179
- self.page.wait.eles_loaded("#login-box", timeout=timeout)
180
- except Exception as e:
181
- self.logger.warning("[auth] Failed to load login box: %s", e)
182
- return False
183
-
184
- for attempt in range(1, self._retry_times + 1):
185
- if self._check_login_status():
186
- self.logger.debug("[auth] Already logged in.")
187
- break
188
- self.logger.debug("[auth] Attempting login click (#%s).", attempt)
189
- if self.click_button("@id=login-btn", timeout=timeout):
190
- self.logger.debug("[auth] Login button clicked.")
191
- else:
192
- self.logger.debug("[auth] Login button not found.")
193
- time.sleep(self._retry_interval)
194
-
195
- self._logged_in = self._check_login_status()
196
- if self._logged_in:
197
- self.logger.info("[auth] Login successful.")
198
- else:
199
- self.logger.warning("[auth] Login failed after max retries.")
200
-
201
- return self._logged_in
202
-
203
- def _login_manual(self) -> bool:
204
- """
205
- Guide the user through an interactive manual login flow.
206
-
207
- Steps:
208
- 1. If the browser is headless, shut it down and restart in headful mode.
209
- 2. Navigate to the Qidian homepage.
210
- 3. Prompt the user to complete login, retrying up to `max_retries` times.
211
- 4. Once logged in, restore original headless mode if needed.
212
-
213
- :param max_retries: Number of times to check for login success.
214
- :return: True if login was detected, False otherwise.
215
- """
216
- original_headless = self._headless
217
-
218
- # 1. Switch to headful mode if needed
219
- if self._disable_images_orig:
220
- self.logger.debug("[auth] Temporarily enabling images for manual login.")
221
- self._options.no_imgs(False)
222
- self.restart_browser(headless=False)
223
- elif original_headless:
224
- self.restart_browser(headless=False)
225
-
226
- # 2. Navigate to home page
227
- try:
228
- self.page.get("https://www.qidian.com/")
229
- except Exception as e:
230
- self.logger.warning(
231
- "[auth] Failed to load homepage for manual login: %s", e
232
- )
233
- return False
234
-
235
- # 3. Retry loop
236
- for attempt in range(1, self._retry_times + 1):
237
- if self._check_login_status():
238
- self.logger.debug("[auth] Already logged in.")
239
- self._logged_in = True
240
- break
241
- if attempt == 1:
242
- print(t("login_prompt_intro"))
243
- input(
244
- t(
245
- "login_prompt_press_enter",
246
- attempt=attempt,
247
- max_retries=self._retry_times,
248
- )
249
- )
250
- else:
251
- self.logger.warning(
252
- "[auth] Manual login failed after %d attempts.", self._retry_times
253
- )
254
- self._logged_in = False
255
- return self._logged_in
256
-
257
- # 4. Restore headless if changed, then re-establish session
258
- if original_headless or self._disable_images_orig:
259
- self.logger.debug("[auth] Restoring browser settings after manual login...")
260
- self._options.no_imgs(self._disable_images_orig)
261
- self.restart_browser(headless=original_headless)
262
- self._logged_in = self._login_auto()
263
- if self._logged_in:
264
- self.logger.info(
265
- "[auth] Login session successfully carried over after restart."
266
- )
267
- else:
268
- self.logger.warning(
269
- "[auth] Lost login session after restoring headless mode."
270
- )
271
-
272
- return self._logged_in
273
-
274
- def _check_login_status(self) -> bool:
275
- """
276
- Check whether the user is currently logged in by inspecting
277
- the visibility of the 'sign-in' element on the page.
278
-
279
- :return: True if the user appears to be logged in, False otherwise.
280
- """
281
- try:
282
- self._dismiss_overlay()
283
- sign_in_elem = self.page.ele("@class=sign-in")
284
- if sign_in_elem:
285
- class_value = sign_in_elem.attr("class")
286
- if class_value and "hidden" not in class_value:
287
- return True
288
- except Exception as e:
289
- self.logger.warning("[auth] Error while checking login status: %s", e)
290
- return False
291
-
292
- def _dismiss_overlay(self, timeout: float = 2.0) -> None:
293
- """
294
- Detect and close any full-page overlay mask that might block the login UI.
295
- """
296
- try:
297
- mask = self.page.ele("@@tag()=div@@class=mask", timeout=timeout)
298
- if not mask:
299
- return
300
- self.logger.debug("[auth] Overlay mask detected; attempting to close.")
301
- iframe = self.get_frame("loginIfr")
302
- if iframe is None:
303
- self.logger.debug("[auth] Login iframe not found.")
304
- return
305
- self.click_button("@id=close", page=iframe)
306
- except Exception as e:
307
- self.logger.debug("[auth] Error handling overlay mask: %s", e)
@@ -1,290 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.requesters.qidian.session
4
- -----------------------------------------------
5
-
6
- This module defines the QidianRequester class for interacting with
7
- the Qidian website.
8
- It extends the BaseSession by adding methods for logging in and
9
- retrieving book information.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import base64
15
- from http.cookies import SimpleCookie
16
- from typing import Any, ClassVar
17
-
18
- from requests import Response
19
-
20
- from novel_downloader.config.models import RequesterConfig
21
- from novel_downloader.core.requesters.base import BaseSession
22
- from novel_downloader.utils.crypto_utils import patch_qd_payload_token
23
- from novel_downloader.utils.i18n import t
24
- from novel_downloader.utils.state import state_mgr
25
-
26
-
27
- class QidianSession(BaseSession):
28
- """
29
- QidianRequester provides methods for interacting with Qidian.com,
30
- including checking login status and preparing book-related URLs.
31
-
32
- Inherits base session setup from BaseSession.
33
- """
34
-
35
- BOOKCASE_URL = "https://my.qidian.com/bookcase/"
36
- BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
37
- CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
38
-
39
- _cookie_keys: ClassVar[list[str]] = [
40
- "X2NzcmZUb2tlbg==",
41
- "eXdndWlk",
42
- "eXdvcGVuaWQ=",
43
- "eXdrZXk=",
44
- "d190c2Zw",
45
- ]
46
-
47
- def __init__(
48
- self,
49
- config: RequesterConfig,
50
- ):
51
- """
52
- Initialize the QidianSession with a session configuration.
53
-
54
- :param config: The RequesterConfig instance containing request settings.
55
- """
56
- super().__init__(config)
57
- self._logged_in: bool = False
58
- self._retry_times = config.retry_times
59
- self._retry_interval = config.backoff_factor
60
- self._timeout = config.timeout
61
-
62
- def login(
63
- self,
64
- username: str = "",
65
- password: str = "",
66
- manual_login: bool = False,
67
- **kwargs: Any,
68
- ) -> bool:
69
- """
70
- Restore cookies persisted by the session-based workflow.
71
- """
72
- cookies: dict[str, str] = state_mgr.get_cookies("qidian")
73
-
74
- # Merge cookies into both the internal cache and the live session
75
- self.update_cookies(cookies)
76
- for attempt in range(1, self._retry_times + 1):
77
- if self._check_login_status():
78
- self.logger.debug("[auth] Already logged in.")
79
- self._logged_in = True
80
- return True
81
-
82
- if attempt == 1:
83
- print(t("session_login_prompt_intro"))
84
- cookie_str = input(
85
- t(
86
- "session_login_prompt_paste_cookie",
87
- attempt=attempt,
88
- max_retries=self._retry_times,
89
- )
90
- ).strip()
91
-
92
- cookies = self._parse_cookie_input(cookie_str)
93
- if not self._check_cookies(cookies):
94
- print(t("session_login_prompt_invalid_cookie"))
95
- continue
96
-
97
- self.update_cookies(cookies)
98
- return self._check_login_status()
99
-
100
- def get_book_info(
101
- self,
102
- book_id: str,
103
- **kwargs: Any,
104
- ) -> list[str]:
105
- """
106
- Fetch the raw HTML of the book info page.
107
-
108
- :param book_id: The book identifier.
109
- :return: The page content as a string.
110
- """
111
- url = self.book_info_url(book_id=book_id)
112
- try:
113
- resp = self.get(url, **kwargs)
114
- resp.raise_for_status()
115
- return [resp.text]
116
- except Exception as exc:
117
- self.logger.warning(
118
- "[session] get_book_info(%s) failed: %s",
119
- book_id,
120
- exc,
121
- )
122
- return []
123
-
124
- def get_book_chapter(
125
- self,
126
- book_id: str,
127
- chapter_id: str,
128
- **kwargs: Any,
129
- ) -> list[str]:
130
- """
131
- Fetch the HTML of a single chapter.
132
-
133
- :param book_id: The book identifier.
134
- :param chapter_id: The chapter identifier.
135
- :return: The chapter content as a string.
136
- """
137
- url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
138
- try:
139
- resp = self.get(url, **kwargs)
140
- resp.raise_for_status()
141
- return [resp.text]
142
- except Exception as exc:
143
- self.logger.warning(
144
- "[session] get_book_chapter(%s) failed: %s",
145
- book_id,
146
- exc,
147
- )
148
- return []
149
-
150
- def get_bookcase(
151
- self,
152
- page: int = 1,
153
- **kwargs: Any,
154
- ) -> list[str]:
155
- """
156
- Retrieve the user's *bookcase* page.
157
-
158
- :return: The HTML markup of the bookcase page.
159
- """
160
- url = self.bookcase_url()
161
- try:
162
- resp = self.get(url, **kwargs)
163
- resp.raise_for_status()
164
- return [resp.text]
165
- except Exception as exc:
166
- self.logger.warning(
167
- "[session] get_bookcase failed: %s",
168
- exc,
169
- )
170
- return []
171
-
172
- def get(
173
- self,
174
- url: str,
175
- params: dict[str, Any] | None = None,
176
- **kwargs: Any,
177
- ) -> Response:
178
- """
179
- Same as :py:meth:`BaseSession.get`, but transparently refreshes
180
- a cookie-based token used for request validation.
181
-
182
- The method:
183
- 1. Reads the existing cookie (if any);
184
- 2. Generates a new value tied to *url*;
185
- 3. Updates both the live ``requests.Session`` and the internal cache;
186
- 4. Delegates the actual request to ``super().get``.
187
- """
188
- if self._session is None:
189
- raise RuntimeError("Session is not initialized or has been shut down.")
190
-
191
- # ---- 1. refresh token cookie --------------------------------------
192
- cookie_key = self._d("d190c2Zw")
193
- old_token = self._session.cookies.get(cookie_key, "")
194
-
195
- if old_token:
196
- refreshed_token = patch_qd_payload_token(old_token, url)
197
- self._session.cookies.set(cookie_key, refreshed_token)
198
- self._cookies[cookie_key] = refreshed_token
199
-
200
- # ---- 2. perform the real GET --------------------------------------------
201
- resp: Response = super().get(url, params=params, **kwargs)
202
-
203
- # ---- 3. persist any server-set cookies (optional) --------------
204
- self.update_cookies(self._session.cookies.get_dict())
205
- state_mgr.set_cookies("qidian", self._cookies)
206
-
207
- return resp
208
-
209
- @classmethod
210
- def book_info_url(cls, book_id: str) -> str:
211
- """
212
- Construct the URL for fetching a book's info page.
213
-
214
- :param book_id: The identifier of the book.
215
- :return: Fully qualified URL for the book info page.
216
- """
217
- return cls.BOOK_INFO_URL.format(book_id=book_id)
218
-
219
- @classmethod
220
- def chapter_url(cls, book_id: str, chapter_id: str) -> str:
221
- """
222
- Construct the URL for fetching a specific chapter.
223
-
224
- :param book_id: The identifier of the book.
225
- :param chapter_id: The identifier of the chapter.
226
- :return: Fully qualified chapter URL.
227
- """
228
- return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
229
-
230
- @classmethod
231
- def bookcase_url(cls) -> str:
232
- """
233
- Construct the URL for the user's bookcase page.
234
-
235
- :return: Fully qualified URL of the bookcase.
236
- """
237
- return cls.BOOKCASE_URL
238
-
239
- def _check_login_status(self) -> bool:
240
- """
241
- Check whether the user is currently logged in by
242
- inspecting the bookcase page content.
243
-
244
- :return: True if the user appears to be logged in, False otherwise.
245
- """
246
- keywords = [
247
- 'var buid = "fffffffffffffffffff"',
248
- "C2WF946J0/probe.js",
249
- ]
250
- resp_text = self.get_bookcase()
251
- if not resp_text:
252
- return False
253
- return not any(kw in resp_text[0] for kw in keywords)
254
-
255
- @staticmethod
256
- def _parse_cookie_input(cookie_str: str) -> dict[str, str]:
257
- """
258
- Parse a raw cookie string (e.g. from browser dev tools) into a dict.
259
- Returns an empty dict if parsing fails.
260
-
261
- :param cookie_str: The raw cookie header string.
262
- :return: Parsed cookie dict.
263
- """
264
- filtered = "; ".join(pair for pair in cookie_str.split(";") if "=" in pair)
265
- parsed = SimpleCookie()
266
- try:
267
- parsed.load(filtered)
268
- return {k: v.value for k, v in parsed.items()}
269
- except Exception:
270
- return {}
271
-
272
- def _check_cookies(self, cookies: dict[str, str]) -> bool:
273
- """
274
- Check if the provided cookies contain all required keys.
275
-
276
- Logs any missing keys as warnings.
277
-
278
- :param cookies: The cookie dictionary to validate.
279
- :return: True if all required keys are present, False otherwise.
280
- """
281
- required = {self._d(k) for k in self._cookie_keys}
282
- actual = set(cookies)
283
- missing = required - actual
284
- if missing:
285
- self.logger.warning("Missing required cookies: %s", ", ".join(missing))
286
- return not missing
287
-
288
- @staticmethod
289
- def _d(b: str) -> str:
290
- return base64.b64decode(b).decode()
@@ -1,13 +0,0 @@
1
- """
2
- novel_downloader.core.requesters.sfacg
3
- --------------------------------------
4
-
5
- """
6
-
7
- from .async_session import SfacgAsyncSession
8
- from .session import SfacgSession
9
-
10
- __all__ = [
11
- "SfacgAsyncSession",
12
- "SfacgSession",
13
- ]