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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +77 -64
- novel_downloader/cli/export.py +16 -20
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +65 -105
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +1 -0
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +14 -9
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +17 -11
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/esjzone.py +61 -66
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/linovelib.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
- novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
- novel_downloader/core/parsers/qidian/main_parser.py +11 -38
- novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +31 -82
- novel_downloader/locales/zh.json +32 -83
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -22
- novel_downloader/utils/chapter_storage.py +3 -2
- novel_downloader/utils/constants.py +4 -29
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +1 -1
- novel_downloader/utils/epub/constants.py +57 -16
- novel_downloader/utils/epub/documents.py +88 -194
- novel_downloader/utils/epub/models.py +0 -14
- novel_downloader/utils/epub/utils.py +63 -96
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +3 -113
- novel_downloader/utils/file_utils/sanitize.py +0 -4
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/logger.py +8 -16
- novel_downloader/utils/network.py +2 -2
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +4 -8
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
novel_downloader/utils/state.py
CHANGED
@@ -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
|
-
|
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,
|
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:
|
23
|
+
:param c: A single character.
|
26
24
|
:param normal_char: Replacement for narrow chars (default U+0020).
|
27
|
-
:param asian_char:
|
28
|
-
:return:
|
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:
|
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
|
-
"
|
17
|
-
"
|
18
|
-
"
|
10
|
+
"time_diff",
|
11
|
+
"async_jitter_sleep",
|
12
|
+
"jitter_sleep",
|
19
13
|
]
|
20
14
|
|
21
|
-
from .datetime_utils import
|
22
|
-
from .sleep_utils import
|
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
|
-
"
|
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:
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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:
|
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
|
-
|
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
|
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:
|
115
|
-
:param to_time_str:
|
116
|
-
:param to_tz_str:
|
117
|
-
:return:
|
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__ = ["
|
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
|
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
|
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,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")
|