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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +79 -66
- novel_downloader/cli/export.py +17 -21
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +17 -12
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +20 -14
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +64 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/esjzone.py +64 -69
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/linovelib.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +429 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +34 -85
- novel_downloader/locales/zh.json +35 -86
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/crypto_utils/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- 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/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +55 -49
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/crypto_utils.py +0 -71
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
novel_downloader/cli/__init__.py
CHANGED
novel_downloader/cli/clean.py
CHANGED
@@ -6,71 +6,45 @@ novel_downloader.cli.clean
|
|
6
6
|
CLI subcommands for clean resources.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import shutil
|
10
12
|
from argparse import Namespace, _SubParsersAction
|
11
13
|
from pathlib import Path
|
12
14
|
|
15
|
+
from novel_downloader.cli import ui
|
13
16
|
from novel_downloader.utils.constants import (
|
14
17
|
CONFIG_DIR,
|
15
18
|
DATA_DIR,
|
16
19
|
JS_SCRIPT_DIR,
|
17
20
|
LOGGER_DIR,
|
18
|
-
MODEL_CACHE_DIR,
|
19
|
-
REC_CHAR_MODEL_REPO,
|
20
21
|
)
|
21
22
|
from novel_downloader.utils.i18n import t
|
22
23
|
|
23
24
|
|
24
25
|
def register_clean_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
26
|
+
"""Register the `clean` subcommand and its options."""
|
25
27
|
parser = subparsers.add_parser("clean", help=t("help_clean"))
|
26
28
|
|
27
29
|
parser.add_argument("--logs", action="store_true", help=t("clean_logs"))
|
28
30
|
parser.add_argument("--cache", action="store_true", help=t("clean_cache"))
|
29
31
|
parser.add_argument("--data", action="store_true", help=t("clean_data"))
|
30
32
|
parser.add_argument("--config", action="store_true", help=t("clean_config"))
|
31
|
-
parser.add_argument("--models", action="store_true", help=t("clean_models"))
|
32
33
|
parser.add_argument("--all", action="store_true", help=t("clean_all"))
|
33
34
|
parser.add_argument("-y", "--yes", action="store_true", help=t("clean_yes"))
|
34
35
|
|
35
|
-
parser.add_argument("--hf-cache", action="store_true", help=t("clean_hf_cache"))
|
36
|
-
parser.add_argument(
|
37
|
-
"--hf-cache-all", action="store_true", help=t("clean_hf_cache_all")
|
38
|
-
)
|
39
|
-
|
40
36
|
parser.set_defaults(func=handle_clean)
|
41
37
|
|
42
38
|
|
43
39
|
def handle_clean(args: Namespace) -> None:
|
40
|
+
"""Handle the `clean` subcommand."""
|
44
41
|
targets: list[Path] = []
|
45
42
|
|
46
|
-
if args.hf_cache_all:
|
47
|
-
try:
|
48
|
-
if _clean_model_repo_cache(all=True):
|
49
|
-
print(t("clean_hf_cache_all_done"))
|
50
|
-
except Exception as e:
|
51
|
-
print(t("clean_hf_cache_all_fail", err=str(e)))
|
52
|
-
elif args.hf_cache:
|
53
|
-
try:
|
54
|
-
if _clean_model_repo_cache(repo_id=REC_CHAR_MODEL_REPO):
|
55
|
-
print(t("clean_hf_model_done", repo=REC_CHAR_MODEL_REPO))
|
56
|
-
else:
|
57
|
-
print(t("clean_hf_model_not_found", repo=REC_CHAR_MODEL_REPO))
|
58
|
-
except Exception as e:
|
59
|
-
print(t("clean_hf_model_fail", err=str(e)))
|
60
|
-
|
61
43
|
if args.all:
|
62
|
-
if not args.yes:
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
return
|
67
|
-
targets = [
|
68
|
-
LOGGER_DIR,
|
69
|
-
JS_SCRIPT_DIR,
|
70
|
-
DATA_DIR,
|
71
|
-
CONFIG_DIR,
|
72
|
-
MODEL_CACHE_DIR,
|
73
|
-
]
|
44
|
+
if not args.yes and not ui.confirm(t("clean_confirm"), default=False):
|
45
|
+
ui.warn(t("clean_cancelled"))
|
46
|
+
return
|
47
|
+
targets = [LOGGER_DIR, JS_SCRIPT_DIR, DATA_DIR, CONFIG_DIR]
|
74
48
|
else:
|
75
49
|
if args.logs:
|
76
50
|
targets.append(LOGGER_DIR)
|
@@ -80,66 +54,25 @@ def handle_clean(args: Namespace) -> None:
|
|
80
54
|
targets.append(DATA_DIR)
|
81
55
|
if args.config:
|
82
56
|
targets.append(CONFIG_DIR)
|
83
|
-
if args.models:
|
84
|
-
targets.append(MODEL_CACHE_DIR)
|
85
57
|
|
86
|
-
if not targets
|
87
|
-
|
58
|
+
if not targets:
|
59
|
+
ui.warn(t("clean_nothing"))
|
88
60
|
return
|
89
61
|
|
90
62
|
for path in targets:
|
91
63
|
_delete_path(path)
|
92
64
|
|
93
65
|
|
94
|
-
def prompt(message: str, default: str = "n") -> str:
|
95
|
-
"""
|
96
|
-
Prompt the user for input with a default option.
|
97
|
-
|
98
|
-
:param message: The prompt message to display to the user.
|
99
|
-
:param default: The default value to use if the user provides no input ("y" or "n").
|
100
|
-
:return: The user's input (lowercased), or the default value if no input is given.
|
101
|
-
"""
|
102
|
-
try:
|
103
|
-
full_prompt = f"{message} [{'Y/n' if default.lower() == 'y' else 'y/N'}]: "
|
104
|
-
response = input(full_prompt).strip().lower()
|
105
|
-
return response if response else default.lower()
|
106
|
-
except (KeyboardInterrupt, EOFError):
|
107
|
-
print("\n" + "Cancelled.")
|
108
|
-
return default.lower()
|
109
|
-
|
110
|
-
|
111
66
|
def _delete_path(p: Path) -> None:
|
67
|
+
"""Delete file or directory at `p`, printing a colored result line."""
|
112
68
|
if p.exists():
|
113
|
-
|
114
|
-
p.
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
def _clean_model_repo_cache(
|
123
|
-
repo_id: str | None = None,
|
124
|
-
all: bool = False,
|
125
|
-
) -> bool:
|
126
|
-
"""
|
127
|
-
Delete Hugging Face cache for a specific repo.
|
128
|
-
"""
|
129
|
-
from huggingface_hub import scan_cache_dir
|
130
|
-
|
131
|
-
cache_info = scan_cache_dir()
|
132
|
-
|
133
|
-
if all:
|
134
|
-
targets = cache_info.repos
|
135
|
-
elif repo_id:
|
136
|
-
targets = [r for r in cache_info.repos if r.repo_id == repo_id]
|
69
|
+
try:
|
70
|
+
if p.is_file():
|
71
|
+
p.unlink()
|
72
|
+
else:
|
73
|
+
shutil.rmtree(p, ignore_errors=True)
|
74
|
+
ui.success(f"[clean] {t('clean_deleted')}: {p}")
|
75
|
+
except Exception as e:
|
76
|
+
ui.error(f"[clean] {t('clean_failed', path=p)}: {p} -> {e}")
|
137
77
|
else:
|
138
|
-
|
139
|
-
|
140
|
-
strategy = cache_info.delete_revisions(
|
141
|
-
*[rev.commit_hash for r in targets for rev in r.revisions]
|
142
|
-
)
|
143
|
-
print(f"[clean] Will free {strategy.expected_freed_size_str}")
|
144
|
-
strategy.execute()
|
145
|
-
return True
|
78
|
+
ui.warn(f"[clean] {t('clean_not_found')}: {p}")
|
novel_downloader/cli/config.py
CHANGED
@@ -3,14 +3,17 @@
|
|
3
3
|
novel_downloader.cli.config
|
4
4
|
---------------------------
|
5
5
|
|
6
|
-
CLI subcommands for configuration management.
|
6
|
+
CLI subcommands for configuration file management.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import shutil
|
10
12
|
from argparse import Namespace, _SubParsersAction
|
11
13
|
from importlib.resources import as_file
|
12
14
|
from pathlib import Path
|
13
15
|
|
16
|
+
from novel_downloader.cli import ui
|
14
17
|
from novel_downloader.config import save_config_file
|
15
18
|
from novel_downloader.utils.constants import DEFAULT_SETTINGS_PATHS
|
16
19
|
from novel_downloader.utils.i18n import t
|
@@ -18,6 +21,7 @@ from novel_downloader.utils.state import state_mgr
|
|
18
21
|
|
19
22
|
|
20
23
|
def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
24
|
+
"""Register `config` with `init`, `set-lang`, and `set-config` subcommands."""
|
21
25
|
parser = subparsers.add_parser("config", help=t("help_config"))
|
22
26
|
config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
23
27
|
|
@@ -27,26 +31,30 @@ def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type:
|
|
27
31
|
|
28
32
|
|
29
33
|
def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
|
30
|
-
parser = subparsers.add_parser("init", help=t("
|
34
|
+
parser = subparsers.add_parser("init", help=t("config_init_help"))
|
31
35
|
parser.add_argument(
|
32
|
-
"--force", action="store_true", help=t("
|
36
|
+
"--force", action="store_true", help=t("config_init_force_help")
|
33
37
|
)
|
34
38
|
parser.set_defaults(func=_handle_init)
|
35
39
|
|
36
40
|
|
37
41
|
def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
|
38
|
-
parser = subparsers.add_parser("set-lang", help=t("
|
42
|
+
parser = subparsers.add_parser("set-lang", help=t("config_set_lang_help"))
|
39
43
|
parser.add_argument("lang", choices=["zh", "en"], help="Language code")
|
40
44
|
parser.set_defaults(func=_handle_set_lang)
|
41
45
|
|
42
46
|
|
43
47
|
def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
|
44
|
-
parser = subparsers.add_parser("set-config", help=t("
|
48
|
+
parser = subparsers.add_parser("set-config", help=t("config_set_config_help"))
|
45
49
|
parser.add_argument("path", type=str, help="Path to YAML config file")
|
46
50
|
parser.set_defaults(func=_handle_set_config)
|
47
51
|
|
48
52
|
|
49
53
|
def _handle_init(args: Namespace) -> None:
|
54
|
+
"""
|
55
|
+
Copy template settings files from package resources into the current working dir.
|
56
|
+
If the target file exists, optionally confirm overwrite (unless --force).
|
57
|
+
"""
|
50
58
|
cwd = Path.cwd()
|
51
59
|
|
52
60
|
for resource in DEFAULT_SETTINGS_PATHS:
|
@@ -55,41 +63,38 @@ def _handle_init(args: Namespace) -> None:
|
|
55
63
|
|
56
64
|
if target_path.exists():
|
57
65
|
if args.force:
|
58
|
-
|
66
|
+
ui.warn(t("config_init_overwrite", filename=resource.name))
|
59
67
|
else:
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
+ " [y/N]: "
|
65
|
-
)
|
66
|
-
.strip()
|
67
|
-
.lower()
|
68
|
+
ui.info(t("config_init_exists", filename=resource.name))
|
69
|
+
should_copy = ui.confirm(
|
70
|
+
t("config_init_confirm_overwrite", filename=resource.name),
|
71
|
+
default=False,
|
68
72
|
)
|
69
|
-
should_copy = resp == "y"
|
70
73
|
|
71
74
|
if not should_copy:
|
72
|
-
|
75
|
+
ui.warn(t("config_init_skip", filename=resource.name))
|
73
76
|
continue
|
74
77
|
|
75
78
|
try:
|
76
79
|
with as_file(resource) as actual_path:
|
77
80
|
shutil.copy(actual_path, target_path)
|
78
|
-
|
81
|
+
ui.success(t("config_init_copy", filename=resource.name))
|
79
82
|
except Exception as e:
|
80
|
-
|
83
|
+
ui.error(t("config_init_error", filename=resource.name, err=str(e)))
|
81
84
|
raise
|
82
85
|
|
83
86
|
|
84
87
|
def _handle_set_lang(args: Namespace) -> None:
|
88
|
+
"""Set the UI language and persist in state manager."""
|
85
89
|
state_mgr.set_language(args.lang)
|
86
|
-
|
90
|
+
ui.success(t("config_set_lang", lang=args.lang))
|
87
91
|
|
88
92
|
|
89
93
|
def _handle_set_config(args: Namespace) -> None:
|
94
|
+
"""Persist a user-supplied TOML config path into the app config."""
|
90
95
|
try:
|
91
96
|
save_config_file(args.path)
|
92
|
-
|
97
|
+
ui.success(t("config_set_config", path=args.path))
|
93
98
|
except Exception as e:
|
94
|
-
|
99
|
+
ui.error(t("config_set_config_fail", err=str(e)))
|
95
100
|
raise
|
novel_downloader/cli/download.py
CHANGED
@@ -3,32 +3,28 @@
|
|
3
3
|
novel_downloader.cli.download
|
4
4
|
-----------------------------
|
5
5
|
|
6
|
-
Download novels from supported sites via CLI.
|
6
|
+
Download novels from supported sites via the CLI.
|
7
7
|
"""
|
8
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
9
11
|
import asyncio
|
10
|
-
import getpass
|
11
12
|
from argparse import Namespace, _SubParsersAction
|
12
13
|
from collections.abc import Iterable
|
13
|
-
from dataclasses import asdict
|
14
14
|
from pathlib import Path
|
15
15
|
from typing import Any
|
16
16
|
|
17
|
+
from novel_downloader.cli import ui
|
17
18
|
from novel_downloader.config import ConfigAdapter, load_config
|
18
|
-
from novel_downloader.core import
|
19
|
-
FetcherProtocol,
|
20
|
-
get_downloader,
|
21
|
-
get_exporter,
|
22
|
-
get_fetcher,
|
23
|
-
get_parser,
|
24
|
-
)
|
19
|
+
from novel_downloader.core import get_downloader, get_exporter, get_fetcher, get_parser
|
25
20
|
from novel_downloader.models import BookConfig, LoginField
|
26
|
-
from novel_downloader.utils.cookies import
|
21
|
+
from novel_downloader.utils.cookies import parse_cookies
|
27
22
|
from novel_downloader.utils.i18n import t
|
28
23
|
from novel_downloader.utils.logger import setup_logging
|
29
24
|
|
30
25
|
|
31
26
|
def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
27
|
+
"""Register the `download` subcommand and its options."""
|
32
28
|
parser = subparsers.add_parser("download", help=t("help_download"))
|
33
29
|
|
34
30
|
parser.add_argument("book_ids", nargs="*", help=t("download_book_ids"))
|
@@ -40,24 +36,30 @@ def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type
|
|
40
36
|
parser.add_argument("--start", type=str, help=t("download_option_start"))
|
41
37
|
parser.add_argument("--end", type=str, help=t("download_option_end"))
|
42
38
|
|
39
|
+
parser.add_argument(
|
40
|
+
"--no-export",
|
41
|
+
action="store_true",
|
42
|
+
help=t("download_option_no_export"),
|
43
|
+
)
|
44
|
+
|
43
45
|
parser.set_defaults(func=handle_download)
|
44
46
|
|
45
47
|
|
46
48
|
def handle_download(args: Namespace) -> None:
|
49
|
+
"""Handle the `download` subcommand."""
|
47
50
|
site: str = args.site
|
48
51
|
config_path: Path | None = Path(args.config) if args.config else None
|
49
52
|
book_ids: list[BookConfig] = _cli_args_to_book_configs(
|
50
|
-
args.book_ids,
|
51
|
-
args.start,
|
52
|
-
args.end,
|
53
|
+
args.book_ids, args.start, args.end
|
53
54
|
)
|
55
|
+
no_export: bool = getattr(args, "no_export", False)
|
54
56
|
|
55
|
-
|
57
|
+
ui.info(t("download_site_info", site=site))
|
56
58
|
|
57
59
|
try:
|
58
60
|
config_data = load_config(config_path)
|
59
61
|
except Exception as e:
|
60
|
-
|
62
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
61
63
|
return
|
62
64
|
|
63
65
|
adapter = ConfigAdapter(config=config_data, site=site)
|
@@ -66,21 +68,21 @@ def handle_download(args: Namespace) -> None:
|
|
66
68
|
try:
|
67
69
|
book_ids = adapter.get_book_ids()
|
68
70
|
except Exception as e:
|
69
|
-
|
71
|
+
ui.error(t("download_fail_get_ids", err=str(e)))
|
70
72
|
return
|
71
73
|
|
72
74
|
valid_books = _filter_valid_book_configs(book_ids)
|
73
75
|
|
74
76
|
if not book_ids:
|
75
|
-
|
77
|
+
ui.warn(t("download_no_ids"))
|
76
78
|
return
|
77
79
|
|
78
80
|
if not valid_books:
|
79
|
-
|
80
|
-
|
81
|
+
ui.warn(t("download_only_example", example="0000000000"))
|
82
|
+
ui.info(t("download_edit_config"))
|
81
83
|
return
|
82
84
|
|
83
|
-
asyncio.run(_download(adapter, site, valid_books))
|
85
|
+
asyncio.run(_download(adapter, site, valid_books, no_export=no_export))
|
84
86
|
|
85
87
|
|
86
88
|
def _cli_args_to_book_configs(
|
@@ -89,14 +91,13 @@ def _cli_args_to_book_configs(
|
|
89
91
|
end_id: str | None,
|
90
92
|
) -> list[BookConfig]:
|
91
93
|
"""
|
92
|
-
Convert CLI
|
93
|
-
Only the first book_id
|
94
|
+
Convert CLI arguments into a list of `BookConfig`.
|
95
|
+
Only the first book_id takes `start_id`/`end_id`.
|
94
96
|
"""
|
95
97
|
if not book_ids:
|
96
98
|
return []
|
97
99
|
|
98
100
|
result: list[BookConfig] = []
|
99
|
-
|
100
101
|
first: BookConfig = {"book_id": book_ids[0]}
|
101
102
|
if start_id:
|
102
103
|
first["start_id"] = start_id
|
@@ -115,9 +116,11 @@ def _filter_valid_book_configs(
|
|
115
116
|
invalid_ids: Iterable[str] = ("0000000000",),
|
116
117
|
) -> list[BookConfig]:
|
117
118
|
"""
|
118
|
-
Filter
|
119
|
-
|
120
|
-
|
119
|
+
Filter out placeholder or duplicate book IDs, preserving order.
|
120
|
+
|
121
|
+
:param books: The list to filter.
|
122
|
+
:param invalid_ids: A set/iterable of IDs to treat as invalid.
|
123
|
+
:return: De-duplicated, valid list.
|
121
124
|
"""
|
122
125
|
seen = set(invalid_ids)
|
123
126
|
result: list[BookConfig] = []
|
@@ -136,88 +139,96 @@ async def _download(
|
|
136
139
|
adapter: ConfigAdapter,
|
137
140
|
site: str,
|
138
141
|
valid_books: list[BookConfig],
|
142
|
+
*,
|
143
|
+
no_export: bool = False,
|
139
144
|
) -> None:
|
145
|
+
"""
|
146
|
+
Perform the download flow:
|
147
|
+
* Init components
|
148
|
+
* Login if required
|
149
|
+
* Download each requested book
|
150
|
+
* Export with configured exporter
|
151
|
+
"""
|
140
152
|
downloader_cfg = adapter.get_downloader_config()
|
141
153
|
fetcher_cfg = adapter.get_fetcher_config()
|
142
154
|
parser_cfg = adapter.get_parser_config()
|
143
155
|
exporter_cfg = adapter.get_exporter_config()
|
156
|
+
login_cfg = adapter.get_login_config()
|
144
157
|
log_level = adapter.get_log_level()
|
158
|
+
setup_logging(console_level=log_level)
|
145
159
|
|
146
160
|
parser = get_parser(site, parser_cfg)
|
147
|
-
exporter =
|
148
|
-
|
161
|
+
exporter = None
|
162
|
+
if not no_export:
|
163
|
+
exporter = get_exporter(site, exporter_cfg)
|
164
|
+
else:
|
165
|
+
ui.info(t("download_export_skipped"))
|
149
166
|
|
150
167
|
async with get_fetcher(site, fetcher_cfg) as fetcher:
|
151
168
|
if downloader_cfg.login_required and not await fetcher.load_state():
|
152
|
-
login_data = await _prompt_login_fields(
|
153
|
-
fetcher, fetcher.login_fields, downloader_cfg
|
154
|
-
)
|
169
|
+
login_data = await _prompt_login_fields(fetcher.login_fields, login_cfg)
|
155
170
|
if not await fetcher.login(**login_data):
|
156
|
-
|
171
|
+
ui.error(t("download_login_failed"))
|
157
172
|
return
|
158
173
|
await fetcher.save_state()
|
159
174
|
|
160
175
|
downloader = get_downloader(
|
161
|
-
fetcher=fetcher,
|
162
|
-
parser=parser,
|
163
|
-
site=site,
|
164
|
-
config=downloader_cfg,
|
176
|
+
fetcher=fetcher, parser=parser, site=site, config=downloader_cfg
|
165
177
|
)
|
166
178
|
|
167
179
|
for book in valid_books:
|
168
|
-
|
169
|
-
await downloader.download(
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
await asyncio.to_thread(exporter.export, book["book_id"])
|
180
|
+
ui.info(t("download_downloading", book_id=book["book_id"], site=site))
|
181
|
+
await downloader.download(book, progress_hook=_print_progress)
|
182
|
+
|
183
|
+
if not no_export and exporter is not None:
|
184
|
+
await asyncio.to_thread(exporter.export, book["book_id"])
|
174
185
|
|
175
186
|
if downloader_cfg.login_required and fetcher.is_logged_in:
|
176
187
|
await fetcher.save_state()
|
177
188
|
|
178
189
|
|
179
190
|
async def _prompt_login_fields(
|
180
|
-
fetcher: FetcherProtocol,
|
181
191
|
fields: list[LoginField],
|
182
|
-
|
192
|
+
login_config: dict[str, str] | None = None,
|
183
193
|
) -> dict[str, Any]:
|
194
|
+
"""
|
195
|
+
Prompt for required login fields, honoring defaults and config-provided values.
|
196
|
+
|
197
|
+
:param fields: Field descriptors from the fetcher (name/label/type/etc.).
|
198
|
+
:param login_config: Optional values already configured by the user.
|
199
|
+
:return: A dict suitable to pass to `fetcher.login(**kwargs)`.
|
200
|
+
"""
|
201
|
+
login_config = login_config or {}
|
184
202
|
result: dict[str, Any] = {}
|
185
|
-
cfg_dict = asdict(cfg) if cfg else {}
|
186
203
|
|
187
204
|
for field in fields:
|
188
|
-
|
205
|
+
ui.info(f"\n{field.label} ({field.name})")
|
189
206
|
if field.description:
|
190
|
-
|
207
|
+
ui.info(f"{t('login_description')}: {field.description}")
|
191
208
|
if field.placeholder:
|
192
|
-
|
209
|
+
ui.info(f"{t('login_hint')}: {field.placeholder}")
|
193
210
|
|
194
|
-
|
195
|
-
await fetcher.set_interactive_mode(True)
|
196
|
-
input(t("login_manual_prompt"))
|
197
|
-
await fetcher.set_interactive_mode(False)
|
198
|
-
continue
|
199
|
-
|
200
|
-
existing_value = cfg_dict.get(field.name, "").strip()
|
211
|
+
existing_value = login_config.get(field.name, "").strip()
|
201
212
|
if existing_value:
|
202
213
|
result[field.name] = existing_value
|
203
|
-
|
214
|
+
ui.info(t("login_use_config"))
|
204
215
|
continue
|
205
216
|
|
206
|
-
value: str | dict[str, str]
|
207
|
-
|
217
|
+
value: str | dict[str, str] = ""
|
218
|
+
for _ in range(5):
|
208
219
|
if field.type == "password":
|
209
|
-
value =
|
220
|
+
value = ui.prompt_password(t("login_enter_password"))
|
210
221
|
elif field.type == "cookie":
|
211
|
-
value =
|
212
|
-
value =
|
222
|
+
value = ui.prompt(t("login_enter_cookie"))
|
223
|
+
value = parse_cookies(value)
|
213
224
|
else:
|
214
|
-
value =
|
225
|
+
value = ui.prompt(t("login_enter_value"))
|
215
226
|
|
216
227
|
if not value and field.default:
|
217
228
|
value = field.default
|
218
229
|
|
219
230
|
if not value and field.required:
|
220
|
-
|
231
|
+
ui.warn(t("login_required_field"))
|
221
232
|
else:
|
222
233
|
break
|
223
234
|
|
@@ -227,5 +238,7 @@ async def _prompt_login_fields(
|
|
227
238
|
|
228
239
|
|
229
240
|
async def _print_progress(done: int, total: int) -> None:
|
230
|
-
|
231
|
-
|
241
|
+
"""Progress hook passed into the downloader."""
|
242
|
+
ui.print_progress(
|
243
|
+
done, total, prefix=t("download_progress_prefix"), unit="chapters"
|
244
|
+
)
|
novel_downloader/cli/export.py
CHANGED
@@ -3,11 +3,15 @@
|
|
3
3
|
novel_downloader.cli.export
|
4
4
|
---------------------------
|
5
5
|
|
6
|
+
Export existing books into TXT/EPUB formats.
|
6
7
|
"""
|
7
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
8
11
|
from argparse import Namespace, _SubParsersAction
|
9
12
|
from pathlib import Path
|
10
13
|
|
14
|
+
from novel_downloader.cli import ui
|
11
15
|
from novel_downloader.config import ConfigAdapter, load_config
|
12
16
|
from novel_downloader.core import get_exporter
|
13
17
|
from novel_downloader.utils.i18n import t
|
@@ -15,13 +19,10 @@ from novel_downloader.utils.logger import setup_logging
|
|
15
19
|
|
16
20
|
|
17
21
|
def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
22
|
+
"""Register the `export` subcommand and its options."""
|
18
23
|
parser = subparsers.add_parser("export", help=t("help_export"))
|
19
24
|
|
20
|
-
parser.add_argument(
|
21
|
-
"book_ids",
|
22
|
-
nargs="+",
|
23
|
-
help=t("download_book_ids"),
|
24
|
-
)
|
25
|
+
parser.add_argument("book_ids", nargs="+", help=t("download_book_ids"))
|
25
26
|
parser.add_argument(
|
26
27
|
"--format",
|
27
28
|
choices=["txt", "epub", "all"],
|
@@ -29,52 +30,47 @@ def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type:
|
|
29
30
|
help=t("export_format_help"),
|
30
31
|
)
|
31
32
|
parser.add_argument(
|
32
|
-
"--site",
|
33
|
-
default="qidian",
|
34
|
-
help=t("download_option_site", default="qidian"),
|
35
|
-
)
|
36
|
-
parser.add_argument(
|
37
|
-
"--config",
|
38
|
-
type=str,
|
39
|
-
help=t("help_config"),
|
33
|
+
"--site", default="qidian", help=t("download_option_site", default="qidian")
|
40
34
|
)
|
35
|
+
parser.add_argument("--config", type=str, help=t("help_config"))
|
41
36
|
|
42
37
|
parser.set_defaults(func=handle_export)
|
43
38
|
|
44
39
|
|
45
40
|
def handle_export(args: Namespace) -> None:
|
41
|
+
"""Handle the `export` subcommand."""
|
46
42
|
site: str = args.site
|
47
43
|
config_path: Path | None = Path(args.config) if args.config else None
|
48
44
|
book_ids: list[str] = args.book_ids
|
49
45
|
export_format: str = args.format
|
50
46
|
|
51
|
-
|
47
|
+
ui.info(t("download_site_info", site=site))
|
52
48
|
|
53
49
|
try:
|
54
50
|
config_data = load_config(config_path)
|
55
51
|
except Exception as e:
|
56
|
-
|
52
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
57
53
|
return
|
58
54
|
|
59
55
|
adapter = ConfigAdapter(config=config_data, site=site)
|
60
56
|
exporter_cfg = adapter.get_exporter_config()
|
61
57
|
log_level = adapter.get_log_level()
|
62
58
|
exporter = get_exporter(site, exporter_cfg)
|
63
|
-
setup_logging(
|
59
|
+
setup_logging(console_level=log_level)
|
64
60
|
|
65
61
|
for book_id in book_ids:
|
66
|
-
|
62
|
+
ui.info(t("export_processing", book_id=book_id, format=export_format))
|
67
63
|
|
68
64
|
if export_format in {"txt", "all"}:
|
69
65
|
try:
|
70
66
|
exporter.export_as_txt(book_id)
|
71
|
-
|
67
|
+
ui.success(t("export_success_txt", book_id=book_id))
|
72
68
|
except Exception as e:
|
73
|
-
|
69
|
+
ui.error(t("export_failed_txt", book_id=book_id, err=str(e)))
|
74
70
|
|
75
71
|
if export_format in {"epub", "all"}:
|
76
72
|
try:
|
77
73
|
exporter.export_as_epub(book_id)
|
78
|
-
|
74
|
+
ui.success(t("export_success_epub", book_id=book_id))
|
79
75
|
except Exception as e:
|
80
|
-
|
76
|
+
ui.error(t("export_failed_epub", book_id=book_id, err=str(e)))
|