novel-downloader 1.3.3__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.
Files changed (211) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/clean.py +97 -78
  3. novel_downloader/cli/config.py +177 -0
  4. novel_downloader/cli/download.py +132 -87
  5. novel_downloader/cli/export.py +77 -0
  6. novel_downloader/cli/main.py +21 -28
  7. novel_downloader/config/__init__.py +1 -25
  8. novel_downloader/config/adapter.py +32 -31
  9. novel_downloader/config/loader.py +3 -3
  10. novel_downloader/config/site_rules.py +1 -2
  11. novel_downloader/core/__init__.py +3 -6
  12. novel_downloader/core/downloaders/__init__.py +10 -13
  13. novel_downloader/core/downloaders/base.py +233 -0
  14. novel_downloader/core/downloaders/biquge.py +27 -0
  15. novel_downloader/core/downloaders/common.py +414 -0
  16. novel_downloader/core/downloaders/esjzone.py +27 -0
  17. novel_downloader/core/downloaders/linovelib.py +27 -0
  18. novel_downloader/core/downloaders/qianbi.py +27 -0
  19. novel_downloader/core/downloaders/qidian.py +352 -0
  20. novel_downloader/core/downloaders/sfacg.py +27 -0
  21. novel_downloader/core/downloaders/yamibo.py +27 -0
  22. novel_downloader/core/exporters/__init__.py +37 -0
  23. novel_downloader/core/{savers → exporters}/base.py +73 -39
  24. novel_downloader/core/exporters/biquge.py +25 -0
  25. novel_downloader/core/exporters/common/__init__.py +12 -0
  26. novel_downloader/core/{savers → exporters}/common/epub.py +22 -22
  27. novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
  28. novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
  29. novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
  30. novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
  31. novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
  32. novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
  33. novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
  34. novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
  35. novel_downloader/core/exporters/esjzone.py +25 -0
  36. novel_downloader/core/exporters/linovelib/__init__.py +10 -0
  37. novel_downloader/core/exporters/linovelib/epub.py +449 -0
  38. novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
  39. novel_downloader/core/exporters/linovelib/txt.py +129 -0
  40. novel_downloader/core/exporters/qianbi.py +25 -0
  41. novel_downloader/core/{savers → exporters}/qidian.py +8 -8
  42. novel_downloader/core/exporters/sfacg.py +25 -0
  43. novel_downloader/core/exporters/yamibo.py +25 -0
  44. novel_downloader/core/factory/__init__.py +5 -17
  45. novel_downloader/core/factory/downloader.py +24 -126
  46. novel_downloader/core/factory/exporter.py +58 -0
  47. novel_downloader/core/factory/fetcher.py +96 -0
  48. novel_downloader/core/factory/parser.py +17 -12
  49. novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
  50. novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
  51. novel_downloader/core/fetchers/base/browser.py +383 -0
  52. novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
  53. novel_downloader/core/fetchers/base/session.py +419 -0
  54. novel_downloader/core/fetchers/biquge/__init__.py +14 -0
  55. novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
  56. novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
  57. novel_downloader/core/fetchers/common/__init__.py +14 -0
  58. novel_downloader/core/fetchers/common/browser.py +79 -0
  59. novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
  60. novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
  61. novel_downloader/core/fetchers/esjzone/browser.py +202 -0
  62. novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
  63. novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
  64. novel_downloader/core/fetchers/linovelib/browser.py +178 -0
  65. novel_downloader/core/fetchers/linovelib/session.py +178 -0
  66. novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
  67. novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
  68. novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
  69. novel_downloader/core/fetchers/qidian/__init__.py +14 -0
  70. novel_downloader/core/fetchers/qidian/browser.py +266 -0
  71. novel_downloader/core/fetchers/qidian/session.py +326 -0
  72. novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
  73. novel_downloader/core/fetchers/sfacg/browser.py +189 -0
  74. novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
  75. novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
  76. novel_downloader/core/fetchers/yamibo/browser.py +229 -0
  77. novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
  78. novel_downloader/core/interfaces/__init__.py +8 -12
  79. novel_downloader/core/interfaces/downloader.py +54 -0
  80. novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
  81. novel_downloader/core/interfaces/fetcher.py +162 -0
  82. novel_downloader/core/interfaces/parser.py +6 -7
  83. novel_downloader/core/parsers/__init__.py +5 -6
  84. novel_downloader/core/parsers/base.py +9 -13
  85. novel_downloader/core/parsers/biquge/main_parser.py +12 -13
  86. novel_downloader/core/parsers/common/helper.py +3 -3
  87. novel_downloader/core/parsers/common/main_parser.py +39 -34
  88. novel_downloader/core/parsers/esjzone/main_parser.py +20 -14
  89. novel_downloader/core/parsers/linovelib/__init__.py +10 -0
  90. novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
  91. novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
  92. novel_downloader/core/parsers/qidian/__init__.py +2 -11
  93. novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
  94. novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
  95. novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
  96. novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
  97. novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
  98. novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
  99. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
  100. novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
  101. novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
  102. novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
  103. novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
  104. novel_downloader/locales/en.json +18 -2
  105. novel_downloader/locales/zh.json +18 -2
  106. novel_downloader/models/__init__.py +64 -0
  107. novel_downloader/models/browser.py +21 -0
  108. novel_downloader/models/chapter.py +25 -0
  109. novel_downloader/models/config.py +100 -0
  110. novel_downloader/models/login.py +20 -0
  111. novel_downloader/models/site_rules.py +99 -0
  112. novel_downloader/models/tasks.py +33 -0
  113. novel_downloader/models/types.py +15 -0
  114. novel_downloader/resources/config/settings.toml +31 -25
  115. novel_downloader/resources/json/linovelib_font_map.json +3573 -0
  116. novel_downloader/tui/__init__.py +7 -0
  117. novel_downloader/tui/app.py +32 -0
  118. novel_downloader/tui/main.py +17 -0
  119. novel_downloader/tui/screens/__init__.py +14 -0
  120. novel_downloader/tui/screens/home.py +191 -0
  121. novel_downloader/tui/screens/login.py +74 -0
  122. novel_downloader/tui/styles/home_layout.tcss +79 -0
  123. novel_downloader/tui/widgets/richlog_handler.py +24 -0
  124. novel_downloader/utils/__init__.py +6 -0
  125. novel_downloader/utils/chapter_storage.py +25 -38
  126. novel_downloader/utils/constants.py +11 -5
  127. novel_downloader/utils/cookies.py +66 -0
  128. novel_downloader/utils/crypto_utils.py +1 -74
  129. novel_downloader/utils/fontocr/ocr_v1.py +2 -1
  130. novel_downloader/utils/fontocr/ocr_v2.py +2 -2
  131. novel_downloader/utils/hash_store.py +10 -18
  132. novel_downloader/utils/hash_utils.py +3 -2
  133. novel_downloader/utils/logger.py +2 -3
  134. novel_downloader/utils/network.py +2 -1
  135. novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
  136. novel_downloader/utils/text_utils/font_mapping.py +1 -1
  137. novel_downloader/utils/text_utils/text_cleaning.py +1 -1
  138. novel_downloader/utils/time_utils/datetime_utils.py +3 -3
  139. novel_downloader/utils/time_utils/sleep_utils.py +1 -1
  140. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +69 -35
  141. novel_downloader-1.4.0.dist-info/RECORD +170 -0
  142. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
  143. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
  144. novel_downloader/cli/interactive.py +0 -66
  145. novel_downloader/cli/settings.py +0 -177
  146. novel_downloader/config/models.py +0 -187
  147. novel_downloader/core/downloaders/base/__init__.py +0 -14
  148. novel_downloader/core/downloaders/base/base_async.py +0 -153
  149. novel_downloader/core/downloaders/base/base_sync.py +0 -208
  150. novel_downloader/core/downloaders/biquge/__init__.py +0 -14
  151. novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
  152. novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
  153. novel_downloader/core/downloaders/common/__init__.py +0 -14
  154. novel_downloader/core/downloaders/common/common_async.py +0 -210
  155. novel_downloader/core/downloaders/common/common_sync.py +0 -202
  156. novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
  157. novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
  158. novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
  159. novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
  160. novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
  161. novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
  162. novel_downloader/core/downloaders/qidian/__init__.py +0 -10
  163. novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -219
  164. novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
  165. novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
  166. novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
  167. novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
  168. novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
  169. novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
  170. novel_downloader/core/factory/requester.py +0 -144
  171. novel_downloader/core/factory/saver.py +0 -56
  172. novel_downloader/core/interfaces/async_downloader.py +0 -36
  173. novel_downloader/core/interfaces/async_requester.py +0 -84
  174. novel_downloader/core/interfaces/sync_downloader.py +0 -36
  175. novel_downloader/core/interfaces/sync_requester.py +0 -82
  176. novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
  177. novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
  178. novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
  179. novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
  180. novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
  181. novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
  182. novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
  183. novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
  184. novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
  185. novel_downloader/core/requesters/base/async_session.py +0 -410
  186. novel_downloader/core/requesters/base/browser.py +0 -337
  187. novel_downloader/core/requesters/base/session.py +0 -378
  188. novel_downloader/core/requesters/biquge/__init__.py +0 -14
  189. novel_downloader/core/requesters/common/__init__.py +0 -17
  190. novel_downloader/core/requesters/common/session.py +0 -113
  191. novel_downloader/core/requesters/esjzone/__init__.py +0 -13
  192. novel_downloader/core/requesters/esjzone/session.py +0 -235
  193. novel_downloader/core/requesters/qianbi/__init__.py +0 -13
  194. novel_downloader/core/requesters/qidian/__init__.py +0 -21
  195. novel_downloader/core/requesters/qidian/broswer.py +0 -307
  196. novel_downloader/core/requesters/qidian/session.py +0 -290
  197. novel_downloader/core/requesters/sfacg/__init__.py +0 -13
  198. novel_downloader/core/requesters/sfacg/session.py +0 -242
  199. novel_downloader/core/requesters/yamibo/__init__.py +0 -13
  200. novel_downloader/core/requesters/yamibo/session.py +0 -237
  201. novel_downloader/core/savers/__init__.py +0 -34
  202. novel_downloader/core/savers/biquge.py +0 -25
  203. novel_downloader/core/savers/common/__init__.py +0 -12
  204. novel_downloader/core/savers/esjzone.py +0 -25
  205. novel_downloader/core/savers/qianbi.py +0 -25
  206. novel_downloader/core/savers/sfacg.py +0 -25
  207. novel_downloader/core/savers/yamibo.py +0 -25
  208. novel_downloader/resources/config/rules.toml +0 -196
  209. novel_downloader-1.3.3.dist-info/RECORD +0 -166
  210. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
  211. {novel_downloader-1.3.3.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)))
