novel-downloader 2.0.1__py3-none-any.whl → 2.0.2__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 +11 -8
- novel_downloader/cli/export.py +17 -17
- novel_downloader/cli/ui.py +28 -1
- novel_downloader/config/adapter.py +27 -1
- novel_downloader/core/archived/deqixs/fetcher.py +1 -28
- novel_downloader/core/downloaders/__init__.py +2 -0
- novel_downloader/core/downloaders/base.py +34 -85
- novel_downloader/core/downloaders/common.py +147 -171
- novel_downloader/core/downloaders/qianbi.py +30 -64
- novel_downloader/core/downloaders/qidian.py +157 -184
- novel_downloader/core/downloaders/qqbook.py +292 -0
- novel_downloader/core/downloaders/registry.py +2 -2
- novel_downloader/core/exporters/__init__.py +2 -0
- novel_downloader/core/exporters/base.py +37 -59
- novel_downloader/core/exporters/common.py +620 -0
- novel_downloader/core/exporters/linovelib.py +47 -0
- novel_downloader/core/exporters/qidian.py +41 -12
- novel_downloader/core/exporters/qqbook.py +28 -0
- novel_downloader/core/exporters/registry.py +2 -2
- novel_downloader/core/fetchers/__init__.py +4 -2
- novel_downloader/core/fetchers/aaatxt.py +2 -22
- novel_downloader/core/fetchers/b520.py +3 -23
- novel_downloader/core/fetchers/base.py +80 -105
- novel_downloader/core/fetchers/biquyuedu.py +2 -22
- novel_downloader/core/fetchers/dxmwx.py +10 -22
- novel_downloader/core/fetchers/esjzone.py +6 -29
- novel_downloader/core/fetchers/guidaye.py +2 -22
- novel_downloader/core/fetchers/hetushu.py +9 -29
- novel_downloader/core/fetchers/i25zw.py +2 -16
- novel_downloader/core/fetchers/ixdzs8.py +2 -16
- novel_downloader/core/fetchers/jpxs123.py +2 -16
- novel_downloader/core/fetchers/lewenn.py +2 -22
- novel_downloader/core/fetchers/linovelib.py +4 -20
- novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
- novel_downloader/core/fetchers/piaotia.py +2 -16
- novel_downloader/core/fetchers/qbtr.py +2 -16
- novel_downloader/core/fetchers/qianbi.py +1 -20
- novel_downloader/core/fetchers/qidian.py +7 -33
- novel_downloader/core/fetchers/qqbook.py +177 -0
- novel_downloader/core/fetchers/quanben5.py +9 -29
- novel_downloader/core/fetchers/rate_limiter.py +22 -53
- novel_downloader/core/fetchers/sfacg.py +3 -16
- novel_downloader/core/fetchers/shencou.py +2 -16
- novel_downloader/core/fetchers/shuhaige.py +2 -22
- novel_downloader/core/fetchers/tongrenquan.py +2 -22
- novel_downloader/core/fetchers/ttkan.py +3 -14
- novel_downloader/core/fetchers/wanbengo.py +2 -22
- novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
- novel_downloader/core/fetchers/xiguashuwu.py +4 -20
- novel_downloader/core/fetchers/xs63b.py +3 -15
- novel_downloader/core/fetchers/xshbook.py +2 -22
- novel_downloader/core/fetchers/yamibo.py +4 -28
- novel_downloader/core/fetchers/yibige.py +13 -26
- novel_downloader/core/interfaces/exporter.py +19 -7
- novel_downloader/core/interfaces/fetcher.py +21 -47
- novel_downloader/core/parsers/__init__.py +4 -2
- novel_downloader/core/parsers/b520.py +2 -2
- novel_downloader/core/parsers/base.py +4 -39
- novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
- novel_downloader/core/parsers/qqbook.py +709 -0
- novel_downloader/core/parsers/xiguashuwu.py +3 -4
- novel_downloader/core/searchers/__init__.py +2 -2
- novel_downloader/core/searchers/b520.py +1 -1
- novel_downloader/core/searchers/base.py +2 -2
- novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/models/__init__.py +2 -0
- novel_downloader/models/book.py +1 -0
- novel_downloader/models/config.py +12 -0
- novel_downloader/resources/config/settings.toml +23 -5
- novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
- novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
- novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
- novel_downloader/utils/constants.py +6 -0
- novel_downloader/utils/crypto_utils/aes_util.py +1 -1
- novel_downloader/utils/epub/constants.py +1 -6
- novel_downloader/utils/fontocr/core.py +2 -0
- novel_downloader/utils/fontocr/loader.py +10 -8
- novel_downloader/utils/node_decryptor/__init__.py +13 -0
- novel_downloader/utils/node_decryptor/decryptor.py +342 -0
- novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
- novel_downloader/web/pages/download.py +1 -1
- novel_downloader/web/pages/search.py +1 -1
- novel_downloader/web/services/task_manager.py +2 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
- novel_downloader/core/exporters/common/__init__.py +0 -11
- novel_downloader/core/exporters/common/epub.py +0 -198
- novel_downloader/core/exporters/common/main_exporter.py +0 -64
- novel_downloader/core/exporters/common/txt.py +0 -146
- novel_downloader/core/exporters/epub_util.py +0 -215
- novel_downloader/core/exporters/linovelib/__init__.py +0 -11
- novel_downloader/core/exporters/linovelib/epub.py +0 -349
- novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
- novel_downloader/core/exporters/linovelib/txt.py +0 -139
- novel_downloader/core/exporters/txt_util.py +0 -67
- novel_downloader/core/parsers/qidian/__init__.py +0 -10
- novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,342 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.node_decryptor.decryptor
|
4
|
+
-----------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
import shutil
|
13
|
+
import subprocess
|
14
|
+
import uuid
|
15
|
+
from dataclasses import dataclass
|
16
|
+
from importlib.resources.abc import Traversable
|
17
|
+
from pathlib import Path
|
18
|
+
from typing import Any, Final
|
19
|
+
|
20
|
+
from novel_downloader.utils.constants import (
|
21
|
+
EXPR_TO_JSON_SCRIPT_PATH,
|
22
|
+
JS_SCRIPT_DIR,
|
23
|
+
QD_DECRYPT_SCRIPT_PATH,
|
24
|
+
QQ_DECRYPT_SCRIPT_PATH,
|
25
|
+
)
|
26
|
+
from novel_downloader.utils.network import download
|
27
|
+
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass(frozen=True)
|
32
|
+
class RemoteAsset:
|
33
|
+
url: str
|
34
|
+
basename: str
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass(frozen=True)
|
38
|
+
class LocalScript:
|
39
|
+
src: Traversable
|
40
|
+
dst_name: str
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass(frozen=True)
|
44
|
+
class ProviderSpec:
|
45
|
+
name: str
|
46
|
+
decrypt_script: LocalScript
|
47
|
+
fock_asset: RemoteAsset | None = None
|
48
|
+
has_binary_fallback: bool = False
|
49
|
+
|
50
|
+
|
51
|
+
QD_SPEC: Final[ProviderSpec] = ProviderSpec(
|
52
|
+
name="qd",
|
53
|
+
decrypt_script=LocalScript(
|
54
|
+
src=QD_DECRYPT_SCRIPT_PATH,
|
55
|
+
dst_name="qidian_decrypt_node.js",
|
56
|
+
),
|
57
|
+
fock_asset=RemoteAsset(
|
58
|
+
basename="4819793b.qeooxh.js",
|
59
|
+
url="https://cococdn.qidian.com/coco/s12062024/4819793b.qeooxh.js",
|
60
|
+
),
|
61
|
+
has_binary_fallback=True,
|
62
|
+
)
|
63
|
+
|
64
|
+
QQ_SPEC: Final[ProviderSpec] = ProviderSpec(
|
65
|
+
name="qq",
|
66
|
+
decrypt_script=LocalScript(
|
67
|
+
src=QQ_DECRYPT_SCRIPT_PATH,
|
68
|
+
dst_name="qq_decrypt_node.js",
|
69
|
+
),
|
70
|
+
fock_asset=RemoteAsset(
|
71
|
+
basename="cefc2a5d.pz1phw.js",
|
72
|
+
url="https://imgservices-1252317822.image.myqcloud.com/coco/s10192022/cefc2a5d.pz1phw.js",
|
73
|
+
),
|
74
|
+
has_binary_fallback=False,
|
75
|
+
)
|
76
|
+
|
77
|
+
EVAL_SPEC: Final[LocalScript] = LocalScript(
|
78
|
+
src=EXPR_TO_JSON_SCRIPT_PATH,
|
79
|
+
dst_name="expr_to_json.js",
|
80
|
+
)
|
81
|
+
|
82
|
+
|
83
|
+
class NodeDecryptor:
|
84
|
+
"""
|
85
|
+
Decrypts chapter payloads using Node-backed scripts and/or a binary fallback.
|
86
|
+
"""
|
87
|
+
|
88
|
+
def __init__(self) -> None:
|
89
|
+
"""
|
90
|
+
Initialise the decryptor environment.
|
91
|
+
"""
|
92
|
+
self.script_dir: Path = JS_SCRIPT_DIR
|
93
|
+
self.script_dir.mkdir(parents=True, exist_ok=True)
|
94
|
+
|
95
|
+
self.has_node: bool = shutil.which("node") is not None
|
96
|
+
|
97
|
+
# Prepared commands (None => unavailable)
|
98
|
+
self._qd_script_cmd: list[str] | None = None
|
99
|
+
self._qq_script_cmd: list[str] | None = None
|
100
|
+
self._eval_script_cmd: list[str] | None = None
|
101
|
+
|
102
|
+
self._prepare_eval_environment()
|
103
|
+
self._prepare_provider(QD_SPEC) # sets _qd_script_cmd
|
104
|
+
self._prepare_provider(QQ_SPEC) # sets _qq_script_cmd
|
105
|
+
|
106
|
+
def decrypt_qd(
|
107
|
+
self,
|
108
|
+
ciphertext: str,
|
109
|
+
chapter_id: str,
|
110
|
+
fkp: str,
|
111
|
+
fuid: str,
|
112
|
+
) -> str:
|
113
|
+
"""
|
114
|
+
Qidian decrypt. Uses Node if present; otherwise tries a fallback binary.
|
115
|
+
|
116
|
+
:param ciphertext: Base64-encoded encrypted content.
|
117
|
+
:param chapter_id: The chapter's numeric ID.
|
118
|
+
:param fkp: Base64-encoded Fock key param from the page.
|
119
|
+
:param fuid: Fock user ID param from the page.
|
120
|
+
:return: "" if unavailable or inputs are missing.
|
121
|
+
:raises RuntimeError: if the Node.js subprocess exits with a non-zero code.
|
122
|
+
"""
|
123
|
+
if not self._qd_script_cmd:
|
124
|
+
logger.warning("QD decryptor unavailable (no Node and no fallback).")
|
125
|
+
return ""
|
126
|
+
if not (ciphertext and chapter_id and fkp and fuid):
|
127
|
+
logger.debug("QD decrypt: missing required inputs.")
|
128
|
+
return ""
|
129
|
+
|
130
|
+
task_id = uuid.uuid4().hex
|
131
|
+
input_path = self.script_dir / f"qd_in_{task_id}.json"
|
132
|
+
output_path = self.script_dir / f"qd_out_{task_id}.txt"
|
133
|
+
|
134
|
+
try:
|
135
|
+
input_path.write_text(
|
136
|
+
json.dumps([ciphertext, chapter_id, fkp, fuid]),
|
137
|
+
encoding="utf-8",
|
138
|
+
)
|
139
|
+
|
140
|
+
cmd = self._qd_script_cmd + [input_path.name, output_path.name]
|
141
|
+
logger.debug("Running QD decrypt: %s (cwd=%s)", cmd, self.script_dir)
|
142
|
+
proc = subprocess.run(
|
143
|
+
cmd,
|
144
|
+
capture_output=True,
|
145
|
+
text=True,
|
146
|
+
cwd=self.script_dir,
|
147
|
+
)
|
148
|
+
|
149
|
+
if proc.returncode != 0:
|
150
|
+
stderr = (proc.stderr or "").strip()
|
151
|
+
raise RuntimeError(
|
152
|
+
f"QD decrypt failed: {stderr or 'non-zero exit code'}"
|
153
|
+
)
|
154
|
+
|
155
|
+
return output_path.read_text(encoding="utf-8").strip()
|
156
|
+
finally:
|
157
|
+
input_path.unlink(missing_ok=True)
|
158
|
+
output_path.unlink(missing_ok=True)
|
159
|
+
|
160
|
+
def decrypt_qq(
|
161
|
+
self,
|
162
|
+
ciphertext: str,
|
163
|
+
chapter_id: str,
|
164
|
+
fkp: str,
|
165
|
+
fuid: str,
|
166
|
+
) -> str:
|
167
|
+
"""
|
168
|
+
QQ decrypt. Node-only.
|
169
|
+
|
170
|
+
:param ciphertext: Base64-encoded encrypted content.
|
171
|
+
:param chapter_id: The chapter's numeric ID.
|
172
|
+
:param fkp: Base64-encoded Fock key param from the page.
|
173
|
+
:param fuid: Fock user ID param from the page.
|
174
|
+
:return: "" if Node/script not available or inputs missing.
|
175
|
+
:raises RuntimeError: if the Node.js subprocess exits with a non-zero code.
|
176
|
+
"""
|
177
|
+
if not self._qq_script_cmd:
|
178
|
+
logger.info(
|
179
|
+
"QQ decrypt skipped: Node not available or script not prepared."
|
180
|
+
)
|
181
|
+
return ""
|
182
|
+
if not (ciphertext and chapter_id and fkp and fuid):
|
183
|
+
logger.debug("QQ decrypt: missing required inputs.")
|
184
|
+
return ""
|
185
|
+
|
186
|
+
task_id = uuid.uuid4().hex
|
187
|
+
input_path = self.script_dir / f"qq_in_{task_id}.json"
|
188
|
+
output_path = self.script_dir / f"qq_out_{task_id}.txt"
|
189
|
+
|
190
|
+
try:
|
191
|
+
input_path.write_text(
|
192
|
+
json.dumps([ciphertext, chapter_id, fkp, fuid]),
|
193
|
+
encoding="utf-8",
|
194
|
+
)
|
195
|
+
|
196
|
+
cmd = self._qq_script_cmd + [input_path.name, output_path.name]
|
197
|
+
logger.debug("Running QQ decrypt: %s (cwd=%s)", cmd, self.script_dir)
|
198
|
+
proc = subprocess.run(
|
199
|
+
cmd,
|
200
|
+
capture_output=True,
|
201
|
+
text=True,
|
202
|
+
cwd=self.script_dir,
|
203
|
+
)
|
204
|
+
|
205
|
+
if proc.returncode != 0:
|
206
|
+
stderr = (proc.stderr or "").strip()
|
207
|
+
raise RuntimeError(
|
208
|
+
f"QQ decrypt failed: {stderr or 'non-zero exit code'}"
|
209
|
+
)
|
210
|
+
|
211
|
+
return output_path.read_text(encoding="utf-8").strip()
|
212
|
+
finally:
|
213
|
+
input_path.unlink(missing_ok=True)
|
214
|
+
output_path.unlink(missing_ok=True)
|
215
|
+
|
216
|
+
def eval_to_json(self, js_code: str) -> dict[str, Any]:
|
217
|
+
"""
|
218
|
+
Evaluate JS and parse JSON result. Node-only.
|
219
|
+
|
220
|
+
:return: {} if unavailable or input empty.
|
221
|
+
:raises RuntimeError: if the invoked process fails or outputs invalid JSON.
|
222
|
+
"""
|
223
|
+
if not self._eval_script_cmd:
|
224
|
+
logger.info(
|
225
|
+
"eval_to_json skipped: Node not available or script not prepared."
|
226
|
+
)
|
227
|
+
return {}
|
228
|
+
if not js_code:
|
229
|
+
logger.debug("eval_to_json: empty input.")
|
230
|
+
return {}
|
231
|
+
|
232
|
+
logger.debug("Running eval_to_json (cwd=%s)", self.script_dir)
|
233
|
+
proc = subprocess.run(
|
234
|
+
self._eval_script_cmd,
|
235
|
+
input=js_code,
|
236
|
+
capture_output=True,
|
237
|
+
text=True,
|
238
|
+
encoding="utf-8",
|
239
|
+
cwd=self.script_dir,
|
240
|
+
)
|
241
|
+
if proc.returncode != 0:
|
242
|
+
stderr = (proc.stderr or "").strip()
|
243
|
+
raise RuntimeError(f"eval_to_json failed: {stderr or 'non-zero exit code'}")
|
244
|
+
|
245
|
+
stdout = (proc.stdout or "").strip()
|
246
|
+
if not stdout:
|
247
|
+
return {}
|
248
|
+
|
249
|
+
try:
|
250
|
+
return json.loads(stdout) or {}
|
251
|
+
except json.JSONDecodeError as exc:
|
252
|
+
logger.error("eval_to_json: invalid JSON output: %s", stdout)
|
253
|
+
raise RuntimeError("eval_to_json produced invalid JSON.") from exc
|
254
|
+
|
255
|
+
def _prepare_provider(self, spec: ProviderSpec) -> None:
|
256
|
+
"""
|
257
|
+
Prepare a provider:
|
258
|
+
* If Node is available: copy node script and fetch Fock asset
|
259
|
+
* Else if provider allows binary fallback (QD): try local binary
|
260
|
+
* Else: leave command None (feature disabled)
|
261
|
+
"""
|
262
|
+
dst_script = self.script_dir / spec.decrypt_script.dst_name
|
263
|
+
|
264
|
+
if self.has_node:
|
265
|
+
# Prepare Node script + assets
|
266
|
+
try:
|
267
|
+
if not dst_script.exists():
|
268
|
+
self._copy_from_traversable(spec.decrypt_script.src, dst_script)
|
269
|
+
|
270
|
+
if spec.fock_asset:
|
271
|
+
fock_path = self.script_dir / spec.fock_asset.basename
|
272
|
+
if not fock_path.exists():
|
273
|
+
download(spec.fock_asset.url, self.script_dir)
|
274
|
+
|
275
|
+
cmd: list[str] | None = ["node", str(dst_script)]
|
276
|
+
logger.debug("%s decryptor prepared with Node.", spec.name.upper())
|
277
|
+
except Exception as exc:
|
278
|
+
logger.warning("%s Node prep failed: %s", spec.name.upper(), exc)
|
279
|
+
cmd = None
|
280
|
+
else:
|
281
|
+
# No Node available
|
282
|
+
if spec.has_binary_fallback and spec.name == "qd":
|
283
|
+
try:
|
284
|
+
from .decryptor_fetcher import ensure_qd_decryptor
|
285
|
+
|
286
|
+
bin_path = ensure_qd_decryptor(self.script_dir)
|
287
|
+
cmd = [str(bin_path)]
|
288
|
+
logger.debug(
|
289
|
+
"QD decryptor prepared with binary fallback at %s.", bin_path
|
290
|
+
)
|
291
|
+
except Exception as exc:
|
292
|
+
logger.error("QD binary fallback unavailable: %s", exc)
|
293
|
+
cmd = None
|
294
|
+
else:
|
295
|
+
logger.info(
|
296
|
+
"%s decryptor skipped: Node not available.", spec.name.upper()
|
297
|
+
)
|
298
|
+
cmd = None
|
299
|
+
|
300
|
+
if spec.name == "qd":
|
301
|
+
self._qd_script_cmd = cmd
|
302
|
+
else:
|
303
|
+
self._qq_script_cmd = cmd
|
304
|
+
|
305
|
+
def _prepare_eval_environment(self) -> None:
|
306
|
+
"""
|
307
|
+
Prepare eval script (Node-only).
|
308
|
+
"""
|
309
|
+
if not self.has_node:
|
310
|
+
logger.info("eval_to_json skipped: Node not available.")
|
311
|
+
self._eval_script_cmd = None
|
312
|
+
return
|
313
|
+
|
314
|
+
dst = self.script_dir / EVAL_SPEC.dst_name
|
315
|
+
try:
|
316
|
+
if not dst.exists():
|
317
|
+
self._copy_from_traversable(EVAL_SPEC.src, dst)
|
318
|
+
self._eval_script_cmd = ["node", str(dst)]
|
319
|
+
logger.debug("eval_to_json prepared with Node.")
|
320
|
+
except Exception as exc:
|
321
|
+
logger.warning("eval_to_json prep failed; feature disabled. (%s)", exc)
|
322
|
+
self._eval_script_cmd = None
|
323
|
+
|
324
|
+
@staticmethod
|
325
|
+
def _copy_from_traversable(src: Traversable, dst: Path) -> None:
|
326
|
+
"""Copy a packaged resource (Traversable) to a filesystem path."""
|
327
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
328
|
+
with src.open("rb") as rf, open(dst, "wb") as wf:
|
329
|
+
shutil.copyfileobj(rf, wf)
|
330
|
+
|
331
|
+
|
332
|
+
_DECRYPTOR: NodeDecryptor | None = None
|
333
|
+
|
334
|
+
|
335
|
+
def get_decryptor() -> NodeDecryptor:
|
336
|
+
"""
|
337
|
+
Return the singleton NodeDecryptor.
|
338
|
+
"""
|
339
|
+
global _DECRYPTOR
|
340
|
+
if _DECRYPTOR is None:
|
341
|
+
_DECRYPTOR = NodeDecryptor()
|
342
|
+
return _DECRYPTOR
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.
|
4
|
-
|
3
|
+
novel_downloader.utils.node_decryptor.decryptor_fetcher
|
4
|
+
-------------------------------------------------------
|
5
5
|
|
6
6
|
Download and cache the *qidian-decryptor* executable from the project's
|
7
7
|
GitHub releases.
|
@@ -10,7 +10,7 @@ GitHub releases.
|
|
10
10
|
from __future__ import annotations
|
11
11
|
|
12
12
|
__all__ = [
|
13
|
-
"
|
13
|
+
"ensure_qd_decryptor",
|
14
14
|
"RELEASE_VERSION",
|
15
15
|
]
|
16
16
|
|
@@ -41,13 +41,12 @@ PLATFORM_BINARIES: Final[dict[str, str]] = {
|
|
41
41
|
# --------------------------------------------------------------------------- #
|
42
42
|
|
43
43
|
|
44
|
-
def
|
44
|
+
def ensure_qd_decryptor(dest_root: Path | None = None) -> Path:
|
45
45
|
"""
|
46
46
|
Ensure that the decryptor executable matching the current platform and
|
47
47
|
:data:`RELEASE_VERSION` exists locally; download it if necessary.
|
48
48
|
|
49
49
|
:param dest_root: Root directory used to cache the binary.
|
50
|
-
If *None*, the global constant ``JS_SCRIPT_DIR`` is used.
|
51
50
|
:return: Path to the ready-to-use executable (inside the version sub-folder).
|
52
51
|
:raises RuntimeError: If the current platform is unsupported.
|
53
52
|
:raises ValueError: If the downloaded file fails SHA-256 verification.
|
@@ -56,7 +55,7 @@ def ensure_decryptor(dest_root: Path | None = None) -> Path:
|
|
56
55
|
platform_key = _get_platform_key()
|
57
56
|
|
58
57
|
bin_name = PLATFORM_BINARIES[platform_key]
|
59
|
-
#
|
58
|
+
# /<version>/<binary>
|
60
59
|
version_dir = dest_root / RELEASE_VERSION.lstrip("v")
|
61
60
|
dest_path = version_dir / bin_name
|
62
61
|
|
@@ -15,7 +15,7 @@ _SUPPORT_SITES = {
|
|
15
15
|
"biquge": "笔趣阁 (biquge)",
|
16
16
|
"biquyuedu": "精彩小说 (biquyuedu)",
|
17
17
|
"dxmwx": "大熊猫文学网 (dxmwx)",
|
18
|
-
"
|
18
|
+
"n8novel": "无限轻小说 (n8novel)",
|
19
19
|
"esjzone": "ESJ Zone (esjzone)",
|
20
20
|
"guidaye": "名著阅读 (guidaye)",
|
21
21
|
"hetushu": "和图书 (hetushu)",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: novel-downloader
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.2
|
4
4
|
Summary: A command-line tool for downloading Chinese web novels from Qidian and similar platforms.
|
5
5
|
Author-email: Saudade Z <saudadez217@gmail.com>
|
6
6
|
License: MIT License
|
@@ -123,6 +123,7 @@ novel-cli download 123456
|
|
123
123
|
|
124
124
|
* 支持站点见: [支持站点列表](https://github.com/saudadez21/novel-downloader/blob/main/docs/4-supported-sites.md)
|
125
125
|
* 更多示例见: [CLI 使用示例](https://github.com/saudadez21/novel-downloader/blob/main/docs/5-cli-usage-examples.md)
|
126
|
+
* 运行中可使用 `CTRL+C` 取消任务
|
126
127
|
|
127
128
|
### 3. 图形界面 (GUI / Web)
|
128
129
|
|
@@ -134,6 +135,8 @@ novel-web
|
|
134
135
|
# novel-web --listen public
|
135
136
|
```
|
136
137
|
|
138
|
+
* 运行中可使用 `CTRL+C` 停止服务
|
139
|
+
|
137
140
|
---
|
138
141
|
|
139
142
|
## 从源码安装 (开发版)
|