novel-downloader 1.5.0__py3-none-any.whl → 2.0.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 (241) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +77 -64
  6. novel_downloader/cli/export.py +16 -20
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +65 -105
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +1 -0
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +14 -9
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +17 -11
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +61 -66
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  100. novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
  101. novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
  102. novel_downloader/core/parsers/qidian/main_parser.py +11 -38
  103. novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
  104. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  105. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  106. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  107. novel_downloader/core/parsers/quanben5.py +103 -0
  108. novel_downloader/core/parsers/registry.py +5 -16
  109. novel_downloader/core/parsers/sfacg.py +38 -45
  110. novel_downloader/core/parsers/shencou.py +215 -0
  111. novel_downloader/core/parsers/shuhaige.py +111 -0
  112. novel_downloader/core/parsers/tongrenquan.py +116 -0
  113. novel_downloader/core/parsers/ttkan.py +132 -0
  114. novel_downloader/core/parsers/wanbengo.py +191 -0
  115. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  116. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  117. novel_downloader/core/parsers/xs63b.py +161 -0
  118. novel_downloader/core/parsers/xshbook.py +134 -0
  119. novel_downloader/core/parsers/yamibo.py +87 -131
  120. novel_downloader/core/parsers/yibige.py +166 -0
  121. novel_downloader/core/searchers/__init__.py +34 -3
  122. novel_downloader/core/searchers/aaatxt.py +107 -0
  123. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  124. novel_downloader/core/searchers/base.py +112 -36
  125. novel_downloader/core/searchers/dxmwx.py +105 -0
  126. novel_downloader/core/searchers/eightnovel.py +84 -0
  127. novel_downloader/core/searchers/esjzone.py +43 -25
  128. novel_downloader/core/searchers/hetushu.py +92 -0
  129. novel_downloader/core/searchers/i25zw.py +93 -0
  130. novel_downloader/core/searchers/ixdzs8.py +107 -0
  131. novel_downloader/core/searchers/jpxs123.py +107 -0
  132. novel_downloader/core/searchers/piaotia.py +100 -0
  133. novel_downloader/core/searchers/qbtr.py +106 -0
  134. novel_downloader/core/searchers/qianbi.py +74 -40
  135. novel_downloader/core/searchers/quanben5.py +144 -0
  136. novel_downloader/core/searchers/registry.py +24 -8
  137. novel_downloader/core/searchers/shuhaige.py +124 -0
  138. novel_downloader/core/searchers/tongrenquan.py +110 -0
  139. novel_downloader/core/searchers/ttkan.py +92 -0
  140. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  141. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  142. novel_downloader/core/searchers/xs63b.py +104 -0
  143. novel_downloader/locales/en.json +31 -82
  144. novel_downloader/locales/zh.json +32 -83
  145. novel_downloader/models/__init__.py +21 -22
  146. novel_downloader/models/book.py +44 -0
  147. novel_downloader/models/config.py +4 -37
  148. novel_downloader/models/login.py +1 -1
  149. novel_downloader/models/search.py +5 -0
  150. novel_downloader/resources/config/settings.toml +8 -70
  151. novel_downloader/resources/json/xiguashuwu.json +718 -0
  152. novel_downloader/utils/__init__.py +13 -22
  153. novel_downloader/utils/chapter_storage.py +3 -2
  154. novel_downloader/utils/constants.py +4 -29
  155. novel_downloader/utils/cookies.py +6 -18
  156. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  157. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  158. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  159. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  160. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  161. novel_downloader/utils/epub/__init__.py +1 -1
  162. novel_downloader/utils/epub/constants.py +57 -16
  163. novel_downloader/utils/epub/documents.py +88 -194
  164. novel_downloader/utils/epub/models.py +0 -14
  165. novel_downloader/utils/epub/utils.py +63 -96
  166. novel_downloader/utils/file_utils/__init__.py +2 -23
  167. novel_downloader/utils/file_utils/io.py +3 -113
  168. novel_downloader/utils/file_utils/sanitize.py +0 -4
  169. novel_downloader/utils/fontocr.py +207 -0
  170. novel_downloader/utils/logger.py +8 -16
  171. novel_downloader/utils/network.py +2 -2
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/time_utils/__init__.py +5 -11
  176. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  177. novel_downloader/utils/time_utils/sleep_utils.py +4 -8
  178. novel_downloader/web/__init__.py +13 -0
  179. novel_downloader/web/components/__init__.py +11 -0
  180. novel_downloader/web/components/navigation.py +35 -0
  181. novel_downloader/web/main.py +66 -0
  182. novel_downloader/web/pages/__init__.py +17 -0
  183. novel_downloader/web/pages/download.py +78 -0
  184. novel_downloader/web/pages/progress.py +147 -0
  185. novel_downloader/web/pages/search.py +329 -0
  186. novel_downloader/web/services/__init__.py +17 -0
  187. novel_downloader/web/services/client_dialog.py +164 -0
  188. novel_downloader/web/services/cred_broker.py +113 -0
  189. novel_downloader/web/services/cred_models.py +35 -0
  190. novel_downloader/web/services/task_manager.py +264 -0
  191. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  192. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  193. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  194. novel_downloader/core/downloaders/biquge.py +0 -29
  195. novel_downloader/core/downloaders/esjzone.py +0 -29
  196. novel_downloader/core/downloaders/linovelib.py +0 -29
  197. novel_downloader/core/downloaders/sfacg.py +0 -29
  198. novel_downloader/core/downloaders/yamibo.py +0 -29
  199. novel_downloader/core/exporters/biquge.py +0 -22
  200. novel_downloader/core/exporters/esjzone.py +0 -22
  201. novel_downloader/core/exporters/qianbi.py +0 -22
  202. novel_downloader/core/exporters/sfacg.py +0 -22
  203. novel_downloader/core/exporters/yamibo.py +0 -22
  204. novel_downloader/core/fetchers/base/__init__.py +0 -14
  205. novel_downloader/core/fetchers/base/browser.py +0 -422
  206. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  207. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  208. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  209. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  210. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  211. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  212. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  213. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  214. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  215. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  216. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  217. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  218. novel_downloader/core/parsers/biquge.py +0 -139
  219. novel_downloader/models/chapter.py +0 -25
  220. novel_downloader/models/types.py +0 -13
  221. novel_downloader/tui/__init__.py +0 -7
  222. novel_downloader/tui/app.py +0 -32
  223. novel_downloader/tui/main.py +0 -17
  224. novel_downloader/tui/screens/__init__.py +0 -14
  225. novel_downloader/tui/screens/home.py +0 -198
  226. novel_downloader/tui/screens/login.py +0 -74
  227. novel_downloader/tui/styles/home_layout.tcss +0 -79
  228. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  229. novel_downloader/utils/cache.py +0 -24
  230. novel_downloader/utils/fontocr/__init__.py +0 -22
  231. novel_downloader/utils/fontocr/hash_store.py +0 -280
  232. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  233. novel_downloader/utils/fontocr/model_loader.py +0 -69
  234. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  235. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  236. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  237. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  238. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  239. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  240. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  241. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.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.5.0"
