novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__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 (248) 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 +79 -66
  6. novel_downloader/cli/export.py +17 -21
  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 +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  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 +17 -12
  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 +20 -14
  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} +56 -64
  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 +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  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 +64 -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 +64 -69
  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/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  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/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,68 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.parsers.qidian.chapter_router
4
- ---------------------------------------------------
5
-
6
- Routing logic for selecting the correct chapter parser for Qidian pages.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import logging
12
- from typing import TYPE_CHECKING
13
-
14
- from novel_downloader.models import ChapterDict
15
-
16
- from .chapter_normal import parse_normal_chapter
17
- from .utils import (
18
- can_view_chapter,
19
- find_ssr_page_context,
20
- is_encrypted,
21
- )
22
-
23
- if TYPE_CHECKING:
24
- from .main_parser import QidianParser
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
-
29
- def parse_chapter(
30
- parser: QidianParser,
31
- html_str: str,
32
- chapter_id: str,
33
- ) -> ChapterDict | None:
34
- """
35
- Extract and return the formatted textual content of chapter.
36
-
37
- :param parser: Instance of QidianParser.
38
- :param html_str: Raw HTML content of the chapter page.
39
- :param chapter_id: Identifier of the chapter being parsed.
40
- :return: Formatted chapter text or empty string if not parsable.
41
- """
42
- try:
43
- ssr_data = find_ssr_page_context(html_str)
44
-
45
- if not can_view_chapter(ssr_data):
46
- logger.warning(
47
- "[Parser] Chapter '%s' is not purchased or inaccessible.", chapter_id
48
- )
49
- return None
50
-
51
- if is_encrypted(ssr_data):
52
- if not parser._decode_font:
53
- return None
54
- try:
55
- from .chapter_encrypted import parse_encrypted_chapter
56
-
57
- return parse_encrypted_chapter(parser, html_str, chapter_id)
58
- except ImportError:
59
- logger.warning(
60
- "[Parser] Encrypted chapter '%s' requires extra dependencies.",
61
- chapter_id,
62
- )
63
- return None
64
-
65
- return parse_normal_chapter(parser, html_str, chapter_id)
66
- except Exception as e:
67
- logger.warning("[Parser] parse error for chapter '%s': %s", chapter_id, e)
68
- return None
@@ -1,114 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.parsers.qidian.utils.helpers
4
- --------------------------------------------------
5
-
6
- Shared utility functions for parsing Qidian pages.
7
-
8
- This module provides reusable helpers to:
9
- - Extract SSR-rendered JSON page context and structured chapter metadata.
10
- - Identify VIP chapters, encrypted content, and viewability conditions.
11
- """
12
-
13
- import json
14
- import logging
15
- from typing import Any
16
-
17
- from lxml import html
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- def find_ssr_page_context(html_str: str) -> dict[str, Any]:
23
- """
24
- Extract SSR JSON from <script id="vite-plugin-ssr_pageContext">.
25
- """
26
- try:
27
- tree = html.fromstring(html_str)
28
- script = tree.xpath('//script[@id="vite-plugin-ssr_pageContext"]/text()')
29
- if script:
30
- data: dict[str, Any] = json.loads(script[0].strip())
31
- return data
32
- except Exception as e:
33
- logger.warning("[Parser] SSR JSON parse error: %s", e)
34
- return {}
35
-
36
-
37
- def extract_chapter_info(ssr_data: dict[str, Any]) -> dict[str, Any]:
38
- """
39
- Extract the 'chapterInfo' dictionary from the SSR page context.
40
-
41
- This handles nested key access and returns an empty dict if missing.
42
-
43
- :param ssr_data: The full SSR data object from _find_ssr_page_context().
44
- :return: A dict with chapter metadata such as chapterName, authorSay, etc.
45
- """
46
- try:
47
- page_context = ssr_data.get("pageContext", {})
48
- page_props = page_context.get("pageProps", {})
49
- page_data = page_props.get("pageData", {})
50
- chapter_info = page_data.get("chapterInfo", {})
51
-
52
- assert isinstance(chapter_info, dict)
53
- return chapter_info
54
- except Exception:
55
- return {}
56
-
57
-
58
- def is_restricted_page(html_str: str) -> bool:
59
- """
60
- Return True if page content indicates access restriction
61
- (e.g. not subscribed/purchased).
62
-
63
- :param html_str: Raw HTML string.
64
- """
65
- markers = ["这是VIP章节", "需要订阅", "订阅后才能阅读"]
66
- return any(m in html_str for m in markers)
67
-
68
-
69
- def vip_status(ssr_data: dict[str, Any]) -> bool:
70
- """
71
- :return: True if VIP, False otherwise.
72
- """
73
- chapter_info = extract_chapter_info(ssr_data)
74
- vip_flag = chapter_info.get("vipStatus", 0)
75
- fens_flag = chapter_info.get("fEnS", 0)
76
- return bool(vip_flag == 1 and fens_flag != 0)
77
-
78
-
79
- def can_view_chapter(ssr_data: dict[str, Any]) -> bool:
80
- """
81
- A chapter is not viewable if it is marked as VIP
82
- and has not been purchased.
83
-
84
- :return: True if viewable, False otherwise.
85
- """
86
- chapter_info = extract_chapter_info(ssr_data)
87
- is_buy = chapter_info.get("isBuy", 0)
88
- vip_status = chapter_info.get("vipStatus", 0)
89
- return not (vip_status == 1 and is_buy == 0)
90
-
91
-
92
- def is_duplicated(ssr_data: dict[str, Any]) -> bool:
93
- """
94
- Check if chapter is marked as duplicated (eFW = 1).
95
- """
96
- chapter_info = extract_chapter_info(ssr_data)
97
- efw_flag = chapter_info.get("eFW", 0)
98
- return bool(efw_flag == 1)
99
-
100
-
101
- def is_encrypted(content: str | dict[str, Any]) -> bool:
102
- """
103
- Return True if content is encrypted.
104
-
105
- Chapter Encryption Status (cES):
106
- - 0: 内容是'明文'
107
- - 2: 字体加密
108
-
109
- :param content: HTML content, either as a raw string or a BeautifulSoup object.
110
- :return: True if encrypted marker is found, else False.
111
- """
112
- ssr_data = find_ssr_page_context(content) if isinstance(content, str) else content
113
- chapter_info = extract_chapter_info(ssr_data)
114
- return int(chapter_info.get("cES", 0)) == 2
@@ -1,25 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.models.chapter
4
- -------------------------------
5
-
6
- """
7
-
8
- from typing import Any, TypedDict
9
-
10
-
11
- class ChapterDict(TypedDict, total=True):
12
- """
13
- TypedDict for a novel chapter.
14
-
15
- Fields:
16
- id -- Unique chapter identifier
17
- title -- Chapter title
18
- content -- Chapter text
19
- extra -- Arbitrary metadata (e.g. author remarks, timestamps)
20
- """
21
-
22
- id: str
23
- title: str
24
- content: str
25
- extra: dict[str, Any]
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.models.types
4
- -----------------------------
5
-
6
- """
7
-
8
- from typing import Literal
9
-
10
- ModeType = Literal["browser", "session"]
11
- SplitMode = Literal["book", "volume"]
12
- LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
13
- BrowserType = Literal["chromium", "firefox", "webkit"]
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui
4
- --------------------
5
-
6
- This module exposes the TUI entry point.
7
- """
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.app
4
- ------------------------
5
-
6
- """
7
-
8
- from typing import Any
9
-
10
- from textual.app import App, ComposeResult
11
- from textual.containers import Container
12
- from textual.widgets import Footer, Header
13
-
14
- from novel_downloader.config import load_config
15
- from novel_downloader.tui.screens import HomeScreen
16
-
17
-
18
- class NovelDownloaderTUI(App): # type: ignore[misc]
19
- TITLE = "Novel Downloader TUI"
20
- SCREENS = {
21
- "home": HomeScreen,
22
- }
23
- config: dict[str, Any]
24
-
25
- def compose(self) -> ComposeResult:
26
- yield Header()
27
- yield Container(id="main_area")
28
- yield Footer()
29
-
30
- def on_mount(self) -> None:
31
- self.config = load_config()
32
- self.push_screen("home")
@@ -1,17 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.main
4
- -------------------------
5
-
6
- """
7
-
8
- from novel_downloader.tui.app import NovelDownloaderTUI
9
-
10
-
11
- def tui_main() -> None:
12
- app = NovelDownloaderTUI()
13
- app.run()
14
-
15
-
16
- if __name__ == "__main__":
17
- tui_main()
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.screens
4
- ----------------------------
5
-
6
- """
7
-
8
- from .home import HomeScreen
9
- from .login import LoginScreen
10
-
11
- __all__ = [
12
- "HomeScreen",
13
- "LoginScreen",
14
- ]
@@ -1,198 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.screens.home
4
- ---------------------------------
5
-
6
- """
7
-
8
- import asyncio
9
- import logging
10
- from typing import Any
11
-
12
- from textual.app import ComposeResult
13
- from textual.containers import Horizontal, Vertical
14
- from textual.screen import Screen
15
- from textual.widgets import Button, Input, ProgressBar, RichLog, Select, Static
16
-
17
- from novel_downloader.config import ConfigAdapter
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
25
- from novel_downloader.models import LoginField
26
- from novel_downloader.tui.widgets.richlog_handler import RichLogHandler
27
- from novel_downloader.utils.i18n import t
28
-
29
-
30
- class HomeScreen(Screen): # type: ignore[misc]
31
- CSS_PATH = "../styles/home_layout.tcss"
32
-
33
- def compose(self) -> ComposeResult:
34
- yield Vertical(
35
- self._make_title_bar(),
36
- self._make_input_row(),
37
- ProgressBar(id="prog", name="下载进度"),
38
- Static("下载进度: 0/0 章", id="label-progress"),
39
- RichLog(id="log", highlight=True, markup=False),
40
- id="main-layout",
41
- )
42
-
43
- def on_mount(self) -> None:
44
- log_widget = self.query_one("#log", RichLog)
45
-
46
- self._log_handler = RichLogHandler(log_widget)
47
- self._log_handler.setLevel(logging.INFO)
48
- self._log_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
49
-
50
- self._setup_logging(self._log_handler)
51
-
52
- async def on_button_pressed(self, event: Button.Pressed) -> None:
53
- if event.button.id == "exit":
54
- logging.info("退出应用")
55
- self.app.exit()
56
-
57
- elif event.button.id == "settings":
58
- logging.info("设置功能暂未实现")
59
-
60
- elif event.button.id == "download":
61
- site = self.query_one("#site", Select).value
62
- ids = self.query_one("#book_ids", Input).value
63
- if not site or not ids.strip():
64
- logging.warning("请填写完整信息")
65
- return
66
- id_list = {x.strip() for x in ids.split(",") if x.strip()}
67
- adapter = ConfigAdapter(config=self.app.config, site=str(site))
68
- # asyncio.create_task(self._download(adapter, str(site), id_list))
69
- self.run_worker(
70
- self._download(adapter, str(site), id_list),
71
- name="download",
72
- group="downloads",
73
- description="正在下载书籍...",
74
- )
75
-
76
- def _make_title_bar(self) -> Horizontal:
77
- return Horizontal(
78
- Static("小说下载器", id="title"),
79
- Button("设置", id="settings"),
80
- Button("关闭", id="exit"),
81
- id="title-bar",
82
- )
83
-
84
- def _make_input_row(self) -> Horizontal:
85
- return Horizontal(
86
- Vertical(self._make_site_select(), classes="left"),
87
- Vertical(
88
- Input(placeholder="输入书籍ID (支持逗号分隔)", id="book_ids"),
89
- classes="middle",
90
- ),
91
- Vertical(Button("下载", id="download"), classes="right"),
92
- id="input-row",
93
- )
94
-
95
- def _make_site_select(self) -> Select:
96
- return Select(
97
- options=[
98
- ("起点中文网", "qidian"),
99
- ("笔趣阁", "biquge"),
100
- ("铅笔小说", "qianbi"),
101
- ("SF轻小说", "sfacg"),
102
- ("ESJ Zone", "esjzone"),
103
- ("百合会", "yamibo"),
104
- ("哔哩轻小说", "linovelib"),
105
- ],
106
- prompt="选择站点",
107
- value="qidian",
108
- id="site",
109
- )
110
-
111
- async def _download(
112
- self,
113
- adapter: ConfigAdapter,
114
- site: str,
115
- book_ids: set[str],
116
- ) -> None:
117
- btn = self.query_one("#download", Button)
118
- btn.disabled = True
119
- try:
120
- logging.info(f"下载请求: {site} | {book_ids}")
121
- downloader_cfg = adapter.get_downloader_config()
122
- fetcher_cfg = adapter.get_fetcher_config()
123
- parser_cfg = adapter.get_parser_config()
124
- exporter_cfg = adapter.get_exporter_config()
125
-
126
- parser = get_parser(site, parser_cfg)
127
- exporter = get_exporter(site, exporter_cfg)
128
- self._setup_logging(self._log_handler)
129
-
130
- async with get_fetcher(site, fetcher_cfg) as fetcher:
131
- if downloader_cfg.login_required and not await fetcher.load_state():
132
- login_data = await self._prompt_login_fields(
133
- fetcher, fetcher.login_fields, downloader_cfg
134
- )
135
- if not await fetcher.login(**login_data):
136
- logging.info(t("download_login_failed"))
137
- return
138
- await fetcher.save_state()
139
-
140
- downloader = get_downloader(
141
- fetcher=fetcher,
142
- parser=parser,
143
- site=site,
144
- config=downloader_cfg,
145
- )
146
-
147
- for book_id in book_ids:
148
- logging.info(t("download_downloading", book_id=book_id, site=site))
149
- await downloader.download(
150
- {"book_id": book_id},
151
- progress_hook=self._update_progress,
152
- )
153
- await asyncio.to_thread(exporter.export, book_id)
154
-
155
- if downloader_cfg.login_required and fetcher.is_logged_in:
156
- await fetcher.save_state()
157
- finally:
158
- btn.disabled = False
159
-
160
- async def _prompt_login_fields(
161
- self,
162
- fetcher: FetcherProtocol,
163
- fields: list[LoginField],
164
- cfg: Any = None,
165
- ) -> dict[str, Any]:
166
- """
167
- Push a LoginScreen to collect all required fields,
168
- then return the dict of values when the user submits.
169
- """
170
- # cfg_dict = asdict(cfg) if cfg else {}
171
- # login_screen = LoginScreen(fields, cfg_dict)
172
- # await self.app.push_screen(login_screen)
173
- # await self.app.pop_screen()
174
- return {}
175
-
176
- def _setup_logging(self, handler: logging.Handler) -> None:
177
- """
178
- Attach the given handler to the root logger.
179
- """
180
- ft_logger = logging.getLogger("fontTools.ttLib.tables._p_o_s_t")
181
- ft_logger.setLevel(logging.ERROR)
182
- ft_logger.propagate = False
183
-
184
- logger = logging.getLogger()
185
- logger.setLevel(logging.INFO)
186
-
187
- logger.handlers = [
188
- h for h in logger.handlers if not isinstance(h, RichLogHandler)
189
- ]
190
- logger.addHandler(handler)
191
-
192
- async def _update_progress(self, done: int, total: int) -> None:
193
- prog = self.query_one("#prog", ProgressBar)
194
- label = self.query_one("#label-progress", Static)
195
-
196
- prog.update(total=total, progress=min(done, total))
197
-
198
- label.update(f"下载进度: {done}/{total} 章")
@@ -1,74 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.screens.login
4
- ----------------------------------
5
-
6
- """
7
-
8
- from typing import Any
9
-
10
- from textual.app import ComposeResult
11
- from textual.containers import Vertical
12
- from textual.screen import Screen
13
- from textual.widgets import Button, Input, Static
14
-
15
- from novel_downloader.models import LoginField
16
-
17
-
18
- class LoginScreen(Screen): # type: ignore[misc]
19
- """
20
- A modal screen that gathers login fields, then fires LoginScreen.Submitted.
21
- """
22
-
23
- BINDINGS = [("escape", "app.pop_screen", "取消")]
24
-
25
- def __init__(
26
- self,
27
- fields: list[LoginField],
28
- cfg: dict[str, Any] | None = None,
29
- ) -> None:
30
- super().__init__()
31
- self.fields = fields
32
- self.cfg = cfg or {}
33
-
34
- def compose(self) -> ComposeResult:
35
- widgets = []
36
- for field in self.fields:
37
- # show label and optional description
38
- widgets.append(Static(field.label))
39
- if field.description:
40
- widgets.append(Static(f"[i]{field.description}[/]"))
41
-
42
- # pick input type
43
- if field.type == "password":
44
- inp = Input(
45
- placeholder=field.placeholder or "",
46
- password=True,
47
- id=field.name,
48
- )
49
- else:
50
- inp = Input(
51
- placeholder=field.placeholder or "",
52
- id=field.name,
53
- )
54
-
55
- # pre-fill from config if present
56
- existing = self.cfg.get(field.name, "").strip()
57
- if existing:
58
- inp.value = existing
59
-
60
- widgets.append(inp)
61
-
62
- # submit button at the end
63
- widgets.append(Button("提交", id="submit"))
64
- yield Vertical(*widgets, id="login-form")
65
-
66
- async def on_button_pressed(self, event: Button.Pressed) -> None:
67
- if event.button.id == "submit":
68
- data: dict[str, Any] = {}
69
- for field in self.fields:
70
- inp = self.query_one(f"#{field.name}", Input)
71
- value = inp.value
72
- if not value and self.cfg.get(field.name):
73
- value = self.cfg[field.name]
74
- data[field.name] = value
@@ -1,79 +0,0 @@
1
- #main-layout {
2
- grid-rows: 3 auto 1 auto 1fr;
3
- grid-columns: 1fr;
4
- grid-gutter: 1;
5
- padding: 1;
6
- height: 100%;
7
- }
8
-
9
- #title-bar {
10
- height: 3;
11
- layout: horizontal;
12
- align: left middle;
13
- padding: 0 1;
14
- background: $boost;
15
- }
16
-
17
- #title {
18
- width: 1fr;
19
- content-align: left middle;
20
- }
21
-
22
- #settings,
23
- #exit {
24
- width: 8;
25
- padding: 0 1;
26
- }
27
-
28
- #input-row {
29
- layout: horizontal;
30
- padding: 1 0;
31
- overflow-x: auto;
32
- }
33
-
34
- #site {
35
- width: 20;
36
- margin-right: 1;
37
- }
38
-
39
- #book_ids {
40
- width: 1fr;
41
- min-width: 0;
42
- margin-right: 1;
43
- }
44
-
45
- #download {
46
- width: 15;
47
- }
48
-
49
- #site,
50
- #book_ids,
51
- #download {
52
- width: 100%;
53
- }
54
-
55
- Button#download {
56
- border: round $accent;
57
- padding: 0 1;
58
- }
59
- Button#download:hover {
60
- background: $accent-lighten-3;
61
- color: $text;
62
- }
63
-
64
-
65
- #prog {
66
- height: 1;
67
- color: $success;
68
- }
69
-
70
- #label {
71
- content-align: left middle;
72
- padding-left: 1;
73
- }
74
-
75
- #log {
76
- border: round $primary;
77
- padding: 1;
78
- overflow-y: auto;
79
- }
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.tui.widgets.richlog_handler
4
- --------------------------------------------
5
-
6
- """
7
-
8
- import logging
9
- from logging import LogRecord
10
-
11
- from textual.widgets import RichLog
12
-
13
-
14
- class RichLogHandler(logging.Handler):
15
- def __init__(self, rich_log_widget: RichLog):
16
- super().__init__()
17
- self.rich_log_widget = rich_log_widget
18
-
19
- def emit(self, record: LogRecord) -> None:
20
- msg = self.format(record)
21
- try:
22
- self.rich_log_widget.write(msg)
23
- except Exception:
24
- self.handleError(record)
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.utils.cache
4
- ----------------------------
5
-
6
- Provides decorators for caching function results,
7
- specifically optimized for configuration loading functions.
8
- """
9
-
10
- from collections.abc import Callable
11
- from functools import lru_cache, wraps
12
- from typing import Any, TypeVar, cast
13
-
14
- T = TypeVar("T", bound=Callable[..., Any])
15
-
16
-
17
- def cached_load_config(func: T) -> T:
18
- """
19
- A decorator to cache the result of a config-loading function.
20
- Uses LRU cache with maxsize=1.
21
- """
22
- cached = lru_cache(maxsize=1)(func)
23
- wrapped = wraps(func)(cached)
24
- return cast(T, wrapped)