novel-downloader 1.5.0__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +77 -64
  6. novel_downloader/cli/export.py +16 -20
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +65 -105
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +1 -0
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +14 -9
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +17 -11
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +63 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +61 -66
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  100. novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
  101. novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
  102. novel_downloader/core/parsers/qidian/main_parser.py +11 -38
  103. novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
  104. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  105. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  106. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  107. novel_downloader/core/parsers/quanben5.py +103 -0
  108. novel_downloader/core/parsers/registry.py +5 -16
  109. novel_downloader/core/parsers/sfacg.py +38 -45
  110. novel_downloader/core/parsers/shencou.py +215 -0
  111. novel_downloader/core/parsers/shuhaige.py +111 -0
  112. novel_downloader/core/parsers/tongrenquan.py +116 -0
  113. novel_downloader/core/parsers/ttkan.py +132 -0
  114. novel_downloader/core/parsers/wanbengo.py +191 -0
  115. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  116. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  117. novel_downloader/core/parsers/xs63b.py +161 -0
  118. novel_downloader/core/parsers/xshbook.py +134 -0
  119. novel_downloader/core/parsers/yamibo.py +87 -131
  120. novel_downloader/core/parsers/yibige.py +166 -0
  121. novel_downloader/core/searchers/__init__.py +34 -3
  122. novel_downloader/core/searchers/aaatxt.py +107 -0
  123. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  124. novel_downloader/core/searchers/base.py +112 -36
  125. novel_downloader/core/searchers/dxmwx.py +105 -0
  126. novel_downloader/core/searchers/eightnovel.py +84 -0
  127. novel_downloader/core/searchers/esjzone.py +43 -25
  128. novel_downloader/core/searchers/hetushu.py +92 -0
  129. novel_downloader/core/searchers/i25zw.py +93 -0
  130. novel_downloader/core/searchers/ixdzs8.py +107 -0
  131. novel_downloader/core/searchers/jpxs123.py +107 -0
  132. novel_downloader/core/searchers/piaotia.py +100 -0
  133. novel_downloader/core/searchers/qbtr.py +106 -0
  134. novel_downloader/core/searchers/qianbi.py +74 -40
  135. novel_downloader/core/searchers/quanben5.py +144 -0
  136. novel_downloader/core/searchers/registry.py +24 -8
  137. novel_downloader/core/searchers/shuhaige.py +124 -0
  138. novel_downloader/core/searchers/tongrenquan.py +110 -0
  139. novel_downloader/core/searchers/ttkan.py +92 -0
  140. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  141. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  142. novel_downloader/core/searchers/xs63b.py +104 -0
  143. novel_downloader/locales/en.json +31 -82
  144. novel_downloader/locales/zh.json +32 -83
  145. novel_downloader/models/__init__.py +21 -22
  146. novel_downloader/models/book.py +44 -0
  147. novel_downloader/models/config.py +4 -37
  148. novel_downloader/models/login.py +1 -1
  149. novel_downloader/models/search.py +5 -0
  150. novel_downloader/resources/config/settings.toml +8 -70
  151. novel_downloader/resources/json/xiguashuwu.json +718 -0
  152. novel_downloader/utils/__init__.py +13 -22
  153. novel_downloader/utils/chapter_storage.py +3 -2
  154. novel_downloader/utils/constants.py +4 -29
  155. novel_downloader/utils/cookies.py +6 -18
  156. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  157. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  158. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  159. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  160. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  161. novel_downloader/utils/epub/__init__.py +1 -1
  162. novel_downloader/utils/epub/constants.py +57 -16
  163. novel_downloader/utils/epub/documents.py +88 -194
  164. novel_downloader/utils/epub/models.py +0 -14
  165. novel_downloader/utils/epub/utils.py +63 -96
  166. novel_downloader/utils/file_utils/__init__.py +2 -23
  167. novel_downloader/utils/file_utils/io.py +3 -113
  168. novel_downloader/utils/file_utils/sanitize.py +0 -4
  169. novel_downloader/utils/fontocr.py +207 -0
  170. novel_downloader/utils/logger.py +8 -16
  171. novel_downloader/utils/network.py +2 -2
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/time_utils/__init__.py +5 -11
  176. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  177. novel_downloader/utils/time_utils/sleep_utils.py +4 -8
  178. novel_downloader/web/__init__.py +13 -0
  179. novel_downloader/web/components/__init__.py +11 -0
  180. novel_downloader/web/components/navigation.py +35 -0
  181. novel_downloader/web/main.py +66 -0
  182. novel_downloader/web/pages/__init__.py +17 -0
  183. novel_downloader/web/pages/download.py +78 -0
  184. novel_downloader/web/pages/progress.py +147 -0
  185. novel_downloader/web/pages/search.py +329 -0
  186. novel_downloader/web/services/__init__.py +17 -0
  187. novel_downloader/web/services/client_dialog.py +164 -0
  188. novel_downloader/web/services/cred_broker.py +113 -0
  189. novel_downloader/web/services/cred_models.py +35 -0
  190. novel_downloader/web/services/task_manager.py +264 -0
  191. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  192. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  193. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  194. novel_downloader/core/downloaders/biquge.py +0 -29
  195. novel_downloader/core/downloaders/esjzone.py +0 -29
  196. novel_downloader/core/downloaders/linovelib.py +0 -29
  197. novel_downloader/core/downloaders/sfacg.py +0 -29
  198. novel_downloader/core/downloaders/yamibo.py +0 -29
  199. novel_downloader/core/exporters/biquge.py +0 -22
  200. novel_downloader/core/exporters/esjzone.py +0 -22
  201. novel_downloader/core/exporters/qianbi.py +0 -22
  202. novel_downloader/core/exporters/sfacg.py +0 -22
  203. novel_downloader/core/exporters/yamibo.py +0 -22
  204. novel_downloader/core/fetchers/base/__init__.py +0 -14
  205. novel_downloader/core/fetchers/base/browser.py +0 -422
  206. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  207. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  208. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  209. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  210. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  211. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  212. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  213. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  214. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  215. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  216. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  217. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  218. novel_downloader/core/parsers/biquge.py +0 -139
  219. novel_downloader/models/chapter.py +0 -25
  220. novel_downloader/models/types.py +0 -13
  221. novel_downloader/tui/__init__.py +0 -7
  222. novel_downloader/tui/app.py +0 -32
  223. novel_downloader/tui/main.py +0 -17
  224. novel_downloader/tui/screens/__init__.py +0 -14
  225. novel_downloader/tui/screens/home.py +0 -198
  226. novel_downloader/tui/screens/login.py +0 -74
  227. novel_downloader/tui/styles/home_layout.tcss +0 -79
  228. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  229. novel_downloader/utils/cache.py +0 -24
  230. novel_downloader/utils/fontocr/__init__.py +0 -22
  231. novel_downloader/utils/fontocr/hash_store.py +0 -280
  232. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  233. novel_downloader/utils/fontocr/model_loader.py +0 -69
  234. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  235. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  236. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  237. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  238. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  239. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  240. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  241. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -2,24 +2,22 @@
