novel-downloader 1.2.1__py3-none-any.whl → 1.3.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 -2
- novel_downloader/cli/__init__.py +0 -1
- novel_downloader/cli/clean.py +2 -10
- novel_downloader/cli/download.py +18 -22
- novel_downloader/cli/interactive.py +0 -1
- novel_downloader/cli/main.py +1 -3
- novel_downloader/cli/settings.py +8 -8
- novel_downloader/config/__init__.py +0 -1
- novel_downloader/config/adapter.py +48 -18
- novel_downloader/config/loader.py +116 -108
- novel_downloader/config/models.py +41 -32
- novel_downloader/config/site_rules.py +2 -4
- novel_downloader/core/__init__.py +0 -1
- novel_downloader/core/downloaders/__init__.py +4 -4
- novel_downloader/core/downloaders/base/__init__.py +14 -0
- novel_downloader/core/downloaders/{base_async_downloader.py → base/base_async.py} +49 -53
- novel_downloader/core/downloaders/{base_downloader.py → base/base_sync.py} +64 -43
- novel_downloader/core/downloaders/biquge/__init__.py +12 -0
- novel_downloader/core/downloaders/biquge/biquge_sync.py +25 -0
- novel_downloader/core/downloaders/common/__init__.py +14 -0
- novel_downloader/core/downloaders/{common_asynb_downloader.py → common/common_async.py} +42 -33
- novel_downloader/core/downloaders/{common_downloader.py → common/common_sync.py} +34 -23
- novel_downloader/core/downloaders/qidian/__init__.py +10 -0
- novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +80 -64
- novel_downloader/core/factory/__init__.py +4 -5
- novel_downloader/core/factory/{downloader_factory.py → downloader.py} +36 -35
- novel_downloader/core/factory/{parser_factory.py → parser.py} +12 -14
- novel_downloader/core/factory/{requester_factory.py → requester.py} +29 -16
- novel_downloader/core/factory/{saver_factory.py → saver.py} +4 -9
- novel_downloader/core/interfaces/__init__.py +8 -9
- novel_downloader/core/interfaces/{async_downloader_protocol.py → async_downloader.py} +4 -5
- novel_downloader/core/interfaces/{async_requester_protocol.py → async_requester.py} +26 -12
- novel_downloader/core/interfaces/{parser_protocol.py → parser.py} +11 -6
- novel_downloader/core/interfaces/{saver_protocol.py → saver.py} +2 -3
- novel_downloader/core/interfaces/{downloader_protocol.py → sync_downloader.py} +6 -7
- novel_downloader/core/interfaces/{requester_protocol.py → sync_requester.py} +34 -17
- novel_downloader/core/parsers/__init__.py +5 -4
- novel_downloader/core/parsers/{base_parser.py → base.py} +20 -11
- novel_downloader/core/parsers/biquge/__init__.py +10 -0
- novel_downloader/core/parsers/biquge/main_parser.py +126 -0
- novel_downloader/core/parsers/{common_parser → common}/__init__.py +2 -3
- novel_downloader/core/parsers/{common_parser → common}/helper.py +20 -18
- novel_downloader/core/parsers/{common_parser → common}/main_parser.py +15 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_encrypted.py +41 -49
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_normal.py +17 -21
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/main_parser.py +16 -12
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +37 -45
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_normal.py +19 -23
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/main_parser.py +16 -12
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/node_decryptor.py +7 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/__init__.py +2 -3
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +150 -0
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +9 -10
- novel_downloader/core/requesters/__init__.py +9 -5
- novel_downloader/core/requesters/base/__init__.py +16 -0
- novel_downloader/core/requesters/{base_async_session.py → base/async_session.py} +180 -73
- novel_downloader/core/requesters/base/browser.py +340 -0
- novel_downloader/core/requesters/base/session.py +364 -0
- novel_downloader/core/requesters/biquge/__init__.py +12 -0
- novel_downloader/core/requesters/biquge/session.py +90 -0
- novel_downloader/core/requesters/{common_requester → common}/__init__.py +4 -5
- novel_downloader/core/requesters/common/async_session.py +96 -0
- novel_downloader/core/requesters/common/session.py +113 -0
- novel_downloader/core/requesters/qidian/__init__.py +21 -0
- novel_downloader/core/requesters/qidian/broswer.py +306 -0
- novel_downloader/core/requesters/qidian/session.py +287 -0
- novel_downloader/core/savers/__init__.py +5 -3
- novel_downloader/core/savers/{base_saver.py → base.py} +12 -13
- novel_downloader/core/savers/biquge.py +25 -0
- novel_downloader/core/savers/{common_saver → common}/__init__.py +2 -3
- novel_downloader/core/savers/{common_saver/common_epub.py → common/epub.py} +24 -52
- novel_downloader/core/savers/{common_saver → common}/main_saver.py +43 -9
- novel_downloader/core/savers/{common_saver/common_txt.py → common/txt.py} +16 -46
- novel_downloader/core/savers/epub_utils/__init__.py +0 -1
- novel_downloader/core/savers/epub_utils/css_builder.py +13 -7
- novel_downloader/core/savers/epub_utils/initializer.py +4 -5
- novel_downloader/core/savers/epub_utils/text_to_html.py +2 -3
- novel_downloader/core/savers/epub_utils/volume_intro.py +1 -3
- novel_downloader/core/savers/{qidian_saver.py → qidian.py} +12 -6
- novel_downloader/locales/en.json +12 -4
- novel_downloader/locales/zh.json +9 -1
- novel_downloader/resources/config/settings.toml +88 -0
- novel_downloader/utils/cache.py +2 -2
- novel_downloader/utils/chapter_storage.py +340 -0
- novel_downloader/utils/constants.py +8 -5
- novel_downloader/utils/crypto_utils.py +3 -3
- novel_downloader/utils/file_utils/__init__.py +0 -1
- novel_downloader/utils/file_utils/io.py +12 -17
- novel_downloader/utils/file_utils/normalize.py +1 -3
- novel_downloader/utils/file_utils/sanitize.py +2 -9
- novel_downloader/utils/fontocr/__init__.py +0 -1
- novel_downloader/utils/fontocr/ocr_v1.py +19 -22
- novel_downloader/utils/fontocr/ocr_v2.py +147 -60
- novel_downloader/utils/hash_store.py +19 -20
- novel_downloader/utils/hash_utils.py +0 -1
- novel_downloader/utils/i18n.py +3 -4
- novel_downloader/utils/logger.py +5 -6
- novel_downloader/utils/model_loader.py +5 -8
- novel_downloader/utils/network.py +9 -10
- novel_downloader/utils/state.py +6 -7
- novel_downloader/utils/text_utils/__init__.py +0 -1
- novel_downloader/utils/text_utils/chapter_formatting.py +2 -7
- novel_downloader/utils/text_utils/diff_display.py +0 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -4
- novel_downloader/utils/text_utils/text_cleaning.py +0 -1
- novel_downloader/utils/time_utils/__init__.py +0 -1
- novel_downloader/utils/time_utils/datetime_utils.py +9 -11
- novel_downloader/utils/time_utils/sleep_utils.py +27 -13
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/METADATA +14 -17
- novel_downloader-1.3.0.dist-info/RECORD +127 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/WHEEL +1 -1
- novel_downloader/core/parsers/qidian_parser/shared/book_info_parser.py +0 -95
- novel_downloader/core/requesters/base_browser.py +0 -210
- novel_downloader/core/requesters/base_session.py +0 -243
- novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
- novel_downloader/core/requesters/common_requester/common_session.py +0 -126
- novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
- novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -377
- novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
- novel_downloader/resources/config/settings.yaml +0 -76
- novel_downloader-1.2.1.dist-info/RECORD +0 -115
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader
|
5
4
|
----------------
|
@@ -7,7 +6,7 @@ novel_downloader
|
|
7
6
|
Core package for the Novel Downloader project.
|
8
7
|
"""
|
9
8
|
|
10
|
-
__version__ = "1.
|
9
|
+
__version__ = "1.3.0"
|
11
10
|
|
12
11
|
__author__ = "Saudade Z"
|
13
12
|
__email__ = "saudadez217@gmail.com"
|
novel_downloader/cli/__init__.py
CHANGED
novel_downloader/cli/clean.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.cli.clean
|
5
4
|
-----------------------------
|
@@ -8,7 +7,6 @@ novel_downloader.cli.clean
|
|
8
7
|
|
9
8
|
import shutil
|
10
9
|
from pathlib import Path
|
11
|
-
from typing import List, Optional
|
12
10
|
|
13
11
|
import click
|
14
12
|
|
@@ -19,7 +17,6 @@ from novel_downloader.utils.constants import (
|
|
19
17
|
LOGGER_DIR,
|
20
18
|
MODEL_CACHE_DIR,
|
21
19
|
REC_CHAR_MODEL_REPO,
|
22
|
-
STATE_DIR,
|
23
20
|
)
|
24
21
|
from novel_downloader.utils.i18n import t
|
25
22
|
|
@@ -35,7 +32,7 @@ def delete_path(p: Path) -> None:
|
|
35
32
|
click.echo(f"[clean] {t('clean_not_found')}: {p}")
|
36
33
|
|
37
34
|
|
38
|
-
def clean_model_repo_cache(repo_id:
|
35
|
+
def clean_model_repo_cache(repo_id: str | None = None, all: bool = False) -> bool:
|
39
36
|
"""
|
40
37
|
Delete Hugging Face cache for a specific repo.
|
41
38
|
"""
|
@@ -61,7 +58,6 @@ def clean_model_repo_cache(repo_id: Optional[str] = None, all: bool = False) ->
|
|
61
58
|
@click.command(name="clean", help=t("help_clean")) # type: ignore
|
62
59
|
@click.option("--logs", is_flag=True, help=t("clean_logs")) # type: ignore
|
63
60
|
@click.option("--cache", is_flag=True, help=t("clean_cache")) # type: ignore
|
64
|
-
@click.option("--state", is_flag=True, help=t("clean_state")) # type: ignore
|
65
61
|
@click.option("--data", is_flag=True, help=t("clean_data")) # type: ignore
|
66
62
|
@click.option("--config", is_flag=True, help=t("clean_config")) # type: ignore
|
67
63
|
@click.option("--models", is_flag=True, help=t("clean_models")) # type: ignore
|
@@ -72,7 +68,6 @@ def clean_model_repo_cache(repo_id: Optional[str] = None, all: bool = False) ->
|
|
72
68
|
def clean_cli(
|
73
69
|
logs: bool,
|
74
70
|
cache: bool,
|
75
|
-
state: bool,
|
76
71
|
data: bool,
|
77
72
|
config: bool,
|
78
73
|
models: bool,
|
@@ -81,7 +76,7 @@ def clean_cli(
|
|
81
76
|
all: bool,
|
82
77
|
yes: bool,
|
83
78
|
) -> None:
|
84
|
-
targets:
|
79
|
+
targets: list[Path] = []
|
85
80
|
|
86
81
|
if all:
|
87
82
|
if not yes:
|
@@ -92,7 +87,6 @@ def clean_cli(
|
|
92
87
|
targets = [
|
93
88
|
LOGGER_DIR,
|
94
89
|
JS_SCRIPT_DIR,
|
95
|
-
STATE_DIR,
|
96
90
|
DATA_DIR,
|
97
91
|
CONFIG_DIR,
|
98
92
|
MODEL_CACHE_DIR,
|
@@ -102,8 +96,6 @@ def clean_cli(
|
|
102
96
|
targets.append(LOGGER_DIR)
|
103
97
|
if cache:
|
104
98
|
targets.append(JS_SCRIPT_DIR)
|
105
|
-
if state:
|
106
|
-
targets.append(STATE_DIR)
|
107
99
|
if data:
|
108
100
|
targets.append(DATA_DIR)
|
109
101
|
if config:
|
novel_downloader/cli/download.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.cli.download
|
5
4
|
-----------------------------
|
@@ -8,7 +7,7 @@ Download full novels by book IDs
|
|
8
7
|
(supports config files, site switching, and localization prompts).
|
9
8
|
"""
|
10
9
|
|
11
|
-
|
10
|
+
import asyncio
|
12
11
|
|
13
12
|
import click
|
14
13
|
from click import Context
|
@@ -41,7 +40,7 @@ from novel_downloader.utils.logger import setup_logging
|
|
41
40
|
help=t("download_option_site", default="qidian"),
|
42
41
|
) # type: ignore
|
43
42
|
@click.pass_context # type: ignore
|
44
|
-
def download_cli(ctx: Context, book_ids:
|
43
|
+
def download_cli(ctx: Context, book_ids: list[str], site: str) -> None:
|
45
44
|
"""Download full novels by book IDs."""
|
46
45
|
config_path = ctx.obj.get("config_path")
|
47
46
|
|
@@ -57,6 +56,8 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
|
|
57
56
|
parser_cfg = adapter.get_parser_config()
|
58
57
|
saver_cfg = adapter.get_saver_config()
|
59
58
|
|
59
|
+
click.echo(t("download_site_mode", mode=downloader_cfg.mode))
|
60
|
+
|
60
61
|
# If no book_ids provided on the command line, try to load them from config
|
61
62
|
if not book_ids:
|
62
63
|
try:
|
@@ -67,7 +68,7 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
|
|
67
68
|
|
68
69
|
# Filter out placeholder/example IDs
|
69
70
|
invalid_ids = {"0000000000"}
|
70
|
-
valid_book_ids =
|
71
|
+
valid_book_ids = set(book_ids) - invalid_ids
|
71
72
|
|
72
73
|
if not book_ids:
|
73
74
|
click.echo(t("download_no_ids"))
|
@@ -80,21 +81,20 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
|
|
80
81
|
|
81
82
|
# Initialize the requester, parser, saver, and downloader components
|
82
83
|
if downloader_cfg.mode == "async":
|
83
|
-
import asyncio
|
84
|
-
|
85
|
-
async_requester = get_async_requester(site, requester_cfg)
|
86
|
-
async_parser = get_parser(site, parser_cfg)
|
87
|
-
async_saver = get_saver(site, saver_cfg)
|
88
84
|
setup_logging()
|
89
|
-
async_downloader = get_async_downloader(
|
90
|
-
requester=async_requester,
|
91
|
-
parser=async_parser,
|
92
|
-
saver=async_saver,
|
93
|
-
site=site,
|
94
|
-
config=downloader_cfg,
|
95
|
-
)
|
96
85
|
|
97
86
|
async def async_download_all() -> None:
|
87
|
+
async_requester = get_async_requester(site, requester_cfg)
|
88
|
+
async_parser = get_parser(site, parser_cfg)
|
89
|
+
async_saver = get_saver(site, saver_cfg)
|
90
|
+
async_downloader = get_async_downloader(
|
91
|
+
requester=async_requester,
|
92
|
+
parser=async_parser,
|
93
|
+
saver=async_saver,
|
94
|
+
site=site,
|
95
|
+
config=downloader_cfg,
|
96
|
+
)
|
97
|
+
|
98
98
|
prepare = getattr(async_downloader, "prepare", None)
|
99
99
|
if prepare and asyncio.iscoroutinefunction(prepare):
|
100
100
|
await prepare()
|
@@ -103,9 +103,7 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
|
|
103
103
|
click.echo(t("download_downloading", book_id=book_id, site=site))
|
104
104
|
await async_downloader.download_one(book_id)
|
105
105
|
|
106
|
-
|
107
|
-
input(t("download_prompt_parse"))
|
108
|
-
await async_requester.shutdown()
|
106
|
+
await async_requester.close()
|
109
107
|
|
110
108
|
asyncio.run(async_download_all())
|
111
109
|
else:
|
@@ -125,8 +123,6 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
|
|
125
123
|
click.echo(t("download_downloading", book_id=book_id, site=site))
|
126
124
|
sync_downloader.download_one(book_id)
|
127
125
|
|
128
|
-
|
129
|
-
input(t("download_prompt_parse"))
|
130
|
-
sync_requester.shutdown()
|
126
|
+
sync_requester.close()
|
131
127
|
|
132
128
|
return
|
novel_downloader/cli/main.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.cli.main
|
5
4
|
--------------------------
|
@@ -7,7 +6,6 @@ novel_downloader.cli.main
|
|
7
6
|
Unified CLI entry point. Parses arguments and delegates to parser or interactive.
|
8
7
|
"""
|
9
8
|
|
10
|
-
from typing import Optional
|
11
9
|
|
12
10
|
import click
|
13
11
|
from click import Context
|
@@ -24,7 +22,7 @@ from novel_downloader.utils.i18n import t
|
|
24
22
|
help=t("help_config"),
|
25
23
|
) # type: ignore
|
26
24
|
@click.pass_context # type: ignore
|
27
|
-
def cli_main(ctx: Context, config:
|
25
|
+
def cli_main(ctx: Context, config: str | None) -> None:
|
28
26
|
"""Novel Downloader CLI."""
|
29
27
|
ctx.ensure_object(dict)
|
30
28
|
ctx.obj["config_path"] = config
|
novel_downloader/cli/settings.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.cli.settings
|
5
4
|
-----------------------------
|
@@ -10,7 +9,6 @@ Commands to configure novel downloader settings.
|
|
10
9
|
import shutil
|
11
10
|
from importlib.resources import as_file
|
12
11
|
from pathlib import Path
|
13
|
-
from typing import Optional
|
14
12
|
|
15
13
|
import click
|
16
14
|
from click import Context
|
@@ -61,7 +59,7 @@ def init_settings(force: bool) -> None:
|
|
61
59
|
except Exception as e:
|
62
60
|
raise click.ClickException(
|
63
61
|
t("settings_init_error", filename=resource.name, err=e)
|
64
|
-
)
|
62
|
+
) from e
|
65
63
|
|
66
64
|
|
67
65
|
@settings_cli.command(name="set-lang", help=t("settings_set_lang_help")) # type: ignore
|
@@ -81,7 +79,7 @@ def set_config(path: str) -> None:
|
|
81
79
|
save_config_file(path)
|
82
80
|
click.echo(t("settings_set_config", path=path))
|
83
81
|
except Exception as e:
|
84
|
-
raise click.ClickException(t("settings_set_config_fail", err=e))
|
82
|
+
raise click.ClickException(t("settings_set_config_fail", err=e)) from e
|
85
83
|
|
86
84
|
|
87
85
|
@settings_cli.command(name="update-rules", help=t("settings_update_rules_help")) # type: ignore
|
@@ -92,7 +90,7 @@ def update_rules(path: str) -> None:
|
|
92
90
|
save_rules_as_json(path)
|
93
91
|
click.echo(t("settings_update_rules", path=path))
|
94
92
|
except Exception as e:
|
95
|
-
raise click.ClickException(t("settings_update_rules_fail", err=e))
|
93
|
+
raise click.ClickException(t("settings_update_rules_fail", err=e)) from e
|
96
94
|
|
97
95
|
|
98
96
|
@settings_cli.command(
|
@@ -120,7 +118,7 @@ def set_cookies(ctx: Context, site: str, cookies: str) -> None:
|
|
120
118
|
state_mgr.set_cookies(site, cookies)
|
121
119
|
click.echo(t("settings_set_cookies_success", site=site))
|
122
120
|
except Exception as e:
|
123
|
-
raise click.ClickException(t("settings_set_cookies_fail", err=e))
|
121
|
+
raise click.ClickException(t("settings_set_cookies_fail", err=e)) from e
|
124
122
|
|
125
123
|
|
126
124
|
@settings_cli.command(name="add-hash", help=t("settings_add_hash_help")) # type: ignore
|
@@ -129,7 +127,7 @@ def set_cookies(ctx: Context, site: str, cookies: str) -> None:
|
|
129
127
|
type=click.Path(exists=True, dir_okay=False),
|
130
128
|
help=t("settings_add_hash_path_help"),
|
131
129
|
) # type: ignore
|
132
|
-
def add_image_hashes(path:
|
130
|
+
def add_image_hashes(path: str | None) -> None:
|
133
131
|
"""
|
134
132
|
Add image hashes to internal store for matching.
|
135
133
|
Can be run in interactive mode (no --path), or with a JSON file.
|
@@ -142,7 +140,9 @@ def add_image_hashes(path: Optional[str]) -> None:
|
|
142
140
|
img_hash_store.save()
|
143
141
|
click.echo(t("settings_add_hash_loaded", path=path))
|
144
142
|
except Exception as e:
|
145
|
-
raise click.ClickException(
|
143
|
+
raise click.ClickException(
|
144
|
+
t("settings_add_hash_load_fail", err=str(e))
|
145
|
+
) from e
|
146
146
|
else:
|
147
147
|
click.echo(t("settings_add_hash_prompt_tip"))
|
148
148
|
while True:
|
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.config.adapter
|
5
4
|
-------------------------------
|
@@ -15,7 +14,9 @@ Supported mappings:
|
|
15
14
|
- sites[site] -> book_ids list
|
16
15
|
"""
|
17
16
|
|
18
|
-
from typing import Any
|
17
|
+
from typing import Any
|
18
|
+
|
19
|
+
from novel_downloader.utils.constants import SUPPORTED_SITES
|
19
20
|
|
20
21
|
from .models import (
|
21
22
|
DownloaderConfig,
|
@@ -23,6 +24,7 @@ from .models import (
|
|
23
24
|
RequesterConfig,
|
24
25
|
SaverConfig,
|
25
26
|
)
|
27
|
+
from .site_rules import load_site_rules
|
26
28
|
|
27
29
|
|
28
30
|
class ConfigAdapter:
|
@@ -30,7 +32,7 @@ class ConfigAdapter:
|
|
30
32
|
Adapter to map a raw config dict + site name into structured dataclass configs.
|
31
33
|
"""
|
32
34
|
|
33
|
-
def __init__(self, config:
|
35
|
+
def __init__(self, config: dict[str, Any], site: str):
|
34
36
|
"""
|
35
37
|
:param config: 完整加载的配置 dict
|
36
38
|
:param site: 当前站点名称 (e.g. "qidian")
|
@@ -38,11 +40,35 @@ class ConfigAdapter:
|
|
38
40
|
self._config = config
|
39
41
|
self._site = site
|
40
42
|
|
41
|
-
|
43
|
+
site_rules = load_site_rules() # -> Dict[str, SiteRules]
|
44
|
+
self._supported_sites = set(site_rules.keys()) | SUPPORTED_SITES
|
45
|
+
|
46
|
+
@property
|
47
|
+
def site(self) -> str:
|
48
|
+
return self._site
|
49
|
+
|
50
|
+
@site.setter
|
51
|
+
def site(self, value: str) -> None:
|
52
|
+
self._site = value
|
53
|
+
|
54
|
+
def _get_site_cfg(self, site: str | None = None) -> dict[str, Any]:
|
42
55
|
"""
|
43
|
-
|
56
|
+
获取指定站点的配置 (默认为当前适配站点)
|
57
|
+
|
58
|
+
1. 如果有 site-specific 配置, 优先返回它
|
59
|
+
2. 否则, 如果该站点在支持站点中, 尝试返回 'common' 配置
|
60
|
+
3. 否则返回空 dict
|
44
61
|
"""
|
45
|
-
|
62
|
+
site = site or self._site
|
63
|
+
sites_cfg = self._config.get("sites", {}) or {}
|
64
|
+
|
65
|
+
if site in sites_cfg:
|
66
|
+
return sites_cfg[site] or {}
|
67
|
+
|
68
|
+
if site in self._supported_sites:
|
69
|
+
return sites_cfg.get("common", {}) or {}
|
70
|
+
|
71
|
+
return {}
|
46
72
|
|
47
73
|
def get_requester_config(self) -> RequesterConfig:
|
48
74
|
"""
|
@@ -50,12 +76,13 @@ class ConfigAdapter:
|
|
50
76
|
返回 RequesterConfig 实例
|
51
77
|
"""
|
52
78
|
req = self._config.get("requests", {})
|
53
|
-
site_cfg = self.
|
79
|
+
site_cfg = self._get_site_cfg()
|
54
80
|
return RequesterConfig(
|
55
|
-
wait_time=req.get("wait_time", 5),
|
56
81
|
retry_times=req.get("retry_times", 3),
|
57
|
-
|
58
|
-
timeout=req.get("timeout", 30),
|
82
|
+
backoff_factor=req.get("backoff_factor", 2.0),
|
83
|
+
timeout=req.get("timeout", 30.0),
|
84
|
+
max_connections=req.get("max_connections", 10),
|
85
|
+
max_rps=req.get("max_rps", None),
|
59
86
|
headless=req.get("headless", True),
|
60
87
|
user_data_folder=req.get("user_data_folder", "./user_data"),
|
61
88
|
profile_name=req.get("profile_name", "Profile_1"),
|
@@ -63,7 +90,6 @@ class ConfigAdapter:
|
|
63
90
|
disable_images=req.get("disable_images", True),
|
64
91
|
mute_audio=req.get("mute_audio", True),
|
65
92
|
mode=site_cfg.get("mode", "session"),
|
66
|
-
max_rps=site_cfg.get("max_rps", None),
|
67
93
|
)
|
68
94
|
|
69
95
|
def get_downloader_config(self) -> DownloaderConfig:
|
@@ -73,11 +99,11 @@ class ConfigAdapter:
|
|
73
99
|
"""
|
74
100
|
gen = self._config.get("general", {})
|
75
101
|
debug = gen.get("debug", {})
|
76
|
-
site_cfg = self.
|
102
|
+
site_cfg = self._get_site_cfg()
|
77
103
|
return DownloaderConfig(
|
78
|
-
request_interval=gen.get("request_interval", 5),
|
104
|
+
request_interval=gen.get("request_interval", 5.0),
|
79
105
|
raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
|
80
|
-
cache_dir=gen.get("cache_dir", "./
|
106
|
+
cache_dir=gen.get("cache_dir", "./novel_cache"),
|
81
107
|
download_workers=gen.get("download_workers", 4),
|
82
108
|
parser_workers=gen.get("parser_workers", 4),
|
83
109
|
use_process_pool=gen.get("use_process_pool", True),
|
@@ -85,6 +111,8 @@ class ConfigAdapter:
|
|
85
111
|
login_required=site_cfg.get("login_required", False),
|
86
112
|
save_html=debug.get("save_html", False),
|
87
113
|
mode=site_cfg.get("mode", "session"),
|
114
|
+
storage_backend=gen.get("storage_backend", "json"),
|
115
|
+
storage_batch_size=gen.get("storage_batch_size", 1),
|
88
116
|
)
|
89
117
|
|
90
118
|
def get_parser_config(self) -> ParserConfig:
|
@@ -94,9 +122,9 @@ class ConfigAdapter:
|
|
94
122
|
"""
|
95
123
|
gen = self._config.get("general", {})
|
96
124
|
font_ocr = gen.get("font_ocr", {})
|
97
|
-
site_cfg = self.
|
125
|
+
site_cfg = self._get_site_cfg()
|
98
126
|
return ParserConfig(
|
99
|
-
cache_dir=gen.get("cache_dir", "./
|
127
|
+
cache_dir=gen.get("cache_dir", "./novel_cache"),
|
100
128
|
decode_font=font_ocr.get("decode_font", False),
|
101
129
|
use_freq=font_ocr.get("use_freq", False),
|
102
130
|
use_ocr=font_ocr.get("use_ocr", True),
|
@@ -124,6 +152,7 @@ class ConfigAdapter:
|
|
124
152
|
return SaverConfig(
|
125
153
|
raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
|
126
154
|
output_dir=gen.get("output_dir", "./downloads"),
|
155
|
+
storage_backend=gen.get("storage_backend", "json"),
|
127
156
|
clean_text=out.get("clean_text", True),
|
128
157
|
make_txt=fmt.get("make_txt", True),
|
129
158
|
make_epub=fmt.get("make_epub", False),
|
@@ -133,13 +162,14 @@ class ConfigAdapter:
|
|
133
162
|
filename_template=naming.get("filename_template", "{title}_{author}"),
|
134
163
|
include_cover=epub_opts.get("include_cover", True),
|
135
164
|
include_toc=epub_opts.get("include_toc", False),
|
165
|
+
include_picture=epub_opts.get("include_picture", False),
|
136
166
|
)
|
137
167
|
|
138
|
-
def get_book_ids(self) ->
|
168
|
+
def get_book_ids(self) -> list[str]:
|
139
169
|
"""
|
140
170
|
从 config["sites"][site]["book_ids"] 中提取目标书籍列表
|
141
171
|
"""
|
142
|
-
site_cfg = self.
|
172
|
+
site_cfg = self._get_site_cfg()
|
143
173
|
raw_ids = site_cfg.get("book_ids", [])
|
144
174
|
|
145
175
|
if isinstance(raw_ids, str):
|