novel-downloader 2.0.1__py3-none-any.whl → 2.0.2__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 +11 -8
- novel_downloader/cli/export.py +17 -17
- novel_downloader/cli/ui.py +28 -1
- novel_downloader/config/adapter.py +27 -1
- novel_downloader/core/archived/deqixs/fetcher.py +1 -28
- novel_downloader/core/downloaders/__init__.py +2 -0
- novel_downloader/core/downloaders/base.py +34 -85
- novel_downloader/core/downloaders/common.py +147 -171
- novel_downloader/core/downloaders/qianbi.py +30 -64
- novel_downloader/core/downloaders/qidian.py +157 -184
- novel_downloader/core/downloaders/qqbook.py +292 -0
- novel_downloader/core/downloaders/registry.py +2 -2
- novel_downloader/core/exporters/__init__.py +2 -0
- novel_downloader/core/exporters/base.py +37 -59
- novel_downloader/core/exporters/common.py +620 -0
- novel_downloader/core/exporters/linovelib.py +47 -0
- novel_downloader/core/exporters/qidian.py +41 -12
- novel_downloader/core/exporters/qqbook.py +28 -0
- novel_downloader/core/exporters/registry.py +2 -2
- novel_downloader/core/fetchers/__init__.py +4 -2
- novel_downloader/core/fetchers/aaatxt.py +2 -22
- novel_downloader/core/fetchers/b520.py +3 -23
- novel_downloader/core/fetchers/base.py +80 -105
- novel_downloader/core/fetchers/biquyuedu.py +2 -22
- novel_downloader/core/fetchers/dxmwx.py +10 -22
- novel_downloader/core/fetchers/esjzone.py +6 -29
- novel_downloader/core/fetchers/guidaye.py +2 -22
- novel_downloader/core/fetchers/hetushu.py +9 -29
- novel_downloader/core/fetchers/i25zw.py +2 -16
- novel_downloader/core/fetchers/ixdzs8.py +2 -16
- novel_downloader/core/fetchers/jpxs123.py +2 -16
- novel_downloader/core/fetchers/lewenn.py +2 -22
- novel_downloader/core/fetchers/linovelib.py +4 -20
- novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
- novel_downloader/core/fetchers/piaotia.py +2 -16
- novel_downloader/core/fetchers/qbtr.py +2 -16
- novel_downloader/core/fetchers/qianbi.py +1 -20
- novel_downloader/core/fetchers/qidian.py +7 -33
- novel_downloader/core/fetchers/qqbook.py +177 -0
- novel_downloader/core/fetchers/quanben5.py +9 -29
- novel_downloader/core/fetchers/rate_limiter.py +22 -53
- novel_downloader/core/fetchers/sfacg.py +3 -16
- novel_downloader/core/fetchers/shencou.py +2 -16
- novel_downloader/core/fetchers/shuhaige.py +2 -22
- novel_downloader/core/fetchers/tongrenquan.py +2 -22
- novel_downloader/core/fetchers/ttkan.py +3 -14
- novel_downloader/core/fetchers/wanbengo.py +2 -22
- novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
- novel_downloader/core/fetchers/xiguashuwu.py +4 -20
- novel_downloader/core/fetchers/xs63b.py +3 -15
- novel_downloader/core/fetchers/xshbook.py +2 -22
- novel_downloader/core/fetchers/yamibo.py +4 -28
- novel_downloader/core/fetchers/yibige.py +13 -26
- novel_downloader/core/interfaces/exporter.py +19 -7
- novel_downloader/core/interfaces/fetcher.py +21 -47
- novel_downloader/core/parsers/__init__.py +4 -2
- novel_downloader/core/parsers/b520.py +2 -2
- novel_downloader/core/parsers/base.py +4 -39
- novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
- novel_downloader/core/parsers/qqbook.py +709 -0
- novel_downloader/core/parsers/xiguashuwu.py +3 -4
- novel_downloader/core/searchers/__init__.py +2 -2
- novel_downloader/core/searchers/b520.py +1 -1
- novel_downloader/core/searchers/base.py +2 -2
- novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
- novel_downloader/models/__init__.py +2 -0
- novel_downloader/models/book.py +1 -0
- novel_downloader/models/config.py +12 -0
- novel_downloader/resources/config/settings.toml +23 -5
- novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
- novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
- novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
- novel_downloader/utils/constants.py +6 -0
- novel_downloader/utils/crypto_utils/aes_util.py +1 -1
- novel_downloader/utils/epub/constants.py +1 -6
- novel_downloader/utils/fontocr/core.py +2 -0
- novel_downloader/utils/fontocr/loader.py +10 -8
- novel_downloader/utils/node_decryptor/__init__.py +13 -0
- novel_downloader/utils/node_decryptor/decryptor.py +342 -0
- novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
- novel_downloader/web/pages/download.py +1 -1
- novel_downloader/web/pages/search.py +1 -1
- novel_downloader/web/services/task_manager.py +2 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
- novel_downloader/core/exporters/common/__init__.py +0 -11
- novel_downloader/core/exporters/common/epub.py +0 -198
- novel_downloader/core/exporters/common/main_exporter.py +0 -64
- novel_downloader/core/exporters/common/txt.py +0 -146
- novel_downloader/core/exporters/epub_util.py +0 -215
- novel_downloader/core/exporters/linovelib/__init__.py +0 -11
- novel_downloader/core/exporters/linovelib/epub.py +0 -349
- novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
- novel_downloader/core/exporters/linovelib/txt.py +0 -139
- novel_downloader/core/exporters/txt_util.py +0 -67
- novel_downloader/core/parsers/qidian/__init__.py +0 -10
- novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -1,146 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.exporters.common.txt
|
4
|
-
------------------------------------------
|
5
|
-
|
6
|
-
Defines `common_export_as_txt` to assemble and export a novel
|
7
|
-
into a single `.txt` file. Intended for use by `CommonExporter`.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from __future__ import annotations
|
11
|
-
|
12
|
-
from pathlib import Path
|
13
|
-
from typing import TYPE_CHECKING
|
14
|
-
|
15
|
-
from novel_downloader.core.exporters.txt_util import (
|
16
|
-
build_txt_chapter,
|
17
|
-
build_txt_header,
|
18
|
-
)
|
19
|
-
from novel_downloader.utils import get_cleaner, write_file
|
20
|
-
|
21
|
-
if TYPE_CHECKING:
|
22
|
-
from .main_exporter import CommonExporter
|
23
|
-
|
24
|
-
|
25
|
-
def common_export_as_txt(
|
26
|
-
exporter: CommonExporter,
|
27
|
-
book_id: str,
|
28
|
-
) -> Path | None:
|
29
|
-
"""
|
30
|
-
Export a novel as a single text file by merging all chapter data.
|
31
|
-
|
32
|
-
Steps:
|
33
|
-
1. Load book metadata.
|
34
|
-
2. For each volume:
|
35
|
-
a. Append the volume title.
|
36
|
-
b. Batch-fetch all chapters in that volume to minimize SQLite calls.
|
37
|
-
c. Append each chapter's title, content, and optional author note.
|
38
|
-
3. Build a header with book metadata and the latest chapter title.
|
39
|
-
4. Concatenate header and all chapter contents.
|
40
|
-
5. Save the resulting .txt file to the output directory
|
41
|
-
(e.g., '{book_name}.txt').
|
42
|
-
|
43
|
-
:param exporter: The CommonExporter instance managing paths and config.
|
44
|
-
:param book_id: Identifier of the novel (subdirectory under raw data).
|
45
|
-
"""
|
46
|
-
TAG = "[Exporter]"
|
47
|
-
# --- Paths & options ---
|
48
|
-
out_dir = exporter.output_dir
|
49
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
50
|
-
cleaner = get_cleaner(
|
51
|
-
enabled=exporter._config.clean_text,
|
52
|
-
config=exporter._config.cleaner_cfg,
|
53
|
-
)
|
54
|
-
|
55
|
-
# --- Load book_info.json ---
|
56
|
-
book_info = exporter._load_book_info(book_id)
|
57
|
-
if not book_info:
|
58
|
-
return None
|
59
|
-
|
60
|
-
# --- Compile chapters ---
|
61
|
-
parts: list[str] = []
|
62
|
-
latest_chapter_title: str = ""
|
63
|
-
|
64
|
-
for vol in book_info.get("volumes", []):
|
65
|
-
vol_name = cleaner.clean_title(vol.get("volume_name", ""))
|
66
|
-
if vol_name:
|
67
|
-
# e.g. "\n\n====== 第三卷 ======\n\n"
|
68
|
-
parts.append(f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n")
|
69
|
-
exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
|
70
|
-
|
71
|
-
# Batch-fetch chapters for this volume
|
72
|
-
chap_ids = [
|
73
|
-
chap["chapterId"]
|
74
|
-
for chap in vol.get("chapters", [])
|
75
|
-
if chap.get("chapterId")
|
76
|
-
]
|
77
|
-
chap_map = exporter._get_chapters(book_id, chap_ids)
|
78
|
-
|
79
|
-
for chap_meta in vol.get("chapters", []):
|
80
|
-
chap_id = chap_meta.get("chapterId")
|
81
|
-
if not chap_id:
|
82
|
-
exporter.logger.warning(
|
83
|
-
"%s Missing chapterId, skipping: %s", TAG, chap_meta
|
84
|
-
)
|
85
|
-
continue
|
86
|
-
|
87
|
-
chap_title = chap_meta.get("title", "")
|
88
|
-
data = chap_map.get(chap_id)
|
89
|
-
if not data:
|
90
|
-
exporter.logger.info(
|
91
|
-
"%s Missing chapter: %s (%s), skipping.",
|
92
|
-
TAG,
|
93
|
-
chap_title,
|
94
|
-
chap_id,
|
95
|
-
)
|
96
|
-
continue
|
97
|
-
|
98
|
-
# Extract and clean fields
|
99
|
-
title = cleaner.clean_title(data.get("title", chap_title))
|
100
|
-
content = cleaner.clean_content(data.get("content", ""))
|
101
|
-
extra = data.get("extra", {})
|
102
|
-
author_note = cleaner.clean_content(extra.get("author_say", ""))
|
103
|
-
|
104
|
-
extras = {"作者说": author_note} if author_note else {}
|
105
|
-
parts.append(
|
106
|
-
build_txt_chapter(title=title, paragraphs=content, extras=extras)
|
107
|
-
)
|
108
|
-
|
109
|
-
latest_chapter_title = title
|
110
|
-
|
111
|
-
# --- Build header ---
|
112
|
-
name = book_info.get("book_name") or ""
|
113
|
-
author = book_info.get("author") or ""
|
114
|
-
words = book_info.get("word_count") or ""
|
115
|
-
updated = book_info.get("update_time") or ""
|
116
|
-
summary = book_info.get("summary") or ""
|
117
|
-
|
118
|
-
header_fields = [
|
119
|
-
("书名", name),
|
120
|
-
("作者", author),
|
121
|
-
("总字数", words),
|
122
|
-
("更新日期", updated),
|
123
|
-
("原文截至", latest_chapter_title),
|
124
|
-
("内容简介", summary),
|
125
|
-
]
|
126
|
-
|
127
|
-
header = build_txt_header(header_fields)
|
128
|
-
|
129
|
-
final_text = header + "\n\n" + "\n\n".join(parts).strip()
|
130
|
-
|
131
|
-
# --- Determine output file path ---
|
132
|
-
out_name = exporter.get_filename(title=name, author=author, ext="txt")
|
133
|
-
out_path = out_dir / out_name
|
134
|
-
|
135
|
-
# --- Save final text ---
|
136
|
-
try:
|
137
|
-
result = write_file(
|
138
|
-
content=final_text,
|
139
|
-
filepath=out_path,
|
140
|
-
on_exist="overwrite",
|
141
|
-
)
|
142
|
-
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
143
|
-
except Exception as e:
|
144
|
-
exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
|
145
|
-
return None
|
146
|
-
return result
|
@@ -1,215 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.exporters.epub_util
|
4
|
-
-----------------------------------------
|
5
|
-
|
6
|
-
Utilities for preparing and formatting chapter HTML for EPUB exports.
|
7
|
-
"""
|
8
|
-
|
9
|
-
__all__ = [
|
10
|
-
"download_cover",
|
11
|
-
"prepare_builder",
|
12
|
-
"finalize_export",
|
13
|
-
"inline_remote_images",
|
14
|
-
"remove_all_images",
|
15
|
-
"build_epub_chapter",
|
16
|
-
]
|
17
|
-
|
18
|
-
import logging
|
19
|
-
import re
|
20
|
-
from html import escape
|
21
|
-
from pathlib import Path
|
22
|
-
|
23
|
-
from novel_downloader.utils import download, sanitize_filename
|
24
|
-
from novel_downloader.utils.constants import (
|
25
|
-
CSS_MAIN_PATH,
|
26
|
-
DEFAULT_HEADERS,
|
27
|
-
DEFAULT_IMAGE_SUFFIX,
|
28
|
-
)
|
29
|
-
from novel_downloader.utils.epub import EpubBuilder, StyleSheet
|
30
|
-
|
31
|
-
_IMAGE_WRAPPER = '<div class="duokan-image-single illus">{img}</div>'
|
32
|
-
_IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
|
33
|
-
_IMG_SRC_RE = re.compile(
|
34
|
-
r'<img[^>]*\bsrc=["\'](https?://[^"\']+)["\'][^>]*>',
|
35
|
-
re.IGNORECASE,
|
36
|
-
)
|
37
|
-
|
38
|
-
|
39
|
-
def download_cover(
|
40
|
-
cover_url: str,
|
41
|
-
raw_base: Path,
|
42
|
-
include_cover: bool,
|
43
|
-
logger: logging.Logger,
|
44
|
-
tag: str,
|
45
|
-
headers: dict[str, str] | None = None,
|
46
|
-
) -> Path | None:
|
47
|
-
if include_cover and cover_url:
|
48
|
-
path = download(
|
49
|
-
cover_url,
|
50
|
-
raw_base,
|
51
|
-
filename="cover",
|
52
|
-
headers=headers or DEFAULT_HEADERS,
|
53
|
-
on_exist="overwrite",
|
54
|
-
default_suffix=DEFAULT_IMAGE_SUFFIX,
|
55
|
-
)
|
56
|
-
if not path:
|
57
|
-
logger.warning("%s Failed to download cover from %s", tag, cover_url)
|
58
|
-
return path
|
59
|
-
return None
|
60
|
-
|
61
|
-
|
62
|
-
def prepare_builder(
|
63
|
-
site_name: str,
|
64
|
-
book_id: str,
|
65
|
-
title: str,
|
66
|
-
author: str,
|
67
|
-
description: str,
|
68
|
-
subject: list[str],
|
69
|
-
serial_status: str,
|
70
|
-
word_count: str,
|
71
|
-
cover_path: Path | None,
|
72
|
-
) -> tuple[EpubBuilder, StyleSheet]:
|
73
|
-
book = EpubBuilder(
|
74
|
-
title=title,
|
75
|
-
author=author,
|
76
|
-
description=description,
|
77
|
-
cover_path=cover_path,
|
78
|
-
subject=subject,
|
79
|
-
serial_status=serial_status,
|
80
|
-
word_count=word_count,
|
81
|
-
uid=f"{site_name}_{book_id}",
|
82
|
-
)
|
83
|
-
css_text = CSS_MAIN_PATH.read_text(encoding="utf-8")
|
84
|
-
main_css = StyleSheet(id="main_style", content=css_text, filename="main.css")
|
85
|
-
book.add_stylesheet(main_css)
|
86
|
-
return book, main_css
|
87
|
-
|
88
|
-
|
89
|
-
def finalize_export(
|
90
|
-
book: EpubBuilder,
|
91
|
-
out_dir: Path,
|
92
|
-
filename: str,
|
93
|
-
logger: logging.Logger,
|
94
|
-
tag: str,
|
95
|
-
) -> Path | None:
|
96
|
-
out_path = out_dir / sanitize_filename(filename)
|
97
|
-
try:
|
98
|
-
book.export(out_path)
|
99
|
-
logger.info("%s EPUB successfully written to %s", tag, out_path)
|
100
|
-
return out_path
|
101
|
-
except OSError as e:
|
102
|
-
logger.error("%s Failed to write EPUB to %s: %s", tag, out_path, e)
|
103
|
-
return None
|
104
|
-
|
105
|
-
|
106
|
-
def inline_remote_images(
|
107
|
-
book: EpubBuilder,
|
108
|
-
content: str,
|
109
|
-
image_dir: Path,
|
110
|
-
headers: dict[str, str] | None = None,
|
111
|
-
) -> str:
|
112
|
-
"""
|
113
|
-
Download every remote `<img src="...">` in `content` into `image_dir`,
|
114
|
-
and replace the original url with local path.
|
115
|
-
|
116
|
-
:param content: HTML/text of the chapter containing <img> tags.
|
117
|
-
:param image_dir: Directory to save downloaded images into.
|
118
|
-
:return: modified_content.
|
119
|
-
"""
|
120
|
-
|
121
|
-
def _replace(m: re.Match[str]) -> str:
|
122
|
-
url = m.group(1)
|
123
|
-
try:
|
124
|
-
local_path = download(
|
125
|
-
url,
|
126
|
-
image_dir,
|
127
|
-
headers=headers or DEFAULT_HEADERS,
|
128
|
-
on_exist="skip",
|
129
|
-
default_suffix=DEFAULT_IMAGE_SUFFIX,
|
130
|
-
)
|
131
|
-
if not local_path:
|
132
|
-
return m.group(0)
|
133
|
-
filename = book.add_image(local_path)
|
134
|
-
return f'<img src="../Images/{filename}" />'
|
135
|
-
except Exception:
|
136
|
-
return m.group(0)
|
137
|
-
|
138
|
-
return _IMG_SRC_RE.sub(_replace, content)
|
139
|
-
|
140
|
-
|
141
|
-
def remove_all_images(content: str) -> str:
|
142
|
-
"""
|
143
|
-
Remove all <img> tags from the given content.
|
144
|
-
|
145
|
-
:param content: HTML/text of the chapter containing <img> tags.
|
146
|
-
"""
|
147
|
-
return _IMG_TAG_RE.sub("", content)
|
148
|
-
|
149
|
-
|
150
|
-
def build_epub_chapter(
|
151
|
-
title: str,
|
152
|
-
paragraphs: str,
|
153
|
-
extras: dict[str, str] | None = None,
|
154
|
-
) -> str:
|
155
|
-
"""
|
156
|
-
Build a formatted chapter epub HTML including title, body paragraphs,
|
157
|
-
and optional extra sections.
|
158
|
-
|
159
|
-
:param title: Chapter title.
|
160
|
-
:param paragraphs: Raw multi-line string. Blank lines are ignored.
|
161
|
-
:param extras: Optional dict mapping section titles to multi-line strings.
|
162
|
-
:return: A HTML include title, paragraphs, and extras.
|
163
|
-
"""
|
164
|
-
|
165
|
-
def _render_block(text: str) -> str:
|
166
|
-
out: list[str] = []
|
167
|
-
for raw in text.splitlines():
|
168
|
-
line = raw.strip()
|
169
|
-
if not line:
|
170
|
-
continue
|
171
|
-
|
172
|
-
# case 1: already wrapped in a <div>...</div>
|
173
|
-
if line.startswith("<div") and line.endswith("</div>"):
|
174
|
-
out.append(line)
|
175
|
-
continue
|
176
|
-
|
177
|
-
# case 2: single <img> line
|
178
|
-
if _IMG_TAG_RE.fullmatch(line):
|
179
|
-
out.append(_IMAGE_WRAPPER.format(img=line))
|
180
|
-
continue
|
181
|
-
|
182
|
-
# case 3: inline <img> in text -> escape other text, preserve <img>
|
183
|
-
if "<img " in line:
|
184
|
-
pieces = []
|
185
|
-
last = 0
|
186
|
-
for m in _IMG_TAG_RE.finditer(line):
|
187
|
-
pieces.append(escape(line[last : m.start()]))
|
188
|
-
pieces.append(m.group(0))
|
189
|
-
last = m.end()
|
190
|
-
pieces.append(escape(line[last:]))
|
191
|
-
out.append("<p>" + "".join(pieces) + "</p>")
|
192
|
-
else:
|
193
|
-
# plain text line
|
194
|
-
out.append(f"<p>{escape(line)}</p>")
|
195
|
-
|
196
|
-
return "\n".join(out)
|
197
|
-
|
198
|
-
parts = []
|
199
|
-
parts.append(f"<h2>{escape(title)}</h2>")
|
200
|
-
parts.append(_render_block(paragraphs))
|
201
|
-
|
202
|
-
if extras:
|
203
|
-
for title, note in extras.items():
|
204
|
-
note = note.strip()
|
205
|
-
if not note:
|
206
|
-
continue
|
207
|
-
parts.extend(
|
208
|
-
[
|
209
|
-
"<hr />",
|
210
|
-
f"<h3>{escape(title)}</h3>",
|
211
|
-
_render_block(note),
|
212
|
-
]
|
213
|
-
)
|
214
|
-
|
215
|
-
return "\n".join(parts)
|
@@ -1,11 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.exporters.linovelib
|
4
|
-
-----------------------------------------
|
5
|
-
|
6
|
-
Exporter implementation for handling Linovelib novels.
|
7
|
-
"""
|
8
|
-
|
9
|
-
__all__ = ["LinovelibExporter"]
|
10
|
-
|
11
|
-
from .main_exporter import LinovelibExporter
|