2
2
  """
3
3
  novel_downloader.utils.state
4
4
  ----------------------------
5
- State management for user preferences and runtime flags.
6
5
 
7
- Supported sections:
8
- - general: global preferences (e.g. language)
9
- - sites: per-site flags & data (e.g. manual_login, cookies)
6
+ State management for user preferences and runtime flags.
10
7
  """
11
8
 
9
+ __all__ = ["StateManager", "state_mgr"]
10
+
12
11
  import json
13
12
  from pathlib import Path
14
13
  from typing import Any
15
14
 
16
- from .constants import STATE_FILE
15
+ from novel_downloader.utils.constants import STATE_FILE
17
16
 
18
17
 
19
18
  class StateManager:
20
19
  """
21
20
  Manages persistent state for user preferences and runtime flags.
22
- Stores data in JSON at STATE_FILE.
23
21
  """
24
22
 
25
23
  def __init__(self, path: Path = STATE_FILE) -> None:
@@ -50,26 +48,6 @@ class StateManager:
50
48
  content = json.dumps(self._data, ensure_ascii=False, indent=2)
51
49
  self._path.write_text(content, encoding="utf-8")
52
50
 
53
- def _parse_cookie_string(self, cookie_str: str) -> dict[str, str]:
54
- """
55
- Parse a Cookie header string into a dict.
56
-
57
- :param cookie_str: e.g. 'k1=v1; k2=v2; k3'
58
- :return: mapping cookie names to values (missing '=' yields empty string)
59
- :rtype: Dict[str, str]
60
- """
61
- cookies: dict[str, str] = {}
62
- for item in cookie_str.split(";"):
63
- item = item.strip()
64
- if not item:
65
- continue
66
- if "=" in item:
67
- k, v = item.split("=", 1)
68
- cookies[k.strip()] = v.strip()
69
- else:
70
- cookies[item] = ""
71
- return cookies
72
-
73
51
  def get_language(self) -> str:
