novel-downloader 1.4.5__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 (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  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/archived/qidian/searcher.py +79 -0
  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 +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  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} +23 -11
  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} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  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} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  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 +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  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} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  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 +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  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/main_parser.py → esjzone.py} +67 -67
  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/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.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.4.5"
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
- from .main import cli_main
9
+ __all__ = ["cli_main"]
10
10
 
11
- __all__ = [
12
- "cli_main",
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,76 +3,58 @@
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
 
14
- from novel_downloader.config import save_config_file, save_rules_as_json
16
+ from novel_downloader.cli import ui
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
17
- from novel_downloader.utils.logger import setup_logging
18
20
  from novel_downloader.utils.state import state_mgr
19
21
 
20
- # from novel_downloader.utils.hash_store import img_hash_store
21
-
22
22
 
23
23
  def register_config_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
24
+ """Register `config` with `init`, `set-lang`, and `set-config` subcommands."""
24
25
  parser = subparsers.add_parser("config", help=t("help_config"))
25
26
  config_subparsers = parser.add_subparsers(dest="subcommand", required=True)
26
27
 
27
28
  _register_init(config_subparsers)
28
29
  _register_set_lang(config_subparsers)
29
30
  _register_set_config(config_subparsers)
30
- _register_update_rules(config_subparsers)
31
- _register_set_cookies(config_subparsers)
32
- # _register_add_hash(config_subparsers)
33
31
 
34
32
 
35
33
  def _register_init(subparsers: _SubParsersAction) -> None: # type: ignore
36
- parser = subparsers.add_parser("init", help=t("settings_init_help"))
34
+ parser = subparsers.add_parser("init", help=t("config_init_help"))
37
35
  parser.add_argument(
38
- "--force", action="store_true", help=t("settings_init_force_help")
36
+ "--force", action="store_true", help=t("config_init_force_help")
39
37
  )
40
38
  parser.set_defaults(func=_handle_init)
41
39
 
42
40
 
43
41
  def _register_set_lang(subparsers: _SubParsersAction) -> None: # type: ignore
44
- 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"))
45
43
  parser.add_argument("lang", choices=["zh", "en"], help="Language code")
46
44
  parser.set_defaults(func=_handle_set_lang)
47
45
 
48
46
 
49
47
  def _register_set_config(subparsers: _SubParsersAction) -> None: # type: ignore
50
- 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"))
51
49
  parser.add_argument("path", type=str, help="Path to YAML config file")
52
50
  parser.set_defaults(func=_handle_set_config)
53
51
 
54
52
 
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
53
  def _handle_init(args: Namespace) -> None:
75
- setup_logging()
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
+ """
76
58
  cwd = Path.cwd()
77
59
 
78
60
  for resource in DEFAULT_SETTINGS_PATHS:
@@ -81,97 +63,38 @@ def _handle_init(args: Namespace) -> None:
81
63
 
82
64
  if target_path.exists():
83
65
  if args.force:
84
- print(t("settings_init_overwrite", filename=resource.name))
66
+ ui.warn(t("config_init_overwrite", filename=resource.name))
85
67
  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()
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,
94
72
  )
95
- should_copy = resp == "y"
96
73
 
97
74
  if not should_copy:
98
- print(t("settings_init_skip", filename=resource.name))
75
+ ui.warn(t("config_init_skip", filename=resource.name))
99
76
  continue
100
77
 
101
78
  try:
102
79
  with as_file(resource) as actual_path:
103
80
  shutil.copy(actual_path, target_path)
104
- print(t("settings_init_copy", filename=resource.name))
81
+ ui.success(t("config_init_copy", filename=resource.name))
105
82
  except Exception as e:
106
- print(t("settings_init_error", filename=resource.name, err=str(e)))
83
+ ui.error(t("config_init_error", filename=resource.name, err=str(e)))
107
84
  raise
108
85
 
109
86
 
110
87
  def _handle_set_lang(args: Namespace) -> None:
88
+ """Set the UI language and persist in state manager."""
111
89
  state_mgr.set_language(args.lang)
112
- print(t("settings_set_lang", lang=args.lang))
90
+ ui.success(t("config_set_lang", lang=args.lang))
113
91
 
114
92
 
115
93
  def _handle_set_config(args: Namespace) -> None:
94
+ """Persist a user-supplied TOML config path into the app config."""
116
95
  try:
117
96
  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))
97
+ ui.success(t("config_set_config", path=args.path))
128
98
  except Exception as e:
129
- print(t("settings_update_rules_fail", err=str(e)))
99
+ ui.error(t("config_set_config_fail", err=str(e)))
130
100
  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,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.factory import (
19
- get_downloader,
20
- get_exporter,
21
- get_fetcher,
22
- get_parser,
23
- )
24
- from novel_downloader.core.interfaces import FetcherProtocol
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,26 +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:
47
- setup_logging()
48
-
49
+ """Handle the `download` subcommand."""
49
50
  site: str = args.site
50
51
  config_path: Path | None = Path(args.config) if args.config else None
51
52
  book_ids: list[BookConfig] = _cli_args_to_book_configs(
52
- args.book_ids,
53
- args.start,
54
- args.end,
53
+ args.book_ids, args.start, args.end
55
54
  )
55
+ no_export: bool = getattr(args, "no_export", False)
56
56
 
57
- print(t("download_site_info", site=site))
57
+ ui.info(t("download_site_info", site=site))
58
58
 
59
59
  try:
60
60
  config_data = load_config(config_path)
61
61
  except Exception as e:
62
- print(t("download_config_load_fail", err=str(e)))
62
+ ui.error(t("download_config_load_fail", err=str(e)))
63
63
  return
64
64
 
65
65
  adapter = ConfigAdapter(config=config_data, site=site)
@@ -68,21 +68,21 @@ def handle_download(args: Namespace) -> None:
68
68
  try:
69
69
  book_ids = adapter.get_book_ids()
70
70
  except Exception as e:
71
- print(t("download_fail_get_ids", err=str(e)))
71
+ ui.error(t("download_fail_get_ids", err=str(e)))
72
72
  return
73
73
 
74
74
  valid_books = _filter_valid_book_configs(book_ids)
75
75
 
76
76
  if not book_ids:
77
- print(t("download_no_ids"))
77
+ ui.warn(t("download_no_ids"))
78
78
  return
79
79
 
80
80
  if not valid_books:
81
- print(t("download_only_example", example="0000000000"))
82
- print(t("download_edit_config"))
81
+ ui.warn(t("download_only_example", example="0000000000"))
82
+ ui.info(t("download_edit_config"))
83
83
  return
84
84
 
85
- asyncio.run(_download(adapter, site, valid_books))
85
+ asyncio.run(_download(adapter, site, valid_books, no_export=no_export))
86
86
 
87
87
 
88
88
  def _cli_args_to_book_configs(
@@ -91,14 +91,13 @@ def _cli_args_to_book_configs(
91
91
  end_id: str | None,
92
92
  ) -> list[BookConfig]:
93
93
  """
94
- Convert CLI book_ids and optional --start/--end into a list of BookConfig.
95
- 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`.
96
96
  """
97
97
  if not book_ids:
98
98
  return []
99
99
 
100
100
  result: list[BookConfig] = []
101
-
102
101
  first: BookConfig = {"book_id": book_ids[0]}
103
102
  if start_id:
104
103
  first["start_id"] = start_id
@@ -117,9 +116,11 @@ def _filter_valid_book_configs(
117
116
  invalid_ids: Iterable[str] = ("0000000000",),
118
117
  ) -> list[BookConfig]:
119
118
  """
120
- Filter a list of BookConfig:
121
- - Removes entries with invalid or placeholder book_ids
122
- - 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.
123
124
  """
124
125
  seen = set(invalid_ids)
125
126
  result: list[BookConfig] = []
@@ -138,87 +139,96 @@ async def _download(
138
139
  adapter: ConfigAdapter,
139
140
  site: str,
140
141
  valid_books: list[BookConfig],
142
+ *,
143
+ no_export: bool = False,
141
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
+ """
142
152
  downloader_cfg = adapter.get_downloader_config()
143
153
  fetcher_cfg = adapter.get_fetcher_config()
144
154
  parser_cfg = adapter.get_parser_config()
145
155
  exporter_cfg = adapter.get_exporter_config()
156
+ login_cfg = adapter.get_login_config()
157
+ log_level = adapter.get_log_level()
158
+ setup_logging(log_level=log_level)
146
159
 
147
160
  parser = get_parser(site, parser_cfg)
148
- exporter = get_exporter(site, exporter_cfg)
149
- setup_logging()
161
+ exporter = None
162
+ if not no_export:
163
+ exporter = get_exporter(site, exporter_cfg)
164
+ else:
165
+ ui.info(t("download_export_skipped"))
150
166
 
151
167
  async with get_fetcher(site, fetcher_cfg) as fetcher:
152
168
  if downloader_cfg.login_required and not await fetcher.load_state():
153
- login_data = await _prompt_login_fields(
154
- fetcher, fetcher.login_fields, downloader_cfg
155
- )
169
+ login_data = await _prompt_login_fields(fetcher.login_fields, login_cfg)
156
170
  if not await fetcher.login(**login_data):
157
- print(t("download_login_failed"))
171
+ ui.error(t("download_login_failed"))
158
172
  return
159
173
  await fetcher.save_state()
160
174
 
161
175
  downloader = get_downloader(
162
- fetcher=fetcher,
163
- parser=parser,
164
- site=site,
165
- config=downloader_cfg,
176
+ fetcher=fetcher, parser=parser, site=site, config=downloader_cfg
166
177
  )
167
178
 
168
179
  for book in valid_books:
169
- print(t("download_downloading", book_id=book["book_id"], site=site))
170
- await downloader.download(
171
- book,
172
- progress_hook=_print_progress,
173
- )
174
- 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"])
175
185
 
176
186
  if downloader_cfg.login_required and fetcher.is_logged_in:
177
187
  await fetcher.save_state()
178
188
 
179
189
 
180
190
  async def _prompt_login_fields(
181
- fetcher: FetcherProtocol,
182
191
  fields: list[LoginField],
183
- cfg: Any = None,
192
+ login_config: dict[str, str] | None = None,
184
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 {}
185
202
  result: dict[str, Any] = {}
186
- cfg_dict = asdict(cfg) if cfg else {}
187
203
 
188
204
  for field in fields:
189
- print(f"\n{field.label} ({field.name})")
205
+ ui.info(f"\n{field.label} ({field.name})")
190
206
  if field.description:
191
- print(f"{t('login_description')}: {field.description}")
207
+ ui.info(f"{t('login_description')}: {field.description}")
192
208
  if field.placeholder:
193
- print(f"{t('login_hint')}: {field.placeholder}")
194
-
195
- if field.type == "manual_login":
196
- await fetcher.set_interactive_mode(True)
197
- input(t("login_manual_prompt"))
198
- await fetcher.set_interactive_mode(False)
199
- continue
209
+ ui.info(f"{t('login_hint')}: {field.placeholder}")
200
210
 
201
- existing_value = cfg_dict.get(field.name, "").strip()
211
+ existing_value = login_config.get(field.name, "").strip()
202
212
  if existing_value:
203
213
  result[field.name] = existing_value
204
- print(t("login_use_config"))
214
+ ui.info(t("login_use_config"))
205
215
  continue
206
216
 
207
217
  value: str | dict[str, str]
208
218
  while True:
209
219
  if field.type == "password":
210
- value = getpass.getpass(t("login_enter_password"))
220
+ value = ui.prompt_password(t("login_enter_password"))
211
221
  elif field.type == "cookie":
212
- value = input(t("login_enter_cookie"))
213
- value = resolve_cookies(value)
222
+ value = ui.prompt(t("login_enter_cookie"))
223
+ value = parse_cookies(value)
214
224
  else:
215
- value = input(t("login_enter_value"))
225
+ value = ui.prompt(t("login_enter_value"))
216
226
 
217
227
  if not value and field.default:
218
228
  value = field.default
219
229
 
220
230
  if not value and field.required:
221
- print(t("login_required_field"))
231
+ ui.warn(t("login_required_field"))
222
232
  else:
223
233
  break
224
234
 
@@ -228,5 +238,7 @@ async def _prompt_login_fields(
228
238
 
229
239
 
230
240
  async def _print_progress(done: int, total: int) -> None:
231
- percent = done / total * 100
232
- 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
+ )