novel-downloader 2.0.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/download.py +3 -3
- novel_downloader/cli/export.py +1 -1
- novel_downloader/cli/ui.py +7 -7
- novel_downloader/config/adapter.py +191 -154
- novel_downloader/core/__init__.py +5 -6
- novel_downloader/core/exporters/common/txt.py +9 -9
- novel_downloader/core/exporters/linovelib/txt.py +9 -9
- novel_downloader/core/fetchers/qidian.py +20 -35
- novel_downloader/core/interfaces/fetcher.py +2 -2
- novel_downloader/core/interfaces/parser.py +2 -2
- novel_downloader/core/parsers/base.py +1 -0
- novel_downloader/core/parsers/eightnovel.py +2 -2
- novel_downloader/core/parsers/esjzone.py +3 -3
- novel_downloader/core/parsers/qidian/main_parser.py +747 -12
- novel_downloader/core/parsers/qidian/utils/__init__.py +2 -21
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/xiguashuwu.py +6 -12
- novel_downloader/locales/en.json +3 -3
- novel_downloader/locales/zh.json +3 -3
- novel_downloader/utils/__init__.py +0 -2
- novel_downloader/utils/chapter_storage.py +2 -3
- novel_downloader/utils/constants.py +1 -3
- novel_downloader/utils/cookies.py +32 -17
- novel_downloader/utils/crypto_utils/__init__.py +0 -6
- novel_downloader/utils/crypto_utils/rc4.py +40 -50
- novel_downloader/utils/epub/__init__.py +2 -3
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +5 -5
- novel_downloader/utils/epub/documents.py +7 -7
- novel_downloader/utils/epub/models.py +8 -8
- novel_downloader/utils/epub/utils.py +10 -10
- novel_downloader/utils/file_utils/io.py +48 -73
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -11
- novel_downloader/utils/fontocr/__init__.py +13 -0
- novel_downloader/utils/{fontocr.py → fontocr/core.py} +70 -61
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +80 -56
- novel_downloader/utils/network.py +16 -40
- 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/sleep_utils.py +53 -43
- novel_downloader/web/main.py +1 -1
- novel_downloader/web/pages/search.py +3 -3
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/METADATA +2 -1
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/RECORD +51 -55
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -89
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -470
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -126
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +0 -143
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -110
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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)
|
@@ -16,50 +16,51 @@ import time
|
|
16
16
|
logger = logging.getLogger(__name__)
|
17
17
|
|
18
18
|
|
19
|
-
def
|
19
|
+
def _calc_sleep_duration(
|
20
20
|
base: float,
|
21
|
-
add_spread: float
|
22
|
-
mul_spread: float
|
23
|
-
*,
|
21
|
+
add_spread: float,
|
22
|
+
mul_spread: float,
|
24
23
|
max_sleep: float | None = None,
|
25
|
-
|
24
|
+
*,
|
25
|
+
log_prefix: str = "sleep",
|
26
|
+
) -> float | None:
|
26
27
|
"""
|
27
|
-
|
28
|
-
|
29
|
-
The total sleep time is computed as:
|
30
|
-
|
31
|
-
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.
|
32
29
|
|
33
|
-
|
30
|
+
duration = base * uniform(1.0, mul_spread) + uniform(0, add_spread)
|
34
31
|
|
35
|
-
|
36
|
-
:param add_spread: Maximum extra seconds to add after scaling base.
|
37
|
-
:param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
|
38
|
-
:param max_sleep: Optional upper limit for the final sleep duration.
|
32
|
+
then optionally capped by max_sleep.
|
39
33
|
"""
|
40
34
|
if base < 0 or add_spread < 0 or mul_spread < 1.0:
|
41
35
|
logger.warning(
|
42
|
-
"[
|
36
|
+
"[%s] Invalid parameters: base=%s, add_spread=%s, mul_spread=%s",
|
37
|
+
log_prefix,
|
43
38
|
base,
|
44
39
|
add_spread,
|
45
40
|
mul_spread,
|
46
41
|
)
|
47
|
-
return
|
42
|
+
return None
|
48
43
|
|
49
|
-
# Calculate the raw duration
|
50
44
|
multiplicative_jitter = random.uniform(1.0, mul_spread)
|
51
|
-
additive_jitter = random.uniform(0, add_spread)
|
45
|
+
additive_jitter = random.uniform(0.0, add_spread)
|
52
46
|
duration = base * multiplicative_jitter + additive_jitter
|
53
47
|
|
54
48
|
if max_sleep is not None:
|
55
49
|
duration = min(duration, max_sleep)
|
56
50
|
|
57
|
-
logger.debug(
|
58
|
-
|
59
|
-
|
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
|
60
61
|
|
61
62
|
|
62
|
-
|
63
|
+
def jitter_sleep(
|
63
64
|
base: float,
|
64
65
|
add_spread: float = 0.0,
|
65
66
|
mul_spread: float = 1.0,
|
@@ -67,34 +68,43 @@ async def async_jitter_sleep(
|
|
67
68
|
max_sleep: float | None = None,
|
68
69
|
) -> None:
|
69
70
|
"""
|
70
|
-
|
71
|
-
|
72
|
-
The total sleep time is computed as:
|
73
|
-
|
74
|
-
duration = base * uniform(1.0, mul_spread) + uniform(0, add_spread)
|
75
|
-
|
76
|
-
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.
|
77
72
|
|
78
73
|
:param base: Base sleep time in seconds. Must be >= 0.
|
79
74
|
:param add_spread: Maximum extra seconds to add after scaling base.
|
80
75
|
:param mul_spread: Maximum multiplier factor for base; drawn from [1.0, mul_spread].
|
81
76
|
:param max_sleep: Optional upper limit for the final sleep duration.
|
82
77
|
"""
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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:
|
90
86
|
return
|
87
|
+
time.sleep(duration)
|
91
88
|
|
92
|
-
multiplicative_jitter = random.uniform(1.0, mul_spread)
|
93
|
-
additive_jitter = random.uniform(0, add_spread)
|
94
|
-
duration = base * multiplicative_jitter + additive_jitter
|
95
89
|
|
96
|
-
|
97
|
-
|
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.
|
98
99
|
|
99
|
-
|
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
|
100
110
|
await asyncio.sleep(duration)
|
novel_downloader/web/main.py
CHANGED
@@ -56,7 +56,7 @@ def web_main() -> None:
|
|
56
56
|
host = "127.0.0.1" if args.listen == "local" else "0.0.0.0"
|
57
57
|
|
58
58
|
log_level = get_config_value(["general", "debug", "log_level"], "INFO")
|
59
|
-
setup_logging(
|
59
|
+
setup_logging(console_level=log_level)
|
60
60
|
|
61
61
|
app.on_startup(mount_exports)
|
62
62
|
ui.run(host=host, port=args.port, reload=args.reload)
|
@@ -145,9 +145,9 @@ def _build_settings_dropdown(
|
|
145
145
|
Create settings button + anchored menu with initial values from state.
|
146
146
|
|
147
147
|
Returns a tuple of getter functions:
|
148
|
-
|
149
|
-
|
150
|
-
|
148
|
+
* get_sites(): list of site keys, or None if none selected
|
149
|
+
* get_psl(): per-site limit (int)
|
150
|
+
* get_timeout(): timeout (float)
|
151
151
|
"""
|
152
152
|
site_cbs: dict[str, Any] = {}
|
153
153
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: novel-downloader
|
3
|
-
Version: 2.0.
|
3
|
+
Version: 2.0.1
|
4
4
|
Summary: A command-line tool for downloading Chinese web novels from Qidian and similar platforms.
|
5
5
|
Author-email: Saudade Z <saudadez217@gmail.com>
|
6
6
|
License: MIT License
|
@@ -47,6 +47,7 @@ Requires-Dist: aiohttp
|
|
47
47
|
Requires-Dist: lxml
|
48
48
|
Requires-Dist: platformdirs
|
49
49
|
Provides-Extra: font-recovery
|
50
|
+
Requires-Dist: brotli; extra == "font-recovery"
|
50
51
|
Requires-Dist: numpy; extra == "font-recovery"
|
51
52
|
Requires-Dist: fonttools; extra == "font-recovery"
|
52
53
|
Requires-Dist: pillow; extra == "font-recovery"
|
@@ -1,16 +1,16 @@
|
|
1
|
-
novel_downloader/__init__.py,sha256=
|
1
|
+
novel_downloader/__init__.py,sha256=rCDXxBxrYs3WYnanTn5QZYhg2-rG48Ry2DS7urOkIws,218
|
2
2
|
novel_downloader/cli/__init__.py,sha256=okUd_09fBWx8lz3zx3329dIK0xNRlNQJkUzDXYoSE1U,167
|
3
3
|
novel_downloader/cli/clean.py,sha256=VaiJox75-ZuXvSDFMaiAUawEbdtuEUsnsVWZGKuirJ4,2395
|
4
4
|
novel_downloader/cli/config.py,sha256=W4-f59dK5jdlg_9XDwGIyzd6VtpyNpB9U4qcxfso8Xg,3598
|
5
|
-
novel_downloader/cli/download.py,sha256
|
6
|
-
novel_downloader/cli/export.py,sha256=
|
5
|
+
novel_downloader/cli/download.py,sha256=2iEW137yI-YbBE-4Um7h4IQRZzX0qbPILwCQeTEfU8o,7603
|
6
|
+
novel_downloader/cli/export.py,sha256=u1Cu-2PcVBny8cMVPS0lTCs34XXcVljkXBZpNUCQQKw,2577
|
7
7
|
novel_downloader/cli/main.py,sha256=GKtxeumw_VvWManp52Am538jL2t7xz7yVrGIaxrvSVk,1009
|
8
8
|
novel_downloader/cli/search.py,sha256=_Ci4fa1ECiYmBZI2OZBxzwddYGIvdUyIp4AOIJiL1Ds,3641
|
9
|
-
novel_downloader/cli/ui.py,sha256=
|
9
|
+
novel_downloader/cli/ui.py,sha256=XyvgTlsiV1NkGLWfCt6WvTIm8h8hXq5_RSfCdUwy8P0,4024
|
10
10
|
novel_downloader/config/__init__.py,sha256=gwaDMlcBX7Lw8Hi8dY2HxNEt9ysMhIFNhkziyjramWo,405
|
11
|
-
novel_downloader/config/adapter.py,sha256=
|
11
|
+
novel_downloader/config/adapter.py,sha256=4pZo-9PA7g2H1tybqdUeGOODZCKtpVyAKUg23VX-LBA,13444
|
12
12
|
novel_downloader/config/file_io.py,sha256=ncKk7rj07UnYYVEPR_RHS2D5igMSv8xaHySNvJQWhB8,6495
|
13
|
-
novel_downloader/core/__init__.py,sha256=
|
13
|
+
novel_downloader/core/__init__.py,sha256=oCmWj9t7CEyNfew5O25YeaMPeMvXt082jprKAPMffKk,1166
|
14
14
|
novel_downloader/core/archived/deqixs/fetcher.py,sha256=JGTLWI-d5jLULB-vS2VUeajwp7tHlb9trUCF4lJrdiQ,3432
|
15
15
|
novel_downloader/core/archived/deqixs/parser.py,sha256=udM6MYdunPVYwUNjEoBDjffHR_SsXPvv_EqsCMJJVrE,4169
|
16
16
|
novel_downloader/core/archived/deqixs/searcher.py,sha256=ezXCQ0pM8AqmTXmkzLrOSebTvHg_GouIywIE23c5hwY,2920
|
@@ -33,11 +33,11 @@ novel_downloader/core/exporters/txt_util.py,sha256=-pkwMFh-69FHEziTW4LiW3-u6f1Gq
|
|
33
33
|
novel_downloader/core/exporters/common/__init__.py,sha256=8qVMdtM_hLo3D3bMMQYYL10I0evHy1BgaceqOMTu9OE,259
|
34
34
|
novel_downloader/core/exporters/common/epub.py,sha256=OZV8KCE9otYLjlDKEogFQ5KElUAXGJwV5zSCtm59FOw,5961
|
35
35
|
novel_downloader/core/exporters/common/main_exporter.py,sha256=uBx67rBaeijZWt3cZDR7xipf-P_3ShyypZROxe8CjEQ,2165
|
36
|
-
novel_downloader/core/exporters/common/txt.py,sha256=
|
36
|
+
novel_downloader/core/exporters/common/txt.py,sha256=Nm4MW0rEIzX-7jzOFTaS4fdkzB1T8zBrIHpM-W7niN0,4755
|
37
37
|
novel_downloader/core/exporters/linovelib/__init__.py,sha256=AFjdUyFalrU8n_OjxqYENZ5zVcfYPONgWbvFkI0Sgls,250
|
38
38
|
novel_downloader/core/exporters/linovelib/epub.py,sha256=BiYgX61lZdgYeipnNuzMHmAjjSXnofs1eKoPR9UduDg,10703
|
39
39
|
novel_downloader/core/exporters/linovelib/main_exporter.py,sha256=qFMKfPobNWTDzW8CLr6FcNpkdqAuH5PMjqAQA_Jb78w,1938
|
40
|
-
novel_downloader/core/exporters/linovelib/txt.py,sha256=
|
40
|
+
novel_downloader/core/exporters/linovelib/txt.py,sha256=cdB_5oZs1SSPZ-TpemZv89goSVpj3jV23A52DPXhYDc,4467
|
41
41
|
novel_downloader/core/fetchers/__init__.py,sha256=V0BXwjULLODbS69YRRfvivEB4HVdUxo4tFMZ44qT6mU,1982
|
42
42
|
novel_downloader/core/fetchers/aaatxt.py,sha256=fupx1NLqHc3PH61_cSbZ0lafkmLmcs2R3FI5z56YyC4,2417
|
43
43
|
novel_downloader/core/fetchers/b520.py,sha256=IuIci62rHLtad3eIU-iR9be56vw7w3W4ctpvQpkOg3I,2458
|
@@ -56,7 +56,7 @@ novel_downloader/core/fetchers/linovelib.py,sha256=5oUji6UkM0XXkDmOPiWelhSo6C0Nk
|
|
56
56
|
novel_downloader/core/fetchers/piaotia.py,sha256=DPtBSBFmrU3Y3ha7u83sx5S9GXTpNxFj5VZtsP6pkmk,3220
|
57
57
|
novel_downloader/core/fetchers/qbtr.py,sha256=EGw8p-J01wu1iJmz4Vm6wjXPuTgVt6Y4BIY8Aun7zsQ,3094
|
58
58
|
novel_downloader/core/fetchers/qianbi.py,sha256=sWMpF78ZXBBVYs-DdUp0LG7khO84AMJzFYkDu_GSqfA,3174
|
59
|
-
novel_downloader/core/fetchers/qidian.py,sha256=
|
59
|
+
novel_downloader/core/fetchers/qidian.py,sha256=9CqpWu1qguAgYb6NmhSIA2O59S_S700H_PsbMf2iZsI,10329
|
60
60
|
novel_downloader/core/fetchers/quanben5.py,sha256=TkLFJOfpMOpKNkq2D9R0HI33zzAV-KuHccXquu6CZZ0,2776
|
61
61
|
novel_downloader/core/fetchers/rate_limiter.py,sha256=Z05dZOATi_-Su14qRmhVinArfLJ0QWpzgDShs2jHSR8,2714
|
62
62
|
novel_downloader/core/fetchers/registry.py,sha256=z9DDTUjc2r-Xiq3MqaPfV_3EmQBXr2TK_G8eARnblkA,1580
|
@@ -75,17 +75,17 @@ novel_downloader/core/fetchers/yibige.py,sha256=CJO26gMj_36XQMFVVnXDr8aoTQAqBA6A
|
|
75
75
|
novel_downloader/core/interfaces/__init__.py,sha256=YwDOPw_UCjuAE5u_LpUYxXjOH4X2kNMXrmUoBa5jp2Q,490
|
76
76
|
novel_downloader/core/interfaces/downloader.py,sha256=nGY2LYCCBzrKhs612L5tevGG8kJp_XfCyKvqjXEIeoI,1763
|
77
77
|
novel_downloader/core/interfaces/exporter.py,sha256=R62GPGzfGQu0B-7nu6y-PTnYF_TkrGir3XG19AW6jNE,1649
|
78
|
-
novel_downloader/core/interfaces/fetcher.py,sha256=
|
79
|
-
novel_downloader/core/interfaces/parser.py,sha256=
|
78
|
+
novel_downloader/core/interfaces/fetcher.py,sha256=2ESTszn-GrXj9rxYr_cQy86R1iCvWH-LbJx63j933YM,3669
|
79
|
+
novel_downloader/core/interfaces/parser.py,sha256=B_HsE_PshnUjz-QibBpuNht6whN9gboW-k1BAsnMe6M,1317
|
80
80
|
novel_downloader/core/interfaces/searcher.py,sha256=1orneqFtwcqM5sFBKmhpWFbHk5bV7oEp0mN_77lgy6E,587
|
81
81
|
novel_downloader/core/parsers/__init__.py,sha256=VfGEMS0PUQLkmc32xiz7l5rCPgzuy3Vz-1ZDxaYNp1Q,1927
|
82
82
|
novel_downloader/core/parsers/aaatxt.py,sha256=1NCrhbbpd5FUi0ybMX2LYf6Sss4pY3eDXTXk7MEkrkk,3633
|
83
83
|
novel_downloader/core/parsers/b520.py,sha256=SUiY1IYOZmu-tY-owbQHMKUyDFxwwOI6bgdjyNw_daM,3254
|
84
|
-
novel_downloader/core/parsers/base.py,sha256=
|
84
|
+
novel_downloader/core/parsers/base.py,sha256=R6xbT9ezFWcdFhqCItj2iigwQSB3MNrH9joC14vx2AI,4689
|
85
85
|
novel_downloader/core/parsers/biquyuedu.py,sha256=IpZfL2AC0oUq5Gx8iqwTH8sItxtzpxvDRKaMmtYf_xU,3867
|
86
86
|
novel_downloader/core/parsers/dxmwx.py,sha256=uj3tj36d8lnXbgFvmpHzrvv9oDYuMXWZCb0d8I32GfA,5006
|
87
|
-
novel_downloader/core/parsers/eightnovel.py,sha256=
|
88
|
-
novel_downloader/core/parsers/esjzone.py,sha256=
|
87
|
+
novel_downloader/core/parsers/eightnovel.py,sha256=M-BFJcqaBjNlf5PhoD-qscTR9o0ydRhQQAMbHzh1Z-o,7294
|
88
|
+
novel_downloader/core/parsers/esjzone.py,sha256=WeBwFn8waUtiZ3rzGJaiuB21ga7mnMnuBCA-0XnXOQI,7958
|
89
89
|
novel_downloader/core/parsers/guidaye.py,sha256=o1d987B5hkCEIVHYhszSavwKJFAErZVwpDaUPkSNUh4,3879
|
90
90
|
novel_downloader/core/parsers/hetushu.py,sha256=WUG4E9lvDJsa_l1xezYt0epPPJ4V9y_C4wwqWB8sxJs,4047
|
91
91
|
novel_downloader/core/parsers/i25zw.py,sha256=qILX6HQifu0hLJDNFc7XCqUNSQpfALhVgPC9jEGQt4g,4221
|
@@ -105,22 +105,16 @@ novel_downloader/core/parsers/tongrenquan.py,sha256=y3ok9g-BzA2YVM4KqoNPPpclE0wd
|
|
105
105
|
novel_downloader/core/parsers/ttkan.py,sha256=_8nc6diogbb9uGHm_lXYJT-RHR9p76o7ARn4NPom2-E,3564
|
106
106
|
novel_downloader/core/parsers/wanbengo.py,sha256=boEnK1gLLpqmf5EUFY9gVEhBxqxYrVka9ou_mDGZ4co,6407
|
107
107
|
novel_downloader/core/parsers/xiaoshuowu.py,sha256=k8j0TY5t7s0n1f-FWuRTZ-eN3A8p-_xnKy7-kp2deK4,5328
|
108
|
-
novel_downloader/core/parsers/xiguashuwu.py,sha256=
|
108
|
+
novel_downloader/core/parsers/xiguashuwu.py,sha256=1Xq00oRcb-vGskisbkNJuSmBGWNK7NpPOHbQHdMks_k,14223
|
109
109
|
novel_downloader/core/parsers/xs63b.py,sha256=A02abnMtsnWTomx-Hcfr97HFnNO38X9mmqKxHKrS610,5312
|
110
110
|
novel_downloader/core/parsers/xshbook.py,sha256=WwIGXEBAY8h-MLeApB1oVko_g3HF1Uj0X-Hko3TTrJ8,3995
|
111
111
|
novel_downloader/core/parsers/yamibo.py,sha256=B2ijBHCS0JW0I387LH8gDjevwakOE0RRKaKRsV4zo7A,5018
|
112
112
|
novel_downloader/core/parsers/yibige.py,sha256=K_nQ1SwVKLndgzlRvoTd2wd0tWXKb-T02c0sE6-bS5M,4846
|
113
113
|
novel_downloader/core/parsers/qidian/__init__.py,sha256=ScY9penE8b1m5HZGu6C1XwbzuqKbeGVCvli0J-xzzKs,173
|
114
|
-
novel_downloader/core/parsers/qidian/
|
115
|
-
novel_downloader/core/parsers/qidian/
|
116
|
-
novel_downloader/core/parsers/qidian/chapter_normal.py,sha256=JitsHCz9FSvslAbFLPdtF0FNr5V311Uj7K0zq7MUDwA,3738
|
117
|
-
novel_downloader/core/parsers/qidian/chapter_router.py,sha256=foVMlWtE-qUOvJD_4EDiuAVaNkFdeV_ZTCvS5IL7Orc,1957
|
118
|
-
novel_downloader/core/parsers/qidian/main_parser.py,sha256=w6s9RkcatA34chyzi4buFKew4-bIexlSHAIR1zBGIxE,2754
|
119
|
-
novel_downloader/core/parsers/qidian/utils/__init__.py,sha256=FuV7f-h76sk1g5BaqWOhsgh9uTykXA7MZ97kmkOB_lY,664
|
114
|
+
novel_downloader/core/parsers/qidian/main_parser.py,sha256=3JpvTFbL3yzGE7sfjgRHtUTid8_TVNaqBYyMNVnTwkM,29833
|
115
|
+
novel_downloader/core/parsers/qidian/utils/__init__.py,sha256=7aCxmMDQ3wkAPPtVIQ_YYprzE8V1IcHh2LK5S2rEc4U,266
|
120
116
|
novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py,sha256=idwsRx9Er5SRb3b_tmKwNxfOmUWR0QwpVf_zpDRJxuw,4687
|
121
|
-
novel_downloader/core/parsers/qidian/utils/
|
122
|
-
novel_downloader/core/parsers/qidian/utils/helpers.py,sha256=_HAOWW3HHtq2FtYlYU1FIebZdRmCo1Rfmh_WC9p_ZLU,3325
|
123
|
-
novel_downloader/core/parsers/qidian/utils/node_decryptor.py,sha256=MQQUI9nNpM23K0sA9bRVhs7pbQX6OvMc1NgvM28gHsU,5949
|
117
|
+
novel_downloader/core/parsers/qidian/utils/node_decryptor.py,sha256=WY2rG2S7GlIqDiFBBJaGW4XJorCwVHfbiYIeytkkmNo,5949
|
124
118
|
novel_downloader/core/searchers/__init__.py,sha256=pCCK1rxfQP8djdsRnpxoVgANe6CTpbP7DNbNond_1P4,1379
|
125
119
|
novel_downloader/core/searchers/aaatxt.py,sha256=KHaX1dpDBaAJz-_7UlaTtnMbFlTplWHaFyOfCkn4gX4,3717
|
126
120
|
novel_downloader/core/searchers/b520.py,sha256=L2fkf1_LxUREdPJJXbSN5c00EXwWVeMwWpXKN144k9c,2621
|
@@ -143,8 +137,8 @@ novel_downloader/core/searchers/ttkan.py,sha256=Y-qjePD7zQx8RUgASTuz4yu5fzT5MOJg
|
|
143
137
|
novel_downloader/core/searchers/xiaoshuowu.py,sha256=gAS3OpoMwfksEtlXPL5XPrfD7KVprl8YA7HGn3gGANg,4132
|
144
138
|
novel_downloader/core/searchers/xiguashuwu.py,sha256=YWxOQpXUhMMsgxt7zc51rtdbe6mn9yv5Vzeg63OCJlQ,2952
|
145
139
|
novel_downloader/core/searchers/xs63b.py,sha256=T4QUQphYFvnbnENxFbYVNtZMi2QhXypxOJU-FjjVYwU,3443
|
146
|
-
novel_downloader/locales/en.json,sha256=
|
147
|
-
novel_downloader/locales/zh.json,sha256=
|
140
|
+
novel_downloader/locales/en.json,sha256=w3TA-AtsQE2C1EVf5tNiOQRnaaXEaXX88-0Bb3xWdU8,3942
|
141
|
+
novel_downloader/locales/zh.json,sha256=XRku9SHycZ8zP0YYBwchBAg4yYjCWpgCLDItW8cmUcI,3779
|
148
142
|
novel_downloader/models/__init__.py,sha256=U7ks1ieTm-VzmH3NcH0Fym7c1zfywIZcBGPOUBWuVLo,690
|
149
143
|
novel_downloader/models/book.py,sha256=kim6Tt0pDDvOhGx6jnRp28BP79yhhsyNiUp56I0cAP8,875
|
150
144
|
novel_downloader/models/config.py,sha256=QNiw1XhbzjAYbfLAZ1bTIQ21nciNxnYszGVtXVG3Uk0,2211
|
@@ -157,54 +151,56 @@ novel_downloader/resources/images/volume_border.png,sha256=2dEVimnTHKOfLMhi7bhkh
|
|
157
151
|
novel_downloader/resources/js_scripts/qidian_decrypt_node.js,sha256=spNrk_gXI7pPW9abr4XGc2LASMe1UuN4BUe4cH24L8s,2195
|
158
152
|
novel_downloader/resources/json/linovelib_font_map.json,sha256=F1IlEcvXGcagnZCu4Gw2vQ2NLnhy6cJOLhabukqnftw,67854
|
159
153
|
novel_downloader/resources/json/xiguashuwu.json,sha256=AT8KSFBWuiHj4Ggz3O1LqED0Tw22k0A4-V4OziHbqfo,16471
|
160
|
-
novel_downloader/utils/__init__.py,sha256=
|
161
|
-
novel_downloader/utils/chapter_storage.py,sha256=
|
162
|
-
novel_downloader/utils/constants.py,sha256=
|
163
|
-
novel_downloader/utils/cookies.py,sha256=
|
164
|
-
novel_downloader/utils/fontocr.py,sha256=gaNH4isYY7zFE--gqtM1YN6bIjWTA-G-60XCAsbja8o,6494
|
154
|
+
novel_downloader/utils/__init__.py,sha256=_l77NlxmGA3HHGSwK68o4_Nc7xRaVP8Bo8SUuImP5Uk,867
|
155
|
+
novel_downloader/utils/chapter_storage.py,sha256=7jpVdOguuylnjsJi_yQ-jCMIPljtJWsq_NqXMDDv0lQ,10315
|
156
|
+
novel_downloader/utils/constants.py,sha256=uff08ll68TD8HwITR-uZ7j_2bQjKPso0-ryKmYhtjAU,3196
|
157
|
+
novel_downloader/utils/cookies.py,sha256=Z_9mjJYfhv6xPVgsm7BzCpWeMTcPKoxAoUFWZYuEFWM,2117
|
165
158
|
novel_downloader/utils/i18n.py,sha256=86Wz4zT0_YTnmOSS6cliqO_uMgJJdfC-hNLyqZnXgvU,1050
|
166
|
-
novel_downloader/utils/logger.py,sha256=
|
167
|
-
novel_downloader/utils/network.py,sha256=
|
159
|
+
novel_downloader/utils/logger.py,sha256=QuPyblYOIXEa4X0YBHLZgwbv1ZhkPNq159qyGmFKOno,3551
|
160
|
+
novel_downloader/utils/network.py,sha256=AIbM2kEfA9OjkMluFRKqGZIY2ZfNxVAoPnUHUkKkzgw,3625
|
168
161
|
novel_downloader/utils/state.py,sha256=QHfvS9CrO08fRZLNM0B_s0n5XIz7ap20NdAfesQI0Ek,1852
|
169
|
-
novel_downloader/utils/crypto_utils/__init__.py,sha256=
|
162
|
+
novel_downloader/utils/crypto_utils/__init__.py,sha256=MOl8pb73dRk8yQDuh2RZULnM3mn9VnJnOkh0SWcvaPs,136
|
170
163
|
novel_downloader/utils/crypto_utils/aes_util.py,sha256=46vKY1CxEnHgJE9t2CRzsrGyUoLRjBysCJCeSF5AiqU,2878
|
171
164
|
novel_downloader/utils/crypto_utils/aes_v1.py,sha256=_jW86_WyUTJfOzqYbZ6QvNPF-V8YRaqaOsyLjMsO610,34247
|
172
165
|
novel_downloader/utils/crypto_utils/aes_v2.py,sha256=OAsMnsztkJilqo8jM6nkrPMyujPqgGhohd-TcwW6fVA,48945
|
173
|
-
novel_downloader/utils/crypto_utils/rc4.py,sha256=
|
174
|
-
novel_downloader/utils/epub/__init__.py,sha256=
|
175
|
-
novel_downloader/utils/epub/builder.py,sha256=
|
176
|
-
novel_downloader/utils/epub/constants.py,sha256=
|
177
|
-
novel_downloader/utils/epub/documents.py,sha256=
|
178
|
-
novel_downloader/utils/epub/models.py,sha256=
|
179
|
-
novel_downloader/utils/epub/utils.py,sha256=
|
166
|
+
novel_downloader/utils/crypto_utils/rc4.py,sha256=SbC0oAdg2N7LWMnq9YbNh-zChI8SfpeBiijTPxFkYFc,1319
|
167
|
+
novel_downloader/utils/epub/__init__.py,sha256=77WorOLdsaxNoiuDPVxvQkiU49NW774bf3E7erPBv1I,725
|
168
|
+
novel_downloader/utils/epub/builder.py,sha256=2Vd7qD12vWqu05hVHrcgn5gfRzxGklY88nv8unuXF4M,11676
|
169
|
+
novel_downloader/utils/epub/constants.py,sha256=8r0TmDkGhBVpLNQXS4eIhC2WYwOvFEmpVDF7Mxzm5sA,3090
|
170
|
+
novel_downloader/utils/epub/documents.py,sha256=UijMa8yHpleZLUtRrwwTsOPZlqYlC5-qqXEuKh_StPI,9882
|
171
|
+
novel_downloader/utils/epub/models.py,sha256=CppM1qIC_G53L-lbouJO-pBBKLMTRfMvOM2PUOSPT78,2489
|
172
|
+
novel_downloader/utils/epub/utils.py,sha256=7eRYXUZFZ3NQU69UDwLK_yxiPQWpRhxBPhzKkQSTS0s,5090
|
180
173
|
novel_downloader/utils/file_utils/__init__.py,sha256=PIE96UKkI73wNr5v8kbP159BIbkEwsOQa9iEFr5RGUw,366
|
181
|
-
novel_downloader/utils/file_utils/io.py,sha256=
|
182
|
-
novel_downloader/utils/file_utils/normalize.py,sha256=
|
183
|
-
novel_downloader/utils/file_utils/sanitize.py,sha256=
|
174
|
+
novel_downloader/utils/file_utils/io.py,sha256=bF1q38BdwjTkM8ylfQGczWAcAMHI0gBB5xNkYwTYqrk,2167
|
175
|
+
novel_downloader/utils/file_utils/normalize.py,sha256=j8d6kmiQyafa-E8NjTv33uUrnNDoxyJ18orXfS5J_Gw,1708
|
176
|
+
novel_downloader/utils/file_utils/sanitize.py,sha256=IuJA2JjcrxxOnJtehPLBpBNOuyNbj97TUZvw-vXB9PE,1713
|
177
|
+
novel_downloader/utils/fontocr/__init__.py,sha256=lWT8hVKeuYatEanyw6EjlgPidlyWSMkgCiRliEG_VtA,314
|
178
|
+
novel_downloader/utils/fontocr/core.py,sha256=6-JhRo6fvdFxxJBsGSaKcyTh3PZSlGSkAIdJwzvShas,7475
|
179
|
+
novel_downloader/utils/fontocr/loader.py,sha256=TGSv9qq-jomky4h6ezr6uoUTgF3-FP385vP-LS7YlBY,1300
|
184
180
|
novel_downloader/utils/text_utils/__init__.py,sha256=cr0HCvgQFJE2ZEXqjkkmrOA7PnH50dKegrjdpIytUJs,601
|
185
181
|
novel_downloader/utils/text_utils/diff_display.py,sha256=5zvVtzsLbRxmUnn_dWUsXFaq6hch02OlOpqKYP8GB3g,2491
|
186
182
|
novel_downloader/utils/text_utils/numeric_conversion.py,sha256=RJ3L5GBxgNEQYJ7xFoihLVsjGcObBfpUAk_Gz5MxlkA,6627
|
187
|
-
novel_downloader/utils/text_utils/text_cleaner.py,sha256=
|
188
|
-
novel_downloader/utils/text_utils/truncate_utils.py,sha256=
|
183
|
+
novel_downloader/utils/text_utils/text_cleaner.py,sha256=9GbJChhZQGW06LApnHNzy6t5gS73AA9BTBigBn9qgcs,6410
|
184
|
+
novel_downloader/utils/text_utils/truncate_utils.py,sha256=xg5nlQvq6O1g3K-I64aTZvtnesN0P5QBGUVCPj09g8s,1284
|
189
185
|
novel_downloader/utils/time_utils/__init__.py,sha256=XbBeSbpNqiJy7Lsv-_udz29hPnIY53F0Drbuh730y44,331
|
190
186
|
novel_downloader/utils/time_utils/datetime_utils.py,sha256=ECidI5yvWVia8BWWLu7U0O0YYMIKgJwzWQmOuA_un3U,4499
|
191
|
-
novel_downloader/utils/time_utils/sleep_utils.py,sha256=
|
187
|
+
novel_downloader/utils/time_utils/sleep_utils.py,sha256=uwQfnhFHqNOku8UwUJ7btD2N0NgDjY925QKDzT-9_sE,3000
|
192
188
|
novel_downloader/web/__init__.py,sha256=ga8qRQhYcGEiL0WKhSSyldLxkOEfrq-UU71T7MYv7EU,174
|
193
|
-
novel_downloader/web/main.py,sha256=
|
189
|
+
novel_downloader/web/main.py,sha256=ZzQcpJkAZb7tRlBkub6LGggNrbM4gtrI4nb-MyiUAlY,1783
|
194
190
|
novel_downloader/web/components/__init__.py,sha256=fxBI7rpAQvdZU3obR6_-bFqgmA7nzvRpW05Rw3b4jp8,193
|
195
191
|
novel_downloader/web/components/navigation.py,sha256=m-rjbNRlYjqXsu2dq5PdCzpqy7oW6_otra2ZufRsh1c,1156
|
196
192
|
novel_downloader/web/pages/__init__.py,sha256=KP_aH8y8dtCeRze8jDJ4tC3Ft7UTGhlW8ZKDdKPdwcM,383
|
197
193
|
novel_downloader/web/pages/download.py,sha256=Qqq4GAZaiMRAuCjBvWiof6kwzSz0yrAj5LWN5EEldqo,2600
|
198
194
|
novel_downloader/web/pages/progress.py,sha256=whWduqLvQ1PPZpDNVfvFCVaFgFhD87LT4CDb8JuqD_w,5207
|
199
|
-
novel_downloader/web/pages/search.py,sha256=
|
195
|
+
novel_downloader/web/pages/search.py,sha256=qV_jgtQtNnzT_EWXUeEZfw1ieOxbaXcx4JSs-UQuS4Q,10235
|
200
196
|
novel_downloader/web/services/__init__.py,sha256=kLW0uyqudnlHtSsMrrghmL6P1-VDrseggNdB3cR5jLA,316
|
201
197
|
novel_downloader/web/services/client_dialog.py,sha256=JwYV8bjxPxLQsuheC8rk_jaeODolz5X9Wo64MsXZoyc,5679
|
202
198
|
novel_downloader/web/services/cred_broker.py,sha256=_R2Szqg5qRBHrFujTlPxrrMpQsnfNozjLUxJ5HhD8_c,2939
|
203
199
|
novel_downloader/web/services/cred_models.py,sha256=8nXbVoAIz3QokctrCVm-8lSnKgCIFhMM6TuaNq6bFGU,846
|
204
200
|
novel_downloader/web/services/task_manager.py,sha256=V6Pp92ZB9aNlliZbdwcaNUojioY1_BGAMGCo9Okob7w,8624
|
205
|
-
novel_downloader-2.0.
|
206
|
-
novel_downloader-2.0.
|
207
|
-
novel_downloader-2.0.
|
208
|
-
novel_downloader-2.0.
|
209
|
-
novel_downloader-2.0.
|
210
|
-
novel_downloader-2.0.
|
201
|
+
novel_downloader-2.0.1.dist-info/licenses/LICENSE,sha256=XgmnH0mBf-qEiizoVAfJQAKzPB9y3rBa-ni7M0Aqv4A,1066
|
202
|
+
novel_downloader-2.0.1.dist-info/METADATA,sha256=1Mw3aNRkbCiKXaTXJvxzICtInOZEVTpY4kGok3CZ19U,6502
|
203
|
+
novel_downloader-2.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
204
|
+
novel_downloader-2.0.1.dist-info/entry_points.txt,sha256=nIgnurS4J6_ce3oIBFV87I1pPwBofuRQvxkoRGn4yEU,112
|
205
|
+
novel_downloader-2.0.1.dist-info/top_level.txt,sha256=hP4jYWM2LTm1jxsW4hqEB8N0dsRvldO2QdhggJT917I,17
|
206
|
+
novel_downloader-2.0.1.dist-info/RECORD,,
|
@@ -1,89 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.book_info_parser
|
4
|
-
-----------------------------------------------------
|
5
|
-
|
6
|
-
This module provides parsing of Qidian book info pages.
|
7
|
-
|
8
|
-
It extracts metadata such as title, author, cover URL, update
|
9
|
-
time, status, word count, summary, and volume-chapter structure.
|
10
|
-
"""
|
11
|
-
|
12
|
-
import logging
|
13
|
-
import re
|
14
|
-
from datetime import datetime
|
15
|
-
|
16
|
-
from lxml import html
|
17
|
-
|
18
|
-
from novel_downloader.models import BookInfoDict, ChapterInfoDict, VolumeInfoDict
|
19
|
-
|
20
|
-
logger = logging.getLogger(__name__)
|
21
|
-
|
22
|
-
|
23
|
-
def _chapter_url_to_id(url: str) -> str:
|
24
|
-
return url.rstrip("/").split("/")[-1]
|
25
|
-
|
26
|
-
|
27
|
-
def parse_book_info(html_str: str) -> BookInfoDict | None:
|
28
|
-
"""
|
29
|
-
Extract metadata: title, author, cover_url, update_time, status,
|
30
|
-
word_count, summary, and volumes with chapters.
|
31
|
-
|
32
|
-
:param html_str: Raw HTML of the book info page.
|
33
|
-
:return: A dict containing book metadata.
|
34
|
-
"""
|
35
|
-
doc = html.fromstring(html_str)
|
36
|
-
|
37
|
-
book_name = doc.xpath('string(//h1[@id="bookName"])').strip()
|
38
|
-
|
39
|
-
author = doc.xpath('string(//a[@class="writer-name"])').strip()
|
40
|
-
|
41
|
-
book_id = doc.xpath('//a[@id="bookImg"]/@data-bid')[0]
|
42
|
-
cover_url = f"https://bookcover.yuewen.com/qdbimg/349573/{book_id}/600.webp"
|
43
|
-
|
44
|
-
ut = doc.xpath('string(//span[@class="update-time"])')
|
45
|
-
ut = ut.replace("更新时间:", "").strip()
|
46
|
-
if re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$", ut):
|
47
|
-
update_time = ut
|
48
|
-
else:
|
49
|
-
update_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
50
|
-
|
51
|
-
serial_status = doc.xpath('string(//p[@class="book-attribute"]/span[1])').strip()
|
52
|
-
|
53
|
-
tags_elem = doc.xpath('//p[contains(@class,"all-label")]//a/text()')
|
54
|
-
tags = [t.strip() for t in tags_elem if t.strip()]
|
55
|
-
|
56
|
-
word_count = doc.xpath('string(//p[@class="count"]/em[1])').strip()
|
57
|
-
|
58
|
-
summary_brief = doc.xpath('string(//p[@class="intro"])').strip()
|
59
|
-
|
60
|
-
raw = doc.xpath('//p[@id="book-intro-detail"]//text()')
|
61
|
-
summary = "\n".join(line.strip() for line in raw if line.strip())
|
62
|
-
|
63
|
-
volumes: list[VolumeInfoDict] = []
|
64
|
-
for vol in doc.xpath('//div[@id="allCatalog"]//div[@class="catalog-volume"]'):
|
65
|
-
vol_name = vol.xpath('string(.//h3[@class="volume-name"])').strip()
|
66
|
-
vol_name = vol_name.split(chr(183))[0].strip()
|
67
|
-
chapters: list[ChapterInfoDict] = []
|
68
|
-
for li in vol.xpath('.//ul[contains(@class,"volume-chapters")]/li'):
|
69
|
-
a = li.xpath('.//a[@class="chapter-name"]')[0]
|
70
|
-
title = a.text.strip()
|
71
|
-
url = a.get("href")
|
72
|
-
chapters.append(
|
73
|
-
{"title": title, "url": url, "chapterId": _chapter_url_to_id(url)}
|
74
|
-
)
|
75
|
-
volumes.append({"volume_name": vol_name, "chapters": chapters})
|
76
|
-
|
77
|
-
return {
|
78
|
-
"book_name": book_name,
|
79
|
-
"author": author,
|
80
|
-
"cover_url": cover_url,
|
81
|
-
"update_time": update_time,
|
82
|
-
"word_count": word_count,
|
83
|
-
"serial_status": serial_status,
|
84
|
-
"tags": tags,
|
85
|
-
"summary_brief": summary_brief,
|
86
|
-
"summary": summary,
|
87
|
-
"volumes": volumes,
|
88
|
-
"extra": {},
|
89
|
-
}
|