novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__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 +79 -66
- novel_downloader/cli/export.py +17 -21
- 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 +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- 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 +17 -12
- 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 +20 -14
- 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} +56 -64
- 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 +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- 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 +64 -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 +64 -69
- 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/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -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 +429 -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 +34 -85
- novel_downloader/locales/zh.json +35 -86
- 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 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -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/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- 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/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- 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 +55 -49
- 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.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.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/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- 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/crypto_utils.py +0 -71
- 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.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.crypto_utils.rc4
|
4
|
+
---------------------------------------
|
5
|
+
|
6
|
+
Minimal RC4 stream cipher implementation.
|
7
|
+
"""
|
8
|
+
|
9
|
+
|
10
|
+
def rc4_init(key: bytes) -> list[int]:
|
11
|
+
"""
|
12
|
+
Key-Scheduling Algorithm (KSA)
|
13
|
+
"""
|
14
|
+
S = list(range(256))
|
15
|
+
j = 0
|
16
|
+
klen = len(key)
|
17
|
+
for i in range(256):
|
18
|
+
j = (j + S[i] + key[i % klen]) & 0xFF
|
19
|
+
S[i], S[j] = S[j], S[i]
|
20
|
+
return S
|
21
|
+
|
22
|
+
|
23
|
+
def rc4_stream(S_init: list[int], data: bytes) -> bytes:
|
24
|
+
"""
|
25
|
+
Pseudo-Random Generation Algorithm (PRGA)
|
26
|
+
"""
|
27
|
+
# make a copy of S since it mutates during PRGA
|
28
|
+
S = S_init.copy()
|
29
|
+
i = 0
|
30
|
+
j = 0
|
31
|
+
out = bytearray(len(data))
|
32
|
+
for idx, ch in enumerate(data):
|
33
|
+
i = (i + 1) & 0xFF
|
34
|
+
j = (j + S[i]) & 0xFF
|
35
|
+
S[i], S[j] = S[j], S[i]
|
36
|
+
K = S[(S[i] + S[j]) & 0xFF]
|
37
|
+
out[idx] = ch ^ K
|
38
|
+
|
39
|
+
return bytes(out)
|
40
|
+
|
41
|
+
|
42
|
+
def rc4_cipher(key: bytes, data: bytes) -> bytes:
|
43
|
+
"""
|
44
|
+
RC4 stream cipher.
|
45
|
+
|
46
|
+
It performs the standard Key-Scheduling Algorithm (KSA) and
|
47
|
+
Pseudo-Random Generation Algorithm (PRGA) to produce the RC4 keystream.
|
48
|
+
|
49
|
+
:param key: RC4 key as bytes (must not be empty)
|
50
|
+
:param data: plaintext or ciphertext as bytes
|
51
|
+
:return: XORed bytes (encrypt/decrypt are identical)
|
52
|
+
"""
|
53
|
+
S = rc4_init(key)
|
54
|
+
return rc4_stream(S, data)
|
@@ -6,15 +6,14 @@ novel_downloader.utils.epub
|
|
6
6
|
Top-level package for EPUB export utilities.
|
7
7
|
|
8
8
|
Key components:
|
9
|
-
|
10
|
-
|
11
|
-
- Chapter, Volume : represent and render content sections and volume intros
|
9
|
+
* EpubBuilder : orchestrates metadata, manifest, spine, navigation, and resources
|
10
|
+
* Chapter, Volume : represent and render content sections and volume intros
|
12
11
|
|
13
12
|
Usage example:
|
14
13
|
|
15
14
|
```python
|
16
15
|
builder = EpubBuilder(title="My Novel", author="Author Name", uid="uuid-1234")
|
17
|
-
builder.
|
16
|
+
builder.chapters.append(Chapter(id="ch1", title="Chapter 1", content="<p>xxx</p>"))
|
18
17
|
builder.export("output/my_novel.epub")
|
19
18
|
```
|
20
19
|
"""
|
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.builder
|
|
4
4
|
-----------------------------------
|
5
5
|
|
6
6
|
Orchestrates the end-to-end EPUB build process by:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
11
|
|
12
12
|
Provides:
|
13
|
-
|
14
|
-
|
13
|
+
* methods to add chapters, volumes, images, and styles
|
14
|
+
* a clean `export()` entry point that writes the final EPUB archive
|
15
15
|
"""
|
16
16
|
|
17
17
|
import zipfile
|
@@ -4,14 +4,13 @@ novel_downloader.utils.epub.constants
|
|
4
4
|
-------------------------------------
|
5
5
|
|
6
6
|
EPUB-specific constants used by the builder, including:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
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
|
@@ -4,27 +4,19 @@ novel_downloader.utils.epub.documents
|
|
4
4
|
-------------------------------------
|
5
5
|
|
6
6
|
Defines the classes that render EPUB navigation and packaging documents:
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
* NavDocument: builds the XHTML nav.xhtml (EPUB 3)
|
8
|
+
* NCXDocument: builds the NCX XML navigation map (EPUB 2)
|
9
|
+
* OpfDocument: builds the content.opf package document
|
10
10
|
"""
|
11
11
|
|
12
12
|
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
|
@@ -309,95 +234,64 @@ class OpfDocument(EpubResource):
|
|
309
234
|
Generate the content.opf XML, which defines metadata, manifest, and spine.
|
310
235
|
|
311
236
|
This function outputs a complete OPF package document that includes:
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
237
|
+
* <metadata>: title, author, language, identifiers, etc.
|
238
|
+
* <manifest>: all resource entries
|
239
|
+
* <spine>: the reading order of the content
|
240
|
+
* <guide>: optional references like cover page
|
316
241
|
|
317
242
|
:return: A string containing the full OPF XML content.
|
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
|
@@ -4,14 +4,14 @@ novel_downloader.utils.epub.models
|
|
4
4
|
----------------------------------
|
5
5
|
|
6
6
|
Defines the core EPUB data models and resource classes used by the builder:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
* Typed entries for table of contents (ChapterEntry, VolumeEntry)
|
8
|
+
* Manifest and spine record types (ManifestEntry, SpineEntry)
|
9
|
+
* Hierarchical NavPoint for NCX navigation
|
10
|
+
* Base resource class (EpubResource) and specializations:
|
11
|
+
* StyleSheet
|
12
|
+
* ImageResource
|
13
|
+
* Chapter (with XHTML serialization)
|
14
|
+
* Volume container for grouping chapters with optional intro and cover
|
15
15
|
"""
|
16
16
|
|
17
17
|
from __future__ import annotations
|
@@ -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)
|