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
@@ -3,24 +3,26 @@
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
- from novel_downloader.core.factory import get_exporter
16
+ from novel_downloader.core import get_exporter
13
17
  from novel_downloader.utils.i18n import t
18
+ from novel_downloader.utils.logger import setup_logging
14
19
 
15
20
 
16
21
  def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
22
+ """Register the `export` subcommand and its options."""
17
23
  parser = subparsers.add_parser("export", help=t("help_export"))
18
24
 
19
- parser.add_argument(
20
- "book_ids",
21
- nargs="+",
22
- help=t("download_book_ids"),
23
- )
25
+ parser.add_argument("book_ids", nargs="+", help=t("download_book_ids"))
24
26
  parser.add_argument(
25
27
  "--format",
26
28
  choices=["txt", "epub", "all"],
@@ -28,50 +30,47 @@ def register_export_subcommand(subparsers: _SubParsersAction) -> None: # type:
28
30
  help=t("export_format_help"),
29
31
  )
30
32
  parser.add_argument(
31
- "--site",
32
- default="qidian",
33
- help=t("download_option_site", default="qidian"),
34
- )
35
- parser.add_argument(
36
- "--config",
37
- type=str,
38
- help=t("help_config"),
33
+ "--site", default="qidian", help=t("download_option_site", default="qidian")
39
34
  )
35
+ parser.add_argument("--config", type=str, help=t("help_config"))
40
36
 
41
37
  parser.set_defaults(func=handle_export)
42
38
 
43
39
 
44
40
  def handle_export(args: Namespace) -> None:
41
+ """Handle the `export` subcommand."""
45
42
  site: str = args.site
46
43
  config_path: Path | None = Path(args.config) if args.config else None
47
44
  book_ids: list[str] = args.book_ids
48
45
  export_format: str = args.format
49
46
 
50
- print(t("download_site_info", site=site))
47
+ ui.info(t("download_site_info", site=site))
51
48
 
52
49
  try:
53
50
  config_data = load_config(config_path)
54
51
  except Exception as e:
55
- print(t("download_config_load_fail", err=str(e)))
52
+ ui.error(t("download_config_load_fail", err=str(e)))
56
53
  return
57
54
 
58
55
  adapter = ConfigAdapter(config=config_data, site=site)
59
56
  exporter_cfg = adapter.get_exporter_config()
57
+ log_level = adapter.get_log_level()
60
58
  exporter = get_exporter(site, exporter_cfg)
59
+ setup_logging(log_level=log_level)
61
60
 
62
61
  for book_id in book_ids:
63
- print(t("export_processing", book_id=book_id, format=export_format))
62
+ ui.info(t("export_processing", book_id=book_id, format=export_format))
64
63
 
65
64
  if export_format in {"txt", "all"}:
66
65
  try:
67
66
  exporter.export_as_txt(book_id)
68
- print(t("export_success_txt", book_id=book_id))
67
+ ui.success(t("export_success_txt", book_id=book_id))
69
68
  except Exception as e:
70
- 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)))
71
70
 
72
71
  if export_format in {"epub", "all"}:
73
72
  try:
74
73
  exporter.export_as_epub(book_id)
75
- print(t("export_success_epub", book_id=book_id))
74
+ ui.success(t("export_success_epub", book_id=book_id))
76
75
  except Exception as e:
77
- 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)))
@@ -14,16 +14,18 @@ from .clean import register_clean_subcommand
14
14
  from .config import register_config_subcommand
15
15
  from .download import register_download_subcommand
16
16
  from .export import register_export_subcommand
17
+ from .search import register_search_subcommand
17
18
 
18
19
 
19
20
  def cli_main() -> None:
20
- parser = argparse.ArgumentParser(description=t("cli_help"))
21
+ parser = argparse.ArgumentParser(description=t("help_cli"))
21
22
  subparsers = parser.add_subparsers(dest="command", required=True)
22
23
 
23
24
  register_clean_subcommand(subparsers)
24
25
  register_config_subcommand(subparsers)
25
26
  register_download_subcommand(subparsers)
26
27
  register_export_subcommand(subparsers)
28
+ register_search_subcommand(subparsers)
27
29
 
28
30
  args = parser.parse_args()
