novel-downloader 1.2.2__py3-none-any.whl → 1.3.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.
Files changed (128) hide show
  1. novel_downloader/__init__.py +1 -2
  2. novel_downloader/cli/__init__.py +0 -1
  3. novel_downloader/cli/clean.py +2 -10
  4. novel_downloader/cli/download.py +16 -22
  5. novel_downloader/cli/interactive.py +0 -1
  6. novel_downloader/cli/main.py +1 -3
  7. novel_downloader/cli/settings.py +8 -8
  8. novel_downloader/config/__init__.py +0 -1
  9. novel_downloader/config/adapter.py +32 -27
  10. novel_downloader/config/loader.py +116 -108
  11. novel_downloader/config/models.py +35 -29
  12. novel_downloader/config/site_rules.py +2 -4
  13. novel_downloader/core/__init__.py +0 -1
  14. novel_downloader/core/downloaders/__init__.py +4 -4
  15. novel_downloader/core/downloaders/base/__init__.py +14 -0
  16. novel_downloader/core/downloaders/{base_async_downloader.py → base/base_async.py} +49 -53
  17. novel_downloader/core/downloaders/{base_downloader.py → base/base_sync.py} +64 -43
  18. novel_downloader/core/downloaders/biquge/__init__.py +12 -0
  19. novel_downloader/core/downloaders/biquge/biquge_sync.py +25 -0
  20. novel_downloader/core/downloaders/common/__init__.py +14 -0
  21. novel_downloader/core/downloaders/{common_asynb_downloader.py → common/common_async.py} +42 -33
  22. novel_downloader/core/downloaders/{common_downloader.py → common/common_sync.py} +33 -21
  23. novel_downloader/core/downloaders/qidian/__init__.py +10 -0
  24. novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +79 -62
  25. novel_downloader/core/factory/__init__.py +4 -5
  26. novel_downloader/core/factory/{downloader_factory.py → downloader.py} +25 -26
  27. novel_downloader/core/factory/{parser_factory.py → parser.py} +12 -14
  28. novel_downloader/core/factory/{requester_factory.py → requester.py} +29 -16
  29. novel_downloader/core/factory/{saver_factory.py → saver.py} +4 -9
  30. novel_downloader/core/interfaces/__init__.py +8 -9
  31. novel_downloader/core/interfaces/{async_downloader_protocol.py → async_downloader.py} +4 -5
  32. novel_downloader/core/interfaces/{async_requester_protocol.py → async_requester.py} +23 -12
  33. novel_downloader/core/interfaces/{parser_protocol.py → parser.py} +11 -6
  34. novel_downloader/core/interfaces/{saver_protocol.py → saver.py} +2 -3
  35. novel_downloader/core/interfaces/{downloader_protocol.py → sync_downloader.py} +6 -7
  36. novel_downloader/core/interfaces/{requester_protocol.py → sync_requester.py} +31 -17
  37. novel_downloader/core/parsers/__init__.py +5 -4
  38. novel_downloader/core/parsers/{base_parser.py → base.py} +18 -9
  39. novel_downloader/core/parsers/biquge/__init__.py +10 -0
  40. novel_downloader/core/parsers/biquge/main_parser.py +126 -0
  41. novel_downloader/core/parsers/{common_parser → common}/__init__.py +2 -3
  42. novel_downloader/core/parsers/{common_parser → common}/helper.py +13 -13
  43. novel_downloader/core/parsers/{common_parser → common}/main_parser.py +15 -9
  44. novel_downloader/core/parsers/{qidian_parser → qidian}/__init__.py +2 -3
  45. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/__init__.py +2 -3
  46. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_encrypted.py +40 -48
  47. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_normal.py +17 -21
  48. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_router.py +10 -9
  49. novel_downloader/core/parsers/{qidian_parser → qidian}/browser/main_parser.py +14 -10
  50. novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
  51. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +36 -44
  52. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_normal.py +19 -23
  53. novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_router.py +10 -9
  54. novel_downloader/core/parsers/{qidian_parser → qidian}/session/main_parser.py +14 -10
  55. novel_downloader/core/parsers/{qidian_parser → qidian}/session/node_decryptor.py +7 -10
  56. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/__init__.py +2 -3
  57. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/book_info_parser.py +5 -6
  58. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +7 -8
  59. novel_downloader/core/requesters/__init__.py +9 -5
  60. novel_downloader/core/requesters/base/__init__.py +16 -0
  61. novel_downloader/core/requesters/{base_async_session.py → base/async_session.py} +177 -73
  62. novel_downloader/core/requesters/base/browser.py +340 -0
  63. novel_downloader/core/requesters/base/session.py +364 -0
  64. novel_downloader/core/requesters/biquge/__init__.py +12 -0
  65. novel_downloader/core/requesters/biquge/session.py +90 -0
  66. novel_downloader/core/requesters/{common_requester → common}/__init__.py +4 -5
  67. novel_downloader/core/requesters/common/async_session.py +96 -0
  68. novel_downloader/core/requesters/common/session.py +113 -0
  69. novel_downloader/core/requesters/qidian/__init__.py +21 -0
  70. novel_downloader/core/requesters/qidian/broswer.py +307 -0
  71. novel_downloader/core/requesters/qidian/session.py +287 -0
  72. novel_downloader/core/savers/__init__.py +5 -3
  73. novel_downloader/core/savers/{base_saver.py → base.py} +12 -13
  74. novel_downloader/core/savers/biquge.py +25 -0
  75. novel_downloader/core/savers/{common_saver → common}/__init__.py +2 -3
  76. novel_downloader/core/savers/{common_saver/common_epub.py → common/epub.py} +23 -51
  77. novel_downloader/core/savers/{common_saver → common}/main_saver.py +43 -9
  78. novel_downloader/core/savers/{common_saver/common_txt.py → common/txt.py} +16 -46
  79. novel_downloader/core/savers/epub_utils/__init__.py +0 -1
  80. novel_downloader/core/savers/epub_utils/css_builder.py +13 -7
  81. novel_downloader/core/savers/epub_utils/initializer.py +4 -5
  82. novel_downloader/core/savers/epub_utils/text_to_html.py +2 -3
  83. novel_downloader/core/savers/epub_utils/volume_intro.py +1 -3
  84. novel_downloader/core/savers/{qidian_saver.py → qidian.py} +12 -6
  85. novel_downloader/locales/en.json +8 -4
  86. novel_downloader/locales/zh.json +5 -1
  87. novel_downloader/resources/config/settings.toml +88 -0
  88. novel_downloader/utils/cache.py +2 -2
  89. novel_downloader/utils/chapter_storage.py +340 -0
  90. novel_downloader/utils/constants.py +6 -4
  91. novel_downloader/utils/crypto_utils.py +3 -3
  92. novel_downloader/utils/file_utils/__init__.py +0 -1
  93. novel_downloader/utils/file_utils/io.py +12 -17
  94. novel_downloader/utils/file_utils/normalize.py +1 -3
  95. novel_downloader/utils/file_utils/sanitize.py +2 -9
  96. novel_downloader/utils/fontocr/__init__.py +0 -1
  97. novel_downloader/utils/fontocr/ocr_v1.py +19 -22
  98. novel_downloader/utils/fontocr/ocr_v2.py +147 -60
  99. novel_downloader/utils/hash_store.py +19 -20
  100. novel_downloader/utils/hash_utils.py +0 -1
  101. novel_downloader/utils/i18n.py +3 -4
  102. novel_downloader/utils/logger.py +5 -6
  103. novel_downloader/utils/model_loader.py +5 -8
  104. novel_downloader/utils/network.py +9 -10
  105. novel_downloader/utils/state.py +6 -7
  106. novel_downloader/utils/text_utils/__init__.py +0 -1
  107. novel_downloader/utils/text_utils/chapter_formatting.py +2 -7
  108. novel_downloader/utils/text_utils/diff_display.py +0 -1
  109. novel_downloader/utils/text_utils/font_mapping.py +1 -4
  110. novel_downloader/utils/text_utils/text_cleaning.py +0 -1
  111. novel_downloader/utils/time_utils/__init__.py +0 -1
  112. novel_downloader/utils/time_utils/datetime_utils.py +8 -10
  113. novel_downloader/utils/time_utils/sleep_utils.py +1 -3
  114. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/METADATA +14 -17
  115. novel_downloader-1.3.1.dist-info/RECORD +127 -0
  116. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/WHEEL +1 -1
  117. novel_downloader/core/requesters/base_browser.py +0 -214
  118. novel_downloader/core/requesters/base_session.py +0 -246
  119. novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
  120. novel_downloader/core/requesters/common_requester/common_session.py +0 -126
  121. novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
  122. novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -396
  123. novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
  124. novel_downloader/resources/config/settings.yaml +0 -76
  125. novel_downloader-1.2.2.dist-info/RECORD +0 -115
  126. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/entry_points.txt +0 -0
  127. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/licenses/LICENSE +0 -0
  128. {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/top_level.txt +0 -0
@@ -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.2.2"
9
+ __version__ = "1.3.1"
11
10
 
12
11
  __author__ = "Saudade Z"
13
12
  __email__ = "saudadez217@gmail.com"
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.cli
5
4
  --------------------
@@ -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: Optional[str] = None, all: bool = False) -> bool:
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: List[Path] = []
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:
@@ -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
- from typing import List
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: List[str], site: str) -> None:
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
 
