novel-downloader 1.4.4__py3-none-any.whl → 1.5.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 (165) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -2
  3. novel_downloader/cli/config.py +1 -83
  4. novel_downloader/cli/download.py +4 -5
  5. novel_downloader/cli/export.py +4 -1
  6. novel_downloader/cli/main.py +2 -0
  7. novel_downloader/cli/search.py +123 -0
  8. novel_downloader/config/__init__.py +3 -10
  9. novel_downloader/config/adapter.py +190 -54
  10. novel_downloader/config/loader.py +2 -3
  11. novel_downloader/core/__init__.py +13 -13
  12. novel_downloader/core/downloaders/__init__.py +10 -11
  13. novel_downloader/core/downloaders/base.py +152 -26
  14. novel_downloader/core/downloaders/biquge.py +5 -1
  15. novel_downloader/core/downloaders/common.py +157 -378
  16. novel_downloader/core/downloaders/esjzone.py +5 -1
  17. novel_downloader/core/downloaders/linovelib.py +5 -1
  18. novel_downloader/core/downloaders/qianbi.py +291 -4
  19. novel_downloader/core/downloaders/qidian.py +199 -285
  20. novel_downloader/core/downloaders/registry.py +67 -0
  21. novel_downloader/core/downloaders/sfacg.py +5 -1
  22. novel_downloader/core/downloaders/yamibo.py +5 -1
  23. novel_downloader/core/exporters/__init__.py +10 -11
  24. novel_downloader/core/exporters/base.py +87 -7
  25. novel_downloader/core/exporters/biquge.py +5 -8
  26. novel_downloader/core/exporters/common/__init__.py +2 -2
  27. novel_downloader/core/exporters/common/epub.py +82 -166
  28. novel_downloader/core/exporters/common/main_exporter.py +0 -60
  29. novel_downloader/core/exporters/common/txt.py +82 -83
  30. novel_downloader/core/exporters/epub_util.py +157 -1330
  31. novel_downloader/core/exporters/esjzone.py +5 -8
  32. novel_downloader/core/exporters/linovelib/__init__.py +2 -2
  33. novel_downloader/core/exporters/linovelib/epub.py +157 -212
  34. novel_downloader/core/exporters/linovelib/main_exporter.py +2 -59
  35. novel_downloader/core/exporters/linovelib/txt.py +67 -63
  36. novel_downloader/core/exporters/qianbi.py +5 -8
  37. novel_downloader/core/exporters/qidian.py +14 -4
  38. novel_downloader/core/exporters/registry.py +53 -0
  39. novel_downloader/core/exporters/sfacg.py +5 -8
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/exporters/yamibo.py +5 -8
  42. novel_downloader/core/fetchers/__init__.py +19 -24
  43. novel_downloader/core/fetchers/base/__init__.py +3 -3
  44. novel_downloader/core/fetchers/base/browser.py +23 -4
  45. novel_downloader/core/fetchers/base/session.py +30 -5
  46. novel_downloader/core/fetchers/biquge/__init__.py +3 -3
  47. novel_downloader/core/fetchers/biquge/browser.py +5 -0
  48. novel_downloader/core/fetchers/biquge/session.py +6 -1
  49. novel_downloader/core/fetchers/esjzone/__init__.py +3 -3
  50. novel_downloader/core/fetchers/esjzone/browser.py +5 -0
  51. novel_downloader/core/fetchers/esjzone/session.py +6 -1
  52. novel_downloader/core/fetchers/linovelib/__init__.py +3 -3
  53. novel_downloader/core/fetchers/linovelib/browser.py +6 -1
  54. novel_downloader/core/fetchers/linovelib/session.py +6 -1
  55. novel_downloader/core/fetchers/qianbi/__init__.py +3 -3
  56. novel_downloader/core/fetchers/qianbi/browser.py +5 -0
  57. novel_downloader/core/fetchers/qianbi/session.py +5 -0
  58. novel_downloader/core/fetchers/qidian/__init__.py +3 -3
  59. novel_downloader/core/fetchers/qidian/browser.py +12 -4
  60. novel_downloader/core/fetchers/qidian/session.py +11 -3
  61. novel_downloader/core/fetchers/registry.py +71 -0
  62. novel_downloader/core/fetchers/sfacg/__init__.py +3 -3
  63. novel_downloader/core/fetchers/sfacg/browser.py +5 -0
  64. novel_downloader/core/fetchers/sfacg/session.py +5 -0
  65. novel_downloader/core/fetchers/yamibo/__init__.py +3 -3
  66. novel_downloader/core/fetchers/yamibo/browser.py +5 -0
  67. novel_downloader/core/fetchers/yamibo/session.py +6 -1
  68. novel_downloader/core/interfaces/__init__.py +7 -5
  69. novel_downloader/core/interfaces/searcher.py +18 -0
  70. novel_downloader/core/parsers/__init__.py +10 -11
  71. novel_downloader/core/parsers/{biquge/main_parser.py → biquge.py} +7 -2
  72. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +7 -2
  73. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +7 -2
  74. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +7 -2
  75. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  76. novel_downloader/core/parsers/qidian/chapter_encrypted.py +23 -21
  77. novel_downloader/core/parsers/qidian/chapter_normal.py +1 -1
  78. novel_downloader/core/parsers/qidian/main_parser.py +10 -21
  79. novel_downloader/core/parsers/qidian/utils/__init__.py +11 -11
  80. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +5 -6
  81. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  82. novel_downloader/core/parsers/registry.py +68 -0
  83. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +7 -2
  84. novel_downloader/core/parsers/{yamibo/main_parser.py → yamibo.py} +7 -2
  85. novel_downloader/core/searchers/__init__.py +20 -0
  86. novel_downloader/core/searchers/base.py +92 -0
  87. novel_downloader/core/searchers/biquge.py +83 -0
  88. novel_downloader/core/searchers/esjzone.py +84 -0
  89. novel_downloader/core/searchers/qianbi.py +131 -0
  90. novel_downloader/core/searchers/qidian.py +87 -0
  91. novel_downloader/core/searchers/registry.py +63 -0
  92. novel_downloader/locales/en.json +12 -4
  93. novel_downloader/locales/zh.json +12 -4
  94. novel_downloader/models/__init__.py +4 -30
  95. novel_downloader/models/config.py +12 -6
  96. novel_downloader/models/search.py +16 -0
  97. novel_downloader/models/types.py +0 -2
  98. novel_downloader/resources/config/settings.toml +31 -4
  99. novel_downloader/resources/css_styles/intro.css +83 -0
  100. novel_downloader/resources/css_styles/main.css +30 -89
  101. novel_downloader/utils/__init__.py +52 -0
  102. novel_downloader/utils/chapter_storage.py +244 -224
  103. novel_downloader/utils/constants.py +1 -21
  104. novel_downloader/utils/epub/__init__.py +34 -0
  105. novel_downloader/utils/epub/builder.py +377 -0
  106. novel_downloader/utils/epub/constants.py +77 -0
  107. novel_downloader/utils/epub/documents.py +403 -0
  108. novel_downloader/utils/epub/models.py +134 -0
  109. novel_downloader/utils/epub/utils.py +212 -0
  110. novel_downloader/utils/file_utils/__init__.py +10 -14
  111. novel_downloader/utils/file_utils/io.py +20 -51
  112. novel_downloader/utils/file_utils/normalize.py +2 -2
  113. novel_downloader/utils/file_utils/sanitize.py +2 -3
  114. novel_downloader/utils/fontocr/__init__.py +5 -5
  115. novel_downloader/utils/{hash_store.py → fontocr/hash_store.py} +4 -3
  116. novel_downloader/utils/{hash_utils.py → fontocr/hash_utils.py} +2 -2
  117. novel_downloader/utils/fontocr/ocr_v1.py +13 -1
  118. novel_downloader/utils/fontocr/ocr_v2.py +13 -1
  119. novel_downloader/utils/fontocr/ocr_v3.py +744 -0
  120. novel_downloader/utils/i18n.py +2 -0
  121. novel_downloader/utils/logger.py +2 -0
  122. novel_downloader/utils/network.py +110 -251
  123. novel_downloader/utils/state.py +1 -0
  124. novel_downloader/utils/text_utils/__init__.py +18 -17
  125. novel_downloader/utils/text_utils/diff_display.py +4 -5
  126. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  127. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  128. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  129. novel_downloader/utils/time_utils/__init__.py +3 -3
  130. novel_downloader/utils/time_utils/datetime_utils.py +4 -5
  131. novel_downloader/utils/time_utils/sleep_utils.py +2 -3
  132. {novel_downloader-1.4.4.dist-info → novel_downloader-1.5.0.dist-info}/METADATA +2 -2
  133. novel_downloader-1.5.0.dist-info/RECORD +164 -0
  134. novel_downloader/config/site_rules.py +0 -94
  135. novel_downloader/core/factory/__init__.py +0 -20
  136. novel_downloader/core/factory/downloader.py +0 -73
  137. novel_downloader/core/factory/exporter.py +0 -58
  138. novel_downloader/core/factory/fetcher.py +0 -96
  139. novel_downloader/core/factory/parser.py +0 -86
  140. novel_downloader/core/fetchers/common/__init__.py +0 -14
  141. novel_downloader/core/fetchers/common/browser.py +0 -79
  142. novel_downloader/core/fetchers/common/session.py +0 -79
  143. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  144. novel_downloader/core/parsers/common/__init__.py +0 -13
  145. novel_downloader/core/parsers/common/helper.py +0 -323
  146. novel_downloader/core/parsers/common/main_parser.py +0 -106
  147. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  148. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  149. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  150. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  151. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  152. novel_downloader/models/browser.py +0 -21
  153. novel_downloader/models/site_rules.py +0 -99
  154. novel_downloader/models/tasks.py +0 -33
  155. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  156. novel_downloader/resources/json/replace_word_map.json +0 -4
  157. novel_downloader/resources/text/blacklist.txt +0 -22
  158. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  159. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  160. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  161. novel_downloader-1.4.4.dist-info/RECORD +0 -165
  162. {novel_downloader-1.4.4.dist-info → novel_downloader-1.5.0.dist-info}/WHEEL +0 -0
  163. {novel_downloader-1.4.4.dist-info → novel_downloader-1.5.0.dist-info}/entry_points.txt +0 -0
  164. {novel_downloader-1.4.4.dist-info → novel_downloader-1.5.0.dist-info}/licenses/LICENSE +0 -0
  165. {novel_downloader-1.4.4.dist-info → novel_downloader-1.5.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ novel_downloader
6
6
  Core package for the Novel Downloader project.
7
7
  """
8
8
 
9
- __version__ = "1.4.4"
9
+ __version__ = "1.5.0"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -6,8 +6,8 @@ novel_downloader.cli
6
6
  This module exposes the CLI entry point.
7
7
  """
8
8
 
9
- from .main import cli_main
10
-
11
9
  __all__ = [
12
10
  "cli_main",
13
11
  ]
12
+
13
+ from .main import cli_main
@@ -11,14 +11,11 @@ from argparse import Namespace, _SubParsersAction
11
11
  from importlib.resources import as_file
12
12
  from pathlib import Path
13
13
 
14
- from novel_downloader.config import save_config_file, save_rules_as_json
14
+ from novel_downloader.config import save_config_file
15
15
  from novel_downloader.utils.constants import DEFAULT_SETTINGS_PATHS
16
16
  from novel_downloader.utils.i18n import t
17
- from novel_downloader.utils.logger import setup_logging
18
17
  from novel_downloader.utils.state import state_mgr
19
18
 
20
- # from novel_downloader.utils.hash_store import img_hash_store
21
-
22
19
 
23
20
  def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
24
21
  parser = subparsers.add_parser("config", help=t("help_config"))
@@ -27,9 +24,6 @@ def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type:
27
24
  _register_init(config_subparsers)
28
25
  _register_set_lang(config_subparsers)
29
26
  _register_set_config(config_subparsers)
30
- _register_update_rules(config_subparsers)
31
- _register_set_cookies(config_subparsers)
32
- # _register_add_hash(config_subparsers)
33
27
 
34
28
 
35
29
  def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
@@ -52,27 +46,7 @@ def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
52
46
  parser.set_defaults(func=_handle_set_config)
53
47
 
54
48
 
55
- def _register_update_rules(subparsers: _SubParsersAction) -> None: # type: ignore
56
- parser = subparsers.add_parser("update-rules", help=t("settings_update_rules_help"))
57
- parser.add_argument("path", type=str, help="Path to TOML/YAML/JSON rule file")
58
- parser.set_defaults(func=_handle_update_rules)
59
-
60
-
61
- def _register_set_cookies(subparsers: _SubParsersAction) -> None: # type: ignore
62
- parser = subparsers.add_parser("set-cookies", help=t("settings_set_cookies_help"))
63
- parser.add_argument("site", nargs="?", help="Site identifier")
64
- parser.add_argument("cookies", nargs="?", help="Cookies string")
65
- parser.set_defaults(func=_handle_set_cookies)
66
-
67
-
68
- # def _register_add_hash(subparsers: _SubParsersAction) -> None: # type: ignore
69
- # parser = subparsers.add_parser("add-hash", help=t("settings_add_hash_help"))
70
- # parser.add_argument("--path", type=str, help=t("settings_add_hash_path_help"))
71
- # parser.set_defaults(func=_handle_add_hash)
72
-
73
-
74
49
  def _handle_init(args: Namespace) -> None:
75
- setup_logging()
76
50
  cwd = Path.cwd()
77
51
 
78
52
  for resource in DEFAULT_SETTINGS_PATHS:
@@ -119,59 +93,3 @@ def _handle_set_config(args: Namespace) -> None:
119
93
  except Exception as e:
120
94
  print(t("settings_set_config_fail", err=str(e)))
121
95
  raise
122
-
123
-
124
- def _handle_update_rules(args: Namespace) -> None:
125
- try:
126
- save_rules_as_json(args.path)
127
- print(t("settings_update_rules", path=args.path))
128
- except Exception as e:
129
- print(t("settings_update_rules_fail", err=str(e)))
130
- raise
131
-
132
-
133
- def _handle_set_cookies(args: Namespace) -> None:
134
- site = args.site or input(t("settings_set_cookies_prompt_site") + ": ").strip()
135
- cookies = (
136
- args.cookies or input(t("settings_set_cookies_prompt_payload") + ": ").strip()
137
- )
138
-
139
- try:
140
- state_mgr.set_cookies(site, cookies)
141
- print(t("settings_set_cookies_success", site=site))
142
- except Exception as e:
143
- print(t("settings_set_cookies_fail", err=str(e)))
144
- raise
145
-
146
-
147
- # def _handle_add_hash(args: Namespace) -> None:
148
- # if args.path:
149
- # try:
150
- # img_hash_store.add_from_map(args.path)
151
- # img_hash_store.save()
152
- # print(t("settings_add_hash_loaded", path=args.path))
153
- # except Exception as e:
154
- # print(t("settings_add_hash_load_fail", err=str(e)))
155
- # raise
156
- # else:
157
- # print(t("settings_add_hash_prompt_tip"))
158
- # while True:
159
- # img_path = input(t("settings_add_hash_prompt_img") + ": ").strip()
160
- # if not img_path or img_path.lower() in {"exit", "quit"}:
161
- # break
162
- # if not Path(img_path).exists():
163
- # print(t("settings_add_hash_path_invalid"))
164
- # continue
165
-
166
- # label = input(t("settings_add_hash_prompt_label") + ": ").strip()
167
- # if not label or label.lower() in {"exit", "quit"}:
168
- # break
169
-
170
- # try:
171
- # img_hash_store.add_image(img_path, label)
172
- # print(t("settings_add_hash_added", img=img_path, label=label))
173
- # except Exception as e:
174
- # print(t("settings_add_hash_failed", err=str(e)))
175
-
176
- # img_hash_store.save()
177
- # print(t("settings_add_hash_saved"))
@@ -15,13 +15,13 @@ from pathlib import Path
15
15
  from typing import Any
16
16
 
17
17
  from novel_downloader.config import ConfigAdapter, load_config
18
- from novel_downloader.core.factory import (
18
+ from novel_downloader.core import (
19
+ FetcherProtocol,
19
20
  get_downloader,
20
21
  get_exporter,
21
22
  get_fetcher,
22
23
  get_parser,
23
24
  )
24
- from novel_downloader.core.interfaces import FetcherProtocol
25
25
  from novel_downloader.models import BookConfig, LoginField
26
26
  from novel_downloader.utils.cookies import resolve_cookies
27
27
  from novel_downloader.utils.i18n import t
@@ -44,8 +44,6 @@ def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type
44
44
 
45
45
 
46
46
  def handle_download(args: Namespace) -> None:
47
- setup_logging()
48
-
49
47
  site: str = args.site
50
48
  config_path: Path | None = Path(args.config) if args.config else None
51
49
  book_ids: list[BookConfig] = _cli_args_to_book_configs(
@@ -143,10 +141,11 @@ async def _download(
143
141
  fetcher_cfg = adapter.get_fetcher_config()
144
142
  parser_cfg = adapter.get_parser_config()
145
143
  exporter_cfg = adapter.get_exporter_config()
144
+ log_level = adapter.get_log_level()
146
145
 
147
146
  parser = get_parser(site, parser_cfg)
148
147
  exporter = get_exporter(site, exporter_cfg)
149
- setup_logging()
148
+ setup_logging(log_level=log_level)
150
149
 
151
150
  async with get_fetcher(site, fetcher_cfg) as fetcher:
152
151
  if downloader_cfg.login_required and not await fetcher.load_state():
@@ -9,8 +9,9 @@ from argparse import Namespace, _SubParsersAction
9
9
  from pathlib import Path
10
10
 
11
11
  from novel_downloader.config import ConfigAdapter, load_config
12
- from novel_downloader.core.factory import get_exporter
12
+ from novel_downloader.core import get_exporter
13
13
  from novel_downloader.utils.i18n import t
14
+ from novel_downloader.utils.logger import setup_logging
14
15
 
15
16
 
16
17
  def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
@@ -57,7 +58,9 @@ def handle_export(args: Namespace) -> None:
57
58
 
58
59
  adapter = ConfigAdapter(config=config_data, site=site)
59
60
  exporter_cfg = adapter.get_exporter_config()
61
+ log_level = adapter.get_log_level()
60
62
  exporter = get_exporter(site, exporter_cfg)
63
+ setup_logging(log_level=log_level)
61
64
 
62
65
  for book_id in book_ids:
63
66
  print(t("export_processing", book_id=book_id, format=export_format))
@@ -14,6 +14,7 @@ from .clean import register_clean_subcommand
14
14
  from .config import register_config_subcommand
15
15
  from .download import register_download_subcommand
16
16
  from .export import register_export_subcommand
17
+ from .search import register_search_subcommand
17
18
 
18
19
 
19
20
  def cli_main() -> None:
@@ -24,6 +25,7 @@ def cli_main() -> None:
24
25
  register_config_subcommand(subparsers)
25
26
  register_download_subcommand(subparsers)
26
27
  register_export_subcommand(subparsers)
28
+ register_search_subcommand(subparsers)
27
29
 
28
30
  args = parser.parse_args()
29
31
  if hasattr(args, "func"):
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.cli.search
4
+ ---------------------------
5
+
6
+ """
7
+
8
+ import asyncio
9
+ from argparse import Namespace, _SubParsersAction
10
+ from collections.abc import Sequence
11
+ from pathlib import Path
12
+
13
+ from novel_downloader.cli.download import _download
14
+ from novel_downloader.config import ConfigAdapter, load_config
15
+ from novel_downloader.core import search
16
+ from novel_downloader.models import BookConfig, SearchResult
17
+ from novel_downloader.utils.i18n import t
18
+
19
+
20
+ def register_search_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
21
+ parser = subparsers.add_parser("search", help=t("help_search"))
22
+
23
+ parser.add_argument(
24
+ "--site",
25
+ "-s",
26
+ action="append",
27
+ metavar="SITE",
28
+ help=t("help_search_sites"),
29
+ )
30
+ parser.add_argument(
31
+ "keyword",
32
+ help=t("help_search_keyword"),
33
+ )
34
+ parser.add_argument(
35
+ "--config",
36
+ type=str,
37
+ help=t("help_config"),
38
+ )
39
+ parser.add_argument(
40
+ "--limit",
41
+ "-l",
42
+ type=int,
43
+ default=10,
44
+ metavar="N",
45
+ help=t("help_search_limit"),
46
+ )
47
+ parser.add_argument(
48
+ "--site-limit",
49
+ type=int,
50
+ default=5,
51
+ metavar="M",
52
+ help=t("help_search_site_limit"),
53
+ )
54
+
55
+ parser.set_defaults(func=handle_search)
56
+
57
+
58
+ def handle_search(args: Namespace) -> None:
59
+ """
60
+ Handler for the `search` subcommand. Loads config, runs the search,
61
+ prompts the user to pick one result, then kicks off download.
62
+ """
63
+ sites: Sequence[str] | None = args.site or None
64
+ keyword: str = args.keyword
65
+ overall_limit = max(1, args.limit)
66
+ per_site_limit = max(1, args.site_limit)
67
+ config_path: Path | None = Path(args.config) if args.config else None
68
+
69
+ try:
70
+ config_data = load_config(config_path)
71
+ except Exception as e:
72
+ print(t("download_config_load_fail", err=str(e)))
73
+ return
74
+
75
+ results = search(
76
+ keyword=keyword,
77
+ sites=sites,
78
+ limit=overall_limit,
79
+ per_site_limit=per_site_limit,
80
+ )
81
+
82
+ chosen = _prompt_user_select(results)
83
+ if chosen is None:
84
+ # user cancelled or no valid choice
85
+ return
86
+
87
+ adapter = ConfigAdapter(config=config_data, site=chosen["site"])
88
+ books: list[BookConfig] = [{"book_id": chosen["book_id"]}]
89
+ asyncio.run(_download(adapter, chosen["site"], books))
90
+
91
+
92
+ def _prompt_user_select(
93
+ results: Sequence[SearchResult],
94
+ max_attempts: int = 3,
95
+ ) -> SearchResult | None:
96
+ """
97
+ Display a numbered list of results and prompt the user to pick one.
98
+
99
+ :param results: A list of SearchResult dicts.
100
+ :param max_attempts: How many bad inputs to tolerate before giving up.
101
+ :return: The chosen SearchResult, or None if cancelled/failed.
102
+ """
103
+ if not results:
104
+ print(t("no_results"))
105
+ return None
106
+
107
+ # Show choices
108
+ for i, r in enumerate(results, start=1):
109
+ print(f"[{i}] {r['title']} - {r['author']} ({r['site']}, id: {r['book_id']})")
110
+
111
+ attempts = 0
112
+ while attempts < max_attempts:
113
+ choice = input(t("prompt_select_index")).strip()
114
+ if choice == "":
115
+ return None
116
+ if choice.isdigit():
117
+ idx = int(choice)
118
+ if 1 <= idx <= len(results):
119
+ return results[idx - 1]
120
+ print(t("invalid_selection"))
121
+ attempts += 1
122
+
123
+ return None
@@ -8,20 +8,13 @@ Unified interface for loading and adapting configuration files.
8
8
  This module provides:
9
9
  - load_config: loads YAML config from file path with fallback support
10
10
  - ConfigAdapter: maps raw config + site name to structured config models
11
- - Configuration dataclasses: RequesterConfig, DownloaderConfig, etc.
12
11
  """
13
12
 
14
- from .adapter import ConfigAdapter
15
- from .loader import load_config, save_config_file
16
- from .site_rules import (
17
- load_site_rules,
18
- save_rules_as_json,
19
- )
20
-
21
13
  __all__ = [
22
14
  "load_config",
23
15
  "save_config_file",
24
16
  "ConfigAdapter",
25
- "load_site_rules",
26
- "save_rules_as_json",
27
17
  ]
18
+
19
+ from .adapter import ConfigAdapter
20
+ from .loader import load_config, save_config_file