9
+ __version__ = "2.0.0"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -6,8 +6,6 @@ novel_downloader.cli
6
6
  This module exposes the CLI entry point.
7
7
  """
8
8
 
9
- __all__ = [
10
- "cli_main",
11
- ]
9
+ __all__ = ["cli_main"]
12
10
 
13
11
  from .main import cli_main
@@ -6,71 +6,45 @@ novel_downloader.cli.clean
6
6
  CLI subcommands for clean resources.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import shutil
10
12
  from argparse import Namespace, _SubParsersAction
11
13
  from pathlib import Path
12
14
 
15
+ from novel_downloader.cli import ui
13
16
  from novel_downloader.utils.constants import (
14
17
  CONFIG_DIR,
15
18
  DATA_DIR,
16
19
  JS_SCRIPT_DIR,
17
20
  LOGGER_DIR,
18
- MODEL_CACHE_DIR,
19
- REC_CHAR_MODEL_REPO,
20
21
  )
21
22
  from novel_downloader.utils.i18n import t
22
23
 
23
24
 
24
25
  def register_clean_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
26
+ """Register the `clean` subcommand and its options."""
25
27
  parser = subparsers.add_parser("clean", help=t("help_clean"))
26
28
 
27
29
  parser.add_argument("--logs", action="store_true", help=t("clean_logs"))
28
30
  parser.add_argument("--cache", action="store_true", help=t("clean_cache"))
29
31
  parser.add_argument("--data", action="store_true", help=t("clean_data"))
30
32
  parser.add_argument("--config", action="store_true", help=t("clean_config"))
31
- parser.add_argument("--models", action="store_true", help=t("clean_models"))
32
33
  parser.add_argument("--all", action="store_true", help=t("clean_all"))
33
34
  parser.add_argument("-y", "--yes", action="store_true", help=t("clean_yes"))
34
35
 
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
36
  parser.set_defaults(func=handle_clean)
41
37
 
42
38
 
43
39
  def handle_clean(args: Namespace) -> None:
40
+ """Handle the `clean` subcommand."""
44
41
  targets: list[Path] = []
45
42
 
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
43
  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
- ]
44
+ if not args.yes and not ui.confirm(t("clean_confirm"), default=False):
45
+ ui.warn(t("clean_cancelled"))
46
+ return
47
+ targets = [LOGGER_DIR, JS_SCRIPT_DIR, DATA_DIR, CONFIG_DIR]
74
48
  else:
75
49
  if args.logs:
76
50
  targets.append(LOGGER_DIR)
@@ -80,66 +54,25 @@ def handle_clean(args: Namespace) -> None:
80
54
  targets.append(DATA_DIR)
81
55
  if args.config:
82
56
  targets.append(CONFIG_DIR)
83
- if args.models:
84
- targets.append(MODEL_CACHE_DIR)
85
57
 
86
- if not targets and not args.hf_cache and not args.hf_cache_all:
87
- print(t("clean_nothing"))
58
+ if not targets:
59
+ ui.warn(t("clean_nothing"))
88
60
  return
89
61
 
90
62
  for path in targets:
91
63
  _delete_path(path)
92
64
 
93
65
 
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
66
  def _delete_path(p: Path) -> None:
67
+ """Delete file or directory at `p`, printing a colored result line."""
112
68
  if p.exists():
113
- if p.is_file():
114
- p.unlink()
115
- else:
116
- shutil.rmtree(p, ignore_errors=True)
117
- print(f"[clean] {t('clean_deleted')}: {p}")
118
- else:
119
- print(f"[clean] {t('clean_not_found')}: {p}")
120
-
121
-
122
- def _clean_model_repo_cache(
123
- repo_id: str | None = None,
124
- all: bool = False,
125
- ) -> bool:
126
- """
127
- Delete Hugging Face cache for a specific repo.
128
- """
129
- from huggingface_hub import scan_cache_dir
130
-
131
- cache_info = scan_cache_dir()
132
-
133
- if all:
134
- targets = cache_info.repos
135
- elif repo_id:
136
- targets = [r for r in cache_info.repos if r.repo_id == repo_id]
69
+ try:
70
+ if p.is_file():
71
+ p.unlink()
72
+ else:
73
+ shutil.rmtree(p, ignore_errors=True)
74
+ ui.success(f"[clean] {t('clean_deleted')}: {p}")
75
+ except Exception as e:
76
+ ui.error(f"[clean] {t('clean_failed', path=p)}: {p} -> {e}")
137
77
  else:
138
- return False
139
-
140
- strategy = cache_info.delete_revisions(
141
- *[rev.commit_hash for r in targets for rev in r.revisions]
142
- )
143
- print(f"[clean] Will free {strategy.expected_freed_size_str}")
144
- strategy.execute()
145
- return True
78
+ ui.warn(f"[clean] {t('clean_not_found')}: {p}")
@@ -3,14 +3,17 @@
3
3
  novel_downloader.cli.config
4
4
  ---------------------------
5
5
 
6
- CLI subcommands for configuration management.
6
+ CLI subcommands for configuration file management.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import shutil
10
12
  from argparse import Namespace, _SubParsersAction
11
13
  from importlib.resources import as_file
12
14
  from pathlib import Path
13
15
 
16
+ from novel_downloader.cli import ui
14
17
  from novel_downloader.config import save_config_file
15
18
  from novel_downloader.utils.constants import DEFAULT_SETTINGS_PATHS
16
19
  from novel_downloader.utils.i18n import t
@@ -18,6 +21,7 @@ from novel_downloader.utils.state import state_mgr
18
21
 
19
22
 
20
23
  def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
24
+ """Register `config` with `init`, `set-lang`, and `set-config` subcommands."""
21
25
  parser = subparsers.add_parser("config", help=t("help_config"))
22
26
  config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
23
27
 
@@ -27,26 +31,30 @@ def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type:
27
31
 
28
32
 
29
33
  def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
30
- parser = subparsers.add_parser("init", help=t("settings_init_help"))
34
+ parser = subparsers.add_parser("init", help=t("config_init_help"))
31
35
  parser.add_argument(
32
- "--force", action="store_true", help=t("settings_init_force_help")
36
+ "--force", action="store_true", help=t("config_init_force_help")
33
37
  )
34
38
  parser.set_defaults(func=_handle_init)
35
39
 
36
40
 
37
41
  def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
38
- parser = subparsers.add_parser("set-lang", help=t("settings_set_lang_help"))
42
+ parser = subparsers.add_parser("set-lang", help=t("config_set_lang_help"))
39
43
  parser.add_argument("lang", choices=["zh", "en"], help="Language code")
40
44
  parser.set_defaults(func=_handle_set_lang)
41
45
 
42
46
 
43
47
  def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
44
- parser = subparsers.add_parser("set-config", help=t("settings_set_config_help"))
48
+ parser = subparsers.add_parser("set-config", help=t("config_set_config_help"))
45
49
  parser.add_argument("path", type=str, help="Path to YAML config file")
46
50
  parser.set_defaults(func=_handle_set_config)
47
51
 
48
52
 
49
53
  def _handle_init(args: Namespace) -> None:
54
+ """
55
+ Copy template settings files from package resources into the current working dir.
56
+ If the target file exists, optionally confirm overwrite (unless --force).
57
+ """
50
58
  cwd = Path.cwd()
51
59
 
52
60
  for resource in DEFAULT_SETTINGS_PATHS:
@@ -55,41 +63,38 @@ def _handle_init(args: Namespace) -> None:
55
63
 
56
64
  if target_path.exists():
57
65
  if args.force:
58
- print(t("settings_init_overwrite", filename=resource.name))
66
+ ui.warn(t("config_init_overwrite", filename=resource.name))
59
67
  else:
60
- print(t("settings_init_exists", filename=resource.name))
61
- resp = (
62
- input(
63
- t("settings_init_confirm_overwrite", filename=resource.name)
64
- + " [y/N]: "
65
- )
66
- .strip()
67
- .lower()
68
+ ui.info(t("config_init_exists", filename=resource.name))
69
+ should_copy = ui.confirm(
70
+ t("config_init_confirm_overwrite", filename=resource.name),
71
+ default=False,
68
72
  )
69
- should_copy = resp == "y"
70
73
 
71
74
  if not should_copy:
72
- print(t("settings_init_skip", filename=resource.name))
75
+ ui.warn(t("config_init_skip", filename=resource.name))
73
76
  continue
74
77
 
75
78
  try:
76
79
  with as_file(resource) as actual_path:
77
80
  shutil.copy(actual_path, target_path)
78
- print(t("settings_init_copy", filename=resource.name))
81
+ ui.success(t("config_init_copy", filename=resource.name))
79
82
  except Exception as e:
80
- print(t("settings_init_error", filename=resource.name, err=str(e)))
83
+ ui.error(t("config_init_error", filename=resource.name, err=str(e)))
81
84
  raise
82
85
 
83
86
 
84
87
  def _handle_set_lang(args: Namespace) -> None:
88
+ """Set the UI language and persist in state manager."""
85
89
  state_mgr.set_language(args.lang)
86
- print(t("settings_set_lang", lang=args.lang))
90
+ ui.success(t("config_set_lang", lang=args.lang))
87
91
 
88
92
 
89
93
  def _handle_set_config(args: Namespace) -> None:
94
+ """Persist a user-supplied TOML config path into the app config."""
90
95
  try:
91
96
  save_config_file(args.path)
92
- print(t("settings_set_config", path=args.path))
97
+ ui.success(t("config_set_config", path=args.path))
93
98
  except Exception as e:
94
- print(t("settings_set_config_fail", err=str(e)))
99
+ ui.error(t("config_set_config_fail", err=str(e)))
95
100
  raise
@@ -3,32 +3,28 @@
3
3
  novel_downloader.cli.download
4
4
  -----------------------------
5
5
 
6
- Download novels from supported sites via CLI.
6
+ Download novels from supported sites via the CLI.
7
7
  """
