novel-downloader 2.0.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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/download.py +3 -3
- novel_downloader/cli/export.py +1 -1
- novel_downloader/cli/ui.py +7 -7
- novel_downloader/config/adapter.py +191 -154
- novel_downloader/core/__init__.py +5 -6
- novel_downloader/core/exporters/common/txt.py +9 -9
- novel_downloader/core/exporters/linovelib/txt.py +9 -9
- novel_downloader/core/fetchers/qidian.py +20 -35
- novel_downloader/core/interfaces/fetcher.py +2 -2
- novel_downloader/core/interfaces/parser.py +2 -2
- novel_downloader/core/parsers/base.py +1 -0
- novel_downloader/core/parsers/eightnovel.py +2 -2
- novel_downloader/core/parsers/esjzone.py +3 -3
- novel_downloader/core/parsers/qidian/main_parser.py +747 -12
- novel_downloader/core/parsers/qidian/utils/__init__.py +2 -21
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/xiguashuwu.py +6 -12
- novel_downloader/locales/en.json +3 -3
- novel_downloader/locales/zh.json +3 -3
- novel_downloader/utils/__init__.py +0 -2
- novel_downloader/utils/chapter_storage.py +2 -3
- novel_downloader/utils/constants.py +1 -3
- novel_downloader/utils/cookies.py +32 -17
- novel_downloader/utils/crypto_utils/__init__.py +0 -6
- novel_downloader/utils/crypto_utils/rc4.py +40 -50
- novel_downloader/utils/epub/__init__.py +2 -3
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +5 -5
- novel_downloader/utils/epub/documents.py +7 -7
- novel_downloader/utils/epub/models.py +8 -8
- novel_downloader/utils/epub/utils.py +10 -10
- novel_downloader/utils/file_utils/io.py +48 -73
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -11
- novel_downloader/utils/fontocr/__init__.py +13 -0
- novel_downloader/utils/{fontocr.py → fontocr/core.py} +70 -61
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +80 -56
- novel_downloader/utils/network.py +16 -40
- novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- novel_downloader/utils/time_utils/sleep_utils.py +53 -43
- novel_downloader/web/main.py +1 -1
- novel_downloader/web/pages/search.py +3 -3
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/METADATA +2 -1
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/RECORD +51 -55
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -18,10 +18,8 @@ import aiohttp
|
|
18
18
|
from novel_downloader.core.fetchers.base import BaseSession
|
19
19
|
from novel_downloader.core.fetchers.registry import register_fetcher
|
20
20
|
from novel_downloader.models import FetcherConfig, LoginField
|
21
|
-
from novel_downloader.utils import
|
22
|
-
|
23
|
-
rc4_crypt,
|
24
|
-
)
|
21
|
+
from novel_downloader.utils import async_jitter_sleep
|
22
|
+
from novel_downloader.utils.crypto_utils.rc4 import rc4_init, rc4_stream
|
25
23
|
|
26
24
|
|
27
25
|
@register_fetcher(
|
@@ -54,6 +52,8 @@ class QidianSession(BaseSession):
|
|
54
52
|
**kwargs: Any,
|
55
53
|
) -> None:
|
56
54
|
super().__init__("qidian", config, cookies, **kwargs)
|
55
|
+
self._s_init = rc4_init(self._d2("dGcwOUl0Myo5aA=="))
|
56
|
+
self._cookie_key = self._d("d190c2Zw")
|
57
57
|
self._fp_key = self._d("ZmluZ2VycHJpbnQ=")
|
58
58
|
self._ab_key = self._d("YWJub3JtYWw=")
|
59
59
|
self._ck_key = self._d("Y2hlY2tzdW0=")
|
@@ -165,12 +165,10 @@ class QidianSession(BaseSession):
|
|
165
165
|
if self._rate_limiter:
|
166
166
|
await self._rate_limiter.wait()
|
167
167
|
|
168
|
-
cookie_key = self._d("d190c2Zw")
|
169
|
-
|
170
168
|
for attempt in range(self.retry_times + 1):
|
171
169
|
try:
|
172
170
|
refreshed_token = self._build_payload_token(url)
|
173
|
-
self.update_cookies({
|
171
|
+
self.update_cookies({self._cookie_key: refreshed_token})
|
174
172
|
|
175
173
|
async with self.session.get(url, **kwargs) as resp:
|
176
174
|
resp.raise_for_status()
|
@@ -227,40 +225,30 @@ class QidianSession(BaseSession):
|
|
227
225
|
"""
|
228
226
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
229
227
|
|
230
|
-
def _update_fp_val(
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
""""""
|
236
|
-
enc_token = self._get_cookie_value(self._d("d190c2Zw"))
|
228
|
+
def _update_fp_val(self) -> None:
|
229
|
+
"""
|
230
|
+
Decrypt the payload from cookie and update `_fp_val` and `_ab_val`.
|
231
|
+
"""
|
232
|
+
enc_token = self._get_cookie_value(self._cookie_key)
|
237
233
|
if not enc_token:
|
238
234
|
return
|
239
|
-
|
240
|
-
|
241
|
-
|
235
|
+
|
236
|
+
cipher_bytes = base64.b64decode(enc_token)
|
237
|
+
plain_bytes = rc4_stream(self._s_init, cipher_bytes)
|
238
|
+
decrypted_json = plain_bytes.decode("utf-8", errors="replace")
|
242
239
|
payload: dict[str, Any] = json.loads(decrypted_json)
|
243
240
|
self._fp_val = payload.get(self._fp_key, "")
|
244
241
|
self._ab_val = payload.get(self._ab_key, "0" * 32)
|
245
242
|
|
246
|
-
def _build_payload_token(
|
247
|
-
self,
|
248
|
-
new_uri: str,
|
249
|
-
*,
|
250
|
-
key: str = "",
|
251
|
-
) -> str:
|
243
|
+
def _build_payload_token(self, new_uri: str) -> str:
|
252
244
|
"""
|
253
245
|
Patch a timestamp-bearing token with fresh timing and checksum info.
|
254
246
|
|
255
247
|
:param new_uri: URI used in checksum generation.
|
256
|
-
:param key: RC4 key extracted from front-end JavaScript (optional).
|
257
|
-
|
258
248
|
:return: Updated token with new timing and checksum values.
|
259
249
|
"""
|
260
250
|
if not self._fp_val or not self._ab_val:
|
261
251
|
self._update_fp_val()
|
262
|
-
if not key:
|
263
|
-
key = self._get_key()
|
264
252
|
|
265
253
|
# rebuild timing fields
|
266
254
|
loadts = int(time.time() * 1000) # ms since epoch
|
@@ -278,9 +266,9 @@ class QidianSession(BaseSession):
|
|
278
266
|
self._ab_key: self._ab_val,
|
279
267
|
self._ck_key: ck_val,
|
280
268
|
}
|
281
|
-
|
282
|
-
|
283
|
-
)
|
269
|
+
plain_bytes = json.dumps(new_payload, separators=(",", ":")).encode("utf-8")
|
270
|
+
cipher_bytes = rc4_stream(self._s_init, plain_bytes)
|
271
|
+
return base64.b64encode(cipher_bytes).decode("utf-8")
|
284
272
|
|
285
273
|
async def _check_login_status(self) -> bool:
|
286
274
|
"""
|
@@ -335,8 +323,5 @@ class QidianSession(BaseSession):
|
|
335
323
|
return base64.b64decode(b).decode()
|
336
324
|
|
337
325
|
@staticmethod
|
338
|
-
def
|
339
|
-
|
340
|
-
decoded = base64.b64decode(encoded)
|
341
|
-
key = "".join([chr(b ^ 0x5A) for b in decoded])
|
342
|
-
return key
|
326
|
+
def _d2(b: str) -> bytes:
|
327
|
+
return base64.b64decode(b)
|
@@ -16,8 +16,8 @@ from novel_downloader.models import LoginField
|
|
16
16
|
class FetcherProtocol(Protocol):
|
17
17
|
"""
|
18
18
|
An async requester must be able to fetch raw HTML/data for:
|
19
|
-
|
20
|
-
|
19
|
+
* a book's info page,
|
20
|
+
* a specific chapter page,
|
21
21
|
and manage login/shutdown asynchronously.
|
22
22
|
"""
|
23
23
|
|
@@ -15,8 +15,8 @@ from novel_downloader.models import BookInfoDict, ChapterDict
|
|
15
15
|
class ParserProtocol(Protocol):
|
16
16
|
"""
|
17
17
|
A parser must be able to:
|
18
|
-
|
19
|
-
|
18
|
+
* extract book metadata from an HTML string,
|
19
|
+
* extract a single chapter's text from an HTML string
|
20
20
|
"""
|
21
21
|
|
22
22
|
def parse_book_info(
|
@@ -43,6 +43,7 @@ class BaseParser(ParserProtocol, abc.ABC):
|
|
43
43
|
self._config = config
|
44
44
|
self._book_id: str | None = None
|
45
45
|
|
46
|
+
self._save_font_debug = config.save_font_debug
|
46
47
|
self._decode_font: bool = config.decode_font
|
47
48
|
self._use_truncation = config.use_truncation
|
48
49
|
self._base_cache_dir = Path(config.cache_dir)
|
@@ -195,8 +195,8 @@ class EightnovelParser(BaseParser):
|
|
195
195
|
def _build_id_title_map(cls, html_str: str) -> dict[str, str]:
|
196
196
|
"""
|
197
197
|
Extracts two comma-split lists from html_str:
|
198
|
-
|
199
|
-
|
198
|
+
* A numeric list of IDs (one element longer)
|
199
|
+
* A list of titles
|
200
200
|
"""
|
201
201
|
id_list = None
|
202
202
|
title_list = None
|
@@ -128,9 +128,9 @@ class EsjzoneParser(BaseParser):
|
|
128
128
|
or tag == "summary"
|
129
129
|
):
|
130
130
|
# Handle possible volume title markers:
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
131
|
+
# * <h2>: standard volume header
|
132
|
+
# * <p class="non">: alternative volume header style
|
133
|
+
# * <summary>: fallback for stray <summary> tags outside <details>
|
134
134
|
_start_volume(node.xpath("string()"))
|
135
135
|
|
136
136
|
elif tag == "a":
|