novel-downloader 1.4.5__py3-none-any.whl → 2.0.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/__init__.py +2 -4
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +27 -104
- novel_downloader/cli/download.py +78 -66
- novel_downloader/cli/export.py +20 -21
- novel_downloader/cli/main.py +3 -1
- novel_downloader/cli/search.py +120 -0
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +10 -14
- novel_downloader/config/adapter.py +195 -99
- novel_downloader/config/{loader.py → file_io.py} +53 -27
- novel_downloader/core/__init__.py +14 -13
- 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/archived/qidian/searcher.py +79 -0
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +8 -30
- novel_downloader/core/downloaders/base.py +182 -30
- novel_downloader/core/downloaders/common.py +217 -384
- novel_downloader/core/downloaders/qianbi.py +332 -4
- novel_downloader/core/downloaders/qidian.py +250 -290
- novel_downloader/core/downloaders/registry.py +69 -0
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +8 -26
- novel_downloader/core/exporters/base.py +107 -31
- novel_downloader/core/exporters/common/__init__.py +3 -4
- novel_downloader/core/exporters/common/epub.py +92 -171
- novel_downloader/core/exporters/common/main_exporter.py +14 -67
- novel_downloader/core/exporters/common/txt.py +90 -86
- novel_downloader/core/exporters/epub_util.py +184 -1327
- novel_downloader/core/exporters/linovelib/__init__.py +3 -2
- novel_downloader/core/exporters/linovelib/epub.py +165 -222
- novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
- novel_downloader/core/exporters/linovelib/txt.py +76 -66
- novel_downloader/core/exporters/qidian.py +15 -11
- novel_downloader/core/exporters/registry.py +55 -0
- novel_downloader/core/exporters/txt_util.py +67 -0
- novel_downloader/core/fetchers/__init__.py +57 -56
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
- novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
- novel_downloader/core/fetchers/biquyuedu.py +83 -0
- 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} +23 -11
- 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} +22 -26
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
- 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} +9 -9
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
- 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 +60 -0
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
- 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} +23 -11
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +8 -14
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +26 -0
- novel_downloader/core/parsers/__init__.py +58 -22
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -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/main_parser.py → esjzone.py} +67 -67
- 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/main_parser.py → linovelib.py} +54 -65
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
- novel_downloader/core/parsers/qidian/__init__.py +2 -2
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
- novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
- novel_downloader/core/parsers/qidian/main_parser.py +19 -57
- novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +57 -0
- novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
- 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 +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +155 -0
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +51 -0
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/b520.py +84 -0
- novel_downloader/core/searchers/base.py +168 -0
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +102 -0
- 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 +165 -0
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +79 -0
- 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 +36 -79
- novel_downloader/locales/zh.json +37 -80
- novel_downloader/models/__init__.py +23 -50
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +16 -43
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +21 -0
- novel_downloader/resources/config/settings.toml +39 -74
- novel_downloader/resources/css_styles/intro.css +83 -0
- novel_downloader/resources/css_styles/main.css +30 -89
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +43 -0
- novel_downloader/utils/chapter_storage.py +247 -226
- novel_downloader/utils/constants.py +5 -50
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -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.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +34 -0
- novel_downloader/utils/epub/builder.py +377 -0
- novel_downloader/utils/epub/constants.py +118 -0
- novel_downloader/utils/epub/documents.py +297 -0
- novel_downloader/utils/epub/models.py +120 -0
- novel_downloader/utils/epub/utils.py +179 -0
- novel_downloader/utils/file_utils/__init__.py +5 -30
- novel_downloader/utils/file_utils/io.py +9 -150
- novel_downloader/utils/file_utils/normalize.py +2 -2
- novel_downloader/utils/file_utils/sanitize.py +2 -7
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/i18n.py +2 -0
- novel_downloader/utils/logger.py +10 -16
- novel_downloader/utils/network.py +111 -252
- novel_downloader/utils/state.py +5 -90
- novel_downloader/utils/text_utils/__init__.py +16 -21
- novel_downloader/utils/text_utils/diff_display.py +6 -9
- novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
- novel_downloader/utils/text_utils/text_cleaner.py +179 -0
- novel_downloader/utils/text_utils/truncate_utils.py +62 -0
- novel_downloader/utils/time_utils/__init__.py +6 -12
- novel_downloader/utils/time_utils/datetime_utils.py +23 -33
- novel_downloader/utils/time_utils/sleep_utils.py +5 -10
- 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.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/config/site_rules.py +0 -94
- novel_downloader/core/downloaders/biquge.py +0 -25
- novel_downloader/core/downloaders/esjzone.py +0 -25
- novel_downloader/core/downloaders/linovelib.py +0 -25
- novel_downloader/core/downloaders/sfacg.py +0 -25
- novel_downloader/core/downloaders/yamibo.py +0 -25
- novel_downloader/core/exporters/biquge.py +0 -25
- novel_downloader/core/exporters/esjzone.py +0 -25
- novel_downloader/core/exporters/qianbi.py +0 -25
- novel_downloader/core/exporters/sfacg.py +0 -25
- novel_downloader/core/exporters/yamibo.py +0 -25
- novel_downloader/core/factory/__init__.py +0 -20
- novel_downloader/core/factory/downloader.py +0 -73
- novel_downloader/core/factory/exporter.py +0 -58
- novel_downloader/core/factory/fetcher.py +0 -96
- novel_downloader/core/factory/parser.py +0 -86
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -403
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/common/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -204
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -193
- 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 -318
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -189
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -229
- novel_downloader/core/parsers/biquge/__init__.py +0 -10
- novel_downloader/core/parsers/biquge/main_parser.py +0 -134
- novel_downloader/core/parsers/common/__init__.py +0 -13
- novel_downloader/core/parsers/common/helper.py +0 -323
- novel_downloader/core/parsers/common/main_parser.py +0 -106
- novel_downloader/core/parsers/esjzone/__init__.py +0 -10
- novel_downloader/core/parsers/linovelib/__init__.py +0 -10
- novel_downloader/core/parsers/qianbi/__init__.py +0 -10
- novel_downloader/core/parsers/sfacg/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
- novel_downloader/models/browser.py +0 -21
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/site_rules.py +0 -99
- novel_downloader/models/tasks.py +0 -33
- novel_downloader/models/types.py +0 -15
- novel_downloader/resources/css_styles/volume-intro.css +0 -56
- novel_downloader/resources/json/replace_word_map.json +0 -4
- novel_downloader/resources/text/blacklist.txt +0 -22
- 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/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -303
- novel_downloader/utils/fontocr/ocr_v2.py +0 -752
- novel_downloader/utils/hash_store.py +0 -279
- novel_downloader/utils/hash_utils.py +0 -103
- novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
- novel_downloader/utils/text_utils/font_mapping.py +0 -28
- novel_downloader/utils/text_utils/text_cleaning.py +0 -107
- novel_downloader-1.4.5.dist-info/METADATA +0 -196
- novel_downloader-1.4.5.dist-info/RECORD +0 -165
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.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,76 +3,58 @@
|
|
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
|
|
14
|
-
from novel_downloader.
|
16
|
+
from novel_downloader.cli import ui
|
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
|
17
|
-
from novel_downloader.utils.logger import setup_logging
|
18
20
|
from novel_downloader.utils.state import state_mgr
|
19
21
|
|
20
|
-
# from novel_downloader.utils.hash_store import img_hash_store
|
21
|
-
|
22
22
|
|
23
23
|
def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
24
|
+
"""Register `config` with `init`, `set-lang`, and `set-config` subcommands."""
|
24
25
|
parser = subparsers.add_parser("config", help=t("help_config"))
|
25
26
|
config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
|
26
27
|
|
27
28
|
_register_init(config_subparsers)
|
28
29
|
_register_set_lang(config_subparsers)
|
29
30
|
_register_set_config(config_subparsers)
|
30
|
-
_register_update_rules(config_subparsers)
|
31
|
-
_register_set_cookies(config_subparsers)
|
32
|
-
# _register_add_hash(config_subparsers)
|
33
31
|
|
34
32
|
|
35
33
|
def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
|
36
|
-
parser = subparsers.add_parser("init", help=t("
|
34
|
+
parser = subparsers.add_parser("init", help=t("config_init_help"))
|
37
35
|
parser.add_argument(
|
38
|
-
"--force", action="store_true", help=t("
|
36
|
+
"--force", action="store_true", help=t("config_init_force_help")
|
39
37
|
)
|
40
38
|
parser.set_defaults(func=_handle_init)
|
41
39
|
|
42
40
|
|
43
41
|
def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
|
44
|
-
parser = subparsers.add_parser("set-lang", help=t("
|
42
|
+
parser = subparsers.add_parser("set-lang", help=t("config_set_lang_help"))
|
45
43
|
parser.add_argument("lang", choices=["zh", "en"], help="Language code")
|
46
44
|
parser.set_defaults(func=_handle_set_lang)
|
47
45
|
|
48
46
|
|
49
47
|
def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
|
50
|
-
parser = subparsers.add_parser("set-config", help=t("
|
48
|
+
parser = subparsers.add_parser("set-config", help=t("config_set_config_help"))
|
51
49
|
parser.add_argument("path", type=str, help="Path to YAML config file")
|
52
50
|
parser.set_defaults(func=_handle_set_config)
|
53
51
|
|
54
52
|
|
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
53
|
def _handle_init(args: Namespace) -> None:
|
75
|
-
|
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
|
+
"""
|
76
58
|
cwd = Path.cwd()
|
77
59
|
|
78
60
|
for resource in DEFAULT_SETTINGS_PATHS:
|
@@ -81,97 +63,38 @@ def _handle_init(args: Namespace) -> None:
|
|
81
63
|
|
82
64
|
if target_path.exists():
|
83
65
|
if args.force:
|
84
|
-
|
66
|
+
ui.warn(t("config_init_overwrite", filename=resource.name))
|
85
67
|
else:
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
+ " [y/N]: "
|
91
|
-
)
|
92
|
-
.strip()
|
93
|
-
.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,
|
94
72
|
)
|
95
|
-
should_copy = resp == "y"
|
96
73
|
|
97
74
|
if not should_copy:
|
98
|
-
|
75
|
+
ui.warn(t("config_init_skip", filename=resource.name))
|
99
76
|
continue
|
100
77
|
|
101
78
|
try:
|
102
79
|
with as_file(resource) as actual_path:
|
103
80
|
shutil.copy(actual_path, target_path)
|
104
|
-
|
81
|
+
ui.success(t("config_init_copy", filename=resource.name))
|
105
82
|
except Exception as e:
|
106
|
-
|
83
|
+
ui.error(t("config_init_error", filename=resource.name, err=str(e)))
|
107
84
|
raise
|
108
85
|
|
109
86
|
|
110
87
|
def _handle_set_lang(args: Namespace) -> None:
|
88
|
+
"""Set the UI language and persist in state manager."""
|
111
89
|
state_mgr.set_language(args.lang)
|
112
|
-
|
90
|
+
ui.success(t("config_set_lang", lang=args.lang))
|
113
91
|
|
114
92
|
|
115
93
|
def _handle_set_config(args: Namespace) -> None:
|
94
|
+
"""Persist a user-supplied TOML config path into the app config."""
|
116
95
|
try:
|
117
96
|
save_config_file(args.path)
|
118
|
-
|
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))
|
97
|
+
ui.success(t("config_set_config", path=args.path))
|
128
98
|
except Exception as e:
|
129
|
-
|
99
|
+
ui.error(t("config_set_config_fail", err=str(e)))
|
130
100
|
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,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
|
19
|
-
get_downloader,
|
20
|
-
get_exporter,
|
21
|
-
get_fetcher,
|
22
|
-
get_parser,
|
23
|
-
)
|
24
|
-
from novel_downloader.core.interfaces import FetcherProtocol
|
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,26 +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:
|
47
|
-
|
48
|
-
|
49
|
+
"""Handle the `download` subcommand."""
|
49
50
|
site: str = args.site
|
50
51
|
config_path: Path | None = Path(args.config) if args.config else None
|
51
52
|
book_ids: list[BookConfig] = _cli_args_to_book_configs(
|
52
|
-
args.book_ids,
|
53
|
-
args.start,
|
54
|
-
args.end,
|
53
|
+
args.book_ids, args.start, args.end
|
55
54
|
)
|
55
|
+
no_export: bool = getattr(args, "no_export", False)
|
56
56
|
|
57
|
-
|
57
|
+
ui.info(t("download_site_info", site=site))
|
58
58
|
|
59
59
|
try:
|
60
60
|
config_data = load_config(config_path)
|
61
61
|
except Exception as e:
|
62
|
-
|
62
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
63
63
|
return
|
64
64
|
|
65
65
|
adapter = ConfigAdapter(config=config_data, site=site)
|
@@ -68,21 +68,21 @@ def handle_download(args: Namespace) -> None:
|
|
68
68
|
try:
|
69
69
|
book_ids = adapter.get_book_ids()
|
70
70
|
except Exception as e:
|
71
|
-
|
71
|
+
ui.error(t("download_fail_get_ids", err=str(e)))
|
72
72
|
return
|
73
73
|
|
74
74
|
valid_books = _filter_valid_book_configs(book_ids)
|
75
75
|
|
76
76
|
if not book_ids:
|
77
|
-
|
77
|
+
ui.warn(t("download_no_ids"))
|
78
78
|
return
|
79
79
|
|
80
80
|
if not valid_books:
|
81
|
-
|
82
|
-
|
81
|
+
ui.warn(t("download_only_example", example="0000000000"))
|
82
|
+
ui.info(t("download_edit_config"))
|
83
83
|
return
|
84
84
|
|
85
|
-
asyncio.run(_download(adapter, site, valid_books))
|
85
|
+
asyncio.run(_download(adapter, site, valid_books, no_export=no_export))
|
86
86
|
|
87
87
|
|
88
88
|
def _cli_args_to_book_configs(
|
@@ -91,14 +91,13 @@ def _cli_args_to_book_configs(
|
|
91
91
|
end_id: str | None,
|
92
92
|
) -> list[BookConfig]:
|
93
93
|
"""
|
94
|
-
Convert CLI
|
95
|
-
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`.
|
96
96
|
"""
|
97
97
|
if not book_ids:
|
98
98
|
return []
|
99
99
|
|
100
100
|
result: list[BookConfig] = []
|
101
|
-
|
102
101
|
first: BookConfig = {"book_id": book_ids[0]}
|
103
102
|
if start_id:
|
104
103
|
first["start_id"] = start_id
|
@@ -117,9 +116,11 @@ def _filter_valid_book_configs(
|
|
117
116
|
invalid_ids: Iterable[str] = ("0000000000",),
|
118
117
|
) -> list[BookConfig]:
|
119
118
|
"""
|
120
|
-
Filter
|
121
|
-
|
122
|
-
|
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.
|
123
124
|
"""
|
124
125
|
seen = set(invalid_ids)
|
125
126
|
result: list[BookConfig] = []
|
@@ -138,87 +139,96 @@ async def _download(
|
|
138
139
|
adapter: ConfigAdapter,
|
139
140
|
site: str,
|
140
141
|
valid_books: list[BookConfig],
|
142
|
+
*,
|
143
|
+
no_export: bool = False,
|
141
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
|
+
"""
|
142
152
|
downloader_cfg = adapter.get_downloader_config()
|
143
153
|
fetcher_cfg = adapter.get_fetcher_config()
|
144
154
|
parser_cfg = adapter.get_parser_config()
|
145
155
|
exporter_cfg = adapter.get_exporter_config()
|
156
|
+
login_cfg = adapter.get_login_config()
|
157
|
+
log_level = adapter.get_log_level()
|
158
|
+
setup_logging(log_level=log_level)
|
146
159
|
|
147
160
|
parser = get_parser(site, parser_cfg)
|
148
|
-
exporter =
|
149
|
-
|
161
|
+
exporter = None
|
162
|
+
if not no_export:
|
163
|
+
exporter = get_exporter(site, exporter_cfg)
|
164
|
+
else:
|
165
|
+
ui.info(t("download_export_skipped"))
|
150
166
|
|
151
167
|
async with get_fetcher(site, fetcher_cfg) as fetcher:
|
152
168
|
if downloader_cfg.login_required and not await fetcher.load_state():
|
153
|
-
login_data = await _prompt_login_fields(
|
154
|
-
fetcher, fetcher.login_fields, downloader_cfg
|
155
|
-
)
|
169
|
+
login_data = await _prompt_login_fields(fetcher.login_fields, login_cfg)
|
156
170
|
if not await fetcher.login(**login_data):
|
157
|
-
|
171
|
+
ui.error(t("download_login_failed"))
|
158
172
|
return
|
159
173
|
await fetcher.save_state()
|
160
174
|
|
161
175
|
downloader = get_downloader(
|
162
|
-
fetcher=fetcher,
|
163
|
-
parser=parser,
|
164
|
-
site=site,
|
165
|
-
config=downloader_cfg,
|
176
|
+
fetcher=fetcher, parser=parser, site=site, config=downloader_cfg
|
166
177
|
)
|
167
178
|
|
168
179
|
for book in valid_books:
|
169
|
-
|
170
|
-
await downloader.download(
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
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"])
|
175
185
|
|
176
186
|
if downloader_cfg.login_required and fetcher.is_logged_in:
|
177
187
|
await fetcher.save_state()
|
178
188
|
|
179
189
|
|
180
190
|
async def _prompt_login_fields(
|
181
|
-
fetcher: FetcherProtocol,
|
182
191
|
fields: list[LoginField],
|
183
|
-
|
192
|
+
login_config: dict[str, str] | None = None,
|
184
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 {}
|
185
202
|
result: dict[str, Any] = {}
|
186
|
-
cfg_dict = asdict(cfg) if cfg else {}
|
187
203
|
|
188
204
|
for field in fields:
|
189
|
-
|
205
|
+
ui.info(f"\n{field.label} ({field.name})")
|
190
206
|
if field.description:
|
191
|
-
|
207
|
+
ui.info(f"{t('login_description')}: {field.description}")
|
192
208
|
if field.placeholder:
|
193
|
-
|
194
|
-
|
195
|
-
if field.type == "manual_login":
|
196
|
-
await fetcher.set_interactive_mode(True)
|
197
|
-
input(t("login_manual_prompt"))
|
198
|
-
await fetcher.set_interactive_mode(False)
|
199
|
-
continue
|
209
|
+
ui.info(f"{t('login_hint')}: {field.placeholder}")
|
200
210
|
|
201
|
-
existing_value =
|
211
|
+
existing_value = login_config.get(field.name, "").strip()
|
202
212
|
if existing_value:
|
203
213
|
result[field.name] = existing_value
|
204
|
-
|
214
|
+
ui.info(t("login_use_config"))
|
205
215
|
continue
|
206
216
|
|
207
217
|
value: str | dict[str, str]
|
208
218
|
while True:
|
209
219
|
if field.type == "password":
|
210
|
-
value =
|
220
|
+
value = ui.prompt_password(t("login_enter_password"))
|
211
221
|
elif field.type == "cookie":
|
212
|
-
value =
|
213
|
-
value =
|
222
|
+
value = ui.prompt(t("login_enter_cookie"))
|
223
|
+
value = parse_cookies(value)
|
214
224
|
else:
|
215
|
-
value =
|
225
|
+
value = ui.prompt(t("login_enter_value"))
|
216
226
|
|
217
227
|
if not value and field.default:
|
218
228
|
value = field.default
|
219
229
|
|
220
230
|
if not value and field.required:
|
221
|
-
|
231
|
+
ui.warn(t("login_required_field"))
|
222
232
|
else:
|
223
233
|
break
|
224
234
|
|
@@ -228,5 +238,7 @@ async def _prompt_login_fields(
|
|
228
238
|
|
229
239
|
|
230
240
|
async def _print_progress(done: int, total: int) -> None:
|
231
|
-
|
232
|
-
|
241
|
+
"""Progress hook passed into the downloader."""
|
242
|
+
ui.print_progress(
|
243
|
+
done, total, prefix=t("download_progress_prefix"), unit="chapters"
|
244
|
+
)
|