novel-downloader 1.4.5__py3-none-any.whl → 2.0.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/__init__.py +2 -4
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +27 -104
- novel_downloader/cli/download.py +78 -66
- novel_downloader/cli/export.py +20 -21
- novel_downloader/cli/main.py +3 -1
- novel_downloader/cli/search.py +120 -0
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +10 -14
- novel_downloader/config/adapter.py +195 -99
- novel_downloader/config/{loader.py → file_io.py} +53 -27
- novel_downloader/core/__init__.py +14 -13
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/archived/qidian/searcher.py +79 -0
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +8 -30
- novel_downloader/core/downloaders/base.py +182 -30
- novel_downloader/core/downloaders/common.py +217 -384
- novel_downloader/core/downloaders/qianbi.py +332 -4
- novel_downloader/core/downloaders/qidian.py +250 -290
- novel_downloader/core/downloaders/registry.py +69 -0
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +8 -26
- novel_downloader/core/exporters/base.py +107 -31
- novel_downloader/core/exporters/common/__init__.py +3 -4
- novel_downloader/core/exporters/common/epub.py +92 -171
- novel_downloader/core/exporters/common/main_exporter.py +14 -67
- novel_downloader/core/exporters/common/txt.py +90 -86
- novel_downloader/core/exporters/epub_util.py +184 -1327
- novel_downloader/core/exporters/linovelib/__init__.py +3 -2
- novel_downloader/core/exporters/linovelib/epub.py +165 -222
- novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
- novel_downloader/core/exporters/linovelib/txt.py +76 -66
- novel_downloader/core/exporters/qidian.py +15 -11
- novel_downloader/core/exporters/registry.py +55 -0
- novel_downloader/core/exporters/txt_util.py +67 -0
- novel_downloader/core/fetchers/__init__.py +57 -56
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
- novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
- novel_downloader/core/fetchers/biquyuedu.py +83 -0
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +60 -0
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +8 -14
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +26 -0
- novel_downloader/core/parsers/__init__.py +58 -22
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
- novel_downloader/core/parsers/qidian/__init__.py +2 -2
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
- novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
- novel_downloader/core/parsers/qidian/main_parser.py +19 -57
- novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +57 -0
- novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +155 -0
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +51 -0
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/b520.py +84 -0
- novel_downloader/core/searchers/base.py +168 -0
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +102 -0
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +165 -0
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +79 -0
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +36 -79
- novel_downloader/locales/zh.json +37 -80
- novel_downloader/models/__init__.py +23 -50
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +16 -43
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +21 -0
- novel_downloader/resources/config/settings.toml +39 -74
- novel_downloader/resources/css_styles/intro.css +83 -0
- novel_downloader/resources/css_styles/main.css +30 -89
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +43 -0
- novel_downloader/utils/chapter_storage.py +247 -226
- novel_downloader/utils/constants.py +5 -50
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +34 -0
- novel_downloader/utils/epub/builder.py +377 -0
- novel_downloader/utils/epub/constants.py +118 -0
- novel_downloader/utils/epub/documents.py +297 -0
- novel_downloader/utils/epub/models.py +120 -0
- novel_downloader/utils/epub/utils.py +179 -0
- novel_downloader/utils/file_utils/__init__.py +5 -30
- novel_downloader/utils/file_utils/io.py +9 -150
- novel_downloader/utils/file_utils/normalize.py +2 -2
- novel_downloader/utils/file_utils/sanitize.py +2 -7
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/i18n.py +2 -0
- novel_downloader/utils/logger.py +10 -16
- novel_downloader/utils/network.py +111 -252
- novel_downloader/utils/state.py +5 -90
- novel_downloader/utils/text_utils/__init__.py +16 -21
- novel_downloader/utils/text_utils/diff_display.py +6 -9
- novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
- novel_downloader/utils/text_utils/text_cleaner.py +179 -0
- novel_downloader/utils/text_utils/truncate_utils.py +62 -0
- novel_downloader/utils/time_utils/__init__.py +6 -12
- novel_downloader/utils/time_utils/datetime_utils.py +23 -33
- novel_downloader/utils/time_utils/sleep_utils.py +5 -10
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/config/site_rules.py +0 -94
- novel_downloader/core/downloaders/biquge.py +0 -25
- novel_downloader/core/downloaders/esjzone.py +0 -25
- novel_downloader/core/downloaders/linovelib.py +0 -25
- novel_downloader/core/downloaders/sfacg.py +0 -25
- novel_downloader/core/downloaders/yamibo.py +0 -25
- novel_downloader/core/exporters/biquge.py +0 -25
- novel_downloader/core/exporters/esjzone.py +0 -25
- novel_downloader/core/exporters/qianbi.py +0 -25
- novel_downloader/core/exporters/sfacg.py +0 -25
- novel_downloader/core/exporters/yamibo.py +0 -25
- novel_downloader/core/factory/__init__.py +0 -20
- novel_downloader/core/factory/downloader.py +0 -73
- novel_downloader/core/factory/exporter.py +0 -58
- novel_downloader/core/factory/fetcher.py +0 -96
- novel_downloader/core/factory/parser.py +0 -86
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -403
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/common/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -204
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -193
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -318
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -189
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -229
- novel_downloader/core/parsers/biquge/__init__.py +0 -10
- novel_downloader/core/parsers/biquge/main_parser.py +0 -134
- novel_downloader/core/parsers/common/__init__.py +0 -13
- novel_downloader/core/parsers/common/helper.py +0 -323
- novel_downloader/core/parsers/common/main_parser.py +0 -106
- novel_downloader/core/parsers/esjzone/__init__.py +0 -10
- novel_downloader/core/parsers/linovelib/__init__.py +0 -10
- novel_downloader/core/parsers/qianbi/__init__.py +0 -10
- novel_downloader/core/parsers/sfacg/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
- novel_downloader/models/browser.py +0 -21
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/site_rules.py +0 -99
- novel_downloader/models/tasks.py +0 -33
- novel_downloader/models/types.py +0 -15
- novel_downloader/resources/css_styles/volume-intro.css +0 -56
- novel_downloader/resources/json/replace_word_map.json +0 -4
- novel_downloader/resources/text/blacklist.txt +0 -22
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -303
- novel_downloader/utils/fontocr/ocr_v2.py +0 -752
- novel_downloader/utils/hash_store.py +0 -279
- novel_downloader/utils/hash_utils.py +0 -103
- novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
- novel_downloader/utils/text_utils/font_mapping.py +0 -28
- novel_downloader/utils/text_utils/text_cleaning.py +0 -107
- novel_downloader-1.4.5.dist-info/METADATA +0 -196
- novel_downloader-1.4.5.dist-info/RECORD +0 -165
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -8,41 +8,35 @@ Contains the logic for exporting novel content as a single `.epub` file.
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
-
import html
|
12
|
-
import json
|
13
|
-
import re
|
14
11
|
from pathlib import Path
|
15
12
|
from typing import TYPE_CHECKING
|
16
13
|
|
17
14
|
from novel_downloader.core.exporters.epub_util import (
|
18
|
-
|
15
|
+
build_epub_chapter,
|
16
|
+
download_cover,
|
17
|
+
finalize_export,
|
18
|
+
inline_remote_images,
|
19
|
+
prepare_builder,
|
20
|
+
remove_all_images,
|
21
|
+
)
|
22
|
+
from novel_downloader.utils import (
|
23
|
+
download,
|
24
|
+
get_cleaner,
|
25
|
+
)
|
26
|
+
from novel_downloader.utils.constants import DEFAULT_IMAGE_SUFFIX
|
27
|
+
from novel_downloader.utils.epub import (
|
19
28
|
Chapter,
|
20
|
-
StyleSheet,
|
21
29
|
Volume,
|
22
30
|
)
|
23
|
-
from novel_downloader.utils.constants import CSS_MAIN_PATH
|
24
|
-
from novel_downloader.utils.file_utils import sanitize_filename
|
25
|
-
from novel_downloader.utils.network import download_image
|
26
|
-
from novel_downloader.utils.text_utils import clean_chapter_title
|
27
31
|
|
28
32
|
if TYPE_CHECKING:
|
29
33
|
from .main_exporter import CommonExporter
|
30
34
|
|
31
|
-
_IMAGE_WRAPPER = (
|
32
|
-
'<div class="duokan-image-single illus"><img src="../Images/{filename}" /></div>'
|
33
|
-
)
|
34
|
-
_IMG_TAG_PATTERN = re.compile(
|
35
|
-
r'<img\s+[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>', re.IGNORECASE
|
36
|
-
)
|
37
|
-
_RAW_HTML_RE = re.compile(
|
38
|
-
r'^(<img\b[^>]*?\/>|<div class="duokan-image-single illus">.*?<\/div>)$', re.DOTALL
|
39
|
-
)
|
40
|
-
|
41
35
|
|
42
36
|
def common_export_as_epub(
|
43
37
|
exporter: CommonExporter,
|
44
38
|
book_id: str,
|
45
|
-
) -> None:
|
39
|
+
) -> Path | None:
|
46
40
|
"""
|
47
41
|
Export a single novel (identified by `book_id`) to an EPUB file.
|
48
42
|
|
@@ -50,132 +44,142 @@ def common_export_as_epub(
|
|
50
44
|
1. Load `book_info.json` for metadata.
|
51
45
|
2. Generate introductory HTML and optionally include the cover image.
|
52
46
|
3. Initialize the EPUB container.
|
53
|
-
4. Iterate through volumes and chapters, convert each to XHTML.
|
47
|
+
4. Iterate through volumes and chapters in volume-batches, convert each to XHTML.
|
54
48
|
5. Assemble the spine, TOC, CSS and write out the final `.epub`.
|
55
49
|
|
56
|
-
:param
|
50
|
+
:param exporter: The exporter instance, carrying config and path info.
|
57
51
|
:param book_id: Identifier of the novel (used as subdirectory name).
|
58
52
|
"""
|
59
53
|
TAG = "[exporter]"
|
60
54
|
config = exporter._config
|
61
|
-
|
55
|
+
|
62
56
|
raw_base = exporter._raw_data_dir / book_id
|
63
|
-
img_dir =
|
57
|
+
img_dir = raw_base / "images"
|
64
58
|
out_dir = exporter.output_dir
|
59
|
+
|
65
60
|
img_dir.mkdir(parents=True, exist_ok=True)
|
66
61
|
out_dir.mkdir(parents=True, exist_ok=True)
|
67
62
|
|
63
|
+
cleaner = get_cleaner(
|
64
|
+
enabled=config.clean_text,
|
65
|
+
config=config.cleaner_cfg,
|
66
|
+
)
|
67
|
+
|
68
68
|
# --- Load book_info.json ---
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
book_info = json.loads(info_text)
|
73
|
-
except Exception as e:
|
74
|
-
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
75
|
-
return
|
69
|
+
book_info = exporter._load_book_info(book_id)
|
70
|
+
if not book_info:
|
71
|
+
return None
|
76
72
|
|
77
73
|
book_name = book_info.get("book_name", book_id)
|
78
74
|
book_author = book_info.get("author", "")
|
75
|
+
|
79
76
|
exporter.logger.info(
|
80
77
|
"%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
|
81
78
|
)
|
82
79
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
target_name="cover",
|
91
|
-
on_exist="overwrite",
|
92
|
-
)
|
93
|
-
if not cover_path:
|
94
|
-
exporter.logger.warning("Failed to download cover from %s", cover_url)
|
80
|
+
cover_path = download_cover(
|
81
|
+
book_info.get("cover_url", ""),
|
82
|
+
raw_base,
|
83
|
+
config.include_cover,
|
84
|
+
exporter.logger,
|
85
|
+
TAG,
|
86
|
+
)
|
95
87
|
|
96
88
|
# --- Initialize EPUB ---
|
97
|
-
book =
|
89
|
+
book, main_css = prepare_builder(
|
90
|
+
site_name=exporter.site,
|
91
|
+
book_id=book_id,
|
98
92
|
title=book_name,
|
99
93
|
author=book_author,
|
100
94
|
description=book_info.get("summary", ""),
|
101
|
-
|
102
|
-
subject=book_info.get("subject", []),
|
95
|
+
subject=book_info.get("tags", []),
|
103
96
|
serial_status=book_info.get("serial_status", ""),
|
104
97
|
word_count=book_info.get("word_count", ""),
|
105
|
-
|
106
|
-
)
|
107
|
-
main_css = StyleSheet(
|
108
|
-
id="main_style",
|
109
|
-
content=CSS_MAIN_PATH.read_text(encoding="utf-8"),
|
110
|
-
filename="main.css",
|
98
|
+
cover_path=cover_path,
|
111
99
|
)
|
112
|
-
book.add_stylesheet(main_css)
|
113
100
|
|
114
101
|
# --- Compile chapters ---
|
115
102
|
volumes = book_info.get("volumes", [])
|
116
|
-
|
117
|
-
|
118
|
-
raw_vol_name = raw_vol_name.replace(book_name, "").strip()
|
119
|
-
vol_name = raw_vol_name or f"Volume {vol_index}"
|
120
|
-
exporter.logger.info("Processing volume %d: %s", vol_index, vol_name)
|
103
|
+
if not volumes:
|
104
|
+
exporter.logger.warning("%s No volumes found in metadata.", TAG)
|
121
105
|
|
122
|
-
|
106
|
+
for vol_index, vol in enumerate(volumes, start=1):
|
107
|
+
raw_name = vol.get("volume_name", "")
|
108
|
+
raw_name = cleaner.clean_title(raw_name.replace(book_name, ""))
|
109
|
+
vol_name = raw_name or f"Volume {vol_index}"
|
110
|
+
exporter.logger.info("%s Processing volume %d: %s", TAG, vol_index, vol_name)
|
111
|
+
|
112
|
+
# Batch-fetch chapters for this volume
|
113
|
+
chap_ids = [
|
114
|
+
chap["chapterId"]
|
115
|
+
for chap in vol.get("chapters", [])
|
116
|
+
if chap.get("chapterId")
|
117
|
+
]
|
118
|
+
chap_map = exporter._get_chapters(book_id, chap_ids)
|
119
|
+
|
120
|
+
vol_cover: Path | None = None
|
123
121
|
vol_cover_url = vol.get("volume_cover", "")
|
124
122
|
if vol_cover_url:
|
125
|
-
|
123
|
+
vol_cover = download(
|
126
124
|
vol_cover_url,
|
127
125
|
img_dir,
|
128
126
|
on_exist="skip",
|
127
|
+
default_suffix=DEFAULT_IMAGE_SUFFIX,
|
129
128
|
)
|
130
129
|
|
131
130
|
curr_vol = Volume(
|
132
131
|
id=f"vol_{vol_index}",
|
133
132
|
title=vol_name,
|
134
|
-
intro=vol.get("volume_intro", ""),
|
135
|
-
cover=
|
133
|
+
intro=cleaner.clean_content(vol.get("volume_intro", "")),
|
134
|
+
cover=vol_cover,
|
136
135
|
)
|
137
136
|
|
138
|
-
for
|
139
|
-
chap_id =
|
140
|
-
chap_title = chap.get("title", "")
|
137
|
+
for chap_meta in vol.get("chapters", []):
|
138
|
+
chap_id = chap_meta.get("chapterId")
|
141
139
|
if not chap_id:
|
142
140
|
exporter.logger.warning(
|
143
141
|
"%s Missing chapterId, skipping: %s",
|
144
142
|
TAG,
|
145
|
-
|
143
|
+
chap_meta,
|
146
144
|
)
|
147
145
|
continue
|
148
146
|
|
149
|
-
|
150
|
-
|
147
|
+
chap_title = chap_meta.get("title", "")
|
148
|
+
data = chap_map.get(chap_id)
|
149
|
+
if not data:
|
151
150
|
exporter.logger.info(
|
152
|
-
"%s Missing chapter
|
151
|
+
"%s Missing chapter: %s (%s), skipping.",
|
153
152
|
TAG,
|
154
153
|
chap_title,
|
155
154
|
chap_id,
|
156
155
|
)
|
157
156
|
continue
|
158
157
|
|
159
|
-
title =
|
160
|
-
content
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
158
|
+
title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
|
159
|
+
content = cleaner.clean_content(data.get("content", ""))
|
160
|
+
extra = data.get("extra", {})
|
161
|
+
author_note = cleaner.clean_content(extra.get("author_say", ""))
|
162
|
+
content = (
|
163
|
+
inline_remote_images(book, content, img_dir)
|
164
|
+
if config.include_picture
|
165
|
+
else remove_all_images(content)
|
166
|
+
)
|
167
|
+
extras = {"作者说": author_note} if author_note else {}
|
168
|
+
|
169
|
+
chap_html = build_epub_chapter(
|
170
|
+
title=title,
|
171
|
+
paragraphs=content,
|
172
|
+
extras=extras,
|
168
173
|
)
|
169
|
-
curr_vol.
|
174
|
+
curr_vol.chapters.append(
|
170
175
|
Chapter(
|
171
176
|
id=f"c_{chap_id}",
|
177
|
+
filename=f"c{chap_id}.xhtml",
|
172
178
|
title=title,
|
173
179
|
content=chap_html,
|
174
180
|
css=[main_css],
|
175
181
|
)
|
176
182
|
)
|
177
|
-
for img_path in img_paths:
|
178
|
-
book.add_image(img_path)
|
179
183
|
|
180
184
|
book.add_volume(curr_vol)
|
181
185
|
|
@@ -185,93 +189,10 @@ def common_export_as_epub(
|
|
185
189
|
author=book_info.get("author"),
|
186
190
|
ext="epub",
|
187
191
|
)
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
exporter.logger
|
193
|
-
|
194
|
-
|
195
|
-
return
|
196
|
-
|
197
|
-
|
198
|
-
def _inline_remote_images(
|
199
|
-
content: str,
|
200
|
-
image_dir: str | Path,
|
201
|
-
) -> tuple[str, list[Path]]:
|
202
|
-
"""
|
203
|
-
Download every remote `<img src="...">` in `content` into `image_dir`,
|
204
|
-
and replace the original tag with _IMAGE_WRAPPER
|
205
|
-
pointing to the local filename.
|
206
|
-
|
207
|
-
:param content: HTML/text of the chapter containing <img> tags.
|
208
|
-
:param image_dir: Directory to save downloaded images into.
|
209
|
-
:return: A tuple (modified_content, list_of_downloaded_image_paths).
|
210
|
-
"""
|
211
|
-
downloaded_images: list[Path] = []
|
212
|
-
|
213
|
-
def _replace(match: re.Match[str]) -> str:
|
214
|
-
url = match.group(1)
|
215
|
-
try:
|
216
|
-
# download_image returns a Path or None
|
217
|
-
local_path = download_image(
|
218
|
-
url,
|
219
|
-
image_dir,
|
220
|
-
target_name=None,
|
221
|
-
on_exist="skip",
|
222
|
-
)
|
223
|
-
if not local_path:
|
224
|
-
return match.group(0)
|
225
|
-
|
226
|
-
downloaded_images.append(local_path)
|
227
|
-
return _IMAGE_WRAPPER.format(filename=local_path.name)
|
228
|
-
except Exception:
|
229
|
-
return match.group(0)
|
230
|
-
|
231
|
-
modified_content = _IMG_TAG_PATTERN.sub(_replace, content)
|
232
|
-
return modified_content, downloaded_images
|
233
|
-
|
234
|
-
|
235
|
-
def _txt_to_html(
|
236
|
-
chapter_title: str,
|
237
|
-
chapter_text: str,
|
238
|
-
extras: dict[str, str] | None = None,
|
239
|
-
) -> str:
|
240
|
-
"""
|
241
|
-
Convert chapter text and author note to styled HTML.
|
242
|
-
|
243
|
-
:param chapter_title: Title of the chapter.
|
244
|
-
:param chapter_text: Main content of the chapter.
|
245
|
-
:param extras: Optional dict of titles and content, e.g. {"作者说": "text"}.
|
246
|
-
:return: Rendered HTML as a string.
|
247
|
-
"""
|
248
|
-
|
249
|
-
def _render_block(text: str) -> str:
|
250
|
-
lines = (line.strip() for line in text.splitlines() if line.strip())
|
251
|
-
out = []
|
252
|
-
for line in lines:
|
253
|
-
# preserve raw HTML, otherwise wrap in <p>
|
254
|
-
if _RAW_HTML_RE.match(line):
|
255
|
-
out.append(line)
|
256
|
-
else:
|
257
|
-
out.append(f"<p>{html.escape(line)}</p>")
|
258
|
-
return "\n".join(out)
|
259
|
-
|
260
|
-
parts = []
|
261
|
-
parts.append(f"<h2>{html.escape(chapter_title)}</h2>")
|
262
|
-
parts.append(_render_block(chapter_text))
|
263
|
-
|
264
|
-
if extras:
|
265
|
-
for title, note in extras.items():
|
266
|
-
note = note.strip()
|
267
|
-
if not note:
|
268
|
-
continue
|
269
|
-
parts.extend(
|
270
|
-
[
|
271
|
-
"<hr />",
|
272
|
-
f"<p>{html.escape(title)}</p>",
|
273
|
-
_render_block(note),
|
274
|
-
]
|
275
|
-
)
|
276
|
-
|
277
|
-
return "\n".join(parts)
|
192
|
+
return finalize_export(
|
193
|
+
book=book,
|
194
|
+
out_dir=out_dir,
|
195
|
+
filename=out_name,
|
196
|
+
logger=exporter.logger,
|
197
|
+
tag=TAG,
|
198
|
+
)
|
@@ -3,40 +3,27 @@
|
|
3
3
|
novel_downloader.core.exporters.common.main_exporter
|
4
4
|
----------------------------------------------------
|
5
5
|
|
6
|
-
|
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.
|
6
|
+
Common exporter implementation for saving novels as TXT and EPUB files.
|
9
7
|
"""
|
10
8
|
|
11
|
-
from
|
12
|
-
from typing import Any
|
9
|
+
from pathlib import Path
|
13
10
|
|
14
11
|
from novel_downloader.core.exporters.base import BaseExporter
|
15
|
-
from novel_downloader.models import ExporterConfig
|
16
|
-
from novel_downloader.utils.chapter_storage import ChapterStorage
|
17
12
|
|
13
|
+
from .epub import common_export_as_epub
|
18
14
|
from .txt import common_export_as_txt
|
19
15
|
|
20
16
|
|
21
17
|
class CommonExporter(BaseExporter):
|
22
18
|
"""
|
23
19
|
CommonExporter is a exporter that processes and exports novels.
|
20
|
+
|
24
21
|
It extends the BaseExporter interface and provides
|
25
22
|
logic for exporting full novels as plain text (.txt) files
|
26
23
|
and EPUB (.epub) files.
|
27
24
|
"""
|
28
25
|
|
29
|
-
def
|
30
|
-
self,
|
31
|
-
config: ExporterConfig,
|
32
|
-
site: str,
|
33
|
-
chap_folders: list[str] | None = None,
|
34
|
-
):
|
35
|
-
super().__init__(config, site)
|
36
|
-
self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
|
37
|
-
self._chap_folders: list[str] = chap_folders or ["chapters"]
|
38
|
-
|
39
|
-
def export_as_txt(self, book_id: str) -> None:
|
26
|
+
def export_as_txt(self, book_id: str) -> Path | None:
|
40
27
|
"""
|
41
28
|
Compile and export a complete novel as a single .txt file.
|
42
29
|
|
@@ -51,67 +38,27 @@ class CommonExporter(BaseExporter):
|
|
51
38
|
|
52
39
|
:param book_id: The book identifier (used to locate raw data)
|
53
40
|
"""
|
41
|
+
book_id = self._normalize_book_id(book_id)
|
54
42
|
self._init_chapter_storages(book_id)
|
55
43
|
return common_export_as_txt(self, book_id)
|
56
44
|
|
57
|
-
def export_as_epub(self, book_id: str) -> None:
|
45
|
+
def export_as_epub(self, book_id: str) -> Path | None:
|
58
46
|
"""
|
59
47
|
Persist the assembled book as a EPUB (.epub) file.
|
60
48
|
|
61
49
|
:param book_id: The book identifier.
|
62
50
|
:raises NotImplementedError: If the method is not overridden.
|
63
51
|
"""
|
64
|
-
|
65
|
-
from .epub import common_export_as_epub
|
66
|
-
except ImportError as err:
|
67
|
-
raise NotImplementedError(
|
68
|
-
"EPUB export not supported. Please install 'ebooklib'"
|
69
|
-
) from err
|
70
|
-
|
52
|
+
book_id = self._normalize_book_id(book_id)
|
71
53
|
self._init_chapter_storages(book_id)
|
72
54
|
return common_export_as_epub(self, book_id)
|
73
55
|
|
74
|
-
@
|
75
|
-
def
|
76
|
-
"""
|
77
|
-
Get the site identifier.
|
78
|
-
|
79
|
-
:return: The site string.
|
56
|
+
@staticmethod
|
57
|
+
def _normalize_book_id(book_id: str) -> str:
|
80
58
|
"""
|
81
|
-
|
82
|
-
|
83
|
-
def _get_chapter(
|
84
|
-
self,
|
85
|
-
book_id: str,
|
86
|
-
chap_id: str,
|
87
|
-
) -> Mapping[str, Any]:
|
88
|
-
for storage in self._chapter_storage_cache[book_id]:
|
89
|
-
data = storage.get(chap_id)
|
90
|
-
if data:
|
91
|
-
return data
|
92
|
-
return {}
|
59
|
+
Normalize a book identifier.
|
93
60
|
|
94
|
-
|
95
|
-
|
96
|
-
return
|
97
|
-
raw_base = self._raw_data_dir / book_id
|
98
|
-
self._chapter_storage_cache[book_id] = [
|
99
|
-
ChapterStorage(
|
100
|
-
raw_base=raw_base,
|
101
|
-
namespace=ns,
|
102
|
-
backend_type=self._config.storage_backend,
|
103
|
-
)
|
104
|
-
for ns in self._chap_folders
|
105
|
-
]
|
106
|
-
|
107
|
-
def _on_close(self) -> None:
|
108
|
-
"""
|
109
|
-
Close all ChapterStorage connections in the cache.
|
61
|
+
Subclasses may override this method to transform the book ID
|
62
|
+
into their preferred format.
|
110
63
|
"""
|
111
|
-
|
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()
|
64
|
+
return book_id.replace("/", "-")
|