novel-downloader 1.3.2__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/clean.py +97 -78
- novel_downloader/cli/config.py +177 -0
- novel_downloader/cli/download.py +132 -87
- novel_downloader/cli/export.py +77 -0
- novel_downloader/cli/main.py +21 -28
- novel_downloader/config/__init__.py +1 -25
- novel_downloader/config/adapter.py +32 -31
- novel_downloader/config/loader.py +3 -3
- novel_downloader/config/site_rules.py +1 -2
- novel_downloader/core/__init__.py +3 -6
- novel_downloader/core/downloaders/__init__.py +10 -13
- novel_downloader/core/downloaders/base.py +233 -0
- novel_downloader/core/downloaders/biquge.py +27 -0
- novel_downloader/core/downloaders/common.py +414 -0
- novel_downloader/core/downloaders/esjzone.py +27 -0
- novel_downloader/core/downloaders/linovelib.py +27 -0
- novel_downloader/core/downloaders/qianbi.py +27 -0
- novel_downloader/core/downloaders/qidian.py +352 -0
- novel_downloader/core/downloaders/sfacg.py +27 -0
- novel_downloader/core/downloaders/yamibo.py +27 -0
- novel_downloader/core/exporters/__init__.py +37 -0
- novel_downloader/core/{savers → exporters}/base.py +73 -44
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +40 -52
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
- novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
- novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +178 -0
- novel_downloader/core/fetchers/linovelib/session.py +178 -0
- novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
- novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
- novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
- novel_downloader/core/fetchers/qidian/__init__.py +14 -0
- novel_downloader/core/fetchers/qidian/browser.py +266 -0
- novel_downloader/core/fetchers/qidian/session.py +326 -0
- novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
- novel_downloader/core/fetchers/sfacg/browser.py +189 -0
- novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
- novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
- novel_downloader/core/fetchers/yamibo/browser.py +229 -0
- novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
- novel_downloader/core/interfaces/__init__.py +8 -12
- novel_downloader/core/interfaces/downloader.py +54 -0
- novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
- novel_downloader/core/interfaces/fetcher.py +162 -0
- novel_downloader/core/interfaces/parser.py +6 -7
- novel_downloader/core/parsers/__init__.py +5 -6
- novel_downloader/core/parsers/base.py +9 -13
- novel_downloader/core/parsers/biquge/main_parser.py +12 -13
- novel_downloader/core/parsers/common/helper.py +3 -3
- novel_downloader/core/parsers/common/main_parser.py +39 -34
- novel_downloader/core/parsers/esjzone/main_parser.py +24 -17
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +15 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +53 -39
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +3 -3
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
- novel_downloader/cli/interactive.py +0 -66
- novel_downloader/cli/settings.py +0 -177
- novel_downloader/config/models.py +0 -187
- novel_downloader/core/downloaders/base/__init__.py +0 -14
- novel_downloader/core/downloaders/base/base_async.py +0 -153
- novel_downloader/core/downloaders/base/base_sync.py +0 -208
- novel_downloader/core/downloaders/biquge/__init__.py +0 -14
- novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
- novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
- novel_downloader/core/downloaders/common/__init__.py +0 -14
- novel_downloader/core/downloaders/common/common_async.py +0 -218
- novel_downloader/core/downloaders/common/common_sync.py +0 -210
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -227
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/epub_utils/__init__.py +0 -26
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.2.dist-info/RECORD +0 -165
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.cli.export
|
4
|
+
---------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from argparse import Namespace, _SubParsersAction
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
from novel_downloader.config import ConfigAdapter, load_config
|
12
|
+
from novel_downloader.core.factory import get_exporter
|
13
|
+
from novel_downloader.utils.i18n import t
|
14
|
+
|
15
|
+
|
16
|
+
def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
|
17
|
+
parser = subparsers.add_parser("export", help=t("help_export"))
|
18
|
+
|
19
|
+
parser.add_argument(
|
20
|
+
"book_ids",
|
21
|
+
nargs="+",
|
22
|
+
help=t("download_book_ids"),
|
23
|
+
)
|
24
|
+
parser.add_argument(
|
25
|
+
"--format",
|
26
|
+
choices=["txt", "epub", "all"],
|
27
|
+
default="all",
|
28
|
+
help=t("export_format_help"),
|
29
|
+
)
|
30
|
+
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"),
|
39
|
+
)
|
40
|
+
|
41
|
+
parser.set_defaults(func=handle_export)
|
42
|
+
|
43
|
+
|
44
|
+
def handle_export(args: Namespace) -> None:
|
45
|
+
site: str = args.site
|
46
|
+
config_path: Path | None = Path(args.config) if args.config else None
|
47
|
+
book_ids: list[str] = args.book_ids
|
48
|
+
export_format: str = args.format
|
49
|
+
|
50
|
+
print(t("download_site_info", site=site))
|
51
|
+
|
52
|
+
try:
|
53
|
+
config_data = load_config(config_path)
|
54
|
+
except Exception as e:
|
55
|
+
print(t("download_config_load_fail", err=str(e)))
|
56
|
+
return
|
57
|
+
|
58
|
+
adapter = ConfigAdapter(config=config_data, site=site)
|
59
|
+
exporter_cfg = adapter.get_exporter_config()
|
60
|
+
exporter = get_exporter(site, exporter_cfg)
|
61
|
+
|
62
|
+
for book_id in book_ids:
|
63
|
+
print(t("export_processing", book_id=book_id, format=export_format))
|
64
|
+
|
65
|
+
if export_format in {"txt", "all"}:
|
66
|
+
try:
|
67
|
+
exporter.export_as_txt(book_id)
|
68
|
+
print(t("export_success_txt", book_id=book_id))
|
69
|
+
except Exception as e:
|
70
|
+
print(t("export_failed_txt", book_id=book_id, err=str(e)))
|
71
|
+
|
72
|
+
if export_format in {"epub", "all"}:
|
73
|
+
try:
|
74
|
+
exporter.export_as_epub(book_id)
|
75
|
+
print(t("export_success_epub", book_id=book_id))
|
76
|
+
except Exception as e:
|
77
|
+
print(t("export_failed_epub", book_id=book_id, err=str(e)))
|
novel_downloader/cli/main.py
CHANGED
@@ -1,42 +1,35 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
novel_downloader.cli.main
|
4
|
-
|
4
|
+
-------------------------
|
5
5
|
|
6
6
|
Unified CLI entry point. Parses arguments and delegates to parser or interactive.
|
7
7
|
"""
|
8
8
|
|
9
|
+
import argparse
|
9
10
|
|
10
|
-
import click
|
11
|
-
from click import Context
|
12
|
-
|
13
|
-
from novel_downloader.cli import clean, download, interactive, settings
|
14
11
|
from novel_downloader.utils.i18n import t
|
15
12
|
|
13
|
+
from .clean import register_clean_subcommand
|
14
|
+
from .config import register_config_subcommand
|
15
|
+
from .download import register_download_subcommand
|
16
|
+
from .export import register_export_subcommand
|
17
|
+
|
18
|
+
|
19
|
+
def cli_main() -> None:
|
20
|
+
parser = argparse.ArgumentParser(description=t("cli_help"))
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
22
|
+
|
23
|
+
register_clean_subcommand(subparsers)
|
24
|
+
register_config_subcommand(subparsers)
|
25
|
+
register_download_subcommand(subparsers)
|
26
|
+
register_export_subcommand(subparsers)
|
16
27
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
help=t("help_config"),
|
23
|
-
) # type: ignore
|
24
|
-
@click.pass_context # type: ignore
|
25
|
-
def cli_main(ctx: Context, config: str | None) -> None:
|
26
|
-
"""Novel Downloader CLI."""
|
27
|
-
ctx.ensure_object(dict)
|
28
|
-
ctx.obj["config_path"] = config
|
29
|
-
|
30
|
-
if ctx.invoked_subcommand is None:
|
31
|
-
click.echo(t("main_no_command"))
|
32
|
-
ctx.invoke(interactive.interactive_cli)
|
33
|
-
|
34
|
-
|
35
|
-
# Register subcommands
|
36
|
-
cli_main.add_command(clean.clean_cli)
|
37
|
-
cli_main.add_command(download.download_cli)
|
38
|
-
cli_main.add_command(interactive.interactive_cli)
|
39
|
-
cli_main.add_command(settings.settings_cli)
|
28
|
+
args = parser.parse_args()
|
29
|
+
if hasattr(args, "func"):
|
30
|
+
args.func(args)
|
31
|
+
else:
|
32
|
+
parser.print_help()
|
40
33
|
|
41
34
|
|
42
35
|
if __name__ == "__main__":
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
novel_downloader.config
|
4
|
-
|
4
|
+
-----------------------
|
5
5
|
|
6
6
|
Unified interface for loading and adapting configuration files.
|
7
7
|
|
@@ -13,19 +13,6 @@ This module provides:
|
|
13
13
|
|
14
14
|
from .adapter import ConfigAdapter
|
15
15
|
from .loader import load_config, save_config_file
|
16
|
-
from .models import (
|
17
|
-
BookInfoRules,
|
18
|
-
DownloaderConfig,
|
19
|
-
FieldRules,
|
20
|
-
ParserConfig,
|
21
|
-
RequesterConfig,
|
22
|
-
RuleStep,
|
23
|
-
SaverConfig,
|
24
|
-
SiteProfile,
|
25
|
-
SiteRules,
|
26
|
-
SiteRulesDict,
|
27
|
-
VolumesRules,
|
28
|
-
)
|
29
16
|
from .site_rules import (
|
30
17
|
load_site_rules,
|
31
18
|
save_rules_as_json,
|
@@ -35,17 +22,6 @@ __all__ = [
|
|
35
22
|
"load_config",
|
36
23
|
"save_config_file",
|
37
24
|
"ConfigAdapter",
|
38
|
-
"RequesterConfig",
|
39
|
-
"DownloaderConfig",
|
40
|
-
"ParserConfig",
|
41
|
-
"SaverConfig",
|
42
|
-
"FieldRules",
|
43
|
-
"RuleStep",
|
44
|
-
"SiteProfile",
|
45
|
-
"SiteRules",
|
46
|
-
"SiteRulesDict",
|
47
|
-
"VolumesRules",
|
48
|
-
"BookInfoRules",
|
49
25
|
"load_site_rules",
|
50
26
|
"save_rules_as_json",
|
51
27
|
]
|
@@ -5,25 +5,18 @@ novel_downloader.config.adapter
|
|
5
5
|
|
6
6
|
Defines ConfigAdapter, which maps a raw configuration dictionary and
|
7
7
|
site name into structured dataclass-based config models.
|
8
|
-
|
9
|
-
Supported mappings:
|
10
|
-
- requests -> RequesterConfig
|
11
|
-
- general+site -> DownloaderConfig
|
12
|
-
- general+site -> ParserConfig
|
13
|
-
- general+output -> SaverConfig
|
14
|
-
- sites[site] -> book_ids list
|
15
8
|
"""
|
16
9
|
|
17
10
|
from typing import Any
|
18
11
|
|
19
|
-
from novel_downloader.
|
20
|
-
|
21
|
-
from .models import (
|
12
|
+
from novel_downloader.models import (
|
22
13
|
DownloaderConfig,
|
14
|
+
ExporterConfig,
|
15
|
+
FetcherConfig,
|
23
16
|
ParserConfig,
|
24
|
-
RequesterConfig,
|
25
|
-
SaverConfig,
|
26
17
|
)
|
18
|
+
from novel_downloader.utils.constants import SUPPORTED_SITES
|
19
|
+
|
27
20
|
from .site_rules import load_site_rules
|
28
21
|
|
29
22
|
|
@@ -70,28 +63,29 @@ class ConfigAdapter:
|
|
70
63
|
|
71
64
|
return {}
|
72
65
|
|
73
|
-
def
|
66
|
+
def get_fetcher_config(self) -> FetcherConfig:
|
74
67
|
"""
|
75
|
-
从 config["requests"] 中读取通用请求配置
|
76
|
-
返回
|
68
|
+
从 config["requests"] 中读取通用请求配置
|
69
|
+
返回 FetcherConfig 实例
|
77
70
|
"""
|
71
|
+
gen = self._config.get("general", {})
|
78
72
|
req = self._config.get("requests", {})
|
79
73
|
site_cfg = self._get_site_cfg()
|
80
|
-
return
|
74
|
+
return FetcherConfig(
|
75
|
+
request_interval=gen.get("request_interval", 2.0),
|
81
76
|
retry_times=req.get("retry_times", 3),
|
82
77
|
backoff_factor=req.get("backoff_factor", 2.0),
|
83
78
|
timeout=req.get("timeout", 30.0),
|
84
79
|
max_connections=req.get("max_connections", 10),
|
85
80
|
max_rps=req.get("max_rps", None),
|
86
|
-
headless=req.get("headless",
|
87
|
-
|
88
|
-
profile_name=req.get("profile_name", "Profile_1"),
|
89
|
-
auto_close=req.get("auto_close", True),
|
90
|
-
disable_images=req.get("disable_images", True),
|
91
|
-
mute_audio=req.get("mute_audio", True),
|
81
|
+
headless=req.get("headless", False),
|
82
|
+
disable_images=req.get("disable_images", False),
|
92
83
|
mode=site_cfg.get("mode", "session"),
|
93
|
-
|
94
|
-
|
84
|
+
proxy=req.get("proxy", None),
|
85
|
+
user_agent=req.get("user_agent", None),
|
86
|
+
headers=req.get("headers", None),
|
87
|
+
browser_type=req.get("browser_type", "chromium"),
|
88
|
+
verify_ssl=req.get("verify_ssl", True),
|
95
89
|
)
|
96
90
|
|
97
91
|
def get_downloader_config(self) -> DownloaderConfig:
|
@@ -100,21 +94,26 @@ class ConfigAdapter:
|
|
100
94
|
返回 DownloaderConfig 实例
|
101
95
|
"""
|
102
96
|
gen = self._config.get("general", {})
|
97
|
+
req = self._config.get("requests", {})
|
103
98
|
debug = gen.get("debug", {})
|
104
99
|
site_cfg = self._get_site_cfg()
|
105
100
|
return DownloaderConfig(
|
106
|
-
request_interval=gen.get("request_interval",
|
101
|
+
request_interval=gen.get("request_interval", 2.0),
|
102
|
+
retry_times=req.get("retry_times", 3),
|
103
|
+
backoff_factor=req.get("backoff_factor", 2.0),
|
107
104
|
raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
|
108
105
|
cache_dir=gen.get("cache_dir", "./novel_cache"),
|
109
|
-
download_workers=gen.get("download_workers",
|
110
|
-
parser_workers=gen.get("parser_workers",
|
111
|
-
use_process_pool=gen.get("use_process_pool", True),
|
106
|
+
download_workers=gen.get("download_workers", 2),
|
107
|
+
parser_workers=gen.get("parser_workers", 2),
|
112
108
|
skip_existing=gen.get("skip_existing", True),
|
113
109
|
login_required=site_cfg.get("login_required", False),
|
114
110
|
save_html=debug.get("save_html", False),
|
115
111
|
mode=site_cfg.get("mode", "session"),
|
116
112
|
storage_backend=gen.get("storage_backend", "json"),
|
117
113
|
storage_batch_size=gen.get("storage_batch_size", 1),
|
114
|
+
username=site_cfg.get("username", ""),
|
115
|
+
password=site_cfg.get("password", ""),
|
116
|
+
cookies=site_cfg.get("cookies", ""),
|
118
117
|
)
|
119
118
|
|
120
119
|
def get_parser_config(self) -> ParserConfig:
|
@@ -141,17 +140,18 @@ class ConfigAdapter:
|
|
141
140
|
mode=site_cfg.get("mode", "session"),
|
142
141
|
)
|
143
142
|
|
144
|
-
def
|
143
|
+
def get_exporter_config(self) -> ExporterConfig:
|
145
144
|
"""
|
146
145
|
从 config["general"] 与 config["output"] 中读取存储器相关配置,
|
147
|
-
返回
|
146
|
+
返回 ExporterConfig 实例
|
148
147
|
"""
|
149
148
|
gen = self._config.get("general", {})
|
150
149
|
out = self._config.get("output", {})
|
151
150
|
fmt = out.get("formats", {})
|
152
151
|
naming = out.get("naming", {})
|
153
152
|
epub_opts = out.get("epub", {})
|
154
|
-
|
153
|
+
site_cfg = self._get_site_cfg()
|
154
|
+
return ExporterConfig(
|
155
155
|
cache_dir=gen.get("cache_dir", "./novel_cache"),
|
156
156
|
raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
|
157
157
|
output_dir=gen.get("output_dir", "./downloads"),
|
@@ -166,6 +166,7 @@ class ConfigAdapter:
|
|
166
166
|
include_cover=epub_opts.get("include_cover", True),
|
167
167
|
include_toc=epub_opts.get("include_toc", False),
|
168
168
|
include_picture=epub_opts.get("include_picture", False),
|
169
|
+
split_mode=site_cfg.get("split_mode", "book"),
|
169
170
|
)
|
170
171
|
|
171
172
|
def get_book_ids(self) -> list[str]:
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
3
|
novel_downloader.config.loader
|
4
|
-
|
4
|
+
------------------------------
|
5
5
|
|
6
6
|
Provides functionality to load Toml configuration files into Python
|
7
7
|
dictionaries, with robust error handling and fallback support.
|
@@ -120,9 +120,9 @@ def load_config(
|
|
120
120
|
config_path: str | Path | None = None,
|
121
121
|
) -> dict[str, Any]:
|
122
122
|
"""
|
123
|
-
Load configuration data from a
|
123
|
+
Load configuration data from a Toml file.
|
124
124
|
|
125
|
-
:param config_path: Optional path to the
|
125
|
+
:param config_path: Optional path to the Toml configuration file.
|
126
126
|
:return: Parsed configuration as a dict.
|
127
127
|
"""
|
128
128
|
path = resolve_file_path(
|
@@ -14,12 +14,11 @@ import json
|
|
14
14
|
import logging
|
15
15
|
from pathlib import Path
|
16
16
|
|
17
|
+
from novel_downloader.models import SiteRulesDict
|
17
18
|
from novel_downloader.utils.cache import cached_load_config
|
18
19
|
from novel_downloader.utils.constants import SITE_RULES_FILE
|
19
20
|
from novel_downloader.utils.file_utils import save_as_json
|
20
21
|
|
21
|
-
from .models import SiteRulesDict
|
22
|
-
|
23
22
|
logger = logging.getLogger(__name__)
|
24
23
|
|
25
24
|
|
@@ -10,15 +10,12 @@ downloading and processing online novel content, including:
|
|
10
10
|
|
11
11
|
- Downloader: Handles the full download lifecycle of a book or a batch of books.
|
12
12
|
- Parser: Extracts structured data from HTML or SSR content.
|
13
|
-
-
|
14
|
-
-
|
13
|
+
- Fetcher: Sends HTTP requests and manages sessions, including login if required.
|
14
|
+
- Exporter: Responsible for exporting downloaded data into various output formats.
|
15
15
|
"""
|
16
16
|
|
17
|
-
from .factory import
|
17
|
+
from .factory import get_parser
|
18
18
|
|
19
19
|
__all__ = [
|
20
|
-
"get_downloader",
|
21
20
|
"get_parser",
|
22
|
-
"get_requester",
|
23
|
-
"get_saver",
|
24
21
|
]
|
@@ -12,6 +12,7 @@ of retrieving, parsing, and saving novel content for a given source.
|
|
12
12
|
Currently supported platforms:
|
13
13
|
- biquge (笔趣阁)
|
14
14
|
- esjzone (ESJ Zone)
|
15
|
+
- linovelib (哔哩轻小说)
|
15
16
|
- qianbi (铅笔小说)
|
16
17
|
- qidian (起点中文网)
|
17
18
|
- sfacg (SF轻小说)
|
@@ -19,26 +20,22 @@ Currently supported platforms:
|
|
19
20
|
- common (通用架构)
|
20
21
|
"""
|
21
22
|
|
22
|
-
from .biquge import
|
23
|
-
from .common import
|
24
|
-
from .esjzone import
|
25
|
-
from .
|
23
|
+
from .biquge import BiqugeDownloader
|
24
|
+
from .common import CommonDownloader
|
25
|
+
from .esjzone import EsjzoneDownloader
|
26
|
+
from .linovelib import LinovelibDownloader
|
27
|
+
from .qianbi import QianbiDownloader
|
26
28
|
from .qidian import QidianDownloader
|
27
|
-
from .sfacg import
|
28
|
-
from .yamibo import
|
29
|
+
from .sfacg import SfacgDownloader
|
30
|
+
from .yamibo import YamiboDownloader
|
29
31
|
|
30
32
|
__all__ = [
|
31
|
-
"BiqugeAsyncDownloader",
|
32
33
|
"BiqugeDownloader",
|
33
|
-
"CommonAsyncDownloader",
|
34
|
-
"CommonDownloader",
|
35
|
-
"EsjzoneAsyncDownloader",
|
36
34
|
"EsjzoneDownloader",
|
37
|
-
"
|
35
|
+
"LinovelibDownloader",
|
38
36
|
"QianbiDownloader",
|
39
37
|
"QidianDownloader",
|
40
|
-
"SfacgAsyncDownloader",
|
41
38
|
"SfacgDownloader",
|
42
|
-
"YamiboAsyncDownloader",
|
43
39
|
"YamiboDownloader",
|
40
|
+
"CommonDownloader",
|
44
41
|
]
|
@@ -0,0 +1,233 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.base
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
Defines the abstract base class `BaseDownloader`, which provides a
|
7
|
+
common interface and reusable logic for all downloader implementations.
|
8
|
+
"""
|
9
|
+
|
10
|
+
import abc
|
11
|
+
import logging
|
12
|
+
from collections.abc import Awaitable, Callable
|
13
|
+
from pathlib import Path
|
14
|
+
from typing import Any
|
15
|
+
|
16
|
+
from novel_downloader.core.interfaces import (
|
17
|
+
DownloaderProtocol,
|
18
|
+
ExporterProtocol,
|
19
|
+
FetcherProtocol,
|
20
|
+
ParserProtocol,
|
21
|
+
)
|
22
|
+
from novel_downloader.models import DownloaderConfig
|
23
|
+
|
24
|
+
|
25
|
+
class BaseDownloader(DownloaderProtocol, abc.ABC):
|
26
|
+
"""
|
27
|
+
Abstract downloader that defines the initialization interface
|
28
|
+
and the general batch download flow.
|
29
|
+
|
30
|
+
Subclasses must implement the logic for downloading a single book.
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(
|
34
|
+
self,
|
35
|
+
fetcher: FetcherProtocol,
|
36
|
+
parser: ParserProtocol,
|
37
|
+
exporter: ExporterProtocol,
|
38
|
+
config: DownloaderConfig,
|
39
|
+
site: str,
|
40
|
+
):
|
41
|
+
self._fetcher = fetcher
|
42
|
+
self._parser = parser
|
43
|
+
self._exporter = exporter
|
44
|
+
self._config = config
|
45
|
+
self._site = site
|
46
|
+
|
47
|
+
self._raw_data_dir = Path(config.raw_data_dir) / site
|
48
|
+
self._cache_dir = Path(config.cache_dir) / site
|
49
|
+
self._raw_data_dir.mkdir(parents=True, exist_ok=True)
|
50
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
51
|
+
|
52
|
+
self.logger = logging.getLogger(f"{self.__class__.__name__}")
|
53
|
+
|
54
|
+
async def download_many(
|
55
|
+
self,
|
56
|
+
book_ids: list[str],
|
57
|
+
*,
|
58
|
+
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
59
|
+
**kwargs: Any,
|
60
|
+
) -> None:
|
61
|
+
"""
|
62
|
+
Download multiple books with pre-download hook and error handling.
|
63
|
+
|
64
|
+
:param book_ids: A list of book identifiers to download.
|
65
|
+
:param progress_hook: (optional) Called after each chapter;
|
66
|
+
args: completed_count, total_count.
|
67
|
+
"""
|
68
|
+
if not await self._ensure_ready():
|
69
|
+
self.logger.warning(
|
70
|
+
"[%s] login failed, skipping download of %s",
|
71
|
+
self._site,
|
72
|
+
book_ids,
|
73
|
+
)
|
74
|
+
return
|
75
|
+
|
76
|
+
for book_id in book_ids:
|
77
|
+
try:
|
78
|
+
await self._download_one(
|
79
|
+
book_id,
|
80
|
+
progress_hook=progress_hook,
|
81
|
+
**kwargs,
|
82
|
+
)
|
83
|
+
except Exception as e:
|
84
|
+
self._handle_download_exception(book_id, e)
|
85
|
+
|
86
|
+
await self._finalize()
|
87
|
+
|
88
|
+
async def download(
|
89
|
+
self,
|
90
|
+
book_id: str,
|
91
|
+
*,
|
92
|
+
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
93
|
+
**kwargs: Any,
|
94
|
+
) -> None:
|
95
|
+
"""
|
96
|
+
Download a single book with pre-download hook and error handling.
|
97
|
+
|
98
|
+
:param book_id: The identifier of the book to download.
|
99
|
+
:param progress_hook: (optional) Called after each chapter;
|
100
|
+
args: completed_count, total_count.
|
101
|
+
"""
|
102
|
+
if not await self._ensure_ready():
|
103
|
+
self.logger.warning(
|
104
|
+
"[%s] login failed, skipping download of %s",
|
105
|
+
self._site,
|
106
|
+
book_id,
|
107
|
+
)
|
108
|
+
return
|
109
|
+
|
110
|
+
try:
|
111
|
+
await self._download_one(
|
112
|
+
book_id,
|
113
|
+
progress_hook=progress_hook,
|
114
|
+
**kwargs,
|
115
|
+
)
|
116
|
+
except Exception as e:
|
117
|
+
self._handle_download_exception(book_id, e)
|
118
|
+
|
119
|
+
await self._finalize()
|
120
|
+
|
121
|
+
@abc.abstractmethod
|
122
|
+
async def _download_one(
|
123
|
+
self,
|
124
|
+
book_id: str,
|
125
|
+
*,
|
126
|
+
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
127
|
+
**kwargs: Any,
|
128
|
+
) -> None:
|
129
|
+
"""
|
130
|
+
Subclasses must implement this to define how to download a single book.
|
131
|
+
"""
|
132
|
+
...
|
133
|
+
|
134
|
+
async def _prepare(self) -> None:
|
135
|
+
"""
|
136
|
+
Optional hook called before downloading.
|
137
|
+
|
138
|
+
Subclasses can override this method to perform pre-download setup.
|
139
|
+
"""
|
140
|
+
return
|
141
|
+
|
142
|
+
async def _finalize(self) -> None:
|
143
|
+
"""
|
144
|
+
Optional hook called after downloading is complete.
|
145
|
+
|
146
|
+
Subclasses can override this method to perform post-download tasks,
|
147
|
+
such as saving state or releasing resources.
|
148
|
+
"""
|
149
|
+
return
|
150
|
+
|
151
|
+
@property
|
152
|
+
def fetcher(self) -> FetcherProtocol:
|
153
|
+
return self._fetcher
|
154
|
+
|
155
|
+
@property
|
156
|
+
def parser(self) -> ParserProtocol:
|
157
|
+
return self._parser
|
158
|
+
|
159
|
+
@property
|
160
|
+
def exporter(self) -> ExporterProtocol:
|
161
|
+
return self._exporter
|
162
|
+
|
163
|
+
@property
|
164
|
+
def config(self) -> DownloaderConfig:
|
165
|
+
return self._config
|
166
|
+
|
167
|
+
@property
|
168
|
+
def raw_data_dir(self) -> Path:
|
169
|
+
return self._raw_data_dir
|
170
|
+
|
171
|
+
@property
|
172
|
+
def cache_dir(self) -> Path:
|
173
|
+
return self._cache_dir
|
174
|
+
|
175
|
+
@property
|
176
|
+
def site(self) -> str:
|
177
|
+
return self._site
|
178
|
+
|
179
|
+
@property
|
180
|
+
def save_html(self) -> bool:
|
181
|
+
return self._config.save_html
|
182
|
+
|
183
|
+
@property
|
184
|
+
def skip_existing(self) -> bool:
|
185
|
+
return self._config.skip_existing
|
186
|
+
|
187
|
+
@property
|
188
|
+
def login_required(self) -> bool:
|
189
|
+
return self._config.login_required
|
190
|
+
|
191
|
+
@property
|
192
|
+
def request_interval(self) -> float:
|
193
|
+
return self._config.request_interval
|
194
|
+
|
195
|
+
@property
|
196
|
+
def retry_times(self) -> int:
|
197
|
+
return self._config.retry_times
|
198
|
+
|
199
|
+
@property
|
200
|
+
def backoff_factor(self) -> float:
|
201
|
+
return self._config.backoff_factor
|
202
|
+
|
203
|
+
@property
|
204
|
+
def parser_workers(self) -> int:
|
205
|
+
return self._config.parser_workers
|
206
|
+
|
207
|
+
@property
|
208
|
+
def download_workers(self) -> int:
|
209
|
+
return self._config.download_workers
|
210
|
+
|
211
|
+
def _handle_download_exception(self, book_id: str, error: Exception) -> None:
|
212
|
+
"""
|
213
|
+
Handle download errors in a consistent way.
|
214
|
+
|
215
|
+
This method can be overridden or extended to implement retry logic, etc.
|
216
|
+
|
217
|
+
:param book_id: The ID of the book that failed.
|
218
|
+
:param error: The exception raised during download.
|
219
|
+
"""
|
220
|
+
self.logger.warning(
|
221
|
+
"[%s] Failed to download %r: %s",
|
222
|
+
self.__class__.__name__,
|
223
|
+
book_id,
|
224
|
+
error,
|
225
|
+
)
|
226
|
+
|
227
|
+
async def _ensure_ready(self) -> bool:
|
228
|
+
"""
|
229
|
+
Run pre-download preparation and check login if needed.
|
230
|
+
"""
|
231
|
+
await self._prepare()
|
232
|
+
|
233
|
+
return self.fetcher.is_logged_in if self.login_required else True
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.biquge
|
4
|
+
----------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.core.downloaders.common import CommonDownloader
|
9
|
+
from novel_downloader.core.interfaces import (
|
10
|
+
ExporterProtocol,
|
11
|
+
FetcherProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
)
|
14
|
+
from novel_downloader.models import DownloaderConfig
|
15
|
+
|
16
|
+
|
17
|
+
class BiqugeDownloader(CommonDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
fetcher: FetcherProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
exporter: ExporterProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(fetcher, parser, exporter, config, "biquge")
|