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
@@ -6,7 +6,7 @@ novel_downloader
6
6
  Core package for the Novel Downloader project.
7
7
  """
8
8
 
9
- __version__ = "1.3.3"
9
+ __version__ = "1.4.0"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.cli.clean
4
- -----------------------------
4
+ --------------------------
5
5
 
6
+ CLI subcommands for clean resources.
6
7
  """
7
8
 
8
9
  import shutil
10
+ from argparse import Namespace, _SubParsersAction
9
11
  from pathlib import Path
10
12
 
11
- import click
12
-
13
13
  from novel_downloader.utils.constants import (
14
14
  CONFIG_DIR,
15
15
  DATA_DIR,
@@ -21,18 +21,108 @@ from novel_downloader.utils.constants import (
21
21
  from novel_downloader.utils.i18n import t
22
22
 
23
23
 
24
- def delete_path(p: Path) -> None:
24
+ def register_clean_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
25
+ parser = subparsers.add_parser("clean", help=t("help_clean"))
26
+
27
+ parser.add_argument("--logs", action="store_true", help=t("clean_logs"))
28
+ parser.add_argument("--cache", action="store_true", help=t("clean_cache"))
29
+ parser.add_argument("--data", action="store_true", help=t("clean_data"))
30
+ parser.add_argument("--config", action="store_true", help=t("clean_config"))
31
+ parser.add_argument("--models", action="store_true", help=t("clean_models"))
32
+ parser.add_argument("--all", action="store_true", help=t("clean_all"))
33
+ parser.add_argument("-y", "--yes", action="store_true", help=t("clean_yes"))
34
+
35
+ parser.add_argument("--hf-cache", action="store_true", help=t("clean_hf_cache"))
36
+ parser.add_argument(
37
+ "--hf-cache-all", action="store_true", help=t("clean_hf_cache_all")
38
+ )
39
+
40
+ parser.set_defaults(func=handle_clean)
41
+
42
+
43
+ def handle_clean(args: Namespace) -> None:
44
+ targets: list[Path] = []
45
+
46
+ if args.hf_cache_all:
47
+ try:
48
+ if _clean_model_repo_cache(all=True):
49
+ print(t("clean_hf_cache_all_done"))
50
+ except Exception as e:
51
+ print(t("clean_hf_cache_all_fail", err=str(e)))
52
+ elif args.hf_cache:
53
+ try:
54
+ if _clean_model_repo_cache(repo_id=REC_CHAR_MODEL_REPO):
55
+ print(t("clean_hf_model_done", repo=REC_CHAR_MODEL_REPO))
56
+ else:
57
+ print(t("clean_hf_model_not_found", repo=REC_CHAR_MODEL_REPO))
58
+ except Exception as e:
59
+ print(t("clean_hf_model_fail", err=str(e)))
60
+
61
+ if args.all:
62
+ if not args.yes:
63
+ confirm = prompt(t("clean_confirm"), default="n")
64
+ if confirm.lower() != "y":
65
+ print(t("clean_cancelled"))
66
+ return
67
+ targets = [
68
+ LOGGER_DIR,
69
+ JS_SCRIPT_DIR,
70
+ DATA_DIR,
71
+ CONFIG_DIR,
72
+ MODEL_CACHE_DIR,
73
+ ]
74
+ else:
75
+ if args.logs:
76
+ targets.append(LOGGER_DIR)
77
+ if args.cache:
78
+ targets.append(JS_SCRIPT_DIR)
79
+ if args.data:
80
+ targets.append(DATA_DIR)
81
+ if args.config:
82
+ targets.append(CONFIG_DIR)
83
+ if args.models:
84
+ targets.append(MODEL_CACHE_DIR)
85
+
86
+ if not targets and not args.hf_cache and not args.hf_cache_all:
87
+ print(t("clean_nothing"))
88
+ return
89
+
90
+ for path in targets:
91
+ _delete_path(path)
92
+
93
+
94
+ def prompt(message: str, default: str = "n") -> str:
95
+ """
96
+ Prompt the user for input with a default option.
97
+
98
+ :param message: The prompt message to display to the user.
99
+ :param default: The default value to use if the user provides no input ("y" or "n").
100
+ :return: The user's input (lowercased), or the default value if no input is given.
101
+ """
102
+ try:
103
+ full_prompt = f"{message} [{'Y/n' if default.lower() == 'y' else 'y/N'}]: "
104
+ response = input(full_prompt).strip().lower()
105
+ return response if response else default.lower()
106
+ except (KeyboardInterrupt, EOFError):
107
+ print("\n" + "Cancelled.")
108
+ return default.lower()
109
+
110
+
111
+ def _delete_path(p: Path) -> None:
25
112
  if p.exists():
26
113
  if p.is_file():
27
114
  p.unlink()
28
115
  else:
29
116
  shutil.rmtree(p, ignore_errors=True)
30
- click.echo(f"[clean] {t('clean_deleted')}: {p}")
117
+ print(f"[clean] {t('clean_deleted')}: {p}")
31
118
  else:
32
- click.echo(f"[clean] {t('clean_not_found')}: {p}")
119
+ print(f"[clean] {t('clean_not_found')}: {p}")
33
120
 
34
121
 
35
- def clean_model_repo_cache(repo_id: str | None = None, all: bool = False) -> bool:
122
+ def _clean_model_repo_cache(
123
+ repo_id: str | None = None,
124
+ all: bool = False,
125
+ ) -> bool:
36
126
  """
37
127
  Delete Hugging Face cache for a specific repo.
38
128
  """
@@ -53,74 +143,3 @@ def clean_model_repo_cache(repo_id: str | None = None, all: bool = False) -> boo
53
143
  print(f"[clean] Will free {strategy.expected_freed_size_str}")
54
144
  strategy.execute()
55
145
  return True
56
-
57
-
58
- @click.command(name="clean", help=t("help_clean")) # type: ignore
59
- @click.option("--logs", is_flag=True, help=t("clean_logs")) # type: ignore
60
- @click.option("--cache", is_flag=True, help=t("clean_cache")) # type: ignore
61
- @click.option("--data", is_flag=True, help=t("clean_data")) # type: ignore
62
- @click.option("--config", is_flag=True, help=t("clean_config")) # type: ignore
63
- @click.option("--models", is_flag=True, help=t("clean_models")) # type: ignore
64
- @click.option("--hf-cache", is_flag=True, help=t("clean_hf_cache")) # type: ignore
65
- @click.option("--hf-cache-all", is_flag=True, help=t("clean_hf_cache_all")) # type: ignore
66
- @click.option("--all", is_flag=True, help=t("clean_all")) # type: ignore
67
- @click.option("--yes", is_flag=True, help=t("clean_yes")) # type: ignore
68
- def clean_cli(
69
- logs: bool,
70
- cache: bool,
71
- data: bool,
72
- config: bool,
73
- models: bool,
74
- hf_cache: bool,
75
- hf_cache_all: bool,
76
- all: bool,
77
- yes: bool,
78
- ) -> None:
79
- targets: list[Path] = []
80
-
81
- if all:
82
- if not yes:
83
- confirm = click.prompt(t("clean_confirm"), default="n")
84
- if confirm.lower() != "y":
85
- click.echo(t("clean_cancelled"))
86
- return
87
- targets = [
88
- LOGGER_DIR,
89
- JS_SCRIPT_DIR,
90
- DATA_DIR,
91
- CONFIG_DIR,
92
- MODEL_CACHE_DIR,
93
- ]
94
- else:
95
- if logs:
96
- targets.append(LOGGER_DIR)
97
- if cache:
98
- targets.append(JS_SCRIPT_DIR)
99
- if data:
100
- targets.append(DATA_DIR)
101
- if config:
102
- targets.append(CONFIG_DIR)
103
- if models:
104
- targets.append(MODEL_CACHE_DIR)
105
-
106
- if hf_cache_all:
107
- try:
108
- if clean_model_repo_cache(all=True):
109
- click.echo(t("clean_hf_cache_all_done"))
110
- except Exception as e:
111
- click.echo(t("clean_hf_cache_all_fail", err=e))
112
- elif hf_cache:
113
- try:
114
- if clean_model_repo_cache(REC_CHAR_MODEL_REPO):
115
- click.echo(t("clean_hf_model_done", repo=REC_CHAR_MODEL_REPO))
116
- else:
117
- click.echo(t("clean_hf_model_not_found", repo=REC_CHAR_MODEL_REPO))
118
- except Exception as e:
119
- click.echo(t("clean_hf_model_fail", err=e))
120
-
121
- if not targets and not hf_cache and not hf_cache_all:
122
- click.echo(t("clean_nothing"))
123
- return
124
-
125
- for path in targets:
126
- delete_path(path)
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.cli.config
4
+ ---------------------------
5
+
6
+ CLI subcommands for configuration management.
7
+ """
8
+
9
+ import shutil
10
+ from argparse import Namespace, _SubParsersAction
11
+ from importlib.resources import as_file
12
+ from pathlib import Path
13
+
14
+ from novel_downloader.config import save_config_file, save_rules_as_json
15
+ from novel_downloader.utils.constants import DEFAULT_SETTINGS_PATHS
16
+ from novel_downloader.utils.i18n import t
17
+ from novel_downloader.utils.logger import setup_logging
18
+ from novel_downloader.utils.state import state_mgr
19
+
20
+ # from novel_downloader.utils.hash_store import img_hash_store
21
+
22
+
23
+ def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
24
+ parser = subparsers.add_parser("config", help=t("help_config"))
25
+ config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
26
+
27
+ _register_init(config_subparsers)
28
+ _register_set_lang(config_subparsers)
29
+ _register_set_config(config_subparsers)
30
+ _register_update_rules(config_subparsers)
31
+ _register_set_cookies(config_subparsers)
32
+ # _register_add_hash(config_subparsers)
33
+
34
+
35
+ def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
36
+ parser = subparsers.add_parser("init", help=t("settings_init_help"))
37
+ parser.add_argument(
38
+ "--force", action="store_true", help=t("settings_init_force_help")
39
+ )
40
+ parser.set_defaults(func=_handle_init)
41
+
42
+
43
+ def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
44
+ parser = subparsers.add_parser("set-lang", help=t("settings_set_lang_help"))
45
+ parser.add_argument("lang", choices=["zh", "en"], help="Language code")
46
+ parser.set_defaults(func=_handle_set_lang)
47
+
48
+
49
+ def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
50
+ parser = subparsers.add_parser("set-config", help=t("settings_set_config_help"))
51
+ parser.add_argument("path", type=str, help="Path to YAML config file")
52
+ parser.set_defaults(func=_handle_set_config)
53
+
54
+
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
+ def _handle_init(args: Namespace) -> None:
75
+ setup_logging()
76
+ cwd = Path.cwd()
77
+
78
+ for resource in DEFAULT_SETTINGS_PATHS:
79
+ target_path = cwd / resource.name
80
+ should_copy = True
81
+
82
+ if target_path.exists():
83
+ if args.force:
84
+ print(t("settings_init_overwrite", filename=resource.name))
85
+ else:
86
+ print(t("settings_init_exists", filename=resource.name))
87
+ resp = (
88
+ input(
89
+ t("settings_init_confirm_overwrite", filename=resource.name)
90
+ + " [y/N]: "
91
+ )
92
+ .strip()
93
+ .lower()
94
+ )
95
+ should_copy = resp == "y"
96
+
97
+ if not should_copy:
98
+ print(t("settings_init_skip", filename=resource.name))
99
+ continue
100
+
101
+ try:
102
+ with as_file(resource) as actual_path:
103
+ shutil.copy(actual_path, target_path)
104
+ print(t("settings_init_copy", filename=resource.name))
105
+ except Exception as e:
106
+ print(t("settings_init_error", filename=resource.name, err=str(e)))
107
+ raise
108
+
109
+
110
+ def _handle_set_lang(args: Namespace) -> None:
111
+ state_mgr.set_language(args.lang)
112
+ print(t("settings_set_lang", lang=args.lang))
113
+
114
+
115
+ def _handle_set_config(args: Namespace) -> None:
116
+ try:
117
+ save_config_file(args.path)
118
+ print(t("settings_set_config", path=args.path))
119
+ except Exception as e:
120
+ print(t("settings_set_config_fail", err=str(e)))
121
+ 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"))
@@ -3,126 +3,171 @@
3
3
  novel_downloader.cli.download
4
4
  -----------------------------
5
5
 
6
- Download full novels by book IDs
7
- (supports config files, site switching, and localization prompts).
6
+ Download novels from supported sites via CLI.
8
7
  """
9
8
 
10
9
  import asyncio
11
-
12
- import click
13
- from click import Context
10
+ import getpass
11
+ from argparse import Namespace, _SubParsersAction
12
+ from dataclasses import asdict
13
+ from pathlib import Path
14
+ from typing import Any
14
15
 
15
16
  from novel_downloader.config import ConfigAdapter, load_config
16
17
  from novel_downloader.core.factory import (
17
- get_async_downloader,
18
- get_async_requester,
19
- # get_downloader,
18
+ get_downloader,
19
+ get_exporter,
20
+ get_fetcher,
20
21
  get_parser,
21
- # get_requester,
22
- get_saver,
23
- get_sync_downloader,
24
- get_sync_requester,
25
22
  )
23
+ from novel_downloader.core.interfaces import FetcherProtocol
24
+ from novel_downloader.models import LoginField
25
+ from novel_downloader.utils.cookies import resolve_cookies
26
26
  from novel_downloader.utils.i18n import t
27
27
  from novel_downloader.utils.logger import setup_logging
28
28
 
29
29
 
30
- @click.command(
31
- name="download",
32
- help=t("download_help"),
33
- short_help=t("download_short_help"),
34
- ) # type: ignore
35
- @click.argument("book_ids", nargs=-1) # type: ignore
36
- @click.option(
37
- "--site",
38
- default="qidian",
39
- show_default=True,
40
- help=t("download_option_site", default="qidian"),
41
- ) # type: ignore
42
- @click.pass_context # type: ignore
43
- def download_cli(ctx: Context, book_ids: list[str], site: str) -> None:
44
- """Download full novels by book IDs."""
45
- config_path = ctx.obj.get("config_path")
46
-
47
- click.echo(t("download_using_config", path=config_path))
48
- click.echo(t("download_site_info", site=site))
49
-
50
- config_data = load_config(config_path)
51
- adapter = ConfigAdapter(config=config_data, site=site)
30
+ def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
31
+ parser = subparsers.add_parser("download", help=t("help_download"))
32
+
33
+ parser.add_argument("book_ids", nargs="*", help=t("download_book_ids"))
34
+ parser.add_argument(
35
+ "--site", default="qidian", help=t("download_option_site", default="qidian")
36
+ )
37
+ parser.add_argument("--config", type=str, help=t("help_config"))
38
+
39
+ parser.set_defaults(func=handle_download)
52
40
 
53
- # Retrieve each sub-component's configuration from the adapter
54
- requester_cfg = adapter.get_requester_config()
55
- downloader_cfg = adapter.get_downloader_config()
56
- parser_cfg = adapter.get_parser_config()
57
- saver_cfg = adapter.get_saver_config()
58
41
 
59
- click.echo(t("download_site_mode", mode=downloader_cfg.mode))
42
+ def handle_download(args: Namespace) -> None:
43
+ setup_logging()
44
+
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 or []
48
+
49
+ print(t("download_site_info", site=site))
50
+
51
+ try:
52
+ config_data = load_config(config_path)
53
+ except Exception as e:
54
+ print(t("download_config_load_fail", err=str(e)))
55
+ return
56
+
57
+ adapter = ConfigAdapter(config=config_data, site=site)
60
58
 
61
- # If no book_ids provided on the command line, try to load them from config
62
59
  if not book_ids:
63
60
  try:
64
61
  book_ids = adapter.get_book_ids()
65
62
  except Exception as e:
66
- click.echo(t("download_fail_get_ids", err=e))
63
+ print(t("download_fail_get_ids", err=str(e)))
67
64
  return
68
65
 
69
- # Filter out placeholder/example IDs
70
66
  invalid_ids = {"0000000000"}
71
67
  valid_book_ids = set(book_ids) - invalid_ids
72
68
 
73
69
  if not book_ids:
74
- click.echo(t("download_no_ids"))
70
+ print(t("download_no_ids"))
75
71
  return
76
72
 
77
73
  if not valid_book_ids:
78
- click.echo(t("download_only_example", example="0000000000"))
79
- click.echo(t("download_edit_config"))
74
+ print(t("download_only_example", example="0000000000"))
75
+ print(t("download_edit_config"))
80
76
  return
81
77
 
82
- # Initialize the requester, parser, saver, and downloader components
83
- if downloader_cfg.mode == "async":
84
- setup_logging()
85
-
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
- )
78
+ asyncio.run(_download(adapter, site, valid_book_ids))
79
+
80
+
81
+ async def _download(
82
+ adapter: ConfigAdapter,
83
+ site: str,
84
+ valid_book_ids: set[str],
85
+ ) -> None:
86
+ downloader_cfg = adapter.get_downloader_config()
87
+ fetcher_cfg = adapter.get_fetcher_config()
88
+ parser_cfg = adapter.get_parser_config()
89
+ exporter_cfg = adapter.get_exporter_config()
90
+
91
+ parser = get_parser(site, parser_cfg)
92
+ exporter = get_exporter(site, exporter_cfg)
93
+ setup_logging()
97
94
 
98
- prepare = getattr(async_downloader, "prepare", None)
99
- if prepare and asyncio.iscoroutinefunction(prepare):
100
- await prepare()
101
-
102
- for book_id in valid_book_ids:
103
- click.echo(t("download_downloading", book_id=book_id, site=site))
104
- await async_downloader.download_one(book_id)
105
-
106
- await async_requester.close()
107
-
108
- asyncio.run(async_download_all())
109
- else:
110
- sync_requester = get_sync_requester(site, requester_cfg)
111
- sync_parser = get_parser(site, parser_cfg)
112
- sync_saver = get_saver(site, saver_cfg)
113
- setup_logging()
114
- sync_downloader = get_sync_downloader(
115
- requester=sync_requester,
116
- parser=sync_parser,
117
- saver=sync_saver,
95
+ async with get_fetcher(site, fetcher_cfg) as fetcher:
96
+ if downloader_cfg.login_required and not await fetcher.load_state():
97
+ login_data = await _prompt_login_fields(
98
+ fetcher, fetcher.login_fields, downloader_cfg
99
+ )
100
+ if not await fetcher.login(**login_data):
101
+ print(t("download_login_failed"))
102
+ return
103
+ await fetcher.save_state()
104
+
105
+ downloader = get_downloader(
106
+ fetcher=fetcher,
107
+ parser=parser,
108
+ exporter=exporter,
118
109
  site=site,
119
110
  config=downloader_cfg,
120
111
  )
121
112
 
122
113
  for book_id in valid_book_ids:
123
- click.echo(t("download_downloading", book_id=book_id, site=site))
124
- sync_downloader.download_one(book_id)
125
-
126
- sync_requester.close()
127
-
128
- return
114
+ print(t("download_downloading", book_id=book_id, site=site))
115
+ await downloader.download(book_id, progress_hook=_print_progress)
116
+
117
+ if downloader_cfg.login_required and fetcher.is_logged_in:
118
+ await fetcher.save_state()
119
+
120
+
121
+ async def _prompt_login_fields(
122
+ fetcher: FetcherProtocol,
123
+ fields: list[LoginField],
124
+ cfg: Any = None,
125
+ ) -> dict[str, Any]:
126
+ result: dict[str, Any] = {}
127
+ cfg_dict = asdict(cfg) if cfg else {}
128
+
129
+ for field in fields:
130
+ print(f"\n{field.label} ({field.name})")
131
+ if field.description:
132
+ print(f"{t('login_description')}: {field.description}")
133
+ if field.placeholder:
134
+ print(f"{t('login_hint')}: {field.placeholder}")
135
+
136
+ if field.type == "manual_login":
137
+ await fetcher.set_interactive_mode(True)
138
+ input(t("login_manual_prompt"))
139
+ await fetcher.set_interactive_mode(False)
140
+ continue
141
+
142
+ existing_value = cfg_dict.get(field.name, "").strip()
143
+ if existing_value:
144
+ result[field.name] = existing_value
145
+ print(t("login_use_config"))
146
+ continue
147
+
148
+ value: str | dict[str, str]
149
+ while True:
150
+ if field.type == "password":
151
+ value = getpass.getpass(t("login_enter_password"))
152
+ elif field.type == "cookie":
153
+ value = input(t("login_enter_cookie"))
154
+ value = resolve_cookies(value)
155
+ else:
156
+ value = input(t("login_enter_value"))
157
+
158
+ if not value and field.default:
159
+ value = field.default
160
+
161
+ if not value and field.required:
162
+ print(t("login_required_field"))
163
+ else:
164
+ break
165
+
166
+ result[field.name] = value
167
+
168
+ return result
169
+
170
+
171
+ async def _print_progress(done: int, total: int) -> None:
172
+ percent = done / total * 100
173
+ print(f"下载进度: {done}/{total} 章 ({percent:.2f}%)")