@@ -69,7 +68,7 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
69
68
 
70
69
  # Filter out placeholder/example IDs
71
70
  invalid_ids = {"0000000000"}
72
- valid_book_ids = [bid for bid in book_ids if bid not in invalid_ids]
71
+ valid_book_ids = set(book_ids) - invalid_ids
73
72
 
74
73
  if not book_ids:
75
74
  click.echo(t("download_no_ids"))
@@ -82,21 +81,20 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
82
81
 
83
82
  # Initialize the requester, parser, saver, and downloader components
84
83
  if downloader_cfg.mode == "async":
85
- import asyncio
86
-
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
84
  setup_logging()
91
- async_downloader = get_async_downloader(
92
- requester=async_requester,
93
- parser=async_parser,
94
- saver=async_saver,
95
- site=site,
96
- config=downloader_cfg,
97
- )
98
85
 
99
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
+
100
98
  prepare = getattr(async_downloader, "prepare", None)
101
99
  if prepare and asyncio.iscoroutinefunction(prepare):
102
100
  await prepare()
@@ -105,9 +103,7 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
105
103
  click.echo(t("download_downloading", book_id=book_id, site=site))
106
104
  await async_downloader.download_one(book_id)
107
105
 
108
- if requester_cfg.auto_close:
109
- input(t("download_prompt_parse"))
110
- await async_requester.shutdown()
106
+ await async_requester.close()
111
107
 