74
52
  """
75
53
  Load the user's language preference, defaulting to 'zh'.
@@ -88,69 +66,5 @@ class StateManager:
88
66
  self._data.setdefault("general", {})["lang"] = lang
89
67
  self._save()
90
68
 
91
- def get_manual_login_flag(self, site: str) -> bool:
92
- """
93
- Retrieve the manual login requirement flag for a specific site.
94
-
95
- :param site: Site identifier (e.g. 'qidian', 'bqg')
96
- :return: True if manual login is required (defaults to True)
97
- """
98
- val = self._data.get("sites", {}).get(site, {}).get("manual_login", True)
99
- return bool(val)
100
-
101
- def set_manual_login_flag(self, site: str, flag: bool) -> None:
102
- """
103
- Set the 'manual_login' flag for a specific site.
104
-
105
- :param flag: True if the site requires manual login.
106
- :param site: Site identifier (e.g. 'qidian', 'bqg')
107
- """
108
- sites = self._data.setdefault("sites", {})
109
- site_data = sites.setdefault(site, {})
110
- site_data["manual_login"] = flag
111
- self._save()
112
-
113
- def get_cookies(self, site: str) -> dict[str, str]:
114
- """
115
- Retrieve the persisted cookies for a specific site.
116
-
117
- :param site: Site identifier (e.g. 'qidian', 'bqg')
118
- :return: A dict mapping cookie names to values. Returns empty dict if not set.
119
- """
120
- cookies = self._data.get("sites", {}).get(site, {}).get("cookies", {})
121
- return {str(k): str(v) for k, v in cookies.items()}
122
-
123
- def set_cookies(self, site: str, cookies: str | dict[str, str]) -> None:
124
- """
125
- Persist (overwrite) the cookies for a specific site.
126
-
127
- :param site: Site identifier (e.g. 'qidian', 'bqg')
128
- :param cookies: Either a dict mapping cookie names to values,
129
- or a string (JSON or 'k=v; k2=v2') to be parsed.
130
- :raises TypeError: if cookies is neither str nor dict
131
- """
132
- # 1) normalize to dict
133
- if isinstance(cookies, dict):
134
- cookies_dict = cookies
135
- elif isinstance(cookies, str):
136
- # try JSON first
137
- try:
138
- parsed = json.loads(cookies)
139
- if isinstance(parsed, dict):
140
- cookies_dict = parsed # OK!
141
- else:
142
- raise ValueError
143
- except Exception:
144
- # fallback to "k=v; k2=v2" format
145
- cookies_dict = self._parse_cookie_string(cookies)
146
- else:
147
- raise TypeError("`cookies` must be a dict or a str")
148
-
149
- # 2) persist
150
- sites = self._data.setdefault("sites", {})
151
- site_data = sites.setdefault(site, {})
152
- site_data["cookies"] = {str(k): str(v) for k, v in cookies_dict.items()}
153
- self._save()
154
-
155
69
 
156
70
  state_mgr = StateManager()
@@ -3,13 +3,7 @@
3
3
  novel_downloader.utils.text_utils
4
4
  ---------------------------------
5
5
 
6
- Utility modules for text formatting, font mapping, cleaning, and diff display.
7
-
8
- Submodules:
9
- - diff_display: Generate inline diffs with aligned character markers
10
- - numeric_conversion: Convert between Chinese and Arabic numerals
11
- - text_cleaner: Text cleaning and normalization utilities
12
- - truncate_utils: Text truncation and content prefix generation
6
+ Utility modules for text formatting, cleaning, and diff display.
13
7
  """
14
8
 