8
8
 
9
+ from __future__ import annotations
10
+
9
11
  import asyncio
10
- import getpass
11
12
  from argparse import Namespace, _SubParsersAction
12
13
  from collections.abc import Iterable
13
- from dataclasses import asdict
14
14
  from pathlib import Path
15
15
  from typing import Any
16
16
 
17
+ from novel_downloader.cli import ui
17
18
  from novel_downloader.config import ConfigAdapter, load_config
18
- from novel_downloader.core import (
19
- FetcherProtocol,
20
- get_downloader,
21
- get_exporter,
22
- get_fetcher,
23
- get_parser,
24
- )
19
+ from novel_downloader.core import get_downloader, get_exporter, get_fetcher, get_parser
25
20
  from novel_downloader.models import BookConfig, LoginField
26
- from novel_downloader.utils.cookies import resolve_cookies
21
+ from novel_downloader.utils.cookies import parse_cookies
27
22
  from novel_downloader.utils.i18n import t
28
23
  from novel_downloader.utils.logger import setup_logging
29
24
 
30
25
 
31
26
  def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
27
+ """Register the `download` subcommand and its options."""
32
28
  parser = subparsers.add_parser("download", help=t("help_download"))
33
29
 
34
30
  parser.add_argument("book_ids", nargs="*", help=t("download_book_ids"))
