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/cli/export.py
CHANGED
@@ -3,24 +3,26 @@
|
|
3
3
|
novel_downloader.cli.export
|
4
4
|
---------------------------
|
5
5
|
|
6
|
+
Export existing books into TXT/EPUB formats.
|
6
7
|
"""
|
7
8
|
|
9
|
+
from __future__ import annotations
|
10
|
+
|
8
11
|
from argparse import Namespace, _SubParsersAction
|
9
12
|
from pathlib import Path
|
10
13
|
|
14
|
+
from novel_downloader.cli import ui
|
11
15
|
from novel_downloader.config import ConfigAdapter, load_config
|
12
|
-
from novel_downloader.core
|
16
|
+
from novel_downloader.core import get_exporter
|
13
17
|
from novel_downloader.utils.i18n import t
|
18
|
+
from novel_downloader.utils.logger import setup_logging
|
14
19
|
|
15
20
|
|
16
21
|
def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
22
|
+
"""Register the `export` subcommand and its options."""
|
17
23
|
parser = subparsers.add_parser("export", help=t("help_export"))
|
18
24
|
|
19
|
-
parser.add_argument(
|
20
|
-
"book_ids",
|
21
|
-
nargs="+",
|
22
|
-
help=t("download_book_ids"),
|
23
|
-
)
|
25
|
+
parser.add_argument("book_ids", nargs="+", help=t("download_book_ids"))
|
24
26
|
parser.add_argument(
|
25
27
|
"--format",
|
26
28
|
choices=["txt", "epub", "all"],
|
@@ -28,50 +30,47 @@ def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type:
|
|
28
30
|
help=t("export_format_help"),
|
29
31
|
)
|
30
32
|
parser.add_argument(
|
31
|
-
"--site",
|
32
|
-
default="qidian",
|
33
|
-
help=t("download_option_site", default="qidian"),
|
34
|
-
)
|
35
|
-
parser.add_argument(
|
36
|
-
"--config",
|
37
|
-
type=str,
|
38
|
-
help=t("help_config"),
|
33
|
+
"--site", default="qidian", help=t("download_option_site", default="qidian")
|
39
34
|
)
|
35
|
+
parser.add_argument("--config", type=str, help=t("help_config"))
|
40
36
|
|
41
37
|
parser.set_defaults(func=handle_export)
|
42
38
|
|
43
39
|
|
44
40
|
def handle_export(args: Namespace) -> None:
|
41
|
+
"""Handle the `export` subcommand."""
|
45
42
|
site: str = args.site
|
46
43
|
config_path: Path | None = Path(args.config) if args.config else None
|
47
44
|
book_ids: list[str] = args.book_ids
|
48
45
|
export_format: str = args.format
|
49
46
|
|
50
|
-
|
47
|
+
ui.info(t("download_site_info", site=site))
|
51
48
|
|
52
49
|
try:
|
53
50
|
config_data = load_config(config_path)
|
54
51
|
except Exception as e:
|
55
|
-
|
52
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
56
53
|
return
|
57
54
|
|
58
55
|
adapter = ConfigAdapter(config=config_data, site=site)
|
59
56
|
exporter_cfg = adapter.get_exporter_config()
|
57
|
+
log_level = adapter.get_log_level()
|
60
58
|
exporter = get_exporter(site, exporter_cfg)
|
59
|
+
setup_logging(log_level=log_level)
|
61
60
|
|
62
61
|
for book_id in book_ids:
|
63
|
-
|
62
|
+
ui.info(t("export_processing", book_id=book_id, format=export_format))
|
64
63
|
|
65
64
|
if export_format in {"txt", "all"}:
|
66
65
|
try:
|
67
66
|
exporter.export_as_txt(book_id)
|
68
|
-
|
67
|
+
ui.success(t("export_success_txt", book_id=book_id))
|
69
68
|
except Exception as e:
|
70
|
-
|
69
|
+
ui.error(t("export_failed_txt", book_id=book_id, err=str(e)))
|
71
70
|
|
72
71
|
if export_format in {"epub", "all"}:
|
73
72
|
try:
|
74
73
|
exporter.export_as_epub(book_id)
|
75
|
-
|
74
|
+
ui.success(t("export_success_epub", book_id=book_id))
|
76
75
|
except Exception as e:
|
77
|
-
|
76
|
+
ui.error(t("export_failed_epub", book_id=book_id, err=str(e)))
|
novel_downloader/cli/main.py
CHANGED
@@ -14,16 +14,18 @@ from .clean import register_clean_subcommand
|
|
14
14
|
from .config import register_config_subcommand
|
15
15
|
from .download import register_download_subcommand
|
16
16
|
from .export import register_export_subcommand
|
17
|
+
from .search import register_search_subcommand
|
17
18
|
|
18
19
|
|
19
20
|
def cli_main() -> None:
|
20
|
-
parser = argparse.ArgumentParser(description=t("
|
21
|
+
parser = argparse.ArgumentParser(description=t("help_cli"))
|
21
22
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
22
23
|
|
23
24
|
register_clean_subcommand(subparsers)
|
24
25
|
register_config_subcommand(subparsers)
|
25
26
|
register_download_subcommand(subparsers)
|
26
27
|
register_export_subcommand(subparsers)
|
28
|
+
register_search_subcommand(subparsers)
|
27
29
|
|
28
30
|
args = parser.parse_args()
|
29
31
|
if hasattr(args, "func"):
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.cli.search
|
4
|
+
---------------------------
|
5
|
+
|
6
|
+
Search across supported sites, let the user pick one result, then
|
7
|
+
hand off to the download flow.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
import asyncio
|
13
|
+
from argparse import Namespace, _SubParsersAction
|
14
|
+
from collections.abc import Sequence
|
15
|
+
from pathlib import Path
|
16
|
+
|
17
|
+
from novel_downloader.cli import ui
|
18
|
+
from novel_downloader.cli.download import _download
|
19
|
+
from novel_downloader.config import ConfigAdapter, load_config
|
20
|
+
from novel_downloader.core import search
|
21
|
+
from novel_downloader.models import BookConfig, SearchResult
|
22
|
+
from novel_downloader.utils.i18n import t
|
23
|
+
|
24
|
+
|
25
|
+
def register_search_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
26
|
+
"""Register the `search` subcommand and its options."""
|
27
|
+
parser = subparsers.add_parser("search", help=t("help_search"))
|
28
|
+
|
29
|
+
parser.add_argument(
|
30
|
+
"--site", "-s", action="append", metavar="SITE", help=t("search_sites_help")
|
31
|
+
)
|
32
|
+
parser.add_argument("keyword", help=t("search_keyword_help"))
|
33
|
+
parser.add_argument("--config", type=str, help=t("help_config"))
|
34
|
+
parser.add_argument(
|
35
|
+
"--limit", "-l", type=int, default=20, metavar="N", help=t("search_limit_help")
|
36
|
+
)
|
37
|
+
parser.add_argument(
|
38
|
+
"--site-limit",
|
39
|
+
type=int,
|
40
|
+
default=5,
|
41
|
+
metavar="M",
|
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"),
|
50
|
+
)
|
51
|
+
|
52
|
+
parser.set_defaults(func=handle_search)
|
53
|
+
|
54
|
+
|
55
|
+
def handle_search(args: Namespace) -> None:
|
56
|
+
"""Handle the `search` subcommand."""
|
57
|
+
sites: Sequence[str] | None = args.site or None
|
58
|
+
keyword: str = args.keyword
|
59
|
+
overall_limit = max(1, args.limit)
|
60
|
+
per_site_limit = max(1, args.site_limit)
|
61
|
+
timeout = max(0.1, float(args.timeout))
|
62
|
+
config_path: Path | None = Path(args.config) if args.config else None
|
63
|
+
|
64
|
+
try:
|
65
|
+
config_data = load_config(config_path)
|
66
|
+
except Exception as e:
|
67
|
+
ui.error(t("download_config_load_fail", err=str(e)))
|
68
|
+
return
|
69
|
+
|
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
|
+
)
|
78
|
+
|
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
|
+
|
87
|
+
asyncio.run(_run())
|
88
|
+
|
89
|
+
|
90
|
+
def _prompt_user_select(results: Sequence[SearchResult]) -> SearchResult | None:
|
91
|
+
"""
|
92
|
+
Show a Rich table of results and ask the user to pick one by index.
|
93
|
+
|
94
|
+
:param results: A sequence of SearchResult dicts.
|
95
|
+
:return: The chosen SearchResult, or None if cancelled/no results.
|
96
|
+
"""
|
97
|
+
if not results:
|
98
|
+
ui.warn(t("no_results"))
|
99
|
+
return None
|
100
|
+
|
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 "")
|
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,24 +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
|
-
- Configuration dataclasses: RequesterConfig, DownloaderConfig, etc.
|
12
7
|
"""
|
13
8
|
|
14
|
-
from .adapter import ConfigAdapter
|
15
|
-
from .loader import load_config, save_config_file
|
16
|
-
from .site_rules import (
|
17
|
-
load_site_rules,
|
18
|
-
save_rules_as_json,
|
19
|
-
)
|
20
|
-
|
21
9
|
__all__ = [
|
10
|
+
"get_config_value",
|
22
11
|
"load_config",
|
12
|
+
"save_config",
|
23
13
|
"save_config_file",
|
24
14
|
"ConfigAdapter",
|
25
|
-
"load_site_rules",
|
26
|
-
"save_rules_as_json",
|
27
15
|
]
|
16
|
+
|
17
|
+
from .adapter import ConfigAdapter
|
18
|
+
from .file_io import (
|
19
|
+
get_config_value,
|
20
|
+
load_config,
|
21
|
+
save_config,
|
22
|
+
save_config_file,
|
23
|
+
)
|