112
108
  asyncio.run(async_download_all())
113
109
  else:
@@ -127,8 +123,6 @@ def download_cli(ctx: Context, book_ids: List[str], site: str) -> None:
127
123
  click.echo(t("download_downloading", book_id=book_id, site=site))
128
124
  sync_downloader.download_one(book_id)
129
125
 
130
- if requester_cfg.auto_close:
131
- input(t("download_prompt_parse"))
132
- sync_requester.shutdown()
126
+ sync_requester.close()
133
127
 
134
128
  return
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  """
4
3
  novel_downloader.cli.interactive
5
4
  --------------------------------
@@ -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: Optional[str]) -> None:
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
@@ -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: Optional[str]) -> None:
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(t("settings_add_hash_load_fail", err=str(e)))
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
5
4
  ------------------------
@@ -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, Dict, List
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,
@@ -31,7 +32,7 @@ class ConfigAdapter:
31
32
  Adapter to map a raw config dict + site name into structured dataclass configs.
32
33
  """
33
34
 
34
- def __init__(self, config: Dict[str, Any], site: str):
35
+ def __init__(self, config: dict[str, Any], site: str):
35
36
  """
36
37
  :param config: 完整加载的配置 dict
37
38
  :param site: 当前站点名称 (e.g. "qidian")
@@ -40,33 +41,33 @@ class ConfigAdapter:
40
41
  self._site = site
41
42
 
42
43
  site_rules = load_site_rules() # -> Dict[str, SiteRules]
43
- self._supported_sites = set(site_rules.keys())
44
+ self._supported_sites = set(site_rules.keys()) | SUPPORTED_SITES
44
45
 
45
- def set_site(self, site: str) -> None:
46
- """
47
- 切换当前适配的站点
48
- """
49
- self._site = site
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
50
53
 
51
- def _get_site_cfg(self) -> Dict[str, Any]:
54
+ def _get_site_cfg(self, site: str | None = None) -> dict[str, Any]:
52
55
  """
53
- 统一获取站点配置:
56
+ 获取指定站点的配置 (默认为当前适配站点)
54
57
 
55
- 1. 先尝试从 self._config["sites"][self._site] 取配置
56
- 2. 如果没有配置, self._site self._supported_sites 中, 则取 sites["common"]
58
+ 1. 如果有 site-specific 配置, 优先返回它
59
+ 2. 否则, 如果该站点在支持站点中, 尝试返回 'common' 配置
57
60
  3. 否则返回空 dict