@@ -40,24 +36,30 @@ def register_download_subcommand(subparsers: _SubParsersAction) -> None: # type
40
36
  parser.add_argument("--start", type=str, help=t("download_option_start"))
41
37
  parser.add_argument("--end", type=str, help=t("download_option_end"))
42
38
 
39
+ parser.add_argument(
40
+ "--no-export",
41
+ action="store_true",
42
+ help=t("download_option_no_export"),
43
+ )
44
+
43
45
  parser.set_defaults(func=handle_download)
44
46
 
45
47
 
46
48
  def handle_download(args: Namespace) -> None:
49
+ """Handle the `download` subcommand."""
47
50
  site: str = args.site
48
51
  config_path: Path | None = Path(args.config) if args.config else None
49
52
  book_ids: list[BookConfig] = _cli_args_to_book_configs(
50
- args.book_ids,
51
- args.start,
52
- args.end,
53
+ args.book_ids, args.start, args.end
53
54
  )
55
+ no_export: bool = getattr(args, "no_export", False)
54
56
 
55
- print(t("download_site_info", site=site))
57
+ ui.info(t("download_site_info", site=site))
56
58
 
57
59
  try:
58
60
  config_data = load_config(config_path)
59
61
  except Exception as e:
60
- print(t("download_config_load_fail", err=str(e)))
62
+ ui.error(t("download_config_load_fail", err=str(e)))
61
63
  return
