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.
Files changed (129) 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 +18 -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 +48 -18
  10. novel_downloader/config/loader.py +116 -108
  11. novel_downloader/config/models.py +41 -32
  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} +34 -23
  23. novel_downloader/core/downloaders/qidian/__init__.py +10 -0
  24. novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +80 -64
  25. novel_downloader/core/factory/__init__.py +4 -5
  26. novel_downloader/core/factory/{downloader_factory.py → downloader.py} +36 -35
  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} +26 -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} +34 -17
  37. novel_downloader/core/parsers/__init__.py +5 -4
  38. novel_downloader/core/parsers/{base_parser.py → base.py} +20 -11
  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 +20 -18
  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 +41 -49
  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 +16 -12
  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 +37 -45
  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 +16 -12
  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/shared/book_info_parser.py +150 -0
  58. novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +9 -10
  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} +180 -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 +306 -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} +24 -52
  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 +12 -4
  86. novel_downloader/locales/zh.json +9 -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 +8 -5
  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 +9 -11
  113. novel_downloader/utils/time_utils/sleep_utils.py +27 -13
  114. {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/METADATA +14 -17
  115. novel_downloader-1.3.0.dist-info/RECORD +127 -0
  116. {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/WHEEL +1 -1
  117. novel_downloader/core/parsers/qidian_parser/shared/book_info_parser.py +0 -95
  118. novel_downloader/core/requesters/base_browser.py +0 -210
  119. novel_downloader/core/requesters/base_session.py +0 -243
  120. novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
  121. novel_downloader/core/requesters/common_requester/common_session.py +0 -126
  122. novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
  123. novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -377
  124. novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
  125. novel_downloader/resources/config/settings.yaml +0 -76
  126. novel_downloader-1.2.1.dist-info/RECORD +0 -115
  127. {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/entry_points.txt +0 -0
  128. {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/licenses/LICENSE +0 -0
  129. {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.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.1"
9
+ __version__ = "1.3.0"
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
 
@@ -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 = [bid for bid in book_ids if bid not in invalid_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
- if requester_cfg.auto_close:
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
- if requester_cfg.auto_close:
129
- input(t("download_prompt_parse"))
130
- sync_requester.shutdown()
126
+ sync_requester.close()
131
127
 
132
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,
@@ -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: Dict[str, Any], site: str):
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
- def set_site(self, site: str) -> None:
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
- self._site = site
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._config.get("sites", {}).get(self._site, {})
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
- retry_interval=req.get("retry_interval", 5),
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._config.get("sites", {}).get(self._site, {})
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", "./cache"),
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._config.get("sites", {}).get(self._site, {})
125
+ site_cfg = self._get_site_cfg()
98
126
  return ParserConfig(
99
- cache_dir=gen.get("cache_dir", "./cache"),
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) -> List[str]:
168
+ def get_book_ids(self) -> list[str]:
139
169
  """
140
170
  从 config["sites"][site]["book_ids"] 中提取目标书籍列表
141
171
  """
142
- site_cfg = self._config.get("sites", {}).get(self._site, {})
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):