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/cli/main.py
CHANGED
@@ -18,7 +18,7 @@ from .search import register_search_subcommand
|
|
18
18
|
|
19
19
|
|
20
20
|
def cli_main() -> None:
|
21
|
-
parser = argparse.ArgumentParser(description=t("
|
21
|
+
parser = argparse.ArgumentParser(description=t("help_cli"))
|
22
22
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
23
23
|
|
24
24
|
register_clean_subcommand(subparsers)
|
novel_downloader/cli/search.py
CHANGED
@@ -3,13 +3,18 @@
|
|
3
3
|
novel_downloader.cli.search
|
4
4
|
---------------------------
|
5
5
|
|
6
|
+
Search across supported sites, let the user pick one result, then
|
7
|
+
hand off to the download flow.
|
6
8
|
"""
|
7
9
|
|
10
|
+
from __future__ import annotations
|
11
|
+
|
8
12
|
import asyncio
|
9
13
|
from argparse import Namespace, _SubParsersAction
|
10
14
|
from collections.abc import Sequence
|
11
15
|
from pathlib import Path
|
12
16
|
|
17
|
+
from novel_downloader.cli import ui
|
13
18
|
from novel_downloader.cli.download import _download
|
14
19
|
from novel_downloader.config import ConfigAdapter, load_config
|
15
20
|
from novel_downloader.core import search
|
@@ -18,106 +23,98 @@ from novel_downloader.utils.i18n import t
|
|
18
23
|
|
19
24
|
|
20
25
|
def register_search_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
26
|
+
"""Register the `search` subcommand and its options."""
|
21
27
|
parser = subparsers.add_parser("search", help=t("help_search"))
|
22
28
|
|
23
29
|
parser.add_argument(
|
24
|
-
"--site",
|
25
|
-
"-s",
|
26
|
-
action="append",
|
27
|
-
metavar="SITE",
|
28
|
-
help=t("help_search_sites"),
|
29
|
-
)
|
30
|
-
parser.add_argument(
|
31
|
-
"keyword",
|
32
|
-
help=t("help_search_keyword"),
|
30
|
+
"--site", "-s", action="append", metavar="SITE", help=t("search_sites_help")
|
33
31
|
)
|
32
|
+
parser.add_argument("keyword", help=t("search_keyword_help"))
|
33
|
+
parser.add_argument("--config", type=str, help=t("help_config"))
|
34
34
|
parser.add_argument(
|
35
|
-
"--
|
36
|
-
type=str,
|
37
|
-
help=t("help_config"),
|
38
|
-
)
|
39
|
-
parser.add_argument(
|
40
|
-
"--limit",
|
41
|
-
"-l",
|
42
|
-
type=int,
|
43
|
-
default=10,
|
44
|
-
metavar="N",
|
45
|
-
help=t("help_search_limit"),
|
35
|
+
"--limit", "-l", type=int, default=20, metavar="N", help=t("search_limit_help")
|
46
36
|
)
|
47
37
|
parser.add_argument(
|
48
38
|
"--site-limit",
|
49
39
|
type=int,
|
50
40
|
default=5,
|
51
41
|
metavar="M",
|
52
|
-
help=t("
|
42
|
+
help=t("search_site_limit_help"),
|
43
|
+
)
|
44
|
+
parser.add_argument(
|
45
|
+
"--timeout",
|
46
|
+
type=float,
|
47
|
+
default=5.0,
|
48
|
+
metavar="SECS",
|
49
|
+
help=t("search_timeout_help", secs="5.0"),
|
53
50
|
)
|
54
51
|
|
55
52
|
parser.set_defaults(func=handle_search)
|
56
53
|
|
57
54
|
|
58
55
|
def handle_search(args: Namespace) -> None:
|
59
|
-
"""
|
60
|
-
Handler for the `search` subcommand. Loads config, runs the search,
|
61
|
-
prompts the user to pick one result, then kicks off download.
|
62
|
-
"""
|
56
|
+
"""Handle the `search` subcommand."""
|
63
57
|
sites: Sequence[str] | None = args.site or None
|
64
58
|
keyword: str = args.keyword
|
65
59
|
overall_limit = max(1, args.limit)
|
66
60
|
per_site_limit = max(1, args.site_limit)
|
61
|
+
timeout = max(0.1, float(args.timeout))
|
67
62
|
config_path: Path | None = Path(args.config) if args.config else None
|
68
63
|
|
69
64
|
try:
|
70
65
|
config_data = load_config(config_path)
|
71
66
|
except Exception as e:
|
72
|
-
|
67
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
73
68
|
return
|
74
69
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
70
|
+
async def _run() -> None:
|
71
|
+
results = await search(
|
72
|
+
keyword=keyword,
|
73
|
+
sites=sites,
|
74
|
+
limit=overall_limit,
|
75
|
+
per_site_limit=per_site_limit,
|
76
|
+
timeout=timeout,
|
77
|
+
)
|
81
78
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
79
|
+
chosen = _prompt_user_select(results)
|
80
|
+
if chosen is None:
|
81
|
+
return
|
82
|
+
|
83
|
+
adapter = ConfigAdapter(config=config_data, site=chosen["site"])
|
84
|
+
books: list[BookConfig] = [{"book_id": chosen["book_id"]}]
|
85
|
+
await _download(adapter, chosen["site"], books)
|
86
86
|
|
87
|
-
|
88
|
-
books: list[BookConfig] = [{"book_id": chosen["book_id"]}]
|
89
|
-
asyncio.run(_download(adapter, chosen["site"], books))
|
87
|
+
asyncio.run(_run())
|
90
88
|
|
91
89
|
|
92
|
-
def _prompt_user_select(
|
93
|
-
results: Sequence[SearchResult],
|
94
|
-
max_attempts: int = 3,
|
95
|
-
) -> SearchResult | None:
|
90
|
+
def _prompt_user_select(results: Sequence[SearchResult]) -> SearchResult | None:
|
96
91
|
"""
|
97
|
-
|
92
|
+
Show a Rich table of results and ask the user to pick one by index.
|
98
93
|
|
99
|
-
:param results:
|
100
|
-
:
|
101
|
-
:return: The chosen SearchResult, or None if cancelled/failed.
|
94
|
+
:param results: A sequence of SearchResult dicts.
|
95
|
+
:return: The chosen SearchResult, or None if cancelled/no results.
|
102
96
|
"""
|
103
97
|
if not results:
|
104
|
-
|
98
|
+
ui.warn(t("no_results"))
|
105
99
|
return None
|
106
100
|
|
107
|
-
#
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
101
|
+
columns = ["#", "Title", "Author", "Latest", "Updated", "Site", "Book ID"]
|
102
|
+
rows = []
|
103
|
+
for i, r in enumerate(results, 1):
|
104
|
+
rows.append(
|
105
|
+
[
|
106
|
+
str(i),
|
107
|
+
r["title"],
|
108
|
+
r["author"],
|
109
|
+
r["latest_chapter"],
|
110
|
+
r["update_date"],
|
111
|
+
r["site"],
|
112
|
+
r["book_id"],
|
113
|
+
]
|
114
|
+
)
|
115
|
+
ui.render_table("Search Results", columns, rows)
|
116
|
+
|
117
|
+
idx = ui.select_index(t("prompt_select_index"), len(results))
|
118
|
+
if idx is None:
|
119
|
+
return None
|
120
|
+
return results[idx - 1]
|
@@ -0,0 +1,156 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.cli.ui
|
4
|
+
-----------------------
|
5
|
+
|
6
|
+
A small set of Rich-based helpers to keep CLI presentation and prompts
|
7
|
+
consistent across subcommands.
|
8
|
+
|
9
|
+
Public API:
|
10
|
+
* info, success, warn, error
|
11
|
+
* confirm
|
12
|
+
* prompt, prompt_password
|
13
|
+
* render_table
|
14
|
+
* select_index
|
15
|
+
* print_progress
|
16
|
+
"""
|
17
|
+
|
18
|
+
from __future__ import annotations
|
19
|
+
|
20
|
+
from collections.abc import Iterable, Sequence
|
21
|
+
|
22
|
+
from rich.console import Console
|
23
|
+
from rich.prompt import Confirm, Prompt
|
24
|
+
from rich.table import Table
|
25
|
+
|
26
|
+
_CONSOLE = Console()
|
27
|
+
|
28
|
+
|
29
|
+
def info(message: str) -> None:
|
30
|
+
"""Print a neutral informational message."""
|
31
|
+
_CONSOLE.print(message)
|
32
|
+
|
33
|
+
|
34
|
+
def success(message: str) -> None:
|
35
|
+
"""Print a success message in a friendly color."""
|
36
|
+
_CONSOLE.print(f"[green]{message}[/]")
|
37
|
+
|
38
|
+
|
39
|
+
def warn(message: str) -> None:
|
40
|
+
"""Print a warning message."""
|
41
|
+
_CONSOLE.print(f"[yellow]{message}[/]")
|
42
|
+
|
43
|
+
|
44
|
+
def error(message: str) -> None:
|
45
|
+
"""Print an error message."""
|
46
|
+
_CONSOLE.print(f"[red]{message}[/]")
|
47
|
+
|
48
|
+
|
49
|
+
def confirm(message: str, *, default: bool = False) -> bool:
|
50
|
+
"""
|
51
|
+
Ask a yes/no question.
|
52
|
+
|
53
|
+
:param message: The question to display (without [y/N] suffix).
|
54
|
+
:param default: Default choice (pressing Enter = Yes if True, No if False).
|
55
|
+
:return: True if user confirms (Yes), otherwise False.
|
56
|
+
"""
|
57
|
+
try:
|
58
|
+
result: bool = Confirm.ask(f"[bold]{message}[/bold]", default=default)
|
59
|
+
return result
|
60
|
+
except (KeyboardInterrupt, EOFError):
|
61
|
+
warn("Cancelled.")
|
62
|
+
return False
|
63
|
+
|
64
|
+
|
65
|
+
def prompt(message: str, *, default: str | None = None) -> str:
|
66
|
+
"""
|
67
|
+
Prompt user for a line of text.
|
68
|
+
|
69
|
+
:param message: Prompt message.
|
70
|
+
:param default: Default value if the user presses Enter.
|
71
|
+
:return: The user's input.
|
72
|
+
"""
|
73
|
+
try:
|
74
|
+
result: str = Prompt.ask(message, default=default or "", show_default=False)
|
75
|
+
return result
|
76
|
+
except (KeyboardInterrupt, EOFError):
|
77
|
+
warn("Cancelled.")
|
78
|
+
return default or ""
|
79
|
+
|
80
|
+
|
81
|
+
def prompt_password(message: str) -> str:
|
82
|
+
"""
|
83
|
+
Prompt user for a password/secret value (no echo).
|
84
|
+
|
85
|
+
:param message: Prompt message.
|
86
|
+
:return: The user's input (may be empty).
|
87
|
+
"""
|
88
|
+
try:
|
89
|
+
result: str = Prompt.ask(message, password=True)
|
90
|
+
return result
|
91
|
+
except (KeyboardInterrupt, EOFError):
|
92
|
+
warn("Cancelled.")
|
93
|
+
return ""
|
94
|
+
|
95
|
+
|
96
|
+
def render_table(
|
97
|
+
title: str,
|
98
|
+
columns: Sequence[str],
|
99
|
+
rows: Iterable[Sequence[str]],
|
100
|
+
) -> None:
|
101
|
+
"""
|
102
|
+
Render a simple full-width table.
|
103
|
+
|
104
|
+
:param title: Table title.
|
105
|
+
:param columns: Column names.
|
106
|
+
:param rows: Row data; each row must have the same length as `columns`.
|
107
|
+
"""
|
108
|
+
table = Table(title=title, show_lines=True, expand=True)
|
109
|
+
for col in columns:
|
110
|
+
table.add_column(col, overflow="fold")
|
111
|
+
for row in rows:
|
112
|
+
table.add_row(*[str(x) for x in row])
|
113
|
+
_CONSOLE.print(table)
|
114
|
+
|
115
|
+
|
116
|
+
def select_index(prompt_text: str, total: int) -> int | None:
|
117
|
+
"""
|
118
|
+
Prompt user to select an index in [1..total]. Empty input cancels.
|
119
|
+
|
120
|
+
:param prompt_text: Displayed prompt (e.g., 'Select index').
|
121
|
+
:param total: Maximum valid index (minimum is 1).
|
122
|
+
:return: Selected 1-based index, or None if user cancels.
|
123
|
+
"""
|
124
|
+
if total <= 0:
|
125
|
+
return None
|
126
|
+
valid_choices = [str(i) for i in range(1, total + 1)]
|
127
|
+
choice = Prompt.ask(
|
128
|
+
prompt_text,
|
129
|
+
choices=valid_choices + [""],
|
130
|
+
show_choices=False,
|
131
|
+
default="",
|
132
|
+
show_default=False,
|
133
|
+
).strip()
|
134
|
+
if not choice:
|
135
|
+
return None
|
136
|
+
return int(choice)
|
137
|
+
|
138
|
+
|
139
|
+
def print_progress(
|
140
|
+
done: int,
|
141
|
+
total: int,
|
142
|
+
*,
|
143
|
+
prefix: str = "Progress",
|
144
|
+
unit: str = "item",
|
145
|
+
) -> None:
|
146
|
+
"""
|
147
|
+
Print a lightweight progress line.
|
148
|
+
|
149
|
+
:param done: Completed count.
|
150
|
+
:param total: Total count.
|
151
|
+
:param prefix: Text prefix shown before numbers.
|
152
|
+
:param unit: Logical unit name (e.g., 'item').
|
153
|
+
"""
|
154
|
+
total = max(1, total)
|
155
|
+
pct = done / total * 100.0
|
156
|
+
_CONSOLE.print(f"[dim]{prefix}[/] {done}/{total} {unit} ({pct:.2f}%)")
|
@@ -4,17 +4,20 @@ novel_downloader.config
|
|
4
4
|
-----------------------
|
5
5
|
|
6
6
|
Unified interface for loading and adapting configuration files.
|
7
|
-
|
8
|
-
This module provides:
|
9
|
-
- load_config: loads YAML config from file path with fallback support
|
10
|
-
- ConfigAdapter: maps raw config + site name to structured config models
|
11
7
|
"""
|
12
8
|
|
13
9
|
__all__ = [
|
10
|
+
"get_config_value",
|
14
11
|
"load_config",
|
12
|
+
"save_config",
|
15
13
|
"save_config_file",
|
16
14
|
"ConfigAdapter",
|
17
15
|
]
|
18
16
|
|
19
17
|
from .adapter import ConfigAdapter
|
20
|
-
from .
|
18
|
+
from .file_io import (
|
19
|
+
get_config_value,
|
20
|
+
load_config,
|
21
|
+
save_config,
|
22
|
+
save_config_file,
|
23
|
+
)
|