novel-downloader 1.3.2__py3-none-any.whl → 1.4.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/clean.py +97 -78
- novel_downloader/cli/config.py +177 -0
- novel_downloader/cli/download.py +132 -87
- novel_downloader/cli/export.py +77 -0
- novel_downloader/cli/main.py +21 -28
- novel_downloader/config/__init__.py +1 -25
- novel_downloader/config/adapter.py +32 -31
- novel_downloader/config/loader.py +3 -3
- novel_downloader/config/site_rules.py +1 -2
- novel_downloader/core/__init__.py +3 -6
- novel_downloader/core/downloaders/__init__.py +10 -13
- novel_downloader/core/downloaders/base.py +233 -0
- novel_downloader/core/downloaders/biquge.py +27 -0
- novel_downloader/core/downloaders/common.py +414 -0
- novel_downloader/core/downloaders/esjzone.py +27 -0
- novel_downloader/core/downloaders/linovelib.py +27 -0
- novel_downloader/core/downloaders/qianbi.py +27 -0
- novel_downloader/core/downloaders/qidian.py +352 -0
- novel_downloader/core/downloaders/sfacg.py +27 -0
- novel_downloader/core/downloaders/yamibo.py +27 -0
- novel_downloader/core/exporters/__init__.py +37 -0
- novel_downloader/core/{savers → exporters}/base.py +73 -44
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +40 -52
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
- novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
- novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +178 -0
- novel_downloader/core/fetchers/linovelib/session.py +178 -0
- novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
- novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
- novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
- novel_downloader/core/fetchers/qidian/__init__.py +14 -0
- novel_downloader/core/fetchers/qidian/browser.py +266 -0
- novel_downloader/core/fetchers/qidian/session.py +326 -0
- novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
- novel_downloader/core/fetchers/sfacg/browser.py +189 -0
- novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
- novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
- novel_downloader/core/fetchers/yamibo/browser.py +229 -0
- novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
- novel_downloader/core/interfaces/__init__.py +8 -12
- novel_downloader/core/interfaces/downloader.py +54 -0
- novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
- novel_downloader/core/interfaces/fetcher.py +162 -0
- novel_downloader/core/interfaces/parser.py +6 -7
- novel_downloader/core/parsers/__init__.py +5 -6
- novel_downloader/core/parsers/base.py +9 -13
- novel_downloader/core/parsers/biquge/main_parser.py +12 -13
- novel_downloader/core/parsers/common/helper.py +3 -3
- novel_downloader/core/parsers/common/main_parser.py +39 -34
- novel_downloader/core/parsers/esjzone/main_parser.py +24 -17
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +15 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +53 -39
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +3 -3
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
- novel_downloader/cli/interactive.py +0 -66
- novel_downloader/cli/settings.py +0 -177
- novel_downloader/config/models.py +0 -187
- novel_downloader/core/downloaders/base/__init__.py +0 -14
- novel_downloader/core/downloaders/base/base_async.py +0 -153
- novel_downloader/core/downloaders/base/base_sync.py +0 -208
- novel_downloader/core/downloaders/biquge/__init__.py +0 -14
- novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
- novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
- novel_downloader/core/downloaders/common/__init__.py +0 -14
- novel_downloader/core/downloaders/common/common_async.py +0 -218
- novel_downloader/core/downloaders/common/common_sync.py +0 -210
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -227
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/epub_utils/__init__.py +0 -26
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.2.dist-info/RECORD +0 -165
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.common
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
This module provides the `CommonExporter` class for
|
7
|
+
handling the saving process of novels.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from .main_exporter import CommonExporter
|
11
|
+
|
12
|
+
__all__ = ["CommonExporter"]
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.common.epub
|
4
|
+
-------------------------------------------
|
5
5
|
|
6
6
|
Contains the logic for exporting novel content as a single `.epub` file.
|
7
7
|
"""
|
@@ -11,55 +11,32 @@ from __future__ import annotations
|
|
11
11
|
import json
|
12
12
|
from pathlib import Path
|
13
13
|
from typing import TYPE_CHECKING
|
14
|
-
from urllib.parse import unquote, urlparse
|
15
14
|
|
16
15
|
from ebooklib import epub
|
17
16
|
|
18
|
-
from novel_downloader.core.
|
17
|
+
from novel_downloader.core.exporters.epub_utils import (
|
18
|
+
add_images_from_dir,
|
19
19
|
chapter_txt_to_html,
|
20
20
|
create_css_items,
|
21
21
|
create_volume_intro,
|
22
22
|
generate_book_intro_html,
|
23
23
|
init_epub,
|
24
|
+
inline_remote_images,
|
24
25
|
)
|
25
26
|
from novel_downloader.utils.constants import (
|
26
|
-
DEFAULT_IMAGE_SUFFIX,
|
27
27
|
EPUB_OPTIONS,
|
28
28
|
EPUB_TEXT_FOLDER,
|
29
29
|
)
|
30
30
|
from novel_downloader.utils.file_utils import sanitize_filename
|
31
|
+
from novel_downloader.utils.network import download_image
|
31
32
|
from novel_downloader.utils.text_utils import clean_chapter_title
|
32
33
|
|
33
34
|
if TYPE_CHECKING:
|
34
|
-
from .
|
35
|
+
from .main_exporter import CommonExporter
|
35
36
|
|
36
37
|
|
37
|
-
def
|
38
|
-
|
39
|
-
Parse and sanitize a image filename from a URL.
|
40
|
-
If no filename or suffix exists, fallback to default name and extension.
|
41
|
-
|
42
|
-
:param url: URL string
|
43
|
-
:return: Safe filename string
|
44
|
-
"""
|
45
|
-
if not url:
|
46
|
-
return ""
|
47
|
-
|
48
|
-
parsed_url = urlparse(url)
|
49
|
-
path = unquote(parsed_url.path)
|
50
|
-
filename = Path(path).name
|
51
|
-
|
52
|
-
if not filename:
|
53
|
-
filename = "image"
|
54
|
-
|
55
|
-
if not Path(filename).suffix:
|
56
|
-
filename += DEFAULT_IMAGE_SUFFIX
|
57
|
-
|
58
|
-
return filename
|
59
|
-
|
60
|
-
|
61
|
-
def common_save_as_epub(
|
62
|
-
saver: CommonSaver,
|
38
|
+
def common_export_as_epub(
|
39
|
+
exporter: CommonExporter,
|
63
40
|
book_id: str,
|
64
41
|
) -> None:
|
65
42
|
"""
|
@@ -75,12 +52,13 @@ def common_save_as_epub(
|
|
75
52
|
:param saver: The saver instance, carrying config and path info.
|
76
53
|
:param book_id: Identifier of the novel (used as subdirectory name).
|
77
54
|
"""
|
78
|
-
TAG = "[
|
79
|
-
|
80
|
-
config = saver._config
|
55
|
+
TAG = "[exporter]"
|
56
|
+
config = exporter._config
|
81
57
|
# --- Paths & options ---
|
82
|
-
raw_base =
|
83
|
-
|
58
|
+
raw_base = exporter._raw_data_dir / book_id
|
59
|
+
img_dir = exporter._cache_dir / book_id / "images"
|
60
|
+
out_dir = exporter.output_dir
|
61
|
+
img_dir.mkdir(parents=True, exist_ok=True)
|
84
62
|
out_dir.mkdir(parents=True, exist_ok=True)
|
85
63
|
|
86
64
|
# --- Load book_info.json ---
|
@@ -89,21 +67,27 @@ def common_save_as_epub(
|
|
89
67
|
info_text = info_path.read_text(encoding="utf-8")
|
90
68
|
book_info = json.loads(info_text)
|
91
69
|
except Exception as e:
|
92
|
-
|
70
|
+
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
93
71
|
return
|
94
72
|
|
95
73
|
book_name = book_info.get("book_name", book_id)
|
96
|
-
|
74
|
+
exporter.logger.info(
|
97
75
|
"%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
|
98
76
|
)
|
99
77
|
|
100
78
|
# --- Generate intro + cover ---
|
101
79
|
intro_html = generate_book_intro_html(book_info)
|
102
80
|
cover_path: Path | None = None
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
81
|
+
cover_url = book_info.get("cover_url", "")
|
82
|
+
if config.include_cover and cover_url:
|
83
|
+
cover_path = download_image(
|
84
|
+
cover_url,
|
85
|
+
raw_base,
|
86
|
+
target_name="cover",
|
87
|
+
on_exist="overwrite",
|
88
|
+
)
|
89
|
+
if not cover_path:
|
90
|
+
exporter.logger.warning("Failed to download cover from %s", cover_url)
|
107
91
|
|
108
92
|
# --- Initialize EPUB ---
|
109
93
|
book, spine, toc_list = init_epub(
|
@@ -124,7 +108,7 @@ def common_save_as_epub(
|
|
124
108
|
for vol_index, vol in enumerate(volumes, start=1):
|
125
109
|
raw_vol_name = vol.get("volume_name", "").strip()
|
126
110
|
vol_name = clean_chapter_title(raw_vol_name) or f"Unknown Volume {vol_index}"
|
127
|
-
|
111
|
+
exporter.logger.info("Processing volume %d: %s", vol_index, vol_name)
|
128
112
|
|
129
113
|
# Volume intro
|
130
114
|
vol_intro = epub.EpubHtml(
|
@@ -148,12 +132,12 @@ def common_save_as_epub(
|
|
148
132
|
chap_id = chap.get("chapterId")
|
149
133
|
chap_title = chap.get("title", "")
|
150
134
|
if not chap_id:
|
151
|
-
|
135
|
+
exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
|
152
136
|
continue
|
153
137
|
|
154
|
-
chapter_data =
|
138
|
+
chapter_data = exporter._get_chapter(book_id, chap_id)
|
155
139
|
if not chapter_data:
|
156
|
-
|
140
|
+
exporter.logger.info(
|
157
141
|
"%s Missing chapter file: %s (%s), skipping.",
|
158
142
|
TAG,
|
159
143
|
chap_title,
|
@@ -162,9 +146,11 @@ def common_save_as_epub(
|
|
162
146
|
continue
|
163
147
|
|
164
148
|
title = clean_chapter_title(chapter_data.get("title", "")) or chap_id
|
149
|
+
content: str = chapter_data.get("content", "")
|
150
|
+
content = inline_remote_images(content, img_dir)
|
165
151
|
chap_html = chapter_txt_to_html(
|
166
152
|
chapter_title=title,
|
167
|
-
chapter_text=
|
153
|
+
chapter_text=content,
|
168
154
|
author_say=chapter_data.get("author_say", ""),
|
169
155
|
)
|
170
156
|
|
@@ -182,14 +168,16 @@ def common_save_as_epub(
|
|
182
168
|
|
183
169
|
toc_list.append((section, chapter_items))
|
184
170
|
|
171
|
+
book = add_images_from_dir(book, img_dir)
|
172
|
+
|
185
173
|
# --- 5. Finalize EPUB ---
|
186
|
-
|
174
|
+
exporter.logger.info("%s Building TOC and spine...", TAG)
|
187
175
|
book.toc = toc_list
|
188
176
|
book.spine = spine
|
189
177
|
book.add_item(epub.EpubNcx())
|
190
178
|
book.add_item(epub.EpubNav())
|
191
179
|
|
192
|
-
out_name =
|
180
|
+
out_name = exporter.get_filename(
|
193
181
|
title=book_name,
|
194
182
|
author=book_info.get("author"),
|
195
183
|
ext="epub",
|
@@ -198,7 +186,7 @@ def common_save_as_epub(
|
|
198
186
|
|
199
187
|
try:
|
200
188
|
epub.write_epub(out_path, book, EPUB_OPTIONS)
|
201
|
-
|
189
|
+
exporter.logger.info("%s EPUB successfully written to %s", TAG, out_path)
|
202
190
|
except Exception as e:
|
203
|
-
|
191
|
+
exporter.logger.error("%s Failed to write EPUB to %s: %s", TAG, out_path, e)
|
204
192
|
return
|
@@ -1,52 +1,44 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.common.main_exporter
|
4
|
+
----------------------------------------------------
|
5
5
|
|
6
|
-
This module implements the `
|
7
|
-
novel data
|
8
|
-
|
9
|
-
and chapter files.
|
6
|
+
This module implements the `CommonExporter` class, a concrete exporter for handling
|
7
|
+
novel data. It defines the logic to compile, structure, and export novel content
|
8
|
+
in plain text format based on the platform's metadata and chapter files.
|
10
9
|
"""
|
11
10
|
|
12
11
|
from collections.abc import Mapping
|
13
12
|
from typing import Any
|
14
13
|
|
15
|
-
from novel_downloader.
|
14
|
+
from novel_downloader.core.exporters.base import BaseExporter
|
15
|
+
from novel_downloader.models import ExporterConfig
|
16
16
|
from novel_downloader.utils.chapter_storage import ChapterStorage
|
17
17
|
|
18
|
-
from
|
19
|
-
from .txt import common_save_as_txt
|
18
|
+
from .txt import common_export_as_txt
|
20
19
|
|
21
20
|
|
22
|
-
class
|
21
|
+
class CommonExporter(BaseExporter):
|
23
22
|
"""
|
24
|
-
|
25
|
-
It extends the
|
26
|
-
logic for exporting full novels as plain text (.txt) files
|
23
|
+
CommonExporter is a exporter that processes and exports novels.
|
24
|
+
It extends the BaseExporter interface and provides
|
25
|
+
logic for exporting full novels as plain text (.txt) files
|
26
|
+
and EPUB (.epub) files.
|
27
27
|
"""
|
28
28
|
|
29
29
|
def __init__(
|
30
30
|
self,
|
31
|
-
config:
|
31
|
+
config: ExporterConfig,
|
32
32
|
site: str,
|
33
33
|
chap_folders: list[str] | None = None,
|
34
34
|
):
|
35
|
-
|
36
|
-
Initialize the common saver with site information.
|
37
|
-
|
38
|
-
:param config: A SaverConfig object that defines
|
39
|
-
save paths, formats, and options.
|
40
|
-
:param site: Identifier for the site the saver is handling.
|
41
|
-
"""
|
42
|
-
super().__init__(config)
|
43
|
-
self._site = site
|
35
|
+
super().__init__(config, site)
|
44
36
|
self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
|
45
37
|
self._chap_folders: list[str] = chap_folders or ["chapters"]
|
46
38
|
|
47
|
-
def
|
39
|
+
def export_as_txt(self, book_id: str) -> None:
|
48
40
|
"""
|
49
|
-
Compile and
|
41
|
+
Compile and export a complete novel as a single .txt file.
|
50
42
|
|
51
43
|
Processing steps:
|
52
44
|
1. Load book metadata from `book_info.json`, including title,
|
@@ -60,9 +52,9 @@ class CommonSaver(BaseSaver):
|
|
60
52
|
:param book_id: The book identifier (used to locate raw data)
|
61
53
|
"""
|
62
54
|
self._init_chapter_storages(book_id)
|
63
|
-
return
|
55
|
+
return common_export_as_txt(self, book_id)
|
64
56
|
|
65
|
-
def
|
57
|
+
def export_as_epub(self, book_id: str) -> None:
|
66
58
|
"""
|
67
59
|
Persist the assembled book as a EPUB (.epub) file.
|
68
60
|
|
@@ -70,14 +62,14 @@ class CommonSaver(BaseSaver):
|
|
70
62
|
:raises NotImplementedError: If the method is not overridden.
|
71
63
|
"""
|
72
64
|
try:
|
73
|
-
from .epub import
|
65
|
+
from .epub import common_export_as_epub
|
74
66
|
except ImportError as err:
|
75
67
|
raise NotImplementedError(
|
76
68
|
"EPUB export not supported. Please install 'ebooklib'"
|
77
69
|
) from err
|
78
70
|
|
79
71
|
self._init_chapter_storages(book_id)
|
80
|
-
return
|
72
|
+
return common_export_as_epub(self, book_id)
|
81
73
|
|
82
74
|
@property
|
83
75
|
def site(self) -> str:
|
@@ -88,15 +80,6 @@ class CommonSaver(BaseSaver):
|
|
88
80
|
"""
|
89
81
|
return self._site
|
90
82
|
|
91
|
-
@site.setter
|
92
|
-
def site(self, value: str) -> None:
|
93
|
-
"""
|
94
|
-
Set the site identifier.
|
95
|
-
|
96
|
-
:param value: New site string to set.
|
97
|
-
"""
|
98
|
-
self._site = value
|
99
|
-
|
100
83
|
def _get_chapter(
|
101
84
|
self,
|
102
85
|
book_id: str,
|
@@ -109,7 +92,9 @@ class CommonSaver(BaseSaver):
|
|
109
92
|
return {}
|
110
93
|
|
111
94
|
def _init_chapter_storages(self, book_id: str) -> None:
|
112
|
-
|
95
|
+
if book_id in self._chapter_storage_cache:
|
96
|
+
return
|
97
|
+
raw_base = self._raw_data_dir / book_id
|
113
98
|
self._chapter_storage_cache[book_id] = [
|
114
99
|
ChapterStorage(
|
115
100
|
raw_base=raw_base,
|
@@ -118,3 +103,15 @@ class CommonSaver(BaseSaver):
|
|
118
103
|
)
|
119
104
|
for ns in self._chap_folders
|
120
105
|
]
|
106
|
+
|
107
|
+
def _on_close(self) -> None:
|
108
|
+
"""
|
109
|
+
Close all ChapterStorage connections in the cache.
|
110
|
+
"""
|
111
|
+
for storages in self._chapter_storage_cache.values():
|
112
|
+
for storage in storages:
|
113
|
+
try:
|
114
|
+
storage.close()
|
115
|
+
except Exception as e:
|
116
|
+
self.logger.warning("Failed to close storage %s: %s", storage, e)
|
117
|
+
self._chapter_storage_cache.clear()
|
@@ -1,13 +1,13 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.common.txt
|
4
|
+
------------------------------------------
|
5
5
|
|
6
6
|
Contains the logic for exporting novel content as a single `.txt` file.
|
7
7
|
|
8
8
|
This module defines `common_save_as_txt` function, which assembles and formats
|
9
9
|
a novel based on metadata and chapter files found in the raw data directory.
|
10
|
-
It is intended to be used by `
|
10
|
+
It is intended to be used by `CommonExporter` as part of the save/export process.
|
11
11
|
"""
|
12
12
|
|
13
13
|
from __future__ import annotations
|
@@ -22,33 +22,29 @@ from novel_downloader.utils.text_utils import (
|
|
22
22
|
)
|
23
23
|
|
24
24
|
if TYPE_CHECKING:
|
25
|
-
from .
|
25
|
+
from .main_exporter import CommonExporter
|
26
26
|
|
27
27
|
|
28
|
-
def
|
29
|
-
|
28
|
+
def common_export_as_txt(
|
29
|
+
exporter: CommonExporter,
|
30
30
|
book_id: str,
|
31
31
|
) -> None:
|
32
32
|
"""
|
33
33
|
将 save_path 文件夹中该小说的所有章节 json 文件合并保存为一个完整的 txt 文件,
|
34
34
|
并保存到 out_path 下
|
35
|
-
假设章节文件名格式为 `{chapterId}.json`
|
36
35
|
|
37
|
-
|
36
|
+
处理流程:
|
38
37
|
1. 从 book_info.json 中加载书籍信息 (包含书名、作者、简介及卷章节列表)
|
39
|
-
2. 遍历各卷, 每个卷先追加卷标题,
|
40
|
-
|
41
|
-
3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接,
|
42
|
-
构成最终完整文本
|
38
|
+
2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容
|
39
|
+
3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接
|
43
40
|
4. 将最终结果保存到 out_path 下 (例如:`{book_name}.txt`)
|
44
41
|
|
45
42
|
:param book_id: Identifier of the novel (used as subdirectory name).
|
46
43
|
"""
|
47
|
-
TAG = "[
|
48
|
-
site = saver.site
|
44
|
+
TAG = "[Exporter]"
|
49
45
|
# --- Paths & options ---
|
50
|
-
raw_base =
|
51
|
-
out_dir =
|
46
|
+
raw_base = exporter._raw_data_dir / book_id
|
47
|
+
out_dir = exporter.output_dir
|
52
48
|
out_dir.mkdir(parents=True, exist_ok=True)
|
53
49
|
|
54
50
|
# --- Load book_info.json ---
|
@@ -57,7 +53,7 @@ def common_save_as_txt(
|
|
57
53
|
info_text = info_path.read_text(encoding="utf-8")
|
58
54
|
book_info = json.loads(info_text)
|
59
55
|
except Exception as e:
|
60
|
-
|
56
|
+
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
61
57
|
return
|
62
58
|
|
63
59
|
# --- Compile chapters ---
|
@@ -71,17 +67,17 @@ def common_save_as_txt(
|
|
71
67
|
if vol_name:
|
72
68
|
volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
|
73
69
|
parts.append(volume_header)
|
74
|
-
|
70
|
+
exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
|
75
71
|
for chap in vol.get("chapters", []):
|
76
72
|
chap_id = chap.get("chapterId")
|
77
73
|
chap_title = chap.get("title", "")
|
78
74
|
if not chap_id:
|
79
|
-
|
75
|
+
exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
|
80
76
|
continue
|
81
77
|
|
82
|
-
chapter_data =
|
78
|
+
chapter_data = exporter._get_chapter(book_id, chap_id)
|
83
79
|
if not chapter_data:
|
84
|
-
|
80
|
+
exporter.logger.info(
|
85
81
|
"%s Missing chapter file in: %s (%s), skipping.",
|
86
82
|
TAG,
|
87
83
|
chap_title,
|
@@ -134,13 +130,13 @@ def common_save_as_txt(
|
|
134
130
|
final_text = header + "\n\n" + "\n\n".join(parts).strip()
|
135
131
|
|
136
132
|
# --- Determine output file path ---
|
137
|
-
out_name =
|
133
|
+
out_name = exporter.get_filename(title=name, author=author, ext="txt")
|
138
134
|
out_path = out_dir / out_name
|
139
135
|
|
140
136
|
# --- Save final text ---
|
141
137
|
try:
|
142
138
|
save_as_txt(content=final_text, filepath=out_path)
|
143
|
-
|
139
|
+
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
144
140
|
except Exception as e:
|
145
|
-
|
141
|
+
exporter.logger.error("%s Failed to save file: %s", TAG, e)
|
146
142
|
return
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.epub_utils
|
4
|
+
------------------------------------------
|
5
|
+
|
6
|
+
This package provides utility functions for constructing EPUB files,
|
7
|
+
including:
|
8
|
+
|
9
|
+
- CSS inclusion (css_builder)
|
10
|
+
- Image embedding (image_loader)
|
11
|
+
- EPUB book initialization (initializer)
|
12
|
+
- Chapter text-to-HTML conversion (text_to_html)
|
13
|
+
- Volume intro HTML generation (volume_intro)
|
14
|
+
"""
|
15
|
+
|
16
|
+
from .css_builder import create_css_items
|
17
|
+
from .image_loader import (
|
18
|
+
add_images_from_dir,
|
19
|
+
add_images_from_dirs,
|
20
|
+
add_images_from_list,
|
21
|
+
)
|
22
|
+
from .initializer import init_epub
|
23
|
+
from .text_to_html import (
|
24
|
+
chapter_txt_to_html,
|
25
|
+
generate_book_intro_html,
|
26
|
+
inline_remote_images,
|
27
|
+
)
|
28
|
+
from .volume_intro import create_volume_intro
|
29
|
+
|
30
|
+
__all__ = [
|
31
|
+
"create_css_items",
|
32
|
+
"add_images_from_dir",
|
33
|
+
"add_images_from_dirs",
|
34
|
+
"add_images_from_list",
|
35
|
+
"init_epub",
|
36
|
+
"chapter_txt_to_html",
|
37
|
+
"create_volume_intro",
|
38
|
+
"generate_book_intro_html",
|
39
|
+
"inline_remote_images",
|
40
|
+
]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
3
|
+
novel_downloader.core.exporters.epub_utils.css_builder
|
4
|
+
------------------------------------------------------
|
4
5
|
|
5
6
|
Reads local CSS files and wraps them into epub.EpubItem objects,
|
6
7
|
returning a list ready to be added to the EPUB.
|
@@ -0,0 +1,131 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.epub_utils.image_loader
|
4
|
+
-------------------------------------------------------
|
5
|
+
|
6
|
+
Utilities for embedding image files into an EpubBook.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import logging
|
10
|
+
from collections.abc import Iterable, Sequence
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
from ebooklib import epub
|
14
|
+
|
15
|
+
from novel_downloader.utils.constants import EPUB_IMAGE_FOLDER
|
16
|
+
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
|
19
|
+
_SUPPORTED_IMAGE_MEDIA_TYPES: dict[str, str] = {
|
20
|
+
"png": "image/png",
|
21
|
+
"jpg": "image/jpeg",
|
22
|
+
"jpeg": "image/jpeg",
|
23
|
+
"gif": "image/gif",
|
24
|
+
"svg": "image/svg+xml",
|
25
|
+
"webp": "image/webp",
|
26
|
+
}
|
27
|
+
_DEFAULT_IMAGE_MEDIA_TYPE = "image/jpeg"
|
28
|
+
|
29
|
+
|
30
|
+
def add_images_from_list(
|
31
|
+
book: epub.EpubBook,
|
32
|
+
image_list: Sequence[str | Path],
|
33
|
+
) -> epub.EpubBook:
|
34
|
+
"""
|
35
|
+
Add a list of image files to the EPUB's image folder.
|
36
|
+
|
37
|
+
:param book: The EpubBook object to modify.
|
38
|
+
:param image_list: List of paths to image files.
|
39
|
+
:return: The same EpubBook instance, with images added.
|
40
|
+
"""
|
41
|
+
for img_path in image_list:
|
42
|
+
img_path = Path(img_path)
|
43
|
+
if not img_path.is_file():
|
44
|
+
continue
|
45
|
+
|
46
|
+
suffix = img_path.suffix.lower().lstrip(".")
|
47
|
+
media_type = _SUPPORTED_IMAGE_MEDIA_TYPES.get(suffix)
|
48
|
+
if media_type is None:
|
49
|
+
media_type = _DEFAULT_IMAGE_MEDIA_TYPE
|
50
|
+
logger.warning(
|
51
|
+
"Unknown image suffix '%s' - defaulting media_type to %s",
|
52
|
+
suffix,
|
53
|
+
media_type,
|
54
|
+
)
|
55
|
+
|
56
|
+
try:
|
57
|
+
content = img_path.read_bytes()
|
58
|
+
item = epub.EpubItem(
|
59
|
+
uid=f"img_{img_path.stem}",
|
60
|
+
file_name=f"{EPUB_IMAGE_FOLDER}/{img_path.name}",
|
61
|
+
media_type=media_type,
|
62
|
+
content=content,
|
63
|
+
)
|
64
|
+
book.add_item(item)
|
65
|
+
logger.debug("Embedded image: %s", img_path.name)
|
66
|
+
except Exception:
|
67
|
+
logger.exception("Failed to embed image %s", img_path)
|
68
|
+
|
69
|
+
return book
|
70
|
+
|
71
|
+
|
72
|
+
def add_images_from_dir(
|
73
|
+
book: epub.EpubBook,
|
74
|
+
image_dir: str | Path,
|
75
|
+
) -> epub.EpubBook:
|
76
|
+
"""
|
77
|
+
Load every file in `image_dir` into the EPUB's image folder.
|
78
|
+
|
79
|
+
:param book: The EpubBook object to modify.
|
80
|
+
:param image_dir: Path to the directory containing image files.
|
81
|
+
:return: The same EpubBook instance, with images added.
|
82
|
+
"""
|
83
|
+
image_dir = Path(image_dir)
|
84
|
+
if not image_dir.is_dir():
|
85
|
+
logger.warning("Image directory not found or not a directory: %s", image_dir)
|
86
|
+
return book
|
87
|
+
|
88
|
+
for img_path in image_dir.iterdir():
|
89
|
+
if not img_path.is_file():
|
90
|
+
continue
|
91
|
+
|
92
|
+
suffix = img_path.suffix.lower().lstrip(".")
|
93
|
+
media_type = _SUPPORTED_IMAGE_MEDIA_TYPES.get(suffix)
|
94
|
+
if media_type is None:
|
95
|
+
media_type = _DEFAULT_IMAGE_MEDIA_TYPE
|
96
|
+
logger.warning(
|
97
|
+
"Unknown image suffix '%s' - defaulting media_type to %s",
|
98
|
+
suffix,
|
99
|
+
media_type,
|
100
|
+
)
|
101
|
+
|
102
|
+
try:
|
103
|
+
content = img_path.read_bytes()
|
104
|
+
item = epub.EpubItem(
|
105
|
+
uid=f"img_{img_path.stem}",
|
106
|
+
file_name=f"{EPUB_IMAGE_FOLDER}/{img_path.name}",
|
107
|
+
media_type=media_type,
|
108
|
+
content=content,
|
109
|
+
)
|
110
|
+
book.add_item(item)
|
111
|
+
logger.debug("Embedded image: %s", img_path.name)
|
112
|
+
except Exception:
|
113
|
+
logger.exception("Failed to embed image %s", img_path)
|
114
|
+
|
115
|
+
return book
|
116
|
+
|
117
|
+
|
118
|
+
def add_images_from_dirs(
|
119
|
+
book: epub.EpubBook,
|
120
|
+
image_dirs: Iterable[str | Path],
|
121
|
+
) -> epub.EpubBook:
|
122
|
+
"""
|
123
|
+
Add all images from multiple directories into the given EpubBook.
|
124
|
+
|
125
|
+
:param book: The EpubBook object to modify.
|
126
|
+
:param image_dirs: An iterable of directory paths to scan for images.
|
127
|
+
:return: The same EpubBook instance, with all images added.
|
128
|
+
"""
|
129
|
+
for img_dir in image_dirs:
|
130
|
+
book = add_images_from_dir(book, img_dir)
|
131
|
+
return book
|
@@ -1,6 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
3
|
+
novel_downloader.core.exporters.epub_utils.initializer
|
4
|
+
------------------------------------------------------
|
4
5
|
|
5
6
|
Initializes an epub.EpubBook object, sets metadata
|
6
7
|
(identifier, title, author, language, description),
|
@@ -41,10 +42,12 @@ def init_epub(
|
|
41
42
|
"""
|
42
43
|
book = epub.EpubBook()
|
43
44
|
book.set_identifier(str(book_id))
|
44
|
-
|
45
|
+
book_name = book_info.get("book_name") or book_info.get("volume_name", "未找到书名")
|
46
|
+
book.set_title(book_name)
|
45
47
|
book.set_language("zh-CN")
|
46
48
|
book.add_author(book_info.get("author", "未找到作者"))
|
47
|
-
|
49
|
+
desc = book_info.get("summary") or book_info.get("volume_intro", "未找到作品简介")
|
50
|
+
book.add_metadata("DC", "description", desc)
|
48
51
|
|
49
52
|
spine = []
|
50
53
|
|