62
64
 
63
65
  adapter = ConfigAdapter(config=config_data, site=site)
@@ -66,21 +68,21 @@ def handle_download(args: Namespace) -> None:
66
68
  try:
67
69
  book_ids = adapter.get_book_ids()
68
70
  except Exception as e:
69
- print(t("download_fail_get_ids", err=str(e)))
71
+ ui.error(t("download_fail_get_ids", err=str(e)))
70
72
  return
71
73
 
72
74
  valid_books = _filter_valid_book_configs(book_ids)
73
75
 
74
76
  if not book_ids:
75
- print(t("download_no_ids"))
77
+ ui.warn(t("download_no_ids"))
76
78
  return
77
79
 
78
80
  if not valid_books:
79
- print(t("download_only_example", example="0000000000"))
80
- print(t("download_edit_config"))
81
+ ui.warn(t("download_only_example", example="0000000000"))
82
+ ui.info(t("download_edit_config"))
81
83
  return
82
84
 
83
- asyncio.run(_download(adapter, site, valid_books))
85
+ asyncio.run(_download(adapter, site, valid_books, no_export=no_export))
84
86
 
85
87
 
86
88
  def _cli_args_to_book_configs(
@@ -89,14 +91,13 @@ def _cli_args_to_book_configs(
89
91
  end_id: str | None,
90
92
  ) -> list[BookConfig]:
91
93
  """
92
- Convert CLI book_ids and optional --start/--end into a list of BookConfig.
93
- Only the first book_id uses start/end; others are minimal.
94
+ Convert CLI arguments into a list of `BookConfig`.
95
+ Only the first book_id takes `start_id`/`end_id`.
94
96
  """
95
97
  if not book_ids:
96
98
  return []
97
99
 
98
100
  result: list[BookConfig] = []
99
-
100
101
  first: BookConfig = {"book_id": book_ids[0]}
101
102
  if start_id:
102
103
  first["start_id"] = start_id
@@ -115,9 +116,11 @@ def _filter_valid_book_configs(
115
116
  invalid_ids: Iterable[str] = ("0000000000",),
116
117
  ) -> list[BookConfig]:
117
118
  """
118
- Filter a list of BookConfig:
119
- - Removes entries with invalid or placeholder book_ids
120
- - Deduplicates based on book_id while preserving order
119
+ Filter out placeholder or duplicate book IDs, preserving order.
120
+
121
+ :param books: The list to filter.
122
+ :param invalid_ids: A set/iterable of IDs to treat as invalid.
123
+ :return: De-duplicated, valid list.
121
124
  """
122
125
  seen = set(invalid_ids)
123
126
  result: list[BookConfig] = []
@@ -136,88 +139,96 @@ async def _download(
136
139
  adapter: ConfigAdapter,
137
140
  site: str,
138
141
  valid_books: list[BookConfig],
142
+ *,
143
+ no_export: bool = False,
139
144
  ) -> None:
145
+ """
146
+ Perform the download flow:
147
+ * Init components
148
+ * Login if required
149
+ * Download each requested book
150
+ * Export with configured exporter
151
+ """
140
152
  downloader_cfg = adapter.get_downloader_config()
141
153
  fetcher_cfg = adapter.get_fetcher_config()
142
154
  parser_cfg = adapter.get_parser_config()
143
155
  exporter_cfg = adapter.get_exporter_config()
156
+ login_cfg = adapter.get_login_config()
144
157
  log_level = adapter.get_log_level()
158
+ setup_logging(log_level=log_level)
145
159
 
146
160
  parser = get_parser(site, parser_cfg)
147
- exporter = get_exporter(site, exporter_cfg)
148
- setup_logging(log_level=log_level)
161
+ exporter = None
162
+ if not no_export:
163
+ exporter = get_exporter(site, exporter_cfg)
164
+ else:
165
+ ui.info(t("download_export_skipped"))
149
166
 
150
167
  async with get_fetcher(site, fetcher_cfg) as fetcher:
151
168
  if downloader_cfg.login_required and not await fetcher.load_state():
152
- login_data = await _prompt_login_fields(
153
- fetcher, fetcher.login_fields, downloader_cfg
154
- )
169
+ login_data = await _prompt_login_fields(fetcher.login_fields, login_cfg)
155
170
  if not await fetcher.login(**login_data):
156
- print(t("download_login_failed"))
171
+ ui.error(t("download_login_failed"))
157
172
  return
158
173
  await fetcher.save_state()
159
174
 
160
175
  downloader = get_downloader(
161
- fetcher=fetcher,
162
- parser=parser,
163
- site=site,
164
- config=downloader_cfg,
176
+ fetcher=fetcher, parser=parser, site=site, config=downloader_cfg
165
177
  )
166
178
 
167
179
  for book in valid_books:
168
- print(t("download_downloading", book_id=book["book_id"], site=site))
169
- await downloader.download(
170
- book,
171
- progress_hook=_print_progress,
172
- )
173
- await asyncio.to_thread(exporter.export, book["book_id"])
180
+ ui.info(t("download_downloading", book_id=book["book_id"], site=site))
181
+ await downloader.download(book, progress_hook=_print_progress)
182
+
183
+ if not no_export and exporter is not None:
184
+ await asyncio.to_thread(exporter.export, book["book_id"])
174
185
 
175
186
  if downloader_cfg.login_required and fetcher.is_logged_in:
176
187
  await fetcher.save_state()
177
188
 
178
189
 
179
190
  async def _prompt_login_fields(
180
- fetcher: FetcherProtocol,
181
191
  fields: list[LoginField],
182
- cfg: Any = None,
192
+ login_config: dict[str, str] | None = None,
183
193
  ) -> dict[str, Any]:
194
+ """
195
+ Prompt for required login fields, honoring defaults and config-provided values.
196
+
197
+ :param fields: Field descriptors from the fetcher (name/label/type/etc.).
198
+ :param login_config: Optional values already configured by the user.
199
+ :return: A dict suitable to pass to `fetcher.login(**kwargs)`.
200
+ """
201
+ login_config = login_config or {}
184
202
  result: dict[str, Any] = {}
185
- cfg_dict = asdict(cfg) if cfg else {}
186
203
 
187
204
  for field in fields:
188
- print(f"\n{field.label} ({field.name})")
205
+ ui.info(f"\n{field.label} ({field.name})")
189
206
  if field.description:
190
- print(f"{t('login_description')}: {field.description}")
207
+ ui.info(f"{t('login_description')}: {field.description}")
191
208
  if field.placeholder:
192
- print(f"{t('login_hint')}: {field.placeholder}")
193
-
194
- if field.type == "manual_login":
195
- await fetcher.set_interactive_mode(True)
196
- input(t("login_manual_prompt"))
197
- await fetcher.set_interactive_mode(False)
198
- continue
209
+ ui.info(f"{t('login_hint')}: {field.placeholder}")
199
210
 
200
- existing_value = cfg_dict.get(field.name, "").strip()
211
+ existing_value = login_config.get(field.name, "").strip()
201
212
  if existing_value:
202
213
  result[field.name] = existing_value
203
- print(t("login_use_config"))
214
+ ui.info(t("login_use_config"))
204
215
  continue
205
216
 
206
217
  value: str | dict[str, str]
207
218
  while True:
208
219
  if field.type == "password":
209
- value = getpass.getpass(t("login_enter_password"))
220
+ value = ui.prompt_password(t("login_enter_password"))
210
221
  elif field.type == "cookie":
211
- value = input(t("login_enter_cookie"))
212
- value = resolve_cookies(value)
222
+ value = ui.prompt(t("login_enter_cookie"))
223
+ value = parse_cookies(value)
213
224
  else:
214
- value = input(t("login_enter_value"))
225
+ value = ui.prompt(t("login_enter_value"))
215
226
 
216
227
  if not value and field.default:
217
228
  value = field.default
218
229
 
219
230
  if not value and field.required:
220
- print(t("login_required_field"))
231
+ ui.warn(t("login_required_field"))
221
232
  else:
222
233
  break
223
234
 
@@ -227,5 +238,7 @@ async def _prompt_login_fields(
227
238
 
228
239
 
229
240
  async def _print_progress(done: int, total: int) -> None:
230
- percent = done / total * 100
231
- print(f"下载进度: {done}/{total} 章 ({percent:.2f}%)")
241
+ """Progress hook passed into the downloader."""
242
+ ui.print_progress(
243
+ done, total, prefix=t("download_progress_prefix"), unit="chapters"
244
+ )
@@ -3,11 +3,15 @@
3
3
  novel_downloader.cli.export
4
4
  ---------------------------
5
5
 
6
+ Export existing books into TXT/EPUB formats.
6
7
  """
7
8
 
9
+ from __future__ import annotations
10
+
8
11
  from argparse import Namespace, _SubParsersAction
9
12
  from pathlib import Path
10
13
 
14
+ from novel_downloader.cli import ui
11
15
  from novel_downloader.config import ConfigAdapter, load_config
12
16
  from novel_downloader.core import get_exporter
13
17
  from novel_downloader.utils.i18n import t
@@ -15,13 +19,10 @@ from novel_downloader.utils.logger import setup_logging
15
19
 
16
20
 
17
21
  def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
22
+ """Register the `export` subcommand and its options."""
18
23
  parser = subparsers.add_parser("export", help=t("help_export"))
19
24
 
20
- parser.add_argument(
21
- "book_ids",
22
- nargs="+",
23
- help=t("download_book_ids"),
24
- )
25
+ parser.add_argument("book_ids", nargs="+", help=t("download_book_ids"))
25
26
  parser.add_argument(
26
27
  "--format",
27
28
  choices=["txt", "epub", "all"],
@@ -29,31 +30,26 @@ def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type:
29
30
  help=t("export_format_help"),
30
31
  )
31
32
  parser.add_argument(
32
- "--site",
33
- default="qidian",
34
- help=t("download_option_site", default="qidian"),
35
- )
36
- parser.add_argument(
37
- "--config",
38
- type=str,
39
- help=t("help_config"),
33
+ "--site", default="qidian", help=t("download_option_site", default="qidian")
40
34
  )
35
+ parser.add_argument("--config", type=str, help=t("help_config"))
41
36
 
42
37
  parser.set_defaults(func=handle_export)
43
38
 
44
39
 
45
40
  def handle_export(args: Namespace) -> None:
41
+ """Handle the `export` subcommand."""
46
42
  site: str = args.site
47
43
  config_path: Path | None = Path(args.config) if args.config else None
48
44
  book_ids: list[str] = args.book_ids
49
45
  export_format: str = args.format
50
46
 
51
- print(t("download_site_info", site=site))
47
+ ui.info(t("download_site_info", site=site))
52
48
 
53
49
  try:
54
50
  config_data = load_config(config_path)
55
51
  except Exception as e:
56
- print(t("download_config_load_fail", err=str(e)))
52
+ ui.error(t("download_config_load_fail", err=str(e)))
57
53
  return
58
54
 
59
55
  adapter = ConfigAdapter(config=config_data, site=site)
@@ -63,18 +59,18 @@ def handle_export(args: Namespace) -> None:
63
59
  setup_logging(log_level=log_level)
64
60
 
65
61
  for book_id in book_ids:
66
- print(t("export_processing", book_id=book_id, format=export_format))
62
+ ui.info(t("export_processing", book_id=book_id, format=export_format))
67
63
 
68
64
  if export_format in {"txt", "all"}:
69
65
  try:
70
66
  exporter.export_as_txt(book_id)
71
- print(t("export_success_txt", book_id=book_id))
67
+ ui.success(t("export_success_txt", book_id=book_id))
72
68
  except Exception as e:
73
- print(t("export_failed_txt", book_id=book_id, err=str(e)))
69
+ ui.error(t("export_failed_txt", book_id=book_id, err=str(e)))
74
70
 
75
71
  if export_format in {"epub", "all"}:
76
72
  try:
77
73
  exporter.export_as_epub(book_id)
78
- print(t("export_success_epub", book_id=book_id))
74
+ ui.success(t("export_success_epub", book_id=book_id))
79
75
  except Exception as e:
80
- print(t("export_failed_epub", book_id=book_id, err=str(e)))
76
+ ui.error(t("export_failed_epub", book_id=book_id, err=str(e)))
@@ -18,7 +18,7 @@ from .search import register_search_subcommand
18
18
 
19
19
 
20
20
  def cli_main() -> None:
21
- parser = argparse.ArgumentParser(description=t("cli_help"))
21
+ parser = argparse.ArgumentParser(description=t("help_cli"))
22
22
  subparsers = parser.add_subparsers(dest="command", required=True)
23
23
 
24
24
  register_clean_subcommand(subparsers)