novel-downloader 1.3.3__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 -39
- 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 +22 -22
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
- novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
- novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
- 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 +20 -14
- 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 +11 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- 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 +2 -1
- 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 +1 -1
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +69 -35
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.3.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 -210
- novel_downloader/core/downloaders/common/common_sync.py +0 -202
- 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 -219
- 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/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.3.dist-info/RECORD +0 -166
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -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
|
"""
|
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING
|
|
14
14
|
|
15
15
|
from ebooklib import epub
|
16
16
|
|
17
|
-
from novel_downloader.core.
|
17
|
+
from novel_downloader.core.exporters.epub_utils import (
|
18
18
|
add_images_from_dir,
|
19
19
|
chapter_txt_to_html,
|
20
20
|
create_css_items,
|
@@ -32,11 +32,11 @@ from novel_downloader.utils.network import download_image
|
|
32
32
|
from novel_downloader.utils.text_utils import clean_chapter_title
|
33
33
|
|
34
34
|
if TYPE_CHECKING:
|
35
|
-
from .
|
35
|
+
from .main_exporter import CommonExporter
|
36
36
|
|
37
37
|
|
38
|
-
def
|
39
|
-
|
38
|
+
def common_export_as_epub(
|
39
|
+
exporter: CommonExporter,
|
40
40
|
book_id: str,
|
41
41
|
) -> None:
|
42
42
|
"""
|
@@ -52,12 +52,12 @@ def common_save_as_epub(
|
|
52
52
|
:param saver: The saver instance, carrying config and path info.
|
53
53
|
:param book_id: Identifier of the novel (used as subdirectory name).
|
54
54
|
"""
|
55
|
-
TAG = "[
|
56
|
-
config =
|
55
|
+
TAG = "[exporter]"
|
56
|
+
config = exporter._config
|
57
57
|
# --- Paths & options ---
|
58
|
-
raw_base =
|
59
|
-
img_dir =
|
60
|
-
out_dir =
|
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
61
|
img_dir.mkdir(parents=True, exist_ok=True)
|
62
62
|
out_dir.mkdir(parents=True, exist_ok=True)
|
63
63
|
|
@@ -67,11 +67,11 @@ def common_save_as_epub(
|
|
67
67
|
info_text = info_path.read_text(encoding="utf-8")
|
68
68
|
book_info = json.loads(info_text)
|
69
69
|
except Exception as e:
|
70
|
-
|
70
|
+
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
71
71
|
return
|
72
72
|
|
73
73
|
book_name = book_info.get("book_name", book_id)
|
74
|
-
|
74
|
+
exporter.logger.info(
|
75
75
|
"%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
|
76
76
|
)
|
77
77
|
|
@@ -87,7 +87,7 @@ def common_save_as_epub(
|
|
87
87
|
on_exist="overwrite",
|
88
88
|
)
|
89
89
|
if not cover_path:
|
90
|
-
|
90
|
+
exporter.logger.warning("Failed to download cover from %s", cover_url)
|
91
91
|
|
92
92
|
# --- Initialize EPUB ---
|
93
93
|
book, spine, toc_list = init_epub(
|
@@ -108,7 +108,7 @@ def common_save_as_epub(
|
|
108
108
|
for vol_index, vol in enumerate(volumes, start=1):
|
109
109
|
raw_vol_name = vol.get("volume_name", "").strip()
|
110
110
|
vol_name = clean_chapter_title(raw_vol_name) or f"Unknown Volume {vol_index}"
|
111
|
-
|
111
|
+
exporter.logger.info("Processing volume %d: %s", vol_index, vol_name)
|
112
112
|
|
113
113
|
# Volume intro
|
114
114
|
vol_intro = epub.EpubHtml(
|
@@ -132,12 +132,12 @@ def common_save_as_epub(
|
|
132
132
|
chap_id = chap.get("chapterId")
|
133
133
|
chap_title = chap.get("title", "")
|
134
134
|
if not chap_id:
|
135
|
-
|
135
|
+
exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
|
136
136
|
continue
|
137
137
|
|
138
|
-
chapter_data =
|
138
|
+
chapter_data = exporter._get_chapter(book_id, chap_id)
|
139
139
|
if not chapter_data:
|
140
|
-
|
140
|
+
exporter.logger.info(
|
141
141
|
"%s Missing chapter file: %s (%s), skipping.",
|
142
142
|
TAG,
|
143
143
|
chap_title,
|
@@ -171,13 +171,13 @@ def common_save_as_epub(
|
|
171
171
|
book = add_images_from_dir(book, img_dir)
|
172
172
|
|
173
173
|
# --- 5. Finalize EPUB ---
|
174
|
-
|
174
|
+
exporter.logger.info("%s Building TOC and spine...", TAG)
|
175
175
|
book.toc = toc_list
|
176
176
|
book.spine = spine
|
177
177
|
book.add_item(epub.EpubNcx())
|
178
178
|
book.add_item(epub.EpubNav())
|
179
179
|
|
180
|
-
out_name =
|
180
|
+
out_name = exporter.get_filename(
|
181
181
|
title=book_name,
|
182
182
|
author=book_info.get("author"),
|
183
183
|
ext="epub",
|
@@ -186,7 +186,7 @@ def common_save_as_epub(
|
|
186
186
|
|
187
187
|
try:
|
188
188
|
epub.write_epub(out_path, book, EPUB_OPTIONS)
|
189
|
-
|
189
|
+
exporter.logger.info("%s EPUB successfully written to %s", TAG, out_path)
|
190
190
|
except Exception as e:
|
191
|
-
|
191
|
+
exporter.logger.error("%s Failed to write EPUB to %s: %s", TAG, out_path, e)
|
192
192
|
return
|
@@ -1,54 +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
|
44
|
-
self._raw_data_dir = self._base_raw_data_dir / site
|
45
|
-
self._cache_dir = self._base_cache_dir / site
|
35
|
+
super().__init__(config, site)
|
46
36
|
self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
|
47
37
|
self._chap_folders: list[str] = chap_folders or ["chapters"]
|
48
38
|
|
49
|
-
def
|
39
|
+
def export_as_txt(self, book_id: str) -> None:
|
50
40
|
"""
|
51
|
-
Compile and
|
41
|
+
Compile and export a complete novel as a single .txt file.
|
52
42
|
|
53
43
|
Processing steps:
|
54
44
|
1. Load book metadata from `book_info.json`, including title,
|
@@ -62,9 +52,9 @@ class CommonSaver(BaseSaver):
|
|
62
52
|
:param book_id: The book identifier (used to locate raw data)
|
63
53
|
"""
|
64
54
|
self._init_chapter_storages(book_id)
|
65
|
-
return
|
55
|
+
return common_export_as_txt(self, book_id)
|
66
56
|
|
67
|
-
def
|
57
|
+
def export_as_epub(self, book_id: str) -> None:
|
68
58
|
"""
|
69
59
|
Persist the assembled book as a EPUB (.epub) file.
|
70
60
|
|
@@ -72,14 +62,14 @@ class CommonSaver(BaseSaver):
|
|
72
62
|
:raises NotImplementedError: If the method is not overridden.
|
73
63
|
"""
|
74
64
|
try:
|
75
|
-
from .epub import
|
65
|
+
from .epub import common_export_as_epub
|
76
66
|
except ImportError as err:
|
77
67
|
raise NotImplementedError(
|
78
68
|
"EPUB export not supported. Please install 'ebooklib'"
|
79
69
|
) from err
|
80
70
|
|
81
71
|
self._init_chapter_storages(book_id)
|
82
|
-
return
|
72
|
+
return common_export_as_epub(self, book_id)
|
83
73
|
|
84
74
|
@property
|
85
75
|
def site(self) -> str:
|
@@ -90,15 +80,6 @@ class CommonSaver(BaseSaver):
|
|
90
80
|
"""
|
91
81
|
return self._site
|
92
82
|
|
93
|
-
@site.setter
|
94
|
-
def site(self, value: str) -> None:
|
95
|
-
"""
|
96
|
-
Set the site identifier.
|
97
|
-
|
98
|
-
:param value: New site string to set.
|
99
|
-
"""
|
100
|
-
self._site = value
|
101
|
-
|
102
83
|
def _get_chapter(
|
103
84
|
self,
|
104
85
|
book_id: str,
|
@@ -111,6 +92,8 @@ class CommonSaver(BaseSaver):
|
|
111
92
|
return {}
|
112
93
|
|
113
94
|
def _init_chapter_storages(self, book_id: str) -> None:
|
95
|
+
if book_id in self._chapter_storage_cache:
|
96
|
+
return
|
114
97
|
raw_base = self._raw_data_dir / book_id
|
115
98
|
self._chapter_storage_cache[book_id] = [
|
116
99
|
ChapterStorage(
|
@@ -120,3 +103,15 @@ class CommonSaver(BaseSaver):
|
|
120
103
|
)
|
121
104
|
for ns in self._chap_folders
|
122
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,32 +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 = "[
|
44
|
+
TAG = "[Exporter]"
|
48
45
|
# --- Paths & options ---
|
49
|
-
raw_base =
|
50
|
-
out_dir =
|
46
|
+
raw_base = exporter._raw_data_dir / book_id
|
47
|
+
out_dir = exporter.output_dir
|
51
48
|
out_dir.mkdir(parents=True, exist_ok=True)
|
52
49
|
|
53
50
|
# --- Load book_info.json ---
|
@@ -56,7 +53,7 @@ def common_save_as_txt(
|
|
56
53
|
info_text = info_path.read_text(encoding="utf-8")
|
57
54
|
book_info = json.loads(info_text)
|
58
55
|
except Exception as e:
|
59
|
-
|
56
|
+
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
60
57
|
return
|
61
58
|
|
62
59
|
# --- Compile chapters ---
|
@@ -70,17 +67,17 @@ def common_save_as_txt(
|
|
70
67
|
if vol_name:
|
71
68
|
volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
|
72
69
|
parts.append(volume_header)
|
73
|
-
|
70
|
+
exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
|
74
71
|
for chap in vol.get("chapters", []):
|
75
72
|
chap_id = chap.get("chapterId")
|
76
73
|
chap_title = chap.get("title", "")
|
77
74
|
if not chap_id:
|
78
|
-
|
75
|
+
exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
|
79
76
|
continue
|
80
77
|
|
81
|
-
chapter_data =
|
78
|
+
chapter_data = exporter._get_chapter(book_id, chap_id)
|
82
79
|
if not chapter_data:
|
83
|
-
|
80
|
+
exporter.logger.info(
|
84
81
|
"%s Missing chapter file in: %s (%s), skipping.",
|
85
82
|
TAG,
|
86
83
|
chap_title,
|
@@ -133,13 +130,13 @@ def common_save_as_txt(
|
|
133
130
|
final_text = header + "\n\n" + "\n\n".join(parts).strip()
|
134
131
|
|
135
132
|
# --- Determine output file path ---
|
136
|
-
out_name =
|
133
|
+
out_name = exporter.get_filename(title=name, author=author, ext="txt")
|
137
134
|
out_path = out_dir / out_name
|
138
135
|
|
139
136
|
# --- Save final text ---
|
140
137
|
try:
|
141
138
|
save_as_txt(content=final_text, filepath=out_path)
|
142
|
-
|
139
|
+
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
143
140
|
except Exception as e:
|
144
|
-
|
141
|
+
exporter.logger.error("%s Failed to save file: %s", TAG, e)
|
145
142
|
return
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils
|
4
|
+
------------------------------------------
|
5
5
|
|
6
6
|
This package provides utility functions for constructing EPUB files,
|
7
7
|
including:
|
@@ -14,7 +14,11 @@ including:
|
|
14
14
|
"""
|
15
15
|
|
16
16
|
from .css_builder import create_css_items
|
17
|
-
from .image_loader import
|
17
|
+
from .image_loader import (
|
18
|
+
add_images_from_dir,
|
19
|
+
add_images_from_dirs,
|
20
|
+
add_images_from_list,
|
21
|
+
)
|
18
22
|
from .initializer import init_epub
|
19
23
|
from .text_to_html import (
|
20
24
|
chapter_txt_to_html,
|
@@ -27,6 +31,7 @@ __all__ = [
|
|
27
31
|
"create_css_items",
|
28
32
|
"add_images_from_dir",
|
29
33
|
"add_images_from_dirs",
|
34
|
+
"add_images_from_list",
|
30
35
|
"init_epub",
|
31
36
|
"chapter_txt_to_html",
|
32
37
|
"create_volume_intro",
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils.css_builder
|
4
|
+
------------------------------------------------------
|
5
5
|
|
6
6
|
Reads local CSS files and wraps them into epub.EpubItem objects,
|
7
7
|
returning a list ready to be added to the EPUB.
|
@@ -1,13 +1,13 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils.image_loader
|
4
|
+
-------------------------------------------------------
|
5
5
|
|
6
6
|
Utilities for embedding image files into an EpubBook.
|
7
7
|
"""
|
8
8
|
|
9
9
|
import logging
|
10
|
-
from collections.abc import Iterable
|
10
|
+
from collections.abc import Iterable, Sequence
|
11
11
|
from pathlib import Path
|
12
12
|
|
13
13
|
from ebooklib import epub
|
@@ -27,6 +27,48 @@ _SUPPORTED_IMAGE_MEDIA_TYPES: dict[str, str] = {
|
|
27
27
|
_DEFAULT_IMAGE_MEDIA_TYPE = "image/jpeg"
|
28
28
|
|
29
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
|
+
|
30
72
|
def add_images_from_dir(
|
31
73
|
book: epub.EpubBook,
|
32
74
|
image_dir: str | Path,
|
@@ -66,7 +108,7 @@ def add_images_from_dir(
|
|
66
108
|
content=content,
|
67
109
|
)
|
68
110
|
book.add_item(item)
|
69
|
-
logger.
|
111
|
+
logger.debug("Embedded image: %s", img_path.name)
|
70
112
|
except Exception:
|
71
113
|
logger.exception("Failed to embed image %s", img_path)
|
72
114
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils.initializer
|
4
|
+
------------------------------------------------------
|
5
5
|
|
6
6
|
Initializes an epub.EpubBook object, sets metadata
|
7
7
|
(identifier, title, author, language, description),
|
@@ -42,10 +42,12 @@ def init_epub(
|
|
42
42
|
"""
|
43
43
|
book = epub.EpubBook()
|
44
44
|
book.set_identifier(str(book_id))
|
45
|
-
|
45
|
+
book_name = book_info.get("book_name") or book_info.get("volume_name", "未找到书名")
|
46
|
+
book.set_title(book_name)
|
46
47
|
book.set_language("zh-CN")
|
47
48
|
book.add_author(book_info.get("author", "未找到作者"))
|
48
|
-
|
49
|
+
desc = book_info.get("summary") or book_info.get("volume_intro", "未找到作品简介")
|
50
|
+
book.add_metadata("DC", "description", desc)
|
49
51
|
|
50
52
|
spine = []
|
51
53
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils.text_to_html
|
4
|
+
-------------------------------------------------------
|
5
5
|
|
6
6
|
Module for converting raw chapter text to formatted HTML,
|
7
7
|
with automatic word correction and optional image/tag support.
|
@@ -103,7 +103,7 @@ def inline_remote_images(
|
|
103
103
|
image_dir: str | Path,
|
104
104
|
) -> str:
|
105
105
|
"""
|
106
|
-
Download every remote
|
106
|
+
Download every remote `<img src="...">` in `content` into `image_dir`,
|
107
107
|
and replace the original tag with EPUB_IMAGE_WRAPPER
|
108
108
|
pointing to the local filename.
|
109
109
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.epub_utils.volume_intro
|
4
|
+
-------------------------------------------------------
|
5
5
|
|
6
6
|
Responsible for generating HTML code for volume introduction pages,
|
7
7
|
including two style variants and a unified entry point.
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.esjzone
|
4
|
+
---------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.models import ExporterConfig
|
9
|
+
|
10
|
+
from .common import CommonExporter
|
11
|
+
|
12
|
+
|
13
|
+
class EsjzoneExporter(CommonExporter):
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
config: ExporterConfig,
|
17
|
+
):
|
18
|
+
super().__init__(
|
19
|
+
config,
|
20
|
+
site="esjzone",
|
21
|
+
chap_folders=["chapters"],
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
__all__ = ["EsjzoneExporter"]
|