novel-downloader 1.3.2__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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/clean.py +97 -78
- novel_downloader/cli/config.py +177 -0
- novel_downloader/cli/download.py +132 -87
- novel_downloader/cli/export.py +77 -0
- novel_downloader/cli/main.py +21 -28
- novel_downloader/config/__init__.py +1 -25
- novel_downloader/config/adapter.py +32 -31
- novel_downloader/config/loader.py +3 -3
- novel_downloader/config/site_rules.py +1 -2
- novel_downloader/core/__init__.py +3 -6
- novel_downloader/core/downloaders/__init__.py +10 -13
- novel_downloader/core/downloaders/base.py +233 -0
- novel_downloader/core/downloaders/biquge.py +27 -0
- novel_downloader/core/downloaders/common.py +414 -0
- novel_downloader/core/downloaders/esjzone.py +27 -0
- novel_downloader/core/downloaders/linovelib.py +27 -0
- novel_downloader/core/downloaders/qianbi.py +27 -0
- novel_downloader/core/downloaders/qidian.py +352 -0
- novel_downloader/core/downloaders/sfacg.py +27 -0
- novel_downloader/core/downloaders/yamibo.py +27 -0
- novel_downloader/core/exporters/__init__.py +37 -0
- novel_downloader/core/{savers → exporters}/base.py +73 -44
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +40 -52
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
- novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
- novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +178 -0
- novel_downloader/core/fetchers/linovelib/session.py +178 -0
- novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
- novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
- novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
- novel_downloader/core/fetchers/qidian/__init__.py +14 -0
- novel_downloader/core/fetchers/qidian/browser.py +266 -0
- novel_downloader/core/fetchers/qidian/session.py +326 -0
- novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
- novel_downloader/core/fetchers/sfacg/browser.py +189 -0
- novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
- novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
- novel_downloader/core/fetchers/yamibo/browser.py +229 -0
- novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
- novel_downloader/core/interfaces/__init__.py +8 -12
- novel_downloader/core/interfaces/downloader.py +54 -0
- novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
- novel_downloader/core/interfaces/fetcher.py +162 -0
- novel_downloader/core/interfaces/parser.py +6 -7
- novel_downloader/core/parsers/__init__.py +5 -6
- novel_downloader/core/parsers/base.py +9 -13
- novel_downloader/core/parsers/biquge/main_parser.py +12 -13
- novel_downloader/core/parsers/common/helper.py +3 -3
- novel_downloader/core/parsers/common/main_parser.py +39 -34
- novel_downloader/core/parsers/esjzone/main_parser.py +24 -17
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +15 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +53 -39
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +3 -3
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
- novel_downloader/cli/interactive.py +0 -66
- novel_downloader/cli/settings.py +0 -177
- novel_downloader/config/models.py +0 -187
- novel_downloader/core/downloaders/base/__init__.py +0 -14
- novel_downloader/core/downloaders/base/base_async.py +0 -153
- novel_downloader/core/downloaders/base/base_sync.py +0 -208
- novel_downloader/core/downloaders/biquge/__init__.py +0 -14
- novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
- novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
- novel_downloader/core/downloaders/common/__init__.py +0 -14
- novel_downloader/core/downloaders/common/common_async.py +0 -218
- novel_downloader/core/downloaders/common/common_sync.py +0 -210
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -227
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/epub_utils/__init__.py +0 -26
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.2.dist-info/RECORD +0 -165
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
novel_downloader/cli/clean.py
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
novel_downloader.cli.clean
|
4
|
-
|
4
|
+
--------------------------
|
5
5
|
|
6
|
+
CLI subcommands for clean resources.
|
6
7
|
"""
|
7
8
|
|
8
9
|
import shutil
|
10
|
+
from argparse import Namespace, _SubParsersAction
|
9
11
|
from pathlib import Path
|
10
12
|
|
11
|
-
import click
|
12
|
-
|
13
13
|
from novel_downloader.utils.constants import (
|
14
14
|
CONFIG_DIR,
|
15
15
|
DATA_DIR,
|
@@ -21,18 +21,108 @@ from novel_downloader.utils.constants import (
|
|
21
21
|
from novel_downloader.utils.i18n import t
|
22
22
|
|
23
23
|
|
24
|
-
def
|
24
|
+
def register_clean_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
25
|
+
parser = subparsers.add_parser("clean", help=t("help_clean"))
|
26
|
+
|
27
|
+
parser.add_argument("--logs", action="store_true", help=t("clean_logs"))
|
28
|
+
parser.add_argument("--cache", action="store_true", help=t("clean_cache"))
|
29
|
+
parser.add_argument("--data", action="store_true", help=t("clean_data"))
|
30
|
+
parser.add_argument("--config", action="store_true", help=t("clean_config"))
|
31
|
+
parser.add_argument("--models", action="store_true", help=t("clean_models"))
|
32
|
+
parser.add_argument("--all", action="store_true", help=t("clean_all"))
|
33
|
+
parser.add_argument("-y", "--yes", action="store_true", help=t("clean_yes"))
|
34
|
+
|
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
|
+
parser.set_defaults(func=handle_clean)
|
41
|
+
|
42
|
+
|
43
|
+
def handle_clean(args: Namespace) -> None:
|
44
|
+
targets: list[Path] = []
|
45
|
+
|
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
|
+
if args.all:
|
62
|
+
if not args.yes:
|
63
|
+
confirm = prompt(t("clean_confirm"), default="n")
|
64
|
+
if confirm.lower() != "y":
|
65
|
+
print(t("clean_cancelled"))
|
66
|
+
return
|
67
|
+
targets = [
|
68
|
+
LOGGER_DIR,
|
69
|
+
JS_SCRIPT_DIR,
|
70
|
+
DATA_DIR,
|
71
|
+
CONFIG_DIR,
|
72
|
+
MODEL_CACHE_DIR,
|
73
|
+
]
|
74
|
+
else:
|
75
|
+
if args.logs:
|
76
|
+
targets.append(LOGGER_DIR)
|
77
|
+
if args.cache:
|
78
|
+
targets.append(JS_SCRIPT_DIR)
|
79
|
+
if args.data:
|
80
|
+
targets.append(DATA_DIR)
|
81
|
+
if args.config:
|
82
|
+
targets.append(CONFIG_DIR)
|
83
|
+
if args.models:
|
84
|
+
targets.append(MODEL_CACHE_DIR)
|
85
|
+
|
86
|
+
if not targets and not args.hf_cache and not args.hf_cache_all:
|
87
|
+
print(t("clean_nothing"))
|
88
|
+
return
|
89
|
+
|
90
|
+
for path in targets:
|
91
|
+
_delete_path(path)
|
92
|
+
|
93
|
+
|
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
|
+
def _delete_path(p: Path) -> None:
|
25
112
|
if p.exists():
|
26
113
|
if p.is_file():
|
27
114
|
p.unlink()
|
28
115
|
else:
|
29
116
|
shutil.rmtree(p, ignore_errors=True)
|
30
|
-
|
117
|
+
print(f"[clean] {t('clean_deleted')}: {p}")
|
31
118
|
else:
|
32
|
-
|
119
|
+
print(f"[clean] {t('clean_not_found')}: {p}")
|
33
120
|
|
34
121
|
|
35
|
-
def
|
122
|
+
def _clean_model_repo_cache(
|
123
|
+
repo_id: str | None = None,
|
124
|
+
all: bool = False,
|
125
|
+
) -> bool:
|
36
126
|
"""
|
37
127
|
Delete Hugging Face cache for a specific repo.
|
38
128
|
"""
|
@@ -53,74 +143,3 @@ def clean_model_repo_cache(repo_id: str | None = None, all: bool = False) -> boo
|
|
53
143
|
print(f"[clean] Will free {strategy.expected_freed_size_str}")
|
54
144
|
strategy.execute()
|
55
145
|
return True
|
56
|
-
|
57
|
-
|
58
|
-
@click.command(name="clean", help=t("help_clean")) # type: ignore
|
59
|
-
@click.option("--logs", is_flag=True, help=t("clean_logs")) # type: ignore
|
60
|
-
@click.option("--cache", is_flag=True, help=t("clean_cache")) # type: ignore
|
61
|
-
@click.option("--data", is_flag=True, help=t("clean_data")) # type: ignore
|
62
|
-
@click.option("--config", is_flag=True, help=t("clean_config")) # type: ignore
|
63
|
-
@click.option("--models", is_flag=True, help=t("clean_models")) # type: ignore
|
64
|
-
@click.option("--hf-cache", is_flag=True, help=t("clean_hf_cache")) # type: ignore
|
65
|
-
@click.option("--hf-cache-all", is_flag=True, help=t("clean_hf_cache_all")) # type: ignore
|
66
|
-
@click.option("--all", is_flag=True, help=t("clean_all")) # type: ignore
|
67
|
-
@click.option("--yes", is_flag=True, help=t("clean_yes")) # type: ignore
|
68
|
-
def clean_cli(
|
69
|
-
logs: bool,
|
70
|
-
cache: bool,
|
71
|
-
data: bool,
|
72
|
-
config: bool,
|
73
|
-
models: bool,
|
74
|
-
hf_cache: bool,
|
75
|
-
hf_cache_all: bool,
|
76
|
-
all: bool,
|
77
|
-
yes: bool,
|
78
|
-
) -> None:
|
79
|
-
targets: list[Path] = []
|
80
|
-
|
81
|
-
if all:
|
82
|
-
if not yes:
|
83
|
-
confirm = click.prompt(t("clean_confirm"), default="n")
|
84
|
-
if confirm.lower() != "y":
|
85
|
-
click.echo(t("clean_cancelled"))
|
86
|
-
return
|
87
|
-
targets = [
|
88
|
-
LOGGER_DIR,
|
89
|
-
JS_SCRIPT_DIR,
|
90
|
-
DATA_DIR,
|
91
|
-
CONFIG_DIR,
|
92
|
-
MODEL_CACHE_DIR,
|
93
|
-
]
|
94
|
-
else:
|
95
|
-
if logs:
|
96
|
-
targets.append(LOGGER_DIR)
|
97
|
-
if cache:
|
98
|
-
targets.append(JS_SCRIPT_DIR)
|
99
|
-
if data:
|
100
|
-
targets.append(DATA_DIR)
|
101
|
-
if config:
|
102
|
-
targets.append(CONFIG_DIR)
|
103
|
-
if models:
|
104
|
-
targets.append(MODEL_CACHE_DIR)
|
105
|
-
|
106
|
-
if hf_cache_all:
|
107
|
-
try:
|
108
|
-
if clean_model_repo_cache(all=True):
|
109
|
-
click.echo(t("clean_hf_cache_all_done"))
|
110
|
-
except Exception as e:
|
111
|
-
click.echo(t("clean_hf_cache_all_fail", err=e))
|
112
|
-
elif hf_cache:
|
113
|
-
try:
|
114
|
-
if clean_model_repo_cache(REC_CHAR_MODEL_REPO):
|
115
|
-
click.echo(t("clean_hf_model_done", repo=REC_CHAR_MODEL_REPO))
|
116
|
-
else:
|
117
|
-
click.echo(t("clean_hf_model_not_found", repo=REC_CHAR_MODEL_REPO))
|
118
|
-
except Exception as e:
|
119
|
-
click.echo(t("clean_hf_model_fail", err=e))
|
120
|
-
|
121
|
-
if not targets and not hf_cache and not hf_cache_all:
|
122
|
-
click.echo(t("clean_nothing"))
|
123
|
-
return
|
124
|
-
|
125
|
-
for path in targets:
|
126
|
-
delete_path(path)
|
@@ -0,0 +1,177 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.cli.config
|
4
|
+
---------------------------
|
5
|
+
|
6
|
+
CLI subcommands for configuration management.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import shutil
|
10
|
+
from argparse import Namespace, _SubParsersAction
|
11
|
+
from importlib.resources import as_file
|
12
|
+
from pathlib import Path
|
13
|
+
|
14
|
+
from novel_downloader.config import save_config_file, save_rules_as_json
|
15
|
+
from novel_downloader.utils.constants import DEFAULT_SETTINGS_PATHS
|
16
|
+
from novel_downloader.utils.i18n import t
|
17
|
+
from novel_downloader.utils.logger import setup_logging
|
18
|
+
from novel_downloader.utils.state import state_mgr
|
19
|
+
|
20
|
+
# from novel_downloader.utils.hash_store import img_hash_store
|
21
|
+
|
22
|
+
|
23
|
+
def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
24
|
+
parser = subparsers.add_parser("config", help=t("help_config"))
|
25
|
+
config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
26
|
+
|
27
|
+
_register_init(config_subparsers)
|
28
|
+
_register_set_lang(config_subparsers)
|
29
|
+
_register_set_config(config_subparsers)
|
30
|
+
_register_update_rules(config_subparsers)
|
31
|
+
_register_set_cookies(config_subparsers)
|
32
|
+
# _register_add_hash(config_subparsers)
|
33
|
+
|
34
|
+
|
35
|
+
def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
|
36
|
+
parser = subparsers.add_parser("init", help=t("settings_init_help"))
|
37
|
+
parser.add_argument(
|
38
|
+
"--force", action="store_true", help=t("settings_init_force_help")
|
39
|
+
)
|
40
|
+
parser.set_defaults(func=_handle_init)
|
41
|
+
|
42
|
+
|
43
|
+
def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
|
44
|
+
parser = subparsers.add_parser("set-lang", help=t("settings_set_lang_help"))
|
45
|
+
parser.add_argument("lang", choices=["zh", "en"], help="Language code")
|
46
|
+
parser.set_defaults(func=_handle_set_lang)
|
47
|
+
|
48
|
+
|
49
|
+
def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
|
50
|
+
parser = subparsers.add_parser("set-config", help=t("settings_set_config_help"))
|
51
|
+
parser.add_argument("path", type=str, help="Path to YAML config file")
|
52
|
+
parser.set_defaults(func=_handle_set_config)
|
53
|
+
|
54
|
+
|
55
|
+
def _register_update_rules(subparsers: _SubParsersAction) -> None: # type: ignore
|
56
|
+
parser = subparsers.add_parser("update-rules", help=t("settings_update_rules_help"))
|
57
|
+
parser.add_argument("path", type=str, help="Path to TOML/YAML/JSON rule file")
|
58
|
+
parser.set_defaults(func=_handle_update_rules)
|
59
|
+
|
60
|
+
|
61
|
+
def _register_set_cookies(subparsers: _SubParsersAction) -> None: # type: ignore
|
62
|
+
parser = subparsers.add_parser("set-cookies", help=t("settings_set_cookies_help"))
|
63
|
+
parser.add_argument("site", nargs="?", help="Site identifier")
|
64
|
+
parser.add_argument("cookies", nargs="?", help="Cookies string")
|
65
|
+
parser.set_defaults(func=_handle_set_cookies)
|
66
|
+
|
67
|
+
|
68
|
+
# def _register_add_hash(subparsers: _SubParsersAction) -> None: # type: ignore
|
69
|
+
# parser = subparsers.add_parser("add-hash", help=t("settings_add_hash_help"))
|
70
|
+
# parser.add_argument("--path", type=str, help=t("settings_add_hash_path_help"))
|
71
|
+
# parser.set_defaults(func=_handle_add_hash)
|
72
|
+
|
73
|
+
|
74
|
+
def _handle_init(args: Namespace) -> None:
|
75
|
+
setup_logging()
|
76
|
+
cwd = Path.cwd()
|
77
|
+
|
78
|
+
for resource in DEFAULT_SETTINGS_PATHS:
|
79
|
+
target_path = cwd / resource.name
|
80
|
+
should_copy = True
|
81
|
+
|
82
|
+
if target_path.exists():
|
83
|
+
if args.force:
|
84
|
+
print(t("settings_init_overwrite", filename=resource.name))
|
85
|
+
else:
|
86
|
+
print(t("settings_init_exists", filename=resource.name))
|
87
|
+
resp = (
|
88
|
+
input(
|
89
|
+
t("settings_init_confirm_overwrite", filename=resource.name)
|
90
|
+
+ " [y/N]: "
|
91
|
+
)
|
92
|
+
.strip()
|
93
|
+
.lower()
|
94
|
+
)
|
95
|
+
should_copy = resp == "y"
|
96
|
+
|
97
|
+
if not should_copy:
|
98
|
+
print(t("settings_init_skip", filename=resource.name))
|
99
|
+
continue
|
100
|
+
|
101
|
+
try:
|
102
|
+
with as_file(resource) as actual_path:
|
103
|
+
shutil.copy(actual_path, target_path)
|
104
|
+
print(t("settings_init_copy", filename=resource.name))
|
105
|
+
except Exception as e:
|
106
|
+
print(t("settings_init_error", filename=resource.name, err=str(e)))
|
107
|
+
raise
|
108
|
+
|
109
|
+
|
110
|
+
def _handle_set_lang(args: Namespace) -> None:
|
111
|
+
state_mgr.set_language(args.lang)
|
112
|
+
print(t("settings_set_lang", lang=args.lang))
|
113
|
+
|
114
|
+
|
115
|
+
def _handle_set_config(args: Namespace) -> None:
|
116
|
+
try:
|
117
|
+
save_config_file(args.path)
|
118
|
+
print(t("settings_set_config", path=args.path))
|
119
|
+
except Exception as e:
|
120
|
+
print(t("settings_set_config_fail", err=str(e)))
|
121
|
+
raise
|
122
|
+
|
123
|
+
|
124
|
+
def _handle_update_rules(args: Namespace) -> None:
|
125
|
+
try:
|
126
|
+
save_rules_as_json(args.path)
|
127
|
+
print(t("settings_update_rules", path=args.path))
|
128
|
+
except Exception as e:
|
129
|
+
print(t("settings_update_rules_fail", err=str(e)))
|
130
|
+
raise
|
131
|
+
|
132
|
+
|
133
|
+
def _handle_set_cookies(args: Namespace) -> None:
|
134
|
+
site = args.site or input(t("settings_set_cookies_prompt_site") + ": ").strip()
|
135
|
+
cookies = (
|
136
|
+
args.cookies or input(t("settings_set_cookies_prompt_payload") + ": ").strip()
|
137
|
+
)
|
138
|
+
|
139
|
+
try:
|
140
|
+
state_mgr.set_cookies(site, cookies)
|
141
|
+
print(t("settings_set_cookies_success", site=site))
|
142
|
+
except Exception as e:
|
143
|
+
print(t("settings_set_cookies_fail", err=str(e)))
|
144
|
+
raise
|
145
|
+
|
146
|
+
|
147
|
+
# def _handle_add_hash(args: Namespace) -> None:
|
148
|
+
# if args.path:
|
149
|
+
# try:
|
150
|
+
# img_hash_store.add_from_map(args.path)
|
151
|
+
# img_hash_store.save()
|
152
|
+
# print(t("settings_add_hash_loaded", path=args.path))
|
153
|
+
# except Exception as e:
|
154
|
+
# print(t("settings_add_hash_load_fail", err=str(e)))
|
155
|
+
# raise
|
156
|
+
# else:
|
157
|
+
# print(t("settings_add_hash_prompt_tip"))
|
158
|
+
# while True:
|
159
|
+
# img_path = input(t("settings_add_hash_prompt_img") + ": ").strip()
|
160
|
+
# if not img_path or img_path.lower() in {"exit", "quit"}:
|
161
|
+
# break
|
162
|
+
# if not Path(img_path).exists():
|
163
|
+
# print(t("settings_add_hash_path_invalid"))
|
164
|
+
# continue
|
165
|
+
|
166
|
+
# label = input(t("settings_add_hash_prompt_label") + ": ").strip()
|
167
|
+
# if not label or label.lower() in {"exit", "quit"}:
|
168
|
+
# break
|
169
|
+
|
170
|
+
# try:
|
171
|
+
# img_hash_store.add_image(img_path, label)
|
172
|
+
# print(t("settings_add_hash_added", img=img_path, label=label))
|
173
|
+
# except Exception as e:
|
174
|
+
# print(t("settings_add_hash_failed", err=str(e)))
|
175
|
+
|
176
|
+
# img_hash_store.save()
|
177
|
+
# print(t("settings_add_hash_saved"))
|
novel_downloader/cli/download.py
CHANGED
@@ -3,126 +3,171 @@
|
|
3
3
|
novel_downloader.cli.download
|
4
4
|
-----------------------------
|
5
5
|
|
6
|
-
Download
|
7
|
-
(supports config files, site switching, and localization prompts).
|
6
|
+
Download novels from supported sites via CLI.
|
8
7
|
"""
|
9
8
|
|
10
9
|
import asyncio
|
11
|
-
|
12
|
-
import
|
13
|
-
from
|
10
|
+
import getpass
|
11
|
+
from argparse import Namespace, _SubParsersAction
|
12
|
+
from dataclasses import asdict
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any
|
14
15
|
|
15
16
|
from novel_downloader.config import ConfigAdapter, load_config
|
16
17
|
from novel_downloader.core.factory import (
|
17
|
-
|
18
|
-
|
19
|
-
|
18
|
+
get_downloader,
|
19
|
+
get_exporter,
|
20
|
+
get_fetcher,
|
20
21
|
get_parser,
|
21
|
-
# get_requester,
|
22
|
-
get_saver,
|
23
|
-
get_sync_downloader,
|
24
|
-
get_sync_requester,
|
25
22
|
)
|
23
|
+
from novel_downloader.core.interfaces import FetcherProtocol
|
24
|
+
from novel_downloader.models import LoginField
|
25
|
+
from novel_downloader.utils.cookies import resolve_cookies
|
26
26
|
from novel_downloader.utils.i18n import t
|
27
27
|
from novel_downloader.utils.logger import setup_logging
|
28
28
|
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
"--
|
38
|
-
|
39
|
-
|
40
|
-
help=t("download_option_site", default="qidian"),
|
41
|
-
) # type: ignore
|
42
|
-
@click.pass_context # type: ignore
|
43
|
-
def download_cli(ctx: Context, book_ids: list[str], site: str) -> None:
|
44
|
-
"""Download full novels by book IDs."""
|
45
|
-
config_path = ctx.obj.get("config_path")
|
46
|
-
|
47
|
-
click.echo(t("download_using_config", path=config_path))
|
48
|
-
click.echo(t("download_site_info", site=site))
|
49
|
-
|
50
|
-
config_data = load_config(config_path)
|
51
|
-
adapter = ConfigAdapter(config=config_data, site=site)
|
30
|
+
def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
31
|
+
parser = subparsers.add_parser("download", help=t("help_download"))
|
32
|
+
|
33
|
+
parser.add_argument("book_ids", nargs="*", help=t("download_book_ids"))
|
34
|
+
parser.add_argument(
|
35
|
+
"--site", default="qidian", help=t("download_option_site", default="qidian")
|
36
|
+
)
|
37
|
+
parser.add_argument("--config", type=str, help=t("help_config"))
|
38
|
+
|
39
|
+
parser.set_defaults(func=handle_download)
|
52
40
|
|
53
|
-
# Retrieve each sub-component's configuration from the adapter
|
54
|
-
requester_cfg = adapter.get_requester_config()
|
55
|
-
downloader_cfg = adapter.get_downloader_config()
|
56
|
-
parser_cfg = adapter.get_parser_config()
|
57
|
-
saver_cfg = adapter.get_saver_config()
|
58
41
|
|
59
|
-
|
42
|
+
def handle_download(args: Namespace) -> None:
|
43
|
+
setup_logging()
|
44
|
+
|
45
|
+
site: str = args.site
|
46
|
+
config_path: Path | None = Path(args.config) if args.config else None
|
47
|
+
book_ids: list[str] = args.book_ids or []
|
48
|
+
|
49
|
+
print(t("download_site_info", site=site))
|
50
|
+
|
51
|
+
try:
|
52
|
+
config_data = load_config(config_path)
|
53
|
+
except Exception as e:
|
54
|
+
print(t("download_config_load_fail", err=str(e)))
|
55
|
+
return
|
56
|
+
|
57
|
+
adapter = ConfigAdapter(config=config_data, site=site)
|
60
58
|
|
61
|
-
# If no book_ids provided on the command line, try to load them from config
|
62
59
|
if not book_ids:
|
63
60
|
try:
|
64
61
|
book_ids = adapter.get_book_ids()
|
65
62
|
except Exception as e:
|
66
|
-
|
63
|
+
print(t("download_fail_get_ids", err=str(e)))
|
67
64
|
return
|
68
65
|
|
69
|
-
# Filter out placeholder/example IDs
|
70
66
|
invalid_ids = {"0000000000"}
|
71
67
|
valid_book_ids = set(book_ids) - invalid_ids
|
72
68
|
|
73
69
|
if not book_ids:
|
74
|
-
|
70
|
+
print(t("download_no_ids"))
|
75
71
|
return
|
76
72
|
|
77
73
|
if not valid_book_ids:
|
78
|
-
|
79
|
-
|
74
|
+
print(t("download_only_example", example="0000000000"))
|
75
|
+
print(t("download_edit_config"))
|
80
76
|
return
|
81
77
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
78
|
+
asyncio.run(_download(adapter, site, valid_book_ids))
|
79
|
+
|
80
|
+
|
81
|
+
async def _download(
|
82
|
+
adapter: ConfigAdapter,
|
83
|
+
site: str,
|
84
|
+
valid_book_ids: set[str],
|
85
|
+
) -> None:
|
86
|
+
downloader_cfg = adapter.get_downloader_config()
|
87
|
+
fetcher_cfg = adapter.get_fetcher_config()
|
88
|
+
parser_cfg = adapter.get_parser_config()
|
89
|
+
exporter_cfg = adapter.get_exporter_config()
|
90
|
+
|
91
|
+
parser = get_parser(site, parser_cfg)
|
92
|
+
exporter = get_exporter(site, exporter_cfg)
|
93
|
+
setup_logging()
|
97
94
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
await
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
sync_saver = get_saver(site, saver_cfg)
|
113
|
-
setup_logging()
|
114
|
-
sync_downloader = get_sync_downloader(
|
115
|
-
requester=sync_requester,
|
116
|
-
parser=sync_parser,
|
117
|
-
saver=sync_saver,
|
95
|
+
async with get_fetcher(site, fetcher_cfg) as fetcher:
|
96
|
+
if downloader_cfg.login_required and not await fetcher.load_state():
|
97
|
+
login_data = await _prompt_login_fields(
|
98
|
+
fetcher, fetcher.login_fields, downloader_cfg
|
99
|
+
)
|
100
|
+
if not await fetcher.login(**login_data):
|
101
|
+
print(t("download_login_failed"))
|
102
|
+
return
|
103
|
+
await fetcher.save_state()
|
104
|
+
|
105
|
+
downloader = get_downloader(
|
106
|
+
fetcher=fetcher,
|
107
|
+
parser=parser,
|
108
|
+
exporter=exporter,
|
118
109
|
site=site,
|
119
110
|
config=downloader_cfg,
|
120
111
|
)
|
121
112
|
|
122
113
|
for book_id in valid_book_ids:
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
114
|
+
print(t("download_downloading", book_id=book_id, site=site))
|
115
|
+
await downloader.download(book_id, progress_hook=_print_progress)
|
116
|
+
|
117
|
+
if downloader_cfg.login_required and fetcher.is_logged_in:
|
118
|
+
await fetcher.save_state()
|
119
|
+
|
120
|
+
|
121
|
+
async def _prompt_login_fields(
|
122
|
+
fetcher: FetcherProtocol,
|
123
|
+
fields: list[LoginField],
|
124
|
+
cfg: Any = None,
|
125
|
+
) -> dict[str, Any]:
|
126
|
+
result: dict[str, Any] = {}
|
127
|
+
cfg_dict = asdict(cfg) if cfg else {}
|
128
|
+
|
129
|
+
for field in fields:
|
130
|
+
print(f"\n{field.label} ({field.name})")
|
131
|
+
if field.description:
|
132
|
+
print(f"{t('login_description')}: {field.description}")
|
133
|
+
if field.placeholder:
|
134
|
+
print(f"{t('login_hint')}: {field.placeholder}")
|
135
|
+
|
136
|
+
if field.type == "manual_login":
|
137
|
+
await fetcher.set_interactive_mode(True)
|
138
|
+
input(t("login_manual_prompt"))
|
139
|
+
await fetcher.set_interactive_mode(False)
|
140
|
+
continue
|
141
|
+
|
142
|
+
existing_value = cfg_dict.get(field.name, "").strip()
|
143
|
+
if existing_value:
|
144
|
+
result[field.name] = existing_value
|
145
|
+
print(t("login_use_config"))
|
146
|
+
continue
|
147
|
+
|
148
|
+
value: str | dict[str, str]
|
149
|
+
while True:
|
150
|
+
if field.type == "password":
|
151
|
+
value = getpass.getpass(t("login_enter_password"))
|
152
|
+
elif field.type == "cookie":
|
153
|
+
value = input(t("login_enter_cookie"))
|
154
|
+
value = resolve_cookies(value)
|
155
|
+
else:
|
156
|
+
value = input(t("login_enter_value"))
|
157
|
+
|
158
|
+
if not value and field.default:
|
159
|
+
value = field.default
|
160
|
+
|
161
|
+
if not value and field.required:
|
162
|
+
print(t("login_required_field"))
|
163
|
+
else:
|
164
|
+
break
|
165
|
+
|
166
|
+
result[field.name] = value
|
167
|
+
|
168
|
+
return result
|
169
|
+
|
170
|
+
|
171
|
+
async def _print_progress(done: int, total: int) -> None:
|
172
|
+
percent = done / total * 100
|
173
|
+
print(f"下载进度: {done}/{total} 章 ({percent:.2f}%)")
|