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
@@ -1,13 +1,11 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.utils.crypto_utils
|
4
|
-
|
3
|
+
novel_downloader.utils.crypto_utils.rc4
|
4
|
+
---------------------------------------
|
5
5
|
|
6
|
-
|
6
|
+
RC4 stream cipher for simple text encryption and decryption.
|
7
7
|
"""
|
8
8
|
|
9
|
-
from __future__ import annotations
|
10
|
-
|
11
9
|
import base64
|
12
10
|
|
13
11
|
|
@@ -22,16 +20,11 @@ def rc4_crypt(
|
|
22
20
|
Encrypt or decrypt data using RC4 and Base64.
|
23
21
|
|
24
22
|
:param key: RC4 key (will be encoded using the specified encoding).
|
25
|
-
:type key: str
|
26
23
|
:param data: Plain-text (for 'encrypt') or Base64 cipher-text (for 'decrypt').
|
27
|
-
:type data: str
|
28
24
|
:param mode: Operation mode, either 'encrypt' or 'decrypt'. Defaults to 'encrypt'.
|
29
|
-
:type mode: str, optional
|
30
25
|
:param encoding: Character encoding for key and returned string. Defaults 'utf-8'.
|
31
|
-
:type encoding: str, optional
|
32
26
|
|
33
27
|
:return: Base64 cipher-text (for encryption) or decoded plain-text (for decryption).
|
34
|
-
:rtype: str
|
35
28
|
|
36
29
|
:raises ValueError: If mode is not 'encrypt' or 'decrypt'.
|
37
30
|
"""
|
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub
|
4
|
+
---------------------------
|
5
|
+
|
6
|
+
Top-level package for EPUB export utilities.
|
7
|
+
|
8
|
+
Key components:
|
9
|
+
|
10
|
+
- EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
|
11
|
+
- Chapter, Volume : represent and render content sections and volume intros
|
12
|
+
|
13
|
+
Usage example:
|
14
|
+
|
15
|
+
```python
|
16
|
+
builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
|
17
|
+
builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
|
18
|
+
builder.export("output/my_novel.epub")
|
19
|
+
```
|
20
|
+
"""
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"EpubBuilder",
|
24
|
+
"Chapter",
|
25
|
+
"Volume",
|
26
|
+
"StyleSheet",
|
27
|
+
]
|
28
|
+
|
29
|
+
from .builder import EpubBuilder
|
30
|
+
from .models import (
|
31
|
+
Chapter,
|
32
|
+
StyleSheet,
|
33
|
+
Volume,
|
34
|
+
)
|
@@ -0,0 +1,377 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub.builder
|
4
|
+
-----------------------------------
|
5
|
+
|
6
|
+
Orchestrates the end-to-end EPUB build process by:
|
7
|
+
- Managing metadata (title, author, description, language, etc.)
|
8
|
+
- Collecting and deduplicating resources (chapters, images, stylesheets)
|
9
|
+
- Registering everything in the OPF manifest and spine
|
10
|
+
- Generating nav.xhtml, toc.ncx, content.opf, and the zipped .epub file
|
11
|
+
|
12
|
+
Provides:
|
13
|
+
- methods to add chapters, volumes, images, and styles
|
14
|
+
- a clean `export()` entry point that writes the final EPUB archive
|
15
|
+
"""
|
16
|
+
|
17
|
+
import zipfile
|
18
|
+
from pathlib import Path
|
19
|
+
from zipfile import ZIP_DEFLATED, ZIP_STORED
|
20
|
+
|
21
|
+
from novel_downloader.utils.constants import (
|
22
|
+
CSS_INTRO_PATH,
|
23
|
+
VOLUME_BORDER_IMAGE_PATH,
|
24
|
+
)
|
25
|
+
|
26
|
+
from .constants import (
|
27
|
+
COVER_IMAGE_TEMPLATE,
|
28
|
+
CSS_FOLDER,
|
29
|
+
IMAGE_FOLDER,
|
30
|
+
IMAGE_MEDIA_TYPES,
|
31
|
+
ROOT_PATH,
|
32
|
+
TEXT_FOLDER,
|
33
|
+
)
|
34
|
+
from .documents import (
|
35
|
+
NavDocument,
|
36
|
+
NCXDocument,
|
37
|
+
OpfDocument,
|
38
|
+
)
|
39
|
+
from .models import (
|
40
|
+
Chapter,
|
41
|
+
ChapterEntry,
|
42
|
+
EpubResource,
|
43
|
+
ImageResource,
|
44
|
+
StyleSheet,
|
45
|
+
Volume,
|
46
|
+
)
|
47
|
+
from .utils import (
|
48
|
+
build_book_intro,
|
49
|
+
build_container_xml,
|
50
|
+
build_volume_intro,
|
51
|
+
hash_file,
|
52
|
+
)
|
53
|
+
|
54
|
+
|
55
|
+
class EpubBuilder:
|
56
|
+
def __init__(
|
57
|
+
self,
|
58
|
+
title: str,
|
59
|
+
author: str = "",
|
60
|
+
description: str = "",
|
61
|
+
cover_path: Path | None = None,
|
62
|
+
subject: list[str] | None = None,
|
63
|
+
serial_status: str = "",
|
64
|
+
word_count: str = "0",
|
65
|
+
uid: str = "",
|
66
|
+
language: str = "zh-CN",
|
67
|
+
):
|
68
|
+
# metadata
|
69
|
+
self.title = title
|
70
|
+
self.author = author
|
71
|
+
self.description = description
|
72
|
+
self.language = language
|
73
|
+
self.subject = subject or []
|
74
|
+
self.serial_status = serial_status
|
75
|
+
self.word_count = word_count
|
76
|
+
self.uid = uid
|
77
|
+
|
78
|
+
# builder state
|
79
|
+
self.chapters: list[Chapter] = []
|
80
|
+
self.images: list[ImageResource] = []
|
81
|
+
self.styles: list[StyleSheet] = []
|
82
|
+
self._img_map: dict[str, str] = {}
|
83
|
+
self._img_idx = 0
|
84
|
+
self._vol_idx = 0
|
85
|
+
|
86
|
+
# core EPUB documents
|
87
|
+
self.nav = NavDocument(title=title, language=language)
|
88
|
+
self.ncx = NCXDocument(title=title, uid=uid)
|
89
|
+
self.opf = OpfDocument(
|
90
|
+
title=title,
|
91
|
+
author=author,
|
92
|
+
description=description,
|
93
|
+
uid=uid,
|
94
|
+
subject=self.subject,
|
95
|
+
language=language,
|
96
|
+
)
|
97
|
+
|
98
|
+
# register the nav & ncx items
|
99
|
+
self.opf.add_manifest_item(
|
100
|
+
"nav",
|
101
|
+
"nav.xhtml",
|
102
|
+
self.nav.media_type,
|
103
|
+
properties="nav",
|
104
|
+
)
|
105
|
+
self.opf.add_manifest_item("ncx", "toc.ncx", self.ncx.media_type)
|
106
|
+
|
107
|
+
self._init_styles()
|
108
|
+
self._init_cover(cover_path)
|
109
|
+
self._init_intro()
|
110
|
+
|
111
|
+
def add_image(self, image_path: Path) -> str:
|
112
|
+
"""
|
113
|
+
Add an image resource (deduped by hash) and register it.
|
114
|
+
"""
|
115
|
+
if not (image_path.exists() and image_path.is_file()):
|
116
|
+
return ""
|
117
|
+
h = hash_file(image_path)
|
118
|
+
if h in self._img_map:
|
119
|
+
return self._img_map[h]
|
120
|
+
|
121
|
+
ext = image_path.suffix.lower().lstrip(".")
|
122
|
+
mtype = IMAGE_MEDIA_TYPES.get(ext)
|
123
|
+
if not mtype:
|
124
|
+
return ""
|
125
|
+
|
126
|
+
res_id = f"img_{self._img_idx}"
|
127
|
+
filename = f"{res_id}.{ext}"
|
128
|
+
data = image_path.read_bytes()
|
129
|
+
img = ImageResource(id=res_id, data=data, media_type=mtype, filename=filename)
|
130
|
+
self.images.append(img)
|
131
|
+
self._register(img, folder=IMAGE_FOLDER, in_spine=False)
|
132
|
+
|
133
|
+
self._img_map[h] = filename
|
134
|
+
self._img_idx += 1
|
135
|
+
return filename
|
136
|
+
|
137
|
+
def add_chapter(self, chap: Chapter) -> None:
|
138
|
+
self.chapters.append(chap)
|
139
|
+
self._register(chap, folder=TEXT_FOLDER)
|
140
|
+
self.nav.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
|
141
|
+
self.ncx.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
|
142
|
+
|
143
|
+
def add_volume(self, volume: Volume) -> None:
|
144
|
+
"""Add a volume cover, intro, and all its chapters to the EPUB."""
|
145
|
+
# volume-specific cover
|
146
|
+
if volume.cover:
|
147
|
+
filename = self.add_image(volume.cover)
|
148
|
+
cover_html = f'<img class="width100" src="../{IMAGE_FOLDER}/{filename}"/>'
|
149
|
+
cover_chap = Chapter(
|
150
|
+
id=f"vol_{self._vol_idx}_cover",
|
151
|
+
title=volume.title,
|
152
|
+
content=cover_html,
|
153
|
+
filename=f"vol_{self._vol_idx}_cover.xhtml",
|
154
|
+
)
|
155
|
+
self.chapters.append(cover_chap)
|
156
|
+
self._register(
|
157
|
+
cover_chap,
|
158
|
+
folder=TEXT_FOLDER,
|
159
|
+
properties="duokan-page-fullscreen",
|
160
|
+
)
|
161
|
+
|
162
|
+
# volume intro page
|
163
|
+
intro_content = build_volume_intro(volume.title, volume.intro)
|
164
|
+
vol_intro = Chapter(
|
165
|
+
id=f"vol_{self._vol_idx}",
|
166
|
+
title=volume.title,
|
167
|
+
content=intro_content,
|
168
|
+
css=[self.intro_css],
|
169
|
+
filename=f"vol_{self._vol_idx}.xhtml",
|
170
|
+
)
|
171
|
+
self.chapters.append(vol_intro)
|
172
|
+
self._register(vol_intro, folder=TEXT_FOLDER)
|
173
|
+
|
174
|
+
# nested chapters
|
175
|
+
entries: list[ChapterEntry] = []
|
176
|
+
for chap in volume.chapters:
|
177
|
+
self.chapters.append(chap)
|
178
|
+
self._register(chap, folder=TEXT_FOLDER)
|
179
|
+
entries.append(
|
180
|
+
ChapterEntry(
|
181
|
+
id=chap.id,
|
182
|
+
label=chap.title,
|
183
|
+
src=f"{TEXT_FOLDER}/{chap.filename}",
|
184
|
+
)
|
185
|
+
)
|
186
|
+
|
187
|
+
# TOC updates
|
188
|
+
self.ncx.add_volume(
|
189
|
+
id=f"vol_{self._vol_idx}",
|
190
|
+
label=volume.title,
|
191
|
+
src=f"{TEXT_FOLDER}/{vol_intro.filename}",
|
192
|
+
chapters=entries,
|
193
|
+
)
|
194
|
+
self.nav.add_volume(
|
195
|
+
id=f"vol_{self._vol_idx}",
|
196
|
+
label=volume.title,
|
197
|
+
src=f"{TEXT_FOLDER}/{vol_intro.filename}",
|
198
|
+
chapters=entries,
|
199
|
+
)
|
200
|
+
|
201
|
+
self._vol_idx += 1
|
202
|
+
|
203
|
+
def add_stylesheet(self, css: StyleSheet) -> None:
|
204
|
+
"""
|
205
|
+
Register an external CSS file in the EPUB.
|
206
|
+
"""
|
207
|
+
self.styles.append(css)
|
208
|
+
self._register(css, folder=CSS_FOLDER, in_spine=False)
|
209
|
+
|
210
|
+
def export(self, output_path: str | Path) -> Path:
|
211
|
+
"""
|
212
|
+
Build and export the current book as an EPUB file.
|
213
|
+
|
214
|
+
:param output_path: Path to save the final .epub file.
|
215
|
+
"""
|
216
|
+
return self._build_epub(output_path=Path(output_path))
|
217
|
+
|
218
|
+
def _register(
|
219
|
+
self,
|
220
|
+
res: EpubResource,
|
221
|
+
folder: str,
|
222
|
+
in_spine: bool = True,
|
223
|
+
properties: str | None = None,
|
224
|
+
) -> None:
|
225
|
+
"""
|
226
|
+
Add resource to the manifest—and optionally to the spine.
|
227
|
+
"""
|
228
|
+
href = f"{folder}/{res.filename}"
|
229
|
+
self.opf.add_manifest_item(res.id, href, res.media_type, properties)
|
230
|
+
if in_spine:
|
231
|
+
self.opf.add_spine_item(res.id, properties)
|
232
|
+
|
233
|
+
def _init_styles(self) -> None:
|
234
|
+
# volume border & intro CSS
|
235
|
+
self.intro_css = StyleSheet(
|
236
|
+
id="intro_style",
|
237
|
+
content=CSS_INTRO_PATH.read_text("utf-8"),
|
238
|
+
filename="intro_style.css",
|
239
|
+
)
|
240
|
+
self.styles.append(self.intro_css)
|
241
|
+
self._register(self.intro_css, folder=CSS_FOLDER, in_spine=False)
|
242
|
+
|
243
|
+
try:
|
244
|
+
border_bytes = VOLUME_BORDER_IMAGE_PATH.read_bytes()
|
245
|
+
except FileNotFoundError:
|
246
|
+
return
|
247
|
+
border = ImageResource(
|
248
|
+
id="img-volume-border",
|
249
|
+
data=border_bytes,
|
250
|
+
media_type="image/png",
|
251
|
+
filename="volume_border.png",
|
252
|
+
)
|
253
|
+
self.images.append(border)
|
254
|
+
self._register(border, folder=IMAGE_FOLDER, in_spine=False)
|
255
|
+
|
256
|
+
def _init_cover(self, cover_path: Path | None) -> None:
|
257
|
+
if not cover_path or not cover_path.is_file():
|
258
|
+
return
|
259
|
+
ext = cover_path.suffix.lower().lstrip(".")
|
260
|
+
mtype = IMAGE_MEDIA_TYPES.get(ext)
|
261
|
+
if not mtype:
|
262
|
+
return
|
263
|
+
|
264
|
+
data = cover_path.read_bytes()
|
265
|
+
cover_img = ImageResource(
|
266
|
+
id="cover-img",
|
267
|
+
data=data,
|
268
|
+
media_type=mtype,
|
269
|
+
filename=f"cover.{ext}",
|
270
|
+
)
|
271
|
+
self.images.append(cover_img)
|
272
|
+
self._register(
|
273
|
+
cover_img,
|
274
|
+
folder=IMAGE_FOLDER,
|
275
|
+
in_spine=False,
|
276
|
+
properties="cover-image",
|
277
|
+
)
|
278
|
+
|
279
|
+
cover_chapter = Chapter(
|
280
|
+
id="cover",
|
281
|
+
title="Cover",
|
282
|
+
content=COVER_IMAGE_TEMPLATE.format(ext=ext),
|
283
|
+
filename="cover.xhtml",
|
284
|
+
)
|
285
|
+
self.chapters.append(cover_chapter)
|
286
|
+
self._register(
|
287
|
+
cover_chapter,
|
288
|
+
folder=TEXT_FOLDER,
|
289
|
+
properties="duokan-page-fullscreen",
|
290
|
+
)
|
291
|
+
self.nav.add_chapter(
|
292
|
+
cover_chapter.id,
|
293
|
+
cover_chapter.title,
|
294
|
+
f"{TEXT_FOLDER}/{cover_chapter.filename}",
|
295
|
+
)
|
296
|
+
self.ncx.add_chapter(
|
297
|
+
cover_chapter.id,
|
298
|
+
cover_chapter.title,
|
299
|
+
f"{TEXT_FOLDER}/{cover_chapter.filename}",
|
300
|
+
)
|
301
|
+
self.opf.include_cover = True
|
302
|
+
|
303
|
+
def _init_intro(self) -> None:
|
304
|
+
intro_html = build_book_intro(
|
305
|
+
book_name=self.title,
|
306
|
+
author=self.author,
|
307
|
+
serial_status=self.serial_status,
|
308
|
+
subject=self.subject,
|
309
|
+
word_count=self.word_count,
|
310
|
+
summary=self.description,
|
311
|
+
)
|
312
|
+
intro = Chapter(
|
313
|
+
id="intro",
|
314
|
+
title="书籍简介",
|
315
|
+
content=intro_html,
|
316
|
+
filename="intro.xhtml",
|
317
|
+
css=[self.intro_css],
|
318
|
+
)
|
319
|
+
self.chapters.append(intro)
|
320
|
+
self._register(intro, folder=TEXT_FOLDER)
|
321
|
+
self.nav.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
|
322
|
+
self.ncx.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
|
323
|
+
|
324
|
+
def _build_epub(self, output_path: Path) -> Path:
|
325
|
+
"""
|
326
|
+
Write out the .epub ZIP file.
|
327
|
+
"""
|
328
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
329
|
+
|
330
|
+
with zipfile.ZipFile(output_path, "w") as epub:
|
331
|
+
# must be first and uncompressed
|
332
|
+
epub.writestr(
|
333
|
+
"mimetype",
|
334
|
+
"application/epub+zip",
|
335
|
+
compress_type=ZIP_STORED,
|
336
|
+
)
|
337
|
+
|
338
|
+
# container
|
339
|
+
epub.writestr(
|
340
|
+
"META-INF/container.xml",
|
341
|
+
build_container_xml(),
|
342
|
+
compress_type=ZIP_DEFLATED,
|
343
|
+
)
|
344
|
+
|
345
|
+
# core documents
|
346
|
+
epub.writestr(
|
347
|
+
f"{ROOT_PATH}/nav.xhtml",
|
348
|
+
self.nav.to_xhtml(),
|
349
|
+
compress_type=ZIP_DEFLATED,
|
350
|
+
)
|
351
|
+
epub.writestr(
|
352
|
+
f"{ROOT_PATH}/toc.ncx",
|
353
|
+
self.ncx.to_xml(),
|
354
|
+
compress_type=ZIP_DEFLATED,
|
355
|
+
)
|
356
|
+
epub.writestr(
|
357
|
+
f"{ROOT_PATH}/content.opf",
|
358
|
+
self.opf.to_xml(),
|
359
|
+
compress_type=ZIP_DEFLATED,
|
360
|
+
)
|
361
|
+
|
362
|
+
# stylesheets
|
363
|
+
for css in self.styles:
|
364
|
+
path = f"{ROOT_PATH}/{CSS_FOLDER}/{css.filename}"
|
365
|
+
epub.writestr(path, css.content, compress_type=ZIP_DEFLATED)
|
366
|
+
|
367
|
+
# chapters
|
368
|
+
for chap in self.chapters:
|
369
|
+
path = f"{ROOT_PATH}/{TEXT_FOLDER}/{chap.filename}"
|
370
|
+
epub.writestr(path, chap.to_xhtml(), compress_type=ZIP_DEFLATED)
|
371
|
+
|
372
|
+
# images
|
373
|
+
for img in self.images:
|
374
|
+
path = f"{ROOT_PATH}/{IMAGE_FOLDER}/{img.filename}"
|
375
|
+
epub.writestr(path, img.data, compress_type=ZIP_DEFLATED)
|
376
|
+
|
377
|
+
return output_path
|
@@ -0,0 +1,118 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub.constants
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
EPUB-specific constants used by the builder, including:
|
7
|
+
- Directory names for OEBPS structure
|
8
|
+
- XML namespace URIs
|
9
|
+
- Package attributes and document-type declarations
|
10
|
+
- Media type mappings for images
|
11
|
+
- Template strings for container.xml and cover image HTML
|
12
|
+
"""
|
13
|
+
|
14
|
+
ROOT_PATH = "OEBPS"
|
15
|
+
IMAGE_FOLDER = "Images"
|
16
|
+
TEXT_FOLDER = "Text"
|
17
|
+
CSS_FOLDER = "Styles"
|
18
|
+
|
19
|
+
XHTML_NS = "http://www.w3.org/1999/xhtml"
|
20
|
+
EPUB_NS = "http://www.idpf.org/2007/ops"
|
21
|
+
XML_NS = "http://www.w3.org/XML/1998/namespace"
|
22
|
+
NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
|
23
|
+
OPF_NS = "http://www.idpf.org/2007/opf"
|
24
|
+
DC_NS = "http://purl.org/dc/elements/1.1/"
|
25
|
+
|
26
|
+
IMAGE_MEDIA_TYPES: dict[str, str] = {
|
27
|
+
"png": "image/png",
|
28
|
+
"jpg": "image/jpeg",
|
29
|
+
"jpeg": "image/jpeg",
|
30
|
+
"gif": "image/gif",
|
31
|
+
"svg": "image/svg+xml",
|
32
|
+
"webp": "image/webp",
|
33
|
+
}
|
34
|
+
|
35
|
+
CONTAINER_TEMPLATE = """\
|
36
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
37
|
+
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
38
|
+
<rootfiles>
|
39
|
+
<rootfile full-path="{root_path}/content.opf"
|
40
|
+
media-type="application/oebps-package+xml"/>
|
41
|
+
</rootfiles>
|
42
|
+
</container>
|
43
|
+
"""
|
44
|
+
|
45
|
+
COVER_IMAGE_TEMPLATE = (
|
46
|
+
f'<div style="text-align: center; margin: 0; padding: 0;">'
|
47
|
+
f'<img src="../{IMAGE_FOLDER}/cover.{{ext}}" alt="cover" '
|
48
|
+
f'style="max-width: 100%; height: auto;" />'
|
49
|
+
f"</div>"
|
50
|
+
)
|
51
|
+
|
52
|
+
CSS_TMPLATE = (
|
53
|
+
f'<link href="../{CSS_FOLDER}/{{filename}}" '
|
54
|
+
f'rel="stylesheet" type="{{media_type}}"/>'
|
55
|
+
)
|
56
|
+
|
57
|
+
CHAP_TMPLATE = f"""\
|
58
|
+
<?xml version="1.0" encoding="utf-8"?>
|
59
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
|
60
|
+
<html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
|
61
|
+
<head>
|
62
|
+
<title>{{title}}</title>
|
63
|
+
{{xlinks}}
|
64
|
+
</head>
|
65
|
+
<body>{{content}}</body>
|
66
|
+
</html>
|
67
|
+
"""
|
68
|
+
|
69
|
+
NAV_TEMPLATE = f"""\
|
70
|
+
<?xml version='1.0' encoding='utf-8'?>
|
71
|
+
<!DOCTYPE html>
|
72
|
+
<html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
|
73
|
+
<head>
|
74
|
+
<title>{{title}}</title>
|
75
|
+
</head>
|
76
|
+
<body>
|
77
|
+
<nav epub:type="toc" id="{{id}}" role="doc-toc">
|
78
|
+
<h2>{{title}}</h2>
|
79
|
+
<ol>
|
80
|
+
{{items}}
|
81
|
+
</ol>
|
82
|
+
</nav>
|
83
|
+
</body>
|
84
|
+
</html>
|
85
|
+
"""
|
86
|
+
|
87
|
+
NCX_TEMPLATE = f"""\
|
88
|
+
<?xml version='1.0' encoding='utf-8'?>
|
89
|
+
<ncx xmlns="{NCX_NS}" version="2005-1">
|
90
|
+
<head>
|
91
|
+
<meta name="dtb:uid" content="{{uid}}"/>
|
92
|
+
<meta name="dtb:depth" content="{{depth}}"/>
|
93
|
+
<meta name="dtb:totalPageCount" content="0"/>
|
94
|
+
<meta name="dtb:maxPageNumber" content="0"/>
|
95
|
+
</head>
|
96
|
+
<docTitle>
|
97
|
+
<text>{{title}}</text>
|
98
|
+
</docTitle>
|
99
|
+
<navMap>
|
100
|
+
{{navpoints}}
|
101
|
+
</navMap>
|
102
|
+
</ncx>
|
103
|
+
"""
|
104
|
+
|
105
|
+
OPF_TEMPLATE = f"""\
|
106
|
+
<?xml version='1.0' encoding='utf-8'?>
|
107
|
+
<package xmlns="{OPF_NS}" xmlns:dc="{DC_NS}" xmlns:opf="{OPF_NS}" version="3.0" unique-identifier="id" prefix="rendition: http://www.idpf.org/vocab/rendition/#">
|
108
|
+
<metadata>
|
109
|
+
{{metadata}}
|
110
|
+
</metadata>
|
111
|
+
<manifest>
|
112
|
+
{{manifest_items}}
|
113
|
+
</manifest>
|
114
|
+
<spine{{spine_toc}}>
|
115
|
+
{{spine_items}}
|
116
|
+
</spine>
|
117
|
+
{{guide_section}}</package>
|
118
|
+
""" # noqa: E501
|