@@ -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
- @click.group(help=t("cli_help"), invoke_without_command=True) # type: ignore
18
- @click.option(
19
- "--config",
20
- type=click.Path(exists=True, dir_okay=False, resolve_path=True),
21
- default=None,
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.utils.constants import SUPPORTED_SITES
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 get_requester_config(self) -> RequesterConfig:
66
+ def get_fetcher_config(self) -> FetcherConfig:
74
67
  """
75
- 从 config["requests"] 中读取通用请求配置 (含 DrissionPage 设置)
76
- 返回 RequesterConfig 实例
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 RequesterConfig(
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", True),
87
- user_data_folder=req.get("user_data_folder", "./user_data"),
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
- username=site_cfg.get("username", ""),
94
- password=site_cfg.get("password", ""),
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", 5.0),
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", 4),
110
- parser_workers=gen.get("parser_workers", 4),
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 get_saver_config(self) -> SaverConfig:
143
+ def get_exporter_config(self) -> ExporterConfig:
145
144
  """
146
145
  从 config["general"] 与 config["output"] 中读取存储器相关配置,
147
- 返回 SaverConfig 实例
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
- return SaverConfig(
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 YAML file.
123
+ Load configuration data from a Toml file.
124
124
 
125
- :param config_path: Optional path to the YAML configuration file.
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
- - Requester: Sends HTTP requests and manages sessions, including login if required.
14
- - Saver: Responsible for exporting downloaded data into various output formats.
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 get_downloader, get_parser, get_requester, get_saver
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 BiqugeAsyncDownloader, BiqugeDownloader
23
- from .common import CommonAsyncDownloader, CommonDownloader
24
- from .esjzone import EsjzoneAsyncDownloader, EsjzoneDownloader
25
- from .qianbi import QianbiAsyncDownloader, QianbiDownloader
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 SfacgAsyncDownloader, SfacgDownloader
28
- from .yamibo import YamiboAsyncDownloader, YamiboDownloader
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
- "QianbiAsyncDownloader",
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")