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.
- 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 +79 -66
- novel_downloader/cli/export.py +17 -21
- 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 +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- 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 +17 -12
- 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 +20 -14
- 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} +56 -64
- 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 +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- 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 +64 -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 +64 -69
- 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/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -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 +429 -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 +34 -85
- novel_downloader/locales/zh.json +35 -86
- 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 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -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/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- 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/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- 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 +55 -49
- 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.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.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/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- 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/crypto_utils.py +0 -71
- 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.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.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"
|
@@ -42,7 +42,7 @@ class TextCleaner(Cleaner):
|
|
42
42
|
TextCleaner removes invisible characters, strips unwanted patterns,
|
43
43
|
and applies literal replacements in a single pass using a combined regex.
|
44
44
|
|
45
|
-
For regex that never matches, reference:
|
45
|
+
For regex that never matches (r"$^"), reference:
|
46
46
|
|
47
47
|
https://stackoverflow.com/questions/2930182/regex-to-not-match-anything
|
48
48
|
"""
|
@@ -53,13 +53,14 @@ class TextCleaner(Cleaner):
|
|
53
53
|
"""
|
54
54
|
Initialize TextCleaner with the given configuration.
|
55
55
|
|
56
|
-
|
56
|
+
Configuration fields (from ``TextCleanerConfig``):
|
57
|
+
* remove_invisible: whether to strip BOM/zero-width chars
|
58
|
+
* title_remove_patterns: list of regex patterns to delete from titles
|
59
|
+
* content_remove_patterns: list of regex patterns to delete from content
|
60
|
+
* title_replacements: dict of literal replacements for titles
|
61
|
+
* content_replacements: dict of literal replacements for content
|
57
62
|
|
58
|
-
|
59
|
-
- title_remove_patterns: list of regex patterns to delete from titles
|
60
|
-
- content_remove_patterns: list of regex patterns to delete from content
|
61
|
-
- title_replacements: dict of literal replacements for titles
|
62
|
-
- content_replacements: dict of literal replacements for content
|
63
|
+
:param config: A ``TextCleanerConfig`` instance.
|
63
64
|
"""
|
64
65
|
self._remove_invisible = config.remove_invisible
|
65
66
|
|
@@ -73,20 +74,23 @@ class TextCleaner(Cleaner):
|
|
73
74
|
|
74
75
|
# Build a single combined regex for title:
|
75
76
|
# all delete‐patterns OR all escaped replacement‐keys
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
77
|
+
self._title_combined_rx: re.Pattern[str] | None = None
|
78
|
+
if title_remove or self._title_repl_map:
|
79
|
+
title_parts = title_remove + [re.escape(k) for k in self._title_repl_map]
|
80
|
+
# longer first to avoid prefix collisions
|
81
|
+
title_parts.sort(key=len, reverse=True)
|
82
|
+
self._title_combined_rx = re.compile("|".join(title_parts))
|
82
83
|
|
83
84
|
# Build a single combined regex for content (multiline mode)
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
85
|
+
self._content_combined_rx: re.Pattern[str] | None = None
|
86
|
+
if content_remove or self._content_repl_map:
|
87
|
+
content_parts = content_remove + [
|
88
|
+
re.escape(k) for k in self._content_repl_map
|
89
|
+
]
|
90
|
+
content_parts.sort(key=len, reverse=True)
|
91
|
+
self._content_combined_rx = re.compile(
|
92
|
+
"|".join(content_parts), flags=re.MULTILINE
|
93
|
+
)
|
90
94
|
|
91
95
|
def clean_title(self, text: str) -> str:
|
92
96
|
"""
|
@@ -132,11 +136,11 @@ class TextCleaner(Cleaner):
|
|
132
136
|
Remove BOM and zero-width/invisible characters from the text.
|
133
137
|
|
134
138
|
Matches:
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
139
|
+
* U+FEFF (BOM)
|
140
|
+
* U+200B ZERO WIDTH SPACE
|
141
|
+
* U+200C ZERO WIDTH NON-JOINER
|
142
|
+
* U+200D ZERO WIDTH JOINER
|
143
|
+
* U+2060 WORD JOINER
|
140
144
|
|
141
145
|
:param text: Input string possibly containing invisible chars.
|
142
146
|
:return: String with those characters stripped.
|
@@ -146,7 +150,7 @@ class TextCleaner(Cleaner):
|
|
146
150
|
def _do_clean(
|
147
151
|
self,
|
148
152
|
text: str,
|
149
|
-
combined_rx: Pattern[str],
|
153
|
+
combined_rx: Pattern[str] | None,
|
150
154
|
repl_map: dict[str, str],
|
151
155
|
) -> str:
|
152
156
|
"""
|
@@ -158,17 +162,22 @@ class TextCleaner(Cleaner):
|
|
158
162
|
:param repl_map: Mapping from matched token to replacement text.
|
159
163
|
:return: Cleaned text.
|
160
164
|
"""
|
165
|
+
if not self._remove_invisible and not combined_rx:
|
166
|
+
return text.strip()
|
167
|
+
|
161
168
|
# Strip invisible chars if configured
|
162
169
|
if self._remove_invisible:
|
163
170
|
text = self._remove_bom_and_invisible(text)
|
164
171
|
|
165
172
|
# Single‐pass removal & replacement
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
173
|
+
if combined_rx:
|
174
|
+
|
175
|
+
def _sub(match: Match[str]) -> str:
|
176
|
+
# If token in repl_map -> replacement; else -> delete (empty string)
|
177
|
+
return repl_map.get(match.group(0), "")
|
178
|
+
|
179
|
+
text = combined_rx.sub(_sub, text)
|
170
180
|
|
171
|
-
text = combined_rx.sub(_sub, text)
|
172
181
|
return text.strip()
|
173
182
|
|
174
183
|
|
@@ -11,8 +11,6 @@ __all__ = [
|
|
11
11
|
"truncate_half_lines",
|
12
12
|
]
|
13
13
|
|
14
|
-
import math
|
15
|
-
|
16
14
|
|
17
15
|
def content_prefix(
|
18
16
|
text: str,
|
@@ -41,22 +39,13 @@ def content_prefix(
|
|
41
39
|
|
42
40
|
def truncate_half_lines(text: str) -> str:
|
43
41
|
"""
|
44
|
-
Keep the first half of the lines
|
42
|
+
Keep the first half of the lines.
|
45
43
|
|
46
44
|
:param text: Full input text
|
47
45
|
:return: Truncated text with first half of lines
|
48
46
|
"""
|
49
47
|
lines = text.splitlines()
|
50
48
|
non_empty_lines = [line for line in lines if line.strip()]
|
51
|
-
keep_count =
|
52
|
-
|
53
|
-
result_lines = []
|
54
|
-
count = 0
|
55
|
-
for line in lines:
|
56
|
-
result_lines.append(line)
|
57
|
-
if line.strip():
|
58
|
-
count += 1
|
59
|
-
if count >= keep_count:
|
60
|
-
break
|
61
|
-
|
49
|
+
keep_count = (len(non_empty_lines) + 1) // 2
|
50
|
+
result_lines = non_empty_lines[:keep_count]
|
62
51
|
return "\n".join(result_lines)
|
@@ -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,50 +16,51 @@ import time
|
|
20
16
|
logger = logging.getLogger(__name__)
|
21
17
|
|
22
18
|
|
23
|
-
def
|
19
|
+
def _calc_sleep_duration(
|
24
20
|
base: float,
|
25
|
-
add_spread: float
|
26
|
-
mul_spread: float
|
27
|
-
*,
|
21
|
+
add_spread: float,
|
22
|
+
mul_spread: float,
|
28
23
|
max_sleep: float | None = None,
|
29
|
-
|
24
|
+
*,
|
25
|
+
log_prefix: str = "sleep",
|
26
|
+
) -> float | None:
|
30
27
|
"""
|
31
|
-
|
32
|
-
|
33
|
-
The total sleep time is computed as:
|
34
|
-
|
35
|
-
duration = base * uniform(1.0, mul_spread) + uniform(0, add_spread)
|
28
|
+
Compute the jittered sleep duration (in seconds) or return None if params invalid.
|
36
29
|
|
37
|
-
|
30
|
+
duration = base * uniform(1.0, mul_spread) + uniform(0, add_spread)
|
38
31
|
|
39
|
-
|
40
|
-
:param add_spread: Maximum extra seconds to add after scaling base.
|
41
|
-
:param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
|
42
|
-
:param max_sleep: Optional upper limit for the final sleep duration.
|
32
|
+
then optionally capped by max_sleep.
|
43
33
|
"""
|
44
|
-
if base < 0 or add_spread < 0 or mul_spread < 0:
|
34
|
+
if base < 0 or add_spread < 0 or mul_spread < 1.0:
|
45
35
|
logger.warning(
|
46
|
-
"[
|
36
|
+
"[%s] Invalid parameters: base=%s, add_spread=%s, mul_spread=%s",
|
37
|
+
log_prefix,
|
47
38
|
base,
|
48
39
|
add_spread,
|
49
40
|
mul_spread,
|
50
41
|
)
|
51
|
-
return
|
42
|
+
return None
|
52
43
|
|
53
|
-
# Calculate the raw duration
|
54
44
|
multiplicative_jitter = random.uniform(1.0, mul_spread)
|
55
|
-
additive_jitter = random.uniform(0, add_spread)
|
45
|
+
additive_jitter = random.uniform(0.0, add_spread)
|
56
46
|
duration = base * multiplicative_jitter + additive_jitter
|
57
47
|
|
58
48
|
if max_sleep is not None:
|
59
49
|
duration = min(duration, max_sleep)
|
60
50
|
|
61
|
-
logger.debug(
|
62
|
-
|
63
|
-
|
51
|
+
logger.debug(
|
52
|
+
"[%s] base=%.3f mul=%.3f add=%.3f max=%s -> duration=%.3f",
|
53
|
+
log_prefix,
|
54
|
+
base,
|
55
|
+
multiplicative_jitter,
|
56
|
+
additive_jitter,
|
57
|
+
max_sleep,
|
58
|
+
duration,
|
59
|
+
)
|
60
|
+
return duration
|
64
61
|
|
65
62
|
|
66
|
-
|
63
|
+
def jitter_sleep(
|
67
64
|
base: float,
|
68
65
|
add_spread: float = 0.0,
|
69
66
|
mul_spread: float = 1.0,
|
@@ -71,34 +68,43 @@ async def async_sleep_with_random_delay(
|
|
71
68
|
max_sleep: float | None = None,
|
72
69
|
) -> None:
|
73
70
|
"""
|
74
|
-
|
75
|
-
|
76
|
-
The total sleep time is computed as:
|
77
|
-
|
78
|
-
duration = base * uniform(1.0, mul_spread) + uniform(0, add_spread)
|
79
|
-
|
80
|
-
If `max_sleep` is provided, the duration will be capped at that value.
|
71
|
+
Sleep for a random duration by combining multiplicative and additive jitter.
|
81
72
|
|
82
73
|
:param base: Base sleep time in seconds. Must be >= 0.
|
83
74
|
:param add_spread: Maximum extra seconds to add after scaling base.
|
84
75
|
:param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
|
85
76
|
:param max_sleep: Optional upper limit for the final sleep duration.
|
86
77
|
"""
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
78
|
+
duration = _calc_sleep_duration(
|
79
|
+
base,
|
80
|
+
add_spread,
|
81
|
+
mul_spread,
|
82
|
+
max_sleep,
|
83
|
+
log_prefix="sleep",
|
84
|
+
)
|
85
|
+
if duration is None:
|
94
86
|
return
|
87
|
+
time.sleep(duration)
|
95
88
|
|
96
|
-
multiplicative_jitter = random.uniform(1.0, mul_spread)
|
97
|
-
additive_jitter = random.uniform(0, add_spread)
|
98
|
-
duration = base * multiplicative_jitter + additive_jitter
|
99
89
|
|
100
|
-
|
101
|
-
|
90
|
+
async def async_jitter_sleep(
|
91
|
+
base: float,
|
92
|
+
add_spread: float = 0.0,
|
93
|
+
mul_spread: float = 1.0,
|
94
|
+
*,
|
95
|
+
max_sleep: float | None = None,
|
96
|
+
) -> None:
|
97
|
+
"""
|
98
|
+
Async sleep for a random duration by combining multiplicative and additive jitter.
|
102
99
|
|
103
|
-
|
100
|
+
:param base: Base sleep time in seconds. Must be >= 0.
|
101
|
+
:param add_spread: Maximum extra seconds to add after scaling base.
|
102
|
+
:param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
|
103
|
+
:param max_sleep: Optional upper limit for the final sleep duration.
|
104
|
+
"""
|
105
|
+
duration = _calc_sleep_duration(
|
106
|
+
base, add_spread, mul_spread, max_sleep, log_prefix="async sleep"
|
107
|
+
)
|
108
|
+
if duration is None:
|
109
|
+
return
|
104
110
|
await asyncio.sleep(duration)
|
@@ -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
|
+
)
|