15
9
  __all__ = [
@@ -6,9 +6,7 @@ novel_downloader.utils.text_utils.diff_display
6
6
  Generate inline character-level diff between two strings with visual markers.
7
7
  """
8
8
 
9
- __all__ = [
10
- "diff_inline_display",
11
- ]
9
+ __all__ = ["diff_inline_display"]
12
10
 
13
11
  import difflib
14
12
  import unicodedata
@@ -22,10 +20,10 @@ def _char_width_space(
22
20
 
23
21
  Fullwidth (F) or Wide (W) characters map to `asian_char`, else `normal_char`.
24
22
 
25
- :param c: A single character.
23
+ :param c: A single character.
26
24
  :param normal_char: Replacement for narrow chars (default U+0020).
27
- :param asian_char: Replacement for wide chars (default U+3000).
28
- :return: The appropriate space character.
25
+ :param asian_char: Replacement for wide chars (default U+3000).
26
+ :return: The appropriate space character.
29
27
  """
30
28
  return asian_char if unicodedata.east_asian_width(c) in ("F", "W") else normal_char
31
29
 
@@ -37,7 +35,7 @@ def diff_inline_display(old_str: str, new_str: str) -> str:
37
35
 
38
36
  :param old_str: Original string (prefixed '-' will be trimmed).
39
37
  :param new_str: Modified string (prefixed '+' will be trimmed).
40
- :return: A multiline diff display with aligned markers.
38
+ :return: A multiline diff display with aligned markers.
41
39
  """
42
40
  space_1 = " "
43
41
  space_2 = "\u3000"
@@ -4,19 +4,13 @@ novel_downloader.utils.time_utils
4
4
  ---------------------------------
5
5
 
6
6
  Utility functions for time and date-related operations.
7
-
8
- Includes:
9
- - calculate_time_difference:
10
- Computes time delta between two timezone-aware datetime strings.
11
- - sleep_with_random_delay:
12
- Sleeps for a random duration, useful for human-like delays or rate limiting.
13
7
  """
14
8
 
15
9
  __all__ = [
16
- "calculate_time_difference",
17
- "async_sleep_with_random_delay",
18
- "sleep_with_random_delay",
10
+ "time_diff",
11
+ "async_jitter_sleep",
12
+ "jitter_sleep",
19
13
  ]
20
14
 
21
- from .datetime_utils import calculate_time_difference
22
- from .sleep_utils import async_sleep_with_random_delay, sleep_with_random_delay
15
+ from .datetime_utils import time_diff
16
+ from .sleep_utils import async_jitter_sleep, jitter_sleep
@@ -4,16 +4,10 @@ novel_downloader.utils.time_utils.datetime_utils
4
4
  ------------------------------------------------
5
5
 
6
6
  Time utility functions for timezone-aware date calculations.
7
-
8
- Includes:
9
- - _parse_utc_offset():
10
- Converts UTC offset string (e.g. 'UTC+8') to a timezone object.
11
- - calculate_time_difference():
12
- Computes timedelta between two datetime strings, with optional timezones.
13
7
  """
14
8
 
15
9
  __all__ = [
16
- "calculate_time_difference",
10
+ "time_diff",
17
11
  ]
18
12
 
19
13
  import logging
@@ -53,7 +47,7 @@ def _parse_utc_offset(tz_str: str) -> timezone:
53
47
  Parse a timezone string like 'UTC+8' or 'UTC-5' into a datetime.timezone object.
54
48
 
55
49
  :param tz_str: Timezone in 'UTC±<hours>' format, e.g. 'UTC', 'UTC+8', 'UTC-05'
56
- :return: Corresponding timezone object
50
+ :return: Corresponding timezone object
57
51
  :raises ValueError: if tz_str is not a valid UTC offset format
58
52
  """
59
53
  tz_str_clean = tz_str.upper().strip()
@@ -74,20 +68,20 @@ def _parse_datetime_flexible(dt_str: str) -> datetime:
74
68
  """
75
69
  Parse a date/time string in any of several common formats:
76
70
 
77
- ISO 8601: 'YYYY-MM-DDTHH:MM:SSZ'
78
- ISO w/ offset: 'YYYY-MM-DDTHH:MM:SS+HH:MM'
79
- 'YYYY-MM-DD HH:MM:SS'
80
- 'YYYY-MM-DD' (time defaults to 00:00:00)
81
- 'YYYY/MM/DD HH:MM:SS'
82
- 'YYYY/MM/DD HH:MM'
83
- 'YYYY/MM/DD'
84
- 'MM/DD/YYYY HH:MM[:SS] AM/PM'
85
- 'MM/DD/YYYY'
86
- 'DD.MM.YYYY HH:MM'
87
- 'DD.MM.YYYY'
71
+ * ISO 8601: 'YYYY-MM-DDTHH:MM:SSZ'
72
+ * ISO w/ offset: 'YYYY-MM-DDTHH:MM:SS+HH:MM'
73
+ * 'YYYY-MM-DD HH:MM:SS'
74
+ * 'YYYY-MM-DD' (time defaults to 00:00:00)
75
+ * 'YYYY/MM/DD HH:MM:SS'
76
+ * 'YYYY/MM/DD HH:MM'
77
+ * 'YYYY/MM/DD'
78
+ * 'MM/DD/YYYY HH:MM[:SS] AM/PM'
79
+ * 'MM/DD/YYYY'
80
+ * 'DD.MM.YYYY HH:MM'
81
+ * 'DD.MM.YYYY'
88
82
 
89
83
  :param dt_str: Date/time string to parse.
90
- :return: A naive datetime object.
84
+ :return: A naive datetime object.
91
85
  :raises ValueError: If dt_str does not match the expected formats.
92
86
  """
93
87
  s = dt_str.strip()
@@ -95,13 +89,10 @@ def _parse_datetime_flexible(dt_str: str) -> datetime:
95
89
  if re.fullmatch(pattern, s):
96
90
  return datetime.strptime(s, fmt)
97
91
 
98
- supported = "\n".join(f" {fmt}" for _, fmt in _DATETIME_FORMATS)
99
- raise ValueError(
100
- f"Invalid date/time format: '{dt_str}'\n" f"Supported formats are:\n{supported}"
101
- )
92
+ raise ValueError(f"Invalid date/time format: '{dt_str}'")
102
93
 
103
94
 
104
- def calculate_time_difference(
95
+ def time_diff(
105
96
  from_time_str: str,
106
97
  tz_str: str = "UTC",
107
98
  to_time_str: str | None = None,
@@ -111,10 +102,10 @@ def calculate_time_difference(
111
102
  Calculate the difference between two datetime values.
112
103
 
113
104
  :param from_time_str: Date-time string "YYYY-MM-DD HH:MM:SS" for the start.
114
- :param tz_str: Timezone of from_time_str, e.g. 'UTC+8'. Defaults to 'UTC'.
115
- :param to_time_str: Optional date-time string for the end; if None, uses now().
116
- :param to_tz_str: Timezone of to_time_str. Defaults to 'UTC'.
117
- :return: Tuple (days, hours, minutes, seconds).
105
+ :param tz_str: Timezone of from_time_str, e.g. 'UTC+8'. Defaults to 'UTC'.
106
+ :param to_time_str: Optional date-time string for the end; if None, uses now().
107
+ :param to_tz_str: Timezone of to_time_str. Defaults to 'UTC'.
108
+ :return: Tuple (days, hours, minutes, seconds).
118
109
  """
119
110
  try:
120
111
  # parse start time
@@ -4,13 +4,9 @@ novel_downloader.utils.time_utils.sleep_utils
4
4
  ---------------------------------------------
5
5
 
6
6
  Utilities for adding randomized delays in scripts and bots.
7
-
8
- Includes:
9
- - sleep_with_random_delay(): Sleep between base and base+spread seconds,
10
- optionally capped with a max_sleep limit.
11
7
  """
12
8
 
13
- __all__ = ["sleep_with_random_delay", "async_sleep_with_random_delay"]
9
+ __all__ = ["jitter_sleep", "async_jitter_sleep"]
14
10
 
15
11
  import asyncio
16
12
  import logging
@@ -20,7 +16,7 @@ import time
20
16
  logger = logging.getLogger(__name__)
21
17
 
22
18
 
23
- def sleep_with_random_delay(
19
+ def jitter_sleep(
24
20
  base: float,
25
21
  add_spread: float = 0.0,
26
22
  mul_spread: float = 1.0,
@@ -41,7 +37,7 @@ def sleep_with_random_delay(
41
37
  :param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
42
38
  :param max_sleep: Optional upper limit for the final sleep duration.
43
39
  """
44
- if base < 0 or add_spread < 0 or mul_spread < 0:
40
+ if base < 0 or add_spread < 0 or mul_spread < 1.0:
45
41
  logger.warning(
46
42
  "[sleep] Invalid parameters: base=%s, add_spread=%s, mul_spread=%s",
47
43
  base,
@@ -63,7 +59,7 @@ def sleep_with_random_delay(
63
59
  return
64
60
 
65
61
 
66
- async def async_sleep_with_random_delay(
62
+ async def async_jitter_sleep(
67
63
  base: float,
68
64
  add_spread: float = 0.0,
69
65
  mul_spread: float = 1.0,
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web
4
+ --------------------
5
+
6
+ This module exposes the WEB entry point.
7
+ """
8
+
9
+ __all__ = [
10
+ "web_main",
11
+ ]
12
+
13
+ from .main import web_main
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web.components
4
+ -------------------------------
5
+
6
+ Entry point for reusable web UI components
7
+ """
8
+
9
+ __all__ = ["navbar"]
10
+
11
+ from .navigation import navbar
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web.components.navigation
4
+ ------------------------------------------
5
+
6
+ A tiny NiceGUI component that renders the app's top navigation bar
7
+ """
8
+
9
+ from nicegui import ui
10
+
11
+
12
+ def navbar(active: str) -> None:
13
+ """
14
+ Render the site-wide navigation header.
15
+
16
+ :param active: Key of the current page to highlight.
17
+ """
18
+ with (
19
+ ui.header().classes("px-3 items-center justify-between bg-primary text-white"),
20
+ ui.row().classes("items-center gap-2 flex-wrap"),
21
+ ):
22
+ _nav_btn("搜索", "/", active == "search", icon="search")
23
+ _nav_btn("下载", "/download", active == "download", icon="download")
24
+ _nav_btn("正在下载", "/progress", active == "progress", icon="cloud_download")
25
+
26
+
27
+ def _nav_btn(label: str, path: str, is_active: bool, icon: str | None = None) -> None:
28
+ if is_active:
29
+ ui.button(label, icon=icon, on_click=lambda: ui.navigate.to(path)).props(
30
+ "unelevated color=white text-color=primary"
31
+ )
32
+ else:
33
+ ui.button(label, icon=icon, on_click=lambda: ui.navigate.to(path)).props(
34
+ "flat text-color=white"
35
+ )
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web.main
4
+ -------------------------
5
+
6
+ Novel Downloader web UI (NiceGUI).
7
+
8
+ This entry point starts the local server and registers the app's pages.
9
+ """
10
+
11
+ import argparse
12
+ from pathlib import Path
13
+
14
+ from nicegui import app, ui
15
+
16
+ import novel_downloader.web.pages # noqa: F401
17
+ from novel_downloader.config import get_config_value
18
+ from novel_downloader.utils.logger import setup_logging
19
+
20
+
21
+ def mount_exports() -> None:
22
+ output_dir = get_config_value(["general", "output_dir"], "./downloads")
23
+ out = Path(output_dir).expanduser().resolve()
24
+ out.mkdir(parents=True, exist_ok=True)
25
+ # serves /download/<filename> from the export dir
26
+ app.add_static_files("/download", local_directory=out)
27
+
28
+
29
+ def web_main() -> None:
30
+ p = argparse.ArgumentParser(
31
+ description="Novel Downloader web UI.",
32
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
33
+ )
34
+ p.add_argument(
35
+ "--listen",
36
+ choices=["local", "public"],
37
+ default="local",
38
+ help=(
39
+ "Bind address mode: 'local' binds to 127.0.0.1; "
40
+ "'public' binds to 0.0.0.0."
41
+ ),
42
+ )
43
+ p.add_argument(
44
+ "--port",
45
+ type=int,
46
+ default=8080,
47
+ help="TCP port to serve the app on.",
48
+ )
49
+ p.add_argument(
50
+ "--reload",
51
+ action="store_true",
52
+ help="Enable autoreload on code changes (development).",
53
+ )
54
+ args = p.parse_args()
55
+
56
+ host = "127.0.0.1" if args.listen == "local" else "0.0.0.0"
57
+
58
+ log_level = get_config_value(["general", "debug", "log_level"], "INFO")
59
+ setup_logging(log_level=log_level)
60
+
61
+ app.on_startup(mount_exports)
62
+ ui.run(host=host, port=args.port, reload=args.reload)
63
+
64
+
65
+ if __name__ in {"__main__", "__mp_main__"}:
66
+ web_main()
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web.pages
4
+ --------------------------
5
+
6
+ NiceGUI page registrations; importing this package exposes and registers all routes.
7
+ """
8
+
9
+ __all__ = [
10
+ "page_download", # /download
11
+ "page_progress", # /progress
12
+ "page_search", # /
13
+ ]
14
+
15
+ from .download import page_download
16
+ from .progress import page_progress
17
+ from .search import page_search
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.web.pages.download
4
+ -----------------------------------
5
+
6
+ """
7
+
8
+ from nicegui import ui
9
+
10
+ from novel_downloader.web.components import navbar
11
+ from novel_downloader.web.services import manager, setup_dialog
12
+
13
+ _SUPPORT_SITES = {
14
+ "aaatxt": "3A电子书 (aaatxt)",
15
+ "biquge": "笔趣阁 (biquge)",
16
+ "biquyuedu": "精彩小说 (biquyuedu)",
17
+ "dxmwx": "大熊猫文学网 (dxmwx)",
18
+ "eightnovel": "无限轻小说 (8novel)",
19
+ "esjzone": "ESJ Zone (esjzone)",
20
+ "guidaye": "名著阅读 (guidaye)",
21
+ "hetushu": "和图书 (hetushu)",
22
+ "i25zw": "25中文网 (i25zw)",
23
+ "ixdzs8": "爱下电子书 (ixdzs8)",
24
+ "jpxs123": "精品小说网 (jpxs123)",
25
+ "lewenn": "乐文小说网 (lewenn)",
26
+ "linovelib": "哔哩轻小说 (linovelib)",
27
+ "piaotia": "飘天文学网 (piaotia)",
28
+ "qbtr": "全本同人小说 (qbtr)",
29
+ "qianbi": "铅笔小说 (qianbi)",
30
+ "qidian": "起点中文网 (qidian)",
31
+ "quanben5": "全本小说网 (quanben5)",
32
+ "sfacg": "SF轻小说 (sfacg)",
33
+ "shencou": "神凑轻小说 (shencou)",
34
+ "shuhaige": "书海阁小说网 (shuhaige)",
35
+ "tongrenquan": "同人圈 (tongrenquan)",
36
+ "ttkan": "天天看小说 (ttkan)",
37
+ "wanbengo": "完本神站 (wanbengo)",
38
+ "xiaoshuowu": "小说屋 (xiaoshuowu)",
39
+ "xiguashuwu": "西瓜书屋 (xiguashuwu)",
40
+ "xs63b": "小说路上 (xs63b)",
41
+ "xshbook": "小说虎 (xshbook)",
42
+ "yamibo": "百合会 (yamibo)",
43
+ "yibige": "一笔阁 (yibige)",
44
+ }
45
+ _DEFAULT_SITE = "qidian"
46
+
47
+
48
+ @ui.page("/download") # type: ignore[misc]
49
+ def page_download() -> None:
50
+ navbar("download")
51
+ ui.label("下载界面").classes("text-lg")
52
+ setup_dialog()
53
+
54
+ with ui.card().classes("max-w-[600px]"):
55
+ site = ui.select(
56
+ _SUPPORT_SITES,
57
+ value=_DEFAULT_SITE,
58
+ label="站点",
59
+ with_input=True,
60
+ ).classes("w-full")
61
+
62
+ book_id = ui.input("书籍ID").props("outlined dense").classes("w-full")
63
+
64
+ async def add_task() -> None:
65
+ bid = (book_id.value or "").strip()
66
+ if not bid:
67
+ ui.notify("请输入书籍ID", type="warning")
68
+ return
69
+ title = f"{site.value} (id = {bid})"
70
+ ui.notify(f"已添加任务: {title}")
71
+ await manager.add_task(title=title, site=str(site.value), book_id=bid)
72
+
73
+ with ui.row().classes("justify-end w-full"):
74
+ ui.button(
75
+ "添加到下载队列",
76
+ on_click=add_task,
77
+ color="primary",
78
+ ).props("unelevated")