58
61
  """
62
+ site = site or self._site
59
63
  sites_cfg = self._config.get("sites", {}) or {}
60
64
 
61
- # 1. site-specific config
62
- if self._site in sites_cfg:
63
- return sites_cfg[self._site] or {}
65
+ if site in sites_cfg:
66
+ return sites_cfg[site] or {}
64
67
 
65
- # 2. fallback to "common" only if site is supported
66
- if self._site in self._supported_sites:
68
+ if site in self._supported_sites:
67
69
  return sites_cfg.get("common", {}) or {}
68
70
 
69
- # 3. completely unsupported site
70
71
  return {}
71
72
 
72
73
  def get_requester_config(self) -> RequesterConfig:
@@ -77,10 +78,11 @@ class ConfigAdapter:
77
78
  req = self._config.get("requests", {})
78
79
  site_cfg = self._get_site_cfg()
79
80
  return RequesterConfig(
80
- wait_time=req.get("wait_time", 5),
81
81
  retry_times=req.get("retry_times", 3),
82
- retry_interval=req.get("retry_interval", 5),
83
- 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),
84
86
  headless=req.get("headless", True),
85
87
  user_data_folder=req.get("user_data_folder", "./user_data"),
86
88
  profile_name=req.get("profile_name", "Profile_1"),
@@ -88,7 +90,6 @@ class ConfigAdapter:
88
90
  disable_images=req.get("disable_images", True),
89
91
  mute_audio=req.get("mute_audio", True),
90
92
  mode=site_cfg.get("mode", "session"),
91
- max_rps=site_cfg.get("max_rps", None),
92
93
  )
93
94
 
94
95
  def get_downloader_config(self) -> DownloaderConfig:
@@ -100,9 +101,9 @@ class ConfigAdapter:
100
101
  debug = gen.get("debug", {})
101
102
  site_cfg = self._get_site_cfg()
102
103
  return DownloaderConfig(
103
- request_interval=gen.get("request_interval", 5),
104
+ request_interval=gen.get("request_interval", 5.0),
104
105
  raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
105
- cache_dir=gen.get("cache_dir", "./cache"),
106
+ cache_dir=gen.get("cache_dir", "./novel_cache"),
106
107
  download_workers=gen.get("download_workers", 4),
107
108
  parser_workers=gen.get("parser_workers", 4),
108
109
  use_process_pool=gen.get("use_process_pool", True),
@@ -110,6 +111,8 @@ class ConfigAdapter:
110
111
  login_required=site_cfg.get("login_required", False),
111
112
  save_html=debug.get("save_html", False),
112
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),
113
116
  )
114
117
 
115
118
  def get_parser_config(self) -> ParserConfig:
@@ -121,7 +124,7 @@ class ConfigAdapter:
121
124
  font_ocr = gen.get("font_ocr", {})
122
125
  site_cfg = self._get_site_cfg()
123
126
  return ParserConfig(
124
- cache_dir=gen.get("cache_dir", "./cache"),
127
+ cache_dir=gen.get("cache_dir", "./novel_cache"),
125
128
  decode_font=font_ocr.get("decode_font", False),
126
129
  use_freq=font_ocr.get("use_freq", False),
127
130
  use_ocr=font_ocr.get("use_ocr", True),
@@ -149,6 +152,7 @@ class ConfigAdapter:
149
152
  return SaverConfig(
150
153
  raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
151
154
  output_dir=gen.get("output_dir", "./downloads"),
155
+ storage_backend=gen.get("storage_backend", "json"),
152
156
  clean_text=out.get("clean_text", True),
153
157
  make_txt=fmt.get("make_txt", True),
154
158
  make_epub=fmt.get("make_epub", False),
@@ -158,9 +162,10 @@ class ConfigAdapter:
158
162
  filename_template=naming.get("filename_template", "{title}_{author}"),
159
163
  include_cover=epub_opts.get("include_cover", True),
160
164
  include_toc=epub_opts.get("include_toc", False),
165
+ include_picture=epub_opts.get("include_picture", False),
161
166
  )
162
167
 
163
- def get_book_ids(self) -> List[str]:
168
+ def get_book_ids(self) -> list[str]:
164
169
  """
165
170
  从 config["sites"][site]["book_ids"] 中提取目标书籍列表
166
171
  """