29
31
  if hasattr(args, "func"):
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.cli.search
4
+ ---------------------------
5
+
6
+ Search across supported sites, let the user pick one result, then
7
+ hand off to the download flow.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from argparse import Namespace, _SubParsersAction
14
+ from collections.abc import Sequence
15
+ from pathlib import Path
16
+
17
+ from novel_downloader.cli import ui
18
+ from novel_downloader.cli.download import _download
19
+ from novel_downloader.config import ConfigAdapter, load_config
20
+ from novel_downloader.core import search
21
+ from novel_downloader.models import BookConfig, SearchResult
22
+ from novel_downloader.utils.i18n import t
23
+
24
+
25
+ def register_search_subcommand(subparsers: _SubParsersAction) -> None: # type: ignore
26
+ """Register the `search` subcommand and its options."""
27
+ parser = subparsers.add_parser("search", help=t("help_search"))
28
+
29
+ parser.add_argument(
30
+ "--site", "-s", action="append", metavar="SITE", help=t("search_sites_help")
31
+ )
32
+ parser.add_argument("keyword", help=t("search_keyword_help"))
33
+ parser.add_argument("--config", type=str, help=t("help_config"))
34
+ parser.add_argument(
35
+ "--limit", "-l", type=int, default=20, metavar="N", help=t("search_limit_help")
36
+ )
37
+ parser.add_argument(
38
+ "--site-limit",
39
+ type=int,
40
+ default=5,
41
+ metavar="M",
42
+ help=t("search_site_limit_help"),
43
+ )
44
+ parser.add_argument(
45
+ "--timeout",
46
+ type=float,
47
+ default=5.0,
48
+ metavar="SECS",
49
+ help=t("search_timeout_help", secs="5.0"),
50
+ )
51
+
52
+ parser.set_defaults(func=handle_search)
53
+
54
+
55
+ def handle_search(args: Namespace) -> None:
56
+ """Handle the `search` subcommand."""
57
+ sites: Sequence[str] | None = args.site or None
58
+ keyword: str = args.keyword
59
+ overall_limit = max(1, args.limit)
60
+ per_site_limit = max(1, args.site_limit)
61
+ timeout = max(0.1, float(args.timeout))
62
+ config_path: Path | None = Path(args.config) if args.config else None
63
+
64
+ try:
65
+ config_data = load_config(config_path)
66
+ except Exception as e:
67
+ ui.error(t("download_config_load_fail", err=str(e)))
68
+ return
69
+
70
+ async def _run() -> None:
71
+ results = await search(
72
+ keyword=keyword,
73
+ sites=sites,
74
+ limit=overall_limit,
75
+ per_site_limit=per_site_limit,
76
+ timeout=timeout,
77
+ )
78
+
79
+ chosen = _prompt_user_select(results)
80
+ if chosen is None:
81
+ return
82
+
83
+ adapter = ConfigAdapter(config=config_data, site=chosen["site"])
84
+ books: list[BookConfig] = [{"book_id": chosen["book_id"]}]
85
+ await _download(adapter, chosen["site"], books)
86
+
87
+ asyncio.run(_run())
88
+
89
+
90
+ def _prompt_user_select(results: Sequence[SearchResult]) -> SearchResult | None:
91
+ """
92
+ Show a Rich table of results and ask the user to pick one by index.
93
+
94
+ :param results: A sequence of SearchResult dicts.
95
+ :return: The chosen SearchResult, or None if cancelled/no results.
96
+ """
97
+ if not results:
98
+ ui.warn(t("no_results"))
99
+ return None
100
+
101
+ columns = ["#", "Title", "Author", "Latest", "Updated", "Site", "Book ID"]
102
+ rows = []
103
+ for i, r in enumerate(results, 1):
104
+ rows.append(
105
+ [
106
+ str(i),
107
+ r["title"],
108
+ r["author"],
109
+ r["latest_chapter"],
110
+ r["update_date"],
111
+ r["site"],
112
+ r["book_id"],
113
+ ]
114
+ )
115
+ ui.render_table("Search Results", columns, rows)
116
+
117
+ idx = ui.select_index(t("prompt_select_index"), len(results))
118
+ if idx is None:
119
+ return None
120
+ return results[idx - 1]
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.cli.ui
4
+ -----------------------
5
+
6
+ A small set of Rich-based helpers to keep CLI presentation and prompts
7
+ consistent across subcommands.
8
+
9
+ Public API:
10
+ - info, success, warn, error
11
+ - confirm
12
+ - prompt, prompt_password
13
+ - render_table
14
+ - select_index
15
+ - print_progress
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Iterable, Sequence
21
+
22
+ from rich.console import Console
23
+ from rich.prompt import Confirm, Prompt
24
+ from rich.table import Table
25
+
26
+ _CONSOLE = Console()
27
+
28
+
29
+ def info(message: str) -> None:
30
+ """Print a neutral informational message."""
31
+ _CONSOLE.print(message)
32
+
33
+
34
+ def success(message: str) -> None:
35
+ """Print a success message in a friendly color."""
36
+ _CONSOLE.print(f"[green]{message}[/]")
37
+
38
+
39
+ def warn(message: str) -> None:
40
+ """Print a warning message."""
41
+ _CONSOLE.print(f"[yellow]{message}[/]")
42
+
43
+
44
+ def error(message: str) -> None:
45
+ """Print an error message."""
46
+ _CONSOLE.print(f"[red]{message}[/]")
47
+
48
+
49
+ def confirm(message: str, *, default: bool = False) -> bool:
50
+ """
51
+ Ask a yes/no question.
52
+
53
+ :param message: The question to display (without [y/N] suffix).
54
+ :param default: Default choice (pressing Enter = Yes if True, No if False).
55
+ :return: True if user confirms (Yes), otherwise False.
56
+ """
57
+ try:
58
+ result: bool = Confirm.ask(f"[bold]{message}[/bold]", default=default)
59
+ return result
60
+ except (KeyboardInterrupt, EOFError):
61
+ warn("Cancelled.")
62
+ return False
63
+
64
+
65
+ def prompt(message: str, *, default: str | None = None) -> str:
66
+ """
67
+ Prompt user for a line of text.
68
+
69
+ :param message: Prompt message.
70
+ :param default: Default value if the user presses Enter.
71
+ :return: The user's input.
72
+ """
73
+ try:
74
+ result: str = Prompt.ask(message, default=default or "")
75
+ return result
76
+ except (KeyboardInterrupt, EOFError):
77
+ warn("Cancelled.")
78
+ return default or ""
79
+
80
+
81
+ def prompt_password(message: str) -> str:
82
+ """
83
+ Prompt user for a password/secret value (no echo).
84
+
85
+ :param message: Prompt message.
86
+ :return: The user's input (may be empty).
87
+ """
88
+ try:
89
+ result: str = Prompt.ask(message, password=True)
90
+ return result
91
+ except (KeyboardInterrupt, EOFError):
92
+ warn("Cancelled.")
93
+ return ""
94
+
95
+
96
+ def render_table(
97
+ title: str,
98
+ columns: Sequence[str],
99
+ rows: Iterable[Sequence[str]],
100
+ ) -> None:
101
+ """
102
+ Render a simple full-width table.
103
+
104
+ :param title: Table title.
105
+ :param columns: Column names.
106
+ :param rows: Row data; each row must have the same length as `columns`.
107
+ """
108
+ table = Table(title=title, show_lines=True, expand=True)
109
+ for col in columns:
110
+ table.add_column(col, overflow="fold")
111
+ for row in rows:
112
+ table.add_row(*[str(x) for x in row])
113
+ _CONSOLE.print(table)
114
+
115
+
116
+ def select_index(prompt_text: str, total: int) -> int | None:
117
+ """
118
+ Prompt user to select an index in [1..total]. Empty input cancels.
119
+
120
+ :param prompt_text: Displayed prompt (e.g., 'Select index').
121
+ :param total: Maximum valid index (minimum is 1).
122
+ :return: Selected 1-based index, or None if user cancels.
123
+ """
124
+ if total <= 0:
125
+ return None
126
+ valid_choices = [str(i) for i in range(1, total + 1)]
127
+ choice = Prompt.ask(
128
+ prompt_text,
129
+ choices=valid_choices + [""],
130
+ show_choices=False,
131
+ default="",
132
+ show_default=False,
133
+ ).strip()
134
+ if not choice:
135
+ return None
136
+ return int(choice)
137
+
138
+
139
+ def print_progress(
140
+ done: int,
141
+ total: int,
142
+ *,
143
+ prefix: str = "Progress",
144
+ unit: str = "item",
145
+ ) -> None:
146
+ """
147
+ Print a lightweight progress line.
148
+
149
+ :param done: Completed count.
150
+ :param total: Total count.
151
+ :param prefix: Text prefix shown before numbers.
152
+ :param unit: Logical unit name (e.g., 'item').
153
+ """
154
+ total = max(1, total)
155
+ pct = done / total * 100.0
156
+ _CONSOLE.print(f"[dim]{prefix}[/] {done}/{total} {unit} ({pct:.2f}%)")
@@ -4,24 +4,20 @@ novel_downloader.config
4
4
  -----------------------
5
5
 
6
6
  Unified interface for loading and adapting configuration files.
7
-
8
- This module provides:
9
- - load_config: loads YAML config from file path with fallback support
10
- - ConfigAdapter: maps raw config + site name to structured config models
11
- - Configuration dataclasses: RequesterConfig, DownloaderConfig, etc.
12
7
  """
13
8
 
14
- from .adapter import ConfigAdapter
15
- from .loader import load_config, save_config_file
16
- from .site_rules import (
17
- load_site_rules,
18
- save_rules_as_json,
19
- )
20
-
21
9
  __all__ = [
10
+ "get_config_value",
22
11
  "load_config",
12
+ "save_config",
23
13
  "save_config_file",
24
14
  "ConfigAdapter",
25
- "load_site_rules",
26
- "save_rules_as_json",
27
15
  ]
16
+
17
+ from .adapter import ConfigAdapter
18
+ from .file_io import (
19
+ get_config_value,
20
+ load_config,
21
+ save_config,
22
+ save_config_file,
23
+ )