novel-downloader 1.5.0__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 +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +77 -64
- novel_downloader/cli/export.py +16 -20
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +65 -105
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +1 -0
- 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/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +14 -9
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +17 -11
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- 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} +19 -12
- 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} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- 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} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
- 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 +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- 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} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- 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 +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- 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.py +61 -66
- 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.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
- novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
- novel_downloader/core/parsers/qidian/main_parser.py +11 -38
- novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- 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/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- 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 +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- 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 +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- 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 +31 -82
- novel_downloader/locales/zh.json +32 -83
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -22
- novel_downloader/utils/chapter_storage.py +3 -2
- novel_downloader/utils/constants.py +4 -29
- 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 +1 -1
- novel_downloader/utils/epub/constants.py +57 -16
- novel_downloader/utils/epub/documents.py +88 -194
- novel_downloader/utils/epub/models.py +0 -14
- novel_downloader/utils/epub/utils.py +63 -96
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +3 -113
- novel_downloader/utils/file_utils/sanitize.py +0 -4
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/logger.py +8 -16
- novel_downloader/utils/network.py +2 -2
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +4 -8
- 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.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- 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 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- 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/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.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
|
"""
|
@@ -14,7 +14,7 @@ Usage example:
|
|
14
14
|
|
15
15
|
```python
|
16
16
|
builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
|
17
|
-
builder.
|
17
|
+
builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
|
18
18
|
builder.export("output/my_novel.epub")
|
19
19
|
```
|
20
20
|
"""
|
@@ -11,7 +11,6 @@ EPUB-specific constants used by the builder, including:
|
|
11
11
|
- Template strings for container.xml and cover image HTML
|
12
12
|
"""
|
13
13
|
|
14
|
-
PRETTY_PRINT_FLAG = True
|
15
14
|
ROOT_PATH = "OEBPS"
|
16
15
|
IMAGE_FOLDER = "Images"
|
17
16
|
TEXT_FOLDER = "Text"
|
@@ -24,18 +23,6 @@ NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
|
|
24
23
|
OPF_NS = "http://www.idpf.org/2007/opf"
|
25
24
|
DC_NS = "http://purl.org/dc/elements/1.1/"
|
26
25
|
|
27
|
-
OPF_PKG_ATTRIB = {
|
28
|
-
"version": "3.0",
|
29
|
-
"unique-identifier": "id",
|
30
|
-
"prefix": "rendition: http://www.idpf.org/vocab/rendition/#",
|
31
|
-
}
|
32
|
-
CHAP_DOC_TYPE = (
|
33
|
-
'<?xml version="1.0" encoding="utf-8"?>\n'
|
34
|
-
"<!DOCTYPE html PUBLIC "
|
35
|
-
'"-//W3C//DTD XHTML 1.1//EN" '
|
36
|
-
'"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'
|
37
|
-
)
|
38
|
-
|
39
26
|
IMAGE_MEDIA_TYPES: dict[str, str] = {
|
40
27
|
"png": "image/png",
|
41
28
|
"jpg": "image/jpeg",
|
@@ -45,13 +32,15 @@ IMAGE_MEDIA_TYPES: dict[str, str] = {
|
|
45
32
|
"webp": "image/webp",
|
46
33
|
}
|
47
34
|
|
48
|
-
CONTAINER_TEMPLATE = """
|
35
|
+
CONTAINER_TEMPLATE = """\
|
36
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
49
37
|
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
50
38
|
<rootfiles>
|
51
39
|
<rootfile full-path="{root_path}/content.opf"
|
52
40
|
media-type="application/oebps-package+xml"/>
|
53
41
|
</rootfiles>
|
54
|
-
</container>
|
42
|
+
</container>
|
43
|
+
"""
|
55
44
|
|
56
45
|
COVER_IMAGE_TEMPLATE = (
|
57
46
|
f'<div style="text-align: center; margin: 0; padding: 0;">'
|
@@ -66,7 +55,8 @@ CSS_TMPLATE = (
|
|
66
55
|
)
|
67
56
|
|
68
57
|
CHAP_TMPLATE = f"""\
|
69
|
-
|
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">
|
70
60
|
<html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
|
71
61
|
<head>
|
72
62
|
<title>{{title}}</title>
|
@@ -75,3 +65,54 @@ CHAP_TMPLATE = f"""\
|
|
75
65
|
<body>{{content}}</body>
|
76
66
|
</html>
|
77
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
|
@@ -13,18 +13,10 @@ from collections.abc import Sequence
|
|
13
13
|
from dataclasses import dataclass, field
|
14
14
|
from datetime import UTC, datetime
|
15
15
|
|
16
|
-
from lxml import etree
|
17
|
-
from lxml.builder import ElementMaker
|
18
|
-
|
19
16
|
from .constants import (
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
OPF_NS,
|
24
|
-
OPF_PKG_ATTRIB,
|
25
|
-
PRETTY_PRINT_FLAG,
|
26
|
-
XHTML_NS,
|
27
|
-
XML_NS,
|
17
|
+
NAV_TEMPLATE,
|
18
|
+
NCX_TEMPLATE,
|
19
|
+
OPF_TEMPLATE,
|
28
20
|
)
|
29
21
|
from .models import (
|
30
22
|
ChapterEntry,
|
@@ -35,21 +27,6 @@ from .models import (
|
|
35
27
|
VolumeEntry,
|
36
28
|
)
|
37
29
|
|
38
|
-
NAV = ElementMaker(
|
39
|
-
namespace=XHTML_NS,
|
40
|
-
nsmap={None: XHTML_NS, "epub": EPUB_NS},
|
41
|
-
)
|
42
|
-
NCX = ElementMaker(namespace=NCX_NS, nsmap={None: NCX_NS})
|
43
|
-
PKG = ElementMaker(
|
44
|
-
namespace=OPF_NS,
|
45
|
-
nsmap={
|
46
|
-
None: OPF_NS,
|
47
|
-
"dc": DC_NS,
|
48
|
-
"opf": OPF_NS,
|
49
|
-
},
|
50
|
-
)
|
51
|
-
DC = ElementMaker(namespace=DC_NS)
|
52
|
-
|
53
30
|
|
54
31
|
@dataclass
|
55
32
|
class NavDocument(EpubResource):
|
@@ -60,12 +37,7 @@ class NavDocument(EpubResource):
|
|
60
37
|
media_type: str = field(init=False, default="application/xhtml+xml")
|
61
38
|
content_items: list[ChapterEntry | VolumeEntry] = field(default_factory=list)
|
62
39
|
|
63
|
-
def add_chapter(
|
64
|
-
self,
|
65
|
-
id: str,
|
66
|
-
label: str,
|
67
|
-
src: str,
|
68
|
-
) -> None:
|
40
|
+
def add_chapter(self, id: str, label: str, src: str) -> None:
|
69
41
|
"""
|
70
42
|
Add a top-level chapter entry to the navigation.
|
71
43
|
|
@@ -100,55 +72,29 @@ class NavDocument(EpubResource):
|
|
100
72
|
|
101
73
|
:return: A string containing the full XHTML for nav.xhtml.
|
102
74
|
"""
|
103
|
-
|
104
|
-
|
105
|
-
# head/title
|
106
|
-
NAV.head(NAV.title(self.title)),
|
107
|
-
# body/nav/ol subtree
|
108
|
-
NAV.body(
|
109
|
-
NAV.nav(
|
110
|
-
NAV.h2(self.title),
|
111
|
-
NAV.ol(*self._render_items(self.content_items)),
|
112
|
-
# namespaced + regular attributes
|
113
|
-
**{
|
114
|
-
f"{{{EPUB_NS}}}type": "toc",
|
115
|
-
"id": self.id,
|
116
|
-
"role": "doc-toc",
|
117
|
-
},
|
118
|
-
)
|
119
|
-
),
|
120
|
-
# html attributes
|
75
|
+
items_str = self._render_items_str(self.content_items)
|
76
|
+
raw = NAV_TEMPLATE.format(
|
121
77
|
lang=self.language,
|
122
|
-
|
78
|
+
id=self.id,
|
79
|
+
title=self.title,
|
80
|
+
items=items_str,
|
123
81
|
)
|
124
|
-
|
125
|
-
xml_bytes = etree.tostring(
|
126
|
-
html_el,
|
127
|
-
xml_declaration=True,
|
128
|
-
encoding="utf-8",
|
129
|
-
pretty_print=PRETTY_PRINT_FLAG,
|
130
|
-
doctype="<!DOCTYPE html>",
|
131
|
-
)
|
132
|
-
xml_string: str = xml_bytes.decode("utf-8")
|
133
|
-
return xml_string
|
82
|
+
return raw
|
134
83
|
|
135
84
|
@classmethod
|
136
|
-
def
|
137
|
-
|
138
|
-
items: Sequence[ChapterEntry | VolumeEntry],
|
139
|
-
) -> list[etree._Element]:
|
140
|
-
"""
|
141
|
-
Recursively build <li> elements (and nested <ol>) for each TOC entry.
|
142
|
-
"""
|
143
|
-
elements: list[etree._Element] = []
|
85
|
+
def _render_items_str(cls, items: Sequence[ChapterEntry | VolumeEntry]) -> str:
|
86
|
+
lines: list[str] = []
|
144
87
|
for item in items:
|
145
88
|
if isinstance(item, VolumeEntry) and item.chapters:
|
146
|
-
|
147
|
-
|
89
|
+
lines.append(f'<li><a href="{item.src}">{item.label}</a>')
|
90
|
+
lines.append(" <ol>")
|
91
|
+
child = cls._render_items_str(item.chapters)
|
92
|
+
lines.extend(child.splitlines())
|
93
|
+
lines.append(" </ol>")
|
94
|
+
lines.append("</li>")
|
148
95
|
else:
|
149
|
-
|
150
|
-
|
151
|
-
return elements
|
96
|
+
lines.append(f'<li><a href="{item.src}">{item.label}</a></li>')
|
97
|
+
return "\n".join(lines)
|
152
98
|
|
153
99
|
|
154
100
|
@dataclass
|
@@ -190,29 +136,19 @@ class NCXDocument(EpubResource):
|
|
190
136
|
|
191
137
|
:return: A string containing the full NCX XML document.
|
192
138
|
"""
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
139
|
+
order = 1
|
140
|
+
lines: list[str] = []
|
141
|
+
for pt in self.nav_points:
|
142
|
+
order, block = self._render_navpoint_str(pt, order)
|
143
|
+
lines.extend(block)
|
144
|
+
navpoints = "\n".join(lines)
|
145
|
+
raw = NCX_TEMPLATE.format(
|
146
|
+
uid=self.uid,
|
147
|
+
depth=self._depth(self.nav_points),
|
148
|
+
title=self.title,
|
149
|
+
navpoints=navpoints,
|
199
150
|
)
|
200
|
-
|
201
|
-
root.append(NCX.docTitle(NCX.text(self.title)))
|
202
|
-
|
203
|
-
navMap = NCX.navMap()
|
204
|
-
root.append(navMap)
|
205
|
-
|
206
|
-
self._render_navpoints(navMap, self.nav_points, start=1)
|
207
|
-
|
208
|
-
xml_bytes = etree.tostring(
|
209
|
-
root,
|
210
|
-
xml_declaration=True,
|
211
|
-
encoding="utf-8",
|
212
|
-
pretty_print=PRETTY_PRINT_FLAG,
|
213
|
-
)
|
214
|
-
xml_string: str = xml_bytes.decode("utf-8")
|
215
|
-
return xml_string
|
151
|
+
return raw
|
216
152
|
|
217
153
|
@classmethod
|
218
154
|
def _depth(cls, points: list[NavPoint]) -> int:
|
@@ -221,32 +157,21 @@ class NCXDocument(EpubResource):
|
|
221
157
|
return 1 + max(cls._depth(child.children) for child in points)
|
222
158
|
|
223
159
|
@classmethod
|
224
|
-
def
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
""
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
"navPoint",
|
240
|
-
id=pt.id,
|
241
|
-
playOrder=str(play),
|
242
|
-
)
|
243
|
-
play += 1
|
244
|
-
navLabel = etree.SubElement(np, "navLabel")
|
245
|
-
lbl_text = etree.SubElement(navLabel, "text")
|
246
|
-
lbl_text.text = pt.label
|
247
|
-
etree.SubElement(np, "content", src=pt.src)
|
248
|
-
play = cls._render_navpoints(np, pt.children, play)
|
249
|
-
return play
|
160
|
+
def _render_navpoint_str(cls, pt: NavPoint, order: int) -> tuple[int, list[str]]:
|
161
|
+
lines: list[str] = []
|
162
|
+
# open navPoint
|
163
|
+
lines.append(f'<navPoint id="{pt.id}" playOrder="{order}">')
|
164
|
+
order += 1
|
165
|
+
# label and content
|
166
|
+
lines.append(f"<navLabel><text>{pt.label}</text></navLabel>")
|
167
|
+
lines.append(f'<content src="{pt.src}"/>')
|
168
|
+
# children
|
169
|
+
for child in pt.children:
|
170
|
+
order, child_lines = cls._render_navpoint_str(child, order)
|
171
|
+
lines.extend(child_lines)
|
172
|
+
# close
|
173
|
+
lines.append("</navPoint>")
|
174
|
+
return order, lines
|
250
175
|
|
251
176
|
|
252
177
|
@dataclass
|
@@ -318,86 +243,55 @@ class OpfDocument(EpubResource):
|
|
318
243
|
"""
|
319
244
|
now_iso = datetime.now(UTC).replace(microsecond=0).isoformat()
|
320
245
|
|
321
|
-
#
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
# modified timestamp
|
329
|
-
modified = PKG.meta(property="dcterms:modified")
|
330
|
-
modified.text = now_iso
|
331
|
-
metadata.append(modified)
|
332
|
-
|
333
|
-
# mandatory DC elements
|
334
|
-
id_el = DC.identifier(id="id")
|
335
|
-
id_el.text = self.uid
|
336
|
-
title_el = DC.title()
|
337
|
-
title_el.text = self.title
|
338
|
-
lang_el = DC.language()
|
339
|
-
lang_el.text = self.language
|
340
|
-
metadata.extend([id_el, title_el, lang_el])
|
341
|
-
|
342
|
-
# optional DC elements
|
246
|
+
# metadata block
|
247
|
+
meta_lines: list[str] = []
|
248
|
+
meta_lines.append(f'<meta property="dcterms:modified">{now_iso}</meta>')
|
249
|
+
meta_lines.append(f'<dc:identifier id="id">{self.uid}</dc:identifier>')
|
250
|
+
meta_lines.append(f"<dc:title>{self.title}</dc:title>")
|
251
|
+
meta_lines.append(f"<dc:language>{self.language}</dc:language>")
|
343
252
|
if self.author:
|
344
|
-
creator
|
345
|
-
creator.text = self.author
|
346
|
-
metadata.append(creator)
|
253
|
+
meta_lines.append(f'<dc:creator id="creator">{self.author}</dc:creator>')
|
347
254
|
if self.description:
|
348
|
-
|
349
|
-
desc.text = self.description
|
350
|
-
metadata.append(desc)
|
255
|
+
meta_lines.append(f"<dc:description>{self.description}</dc:description>")
|
351
256
|
if self.subject:
|
352
|
-
|
353
|
-
|
354
|
-
metadata.append(subj)
|
257
|
+
joined = ",".join(self.subject)
|
258
|
+
meta_lines.append(f"<dc:subject>{joined}</dc:subject>")
|
355
259
|
if self.include_cover and self._cover_item:
|
356
|
-
|
357
|
-
|
260
|
+
meta_lines.append(f'<meta name="cover" content="{self._cover_item.id}"/>')
|
261
|
+
metadata = "\n".join(meta_lines)
|
358
262
|
|
359
|
-
#
|
360
|
-
|
263
|
+
# manifest block
|
264
|
+
man_lines: list[str] = []
|
361
265
|
for item in self.manifest:
|
362
|
-
|
363
|
-
|
364
|
-
"href"
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
# <spine>
|
373
|
-
spine_attrs = {}
|
374
|
-
if self._toc_item:
|
375
|
-
spine_attrs["toc"] = self._toc_item.id
|
376
|
-
spine_el = PKG.spine(**spine_attrs)
|
266
|
+
props = f' properties="{item.properties}"' if item.properties else ""
|
267
|
+
man_lines.append(
|
268
|
+
f'<item id="{item.id}" href="{item.href}" media-type="{item.media_type}"{props}/>' # noqa: E501
|
269
|
+
)
|
270
|
+
manifest_items = "\n".join(man_lines)
|
271
|
+
|
272
|
+
# spine block
|
273
|
+
toc_attr = f' toc="{self._toc_item.id}"' if self._toc_item else ""
|
274
|
+
spine_lines: list[str] = []
|
377
275
|
for ref in self.spine:
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
spine_el.append(PKG.itemref(**attrs))
|
382
|
-
package.append(spine_el)
|
276
|
+
props = f' properties="{ref.properties}"' if ref.properties else ""
|
277
|
+
spine_lines.append(f' <itemref idref="{ref.idref}"{props}/>')
|
278
|
+
spine_items = "\n".join(spine_lines)
|
383
279
|
|
384
|
-
#
|
280
|
+
# guide block
|
385
281
|
if self.include_cover and self._cover_doc:
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
title="Cover",
|
391
|
-
href=self._cover_doc.href,
|
392
|
-
)
|
282
|
+
guide_section = (
|
283
|
+
" <guide>\n"
|
284
|
+
f' <reference type="cover" title="Cover" href="{self._cover_doc.href}"/>\n' # noqa: E501
|
285
|
+
" </guide>\n"
|
393
286
|
)
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
287
|
+
else:
|
288
|
+
guide_section = ""
|
289
|
+
|
290
|
+
raw = OPF_TEMPLATE.format(
|
291
|
+
metadata=metadata,
|
292
|
+
manifest_items=manifest_items,
|
293
|
+
spine_toc=toc_attr,
|
294
|
+
spine_items=spine_items,
|
295
|
+
guide_section=guide_section,
|
401
296
|
)
|
402
|
-
|
403
|
-
return xml_string
|
297
|
+
return raw
|
@@ -65,12 +65,6 @@ class NavPoint:
|
|
65
65
|
src: str
|
66
66
|
children: list[NavPoint] = field(default_factory=list)
|
67
67
|
|
68
|
-
def add_child(self, point: NavPoint) -> None:
|
69
|
-
"""
|
70
|
-
Append a child nav point under this one.
|
71
|
-
"""
|
72
|
-
self.children.append(point)
|
73
|
-
|
74
68
|
|
75
69
|
@dataclass
|
76
70
|
class EpubResource:
|
@@ -101,10 +95,6 @@ class Chapter(EpubResource):
|
|
101
95
|
css: list[StyleSheet] = field(default_factory=list)
|
102
96
|
media_type: str = field(init=False, default="application/xhtml+xml")
|
103
97
|
|
104
|
-
def __post_init__(self) -> None:
|
105
|
-
if not self.filename:
|
106
|
-
object.__setattr__(self, "filename", f"{self.id}.xhtml")
|
107
|
-
|
108
98
|
def to_xhtml(self, lang: str = "zh-CN") -> str:
|
109
99
|
"""
|
110
100
|
Generate the XHTML for a chapter.
|
@@ -128,7 +118,3 @@ class Volume:
|
|
128
118
|
intro: str = ""
|
129
119
|
cover: Path | None = None
|
130
120
|
chapters: list[Chapter] = field(default_factory=list)
|
131
|
-
|
132
|
-
def add_chapter(self, chapter: Chapter) -> None:
|
133
|
-
"""Append a chapter to this volume."""
|
134
|
-
self.chapters.append(chapter)
|