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
@@ -0,0 +1,297 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub.documents
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
Defines the classes that render EPUB navigation and packaging documents:
|
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
|
+
"""
|
11
|
+
|
12
|
+
from collections.abc import Sequence
|
13
|
+
from dataclasses import dataclass, field
|
14
|
+
from datetime import UTC, datetime
|
15
|
+
|
16
|
+
from .constants import (
|
17
|
+
NAV_TEMPLATE,
|
18
|
+
NCX_TEMPLATE,
|
19
|
+
OPF_TEMPLATE,
|
20
|
+
)
|
21
|
+
from .models import (
|
22
|
+
ChapterEntry,
|
23
|
+
EpubResource,
|
24
|
+
ManifestEntry,
|
25
|
+
NavPoint,
|
26
|
+
SpineEntry,
|
27
|
+
VolumeEntry,
|
28
|
+
)
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class NavDocument(EpubResource):
|
33
|
+
title: str = "未命名"
|
34
|
+
language: str = "zh-CN"
|
35
|
+
id: str = "nav"
|
36
|
+
filename: str = "nav.xhtml"
|
37
|
+
media_type: str = field(init=False, default="application/xhtml+xml")
|
38
|
+
content_items: list[ChapterEntry | VolumeEntry] = field(default_factory=list)
|
39
|
+
|
40
|
+
def add_chapter(self, id: str, label: str, src: str) -> None:
|
41
|
+
"""
|
42
|
+
Add a top-level chapter entry to the navigation.
|
43
|
+
|
44
|
+
:param id: The unique ID for the chapter.
|
45
|
+
:param label: The display title for the chapter.
|
46
|
+
:param src: The href target for the chapter's XHTML file.
|
47
|
+
"""
|
48
|
+
self.content_items.append(ChapterEntry(id=id, label=label, src=src))
|
49
|
+
|
50
|
+
def add_volume(
|
51
|
+
self,
|
52
|
+
id: str,
|
53
|
+
label: str,
|
54
|
+
src: str,
|
55
|
+
chapters: list[ChapterEntry],
|
56
|
+
) -> None:
|
57
|
+
"""
|
58
|
+
Add a volume entry with nested chapters to the navigation.
|
59
|
+
|
60
|
+
:param id: The unique ID for the volume.
|
61
|
+
:param label: The display title for the volume.
|
62
|
+
:param src: The href target for the volume's intro XHTML file.
|
63
|
+
:param chapters: A list of chapter entries under this volume.
|
64
|
+
"""
|
65
|
+
self.content_items.append(
|
66
|
+
VolumeEntry(id=id, label=label, src=src, chapters=chapters)
|
67
|
+
)
|
68
|
+
|
69
|
+
def to_xhtml(self) -> str:
|
70
|
+
"""
|
71
|
+
Generate the XHTML content for nav.xhtml based on the NavDocument.
|
72
|
+
|
73
|
+
:return: A string containing the full XHTML for nav.xhtml.
|
74
|
+
"""
|
75
|
+
items_str = self._render_items_str(self.content_items)
|
76
|
+
raw = NAV_TEMPLATE.format(
|
77
|
+
lang=self.language,
|
78
|
+
id=self.id,
|
79
|
+
title=self.title,
|
80
|
+
items=items_str,
|
81
|
+
)
|
82
|
+
return raw
|
83
|
+
|
84
|
+
@classmethod
|
85
|
+
def _render_items_str(cls, items: Sequence[ChapterEntry | VolumeEntry]) -> str:
|
86
|
+
lines: list[str] = []
|
87
|
+
for item in items:
|
88
|
+
if isinstance(item, VolumeEntry) and item.chapters:
|
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>")
|
95
|
+
else:
|
96
|
+
lines.append(f'<li><a href="{item.src}">{item.label}</a></li>')
|
97
|
+
return "\n".join(lines)
|
98
|
+
|
99
|
+
|
100
|
+
@dataclass
|
101
|
+
class NCXDocument(EpubResource):
|
102
|
+
title: str = "未命名"
|
103
|
+
uid: str = ""
|
104
|
+
id: str = "ncx"
|
105
|
+
filename: str = "toc.ncx"
|
106
|
+
media_type: str = field(init=False, default="application/x-dtbncx+xml")
|
107
|
+
nav_points: list[NavPoint] = field(default_factory=list)
|
108
|
+
|
109
|
+
def add_chapter(
|
110
|
+
self,
|
111
|
+
id: str,
|
112
|
+
label: str,
|
113
|
+
src: str,
|
114
|
+
) -> None:
|
115
|
+
"""
|
116
|
+
Add a single flat chapter entry to the NCX nav map.
|
117
|
+
"""
|
118
|
+
self.nav_points.append(NavPoint(id=id, label=label, src=src))
|
119
|
+
|
120
|
+
def add_volume(
|
121
|
+
self,
|
122
|
+
id: str,
|
123
|
+
label: str,
|
124
|
+
src: str,
|
125
|
+
chapters: list[ChapterEntry],
|
126
|
+
) -> None:
|
127
|
+
"""
|
128
|
+
Add a volume with nested chapters to the NCX nav map.
|
129
|
+
"""
|
130
|
+
children = [NavPoint(id=c.id, label=c.label, src=c.src) for c in chapters]
|
131
|
+
self.nav_points.append(NavPoint(id=id, label=label, src=src, children=children))
|
132
|
+
|
133
|
+
def to_xml(self) -> str:
|
134
|
+
"""
|
135
|
+
Generate the XML content for toc.ncx used in EPUB 2 navigation.
|
136
|
+
|
137
|
+
:return: A string containing the full NCX XML document.
|
138
|
+
"""
|
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,
|
150
|
+
)
|
151
|
+
return raw
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
def _depth(cls, points: list[NavPoint]) -> int:
|
155
|
+
if not points:
|
156
|
+
return 0
|
157
|
+
return 1 + max(cls._depth(child.children) for child in points)
|
158
|
+
|
159
|
+
@classmethod
|
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
|
175
|
+
|
176
|
+
|
177
|
+
@dataclass
|
178
|
+
class OpfDocument(EpubResource):
|
179
|
+
# metadata fields
|
180
|
+
title: str = ""
|
181
|
+
author: str = ""
|
182
|
+
description: str = ""
|
183
|
+
uid: str = ""
|
184
|
+
subject: list[str] = field(default_factory=list)
|
185
|
+
language: str = "zh-CN"
|
186
|
+
|
187
|
+
# resource identity
|
188
|
+
id: str = "opf"
|
189
|
+
filename: str = "content.opf"
|
190
|
+
media_type: str = field(init=False, default="application/oebps-package+xml")
|
191
|
+
|
192
|
+
# internal state
|
193
|
+
include_cover: bool = False
|
194
|
+
manifest: list[ManifestEntry] = field(default_factory=list)
|
195
|
+
spine: list[SpineEntry] = field(default_factory=list)
|
196
|
+
_cover_item: ManifestEntry | None = field(init=False, default=None)
|
197
|
+
_toc_item: ManifestEntry | None = field(init=False, default=None)
|
198
|
+
_cover_doc: ManifestEntry | None = field(init=False, default=None)
|
199
|
+
|
200
|
+
def add_manifest_item(
|
201
|
+
self,
|
202
|
+
id: str,
|
203
|
+
href: str,
|
204
|
+
media_type: str,
|
205
|
+
properties: str | None = None,
|
206
|
+
) -> None:
|
207
|
+
entry = ManifestEntry(
|
208
|
+
id=id,
|
209
|
+
href=href,
|
210
|
+
media_type=media_type,
|
211
|
+
properties=properties,
|
212
|
+
)
|
213
|
+
self.manifest.append(entry)
|
214
|
+
|
215
|
+
if properties == "cover-image":
|
216
|
+
self._cover_item = entry
|
217
|
+
if media_type == "application/x-dtbncx+xml":
|
218
|
+
self._toc_item = entry
|
219
|
+
if id == "cover":
|
220
|
+
self._cover_doc = entry
|
221
|
+
|
222
|
+
def add_spine_item(
|
223
|
+
self,
|
224
|
+
idref: str,
|
225
|
+
properties: str | None = None,
|
226
|
+
) -> None:
|
227
|
+
self.spine.append(SpineEntry(idref=idref, properties=properties))
|
228
|
+
|
229
|
+
def set_subject(self, subject: list[str]) -> None:
|
230
|
+
self.subject = subject
|
231
|
+
|
232
|
+
def to_xml(self) -> str:
|
233
|
+
"""
|
234
|
+
Generate the content.opf XML, which defines metadata, manifest, and spine.
|
235
|
+
|
236
|
+
This function outputs a complete OPF package document that includes:
|
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
|
241
|
+
|
242
|
+
:return: A string containing the full OPF XML content.
|
243
|
+
"""
|
244
|
+
now_iso = datetime.now(UTC).replace(microsecond=0).isoformat()
|
245
|
+
|
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>")
|
252
|
+
if self.author:
|
253
|
+
meta_lines.append(f'<dc:creator id="creator">{self.author}</dc:creator>')
|
254
|
+
if self.description:
|
255
|
+
meta_lines.append(f"<dc:description>{self.description}</dc:description>")
|
256
|
+
if self.subject:
|
257
|
+
joined = ",".join(self.subject)
|
258
|
+
meta_lines.append(f"<dc:subject>{joined}</dc:subject>")
|
259
|
+
if self.include_cover and self._cover_item:
|
260
|
+
meta_lines.append(f'<meta name="cover" content="{self._cover_item.id}"/>')
|
261
|
+
metadata = "\n".join(meta_lines)
|
262
|
+
|
263
|
+
# manifest block
|
264
|
+
man_lines: list[str] = []
|
265
|
+
for item in self.manifest:
|
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] = []
|
275
|
+
for ref in self.spine:
|
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)
|
279
|
+
|
280
|
+
# guide block
|
281
|
+
if self.include_cover and self._cover_doc:
|
282
|
+
guide_section = (
|
283
|
+
" <guide>\n"
|
284
|
+
f' <reference type="cover" title="Cover" href="{self._cover_doc.href}"/>\n' # noqa: E501
|
285
|
+
" </guide>\n"
|
286
|
+
)
|
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,
|
296
|
+
)
|
297
|
+
return raw
|
@@ -0,0 +1,120 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub.models
|
4
|
+
----------------------------------
|
5
|
+
|
6
|
+
Defines the core EPUB data models and resource classes used by the builder:
|
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
|
+
"""
|
16
|
+
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from dataclasses import dataclass, field
|
20
|
+
from pathlib import Path
|
21
|
+
|
22
|
+
from .constants import (
|
23
|
+
CHAP_TMPLATE,
|
24
|
+
CSS_TMPLATE,
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
@dataclass(frozen=True)
|
29
|
+
class ChapterEntry:
|
30
|
+
id: str
|
31
|
+
label: str
|
32
|
+
src: str
|
33
|
+
|
34
|
+
|
35
|
+
@dataclass(frozen=True)
|
36
|
+
class VolumeEntry:
|
37
|
+
id: str
|
38
|
+
label: str
|
39
|
+
src: str
|
40
|
+
chapters: list[ChapterEntry]
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass(frozen=True)
|
44
|
+
class ManifestEntry:
|
45
|
+
id: str
|
46
|
+
href: str
|
47
|
+
media_type: str
|
48
|
+
properties: str | None = None
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass(frozen=True)
|
52
|
+
class SpineEntry:
|
53
|
+
idref: str
|
54
|
+
properties: str | None = None
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass
|
58
|
+
class NavPoint:
|
59
|
+
"""
|
60
|
+
A table-of-contents entry, possibly with nested children.
|
61
|
+
"""
|
62
|
+
|
63
|
+
id: str
|
64
|
+
label: str
|
65
|
+
src: str
|
66
|
+
children: list[NavPoint] = field(default_factory=list)
|
67
|
+
|
68
|
+
|
69
|
+
@dataclass
|
70
|
+
class EpubResource:
|
71
|
+
"""
|
72
|
+
Base class for any EPUB-packaged resource.
|
73
|
+
"""
|
74
|
+
|
75
|
+
id: str
|
76
|
+
filename: str
|
77
|
+
media_type: str
|
78
|
+
|
79
|
+
|
80
|
+
@dataclass
|
81
|
+
class StyleSheet(EpubResource):
|
82
|
+
content: str
|
83
|
+
media_type: str = field(init=False, default="text/css")
|
84
|
+
|
85
|
+
|
86
|
+
@dataclass
|
87
|
+
class ImageResource(EpubResource):
|
88
|
+
data: bytes
|
89
|
+
|
90
|
+
|
91
|
+
@dataclass
|
92
|
+
class Chapter(EpubResource):
|
93
|
+
title: str
|
94
|
+
content: str
|
95
|
+
css: list[StyleSheet] = field(default_factory=list)
|
96
|
+
media_type: str = field(init=False, default="application/xhtml+xml")
|
97
|
+
|
98
|
+
def to_xhtml(self, lang: str = "zh-CN") -> str:
|
99
|
+
"""
|
100
|
+
Generate the XHTML for a chapter.
|
101
|
+
"""
|
102
|
+
links = "\n".join(
|
103
|
+
CSS_TMPLATE.format(filename=css.filename, media_type=css.media_type)
|
104
|
+
for css in self.css
|
105
|
+
)
|
106
|
+
return CHAP_TMPLATE.format(
|
107
|
+
lang=lang,
|
108
|
+
title=self.title,
|
109
|
+
xlinks=links,
|
110
|
+
content=self.content,
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
@dataclass
|
115
|
+
class Volume:
|
116
|
+
id: str
|
117
|
+
title: str
|
118
|
+
intro: str = ""
|
119
|
+
cover: Path | None = None
|
120
|
+
chapters: list[Chapter] = field(default_factory=list)
|
@@ -0,0 +1,179 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.utils.epub.utils
|
4
|
+
---------------------------------
|
5
|
+
|
6
|
+
Pure utility functions for EPUB assembly, including:
|
7
|
+
- Computing file hashes
|
8
|
+
- Generating META-INF/container.xml
|
9
|
+
- Constructing HTML snippets for the book intro and volume intro
|
10
|
+
"""
|
11
|
+
|
12
|
+
import hashlib
|
13
|
+
from html import escape
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
from .constants import (
|
17
|
+
CONTAINER_TEMPLATE,
|
18
|
+
IMAGE_FOLDER,
|
19
|
+
ROOT_PATH,
|
20
|
+
)
|
21
|
+
|
22
|
+
|
23
|
+
def hash_file(file_path: Path, chunk_size: int = 8192) -> str:
|
24
|
+
"""
|
25
|
+
Compute the SHA256 hash of a file.
|
26
|
+
|
27
|
+
:param file_path: The Path object of the file to hash.
|
28
|
+
:param chunk_size: The chunk size to read the file (default: 8192).
|
29
|
+
:return: The SHA256 hash string (lowercase hex) of the file content.
|
30
|
+
"""
|
31
|
+
h = hashlib.sha256()
|
32
|
+
with file_path.open("rb") as f:
|
33
|
+
while chunk := f.read(chunk_size):
|
34
|
+
h.update(chunk)
|
35
|
+
return h.hexdigest()
|
36
|
+
|
37
|
+
|
38
|
+
def build_container_xml(
|
39
|
+
root_path: str = ROOT_PATH,
|
40
|
+
) -> str:
|
41
|
+
"""
|
42
|
+
Generate the XML content for META-INF/container.xml in an EPUB archive.
|
43
|
+
|
44
|
+
:param root_path: The folder where the OPF file is stored.
|
45
|
+
:return: A string containing the full XML for container.xml.
|
46
|
+
"""
|
47
|
+
return CONTAINER_TEMPLATE.format(root_path=root_path)
|
48
|
+
|
49
|
+
|
50
|
+
def build_book_intro(
|
51
|
+
book_name: str,
|
52
|
+
author: str,
|
53
|
+
serial_status: str,
|
54
|
+
subject: list[str],
|
55
|
+
word_count: str,
|
56
|
+
summary: str,
|
57
|
+
) -> str:
|
58
|
+
"""
|
59
|
+
Build the HTML snippet for the overall book introduction.
|
60
|
+
|
61
|
+
This includes:
|
62
|
+
- A main heading ("Book Introduction")
|
63
|
+
- A list of metadata items (title, author, categories, word count, status)
|
64
|
+
- A "Summary" subheading and one or more paragraphs of summary text
|
65
|
+
|
66
|
+
:return: A HTML string for inclusion in `intro.xhtml`
|
67
|
+
"""
|
68
|
+
lines = []
|
69
|
+
|
70
|
+
lines.append("<div>")
|
71
|
+
lines.append("<h1>书籍简介</h1>")
|
72
|
+
lines.append('<div class="intro-info">')
|
73
|
+
lines.append("<ul>")
|
74
|
+
|
75
|
+
name_val = f"《{book_name}》" if book_name else ""
|
76
|
+
subj_val = ", ".join(subject) if subject else ""
|
77
|
+
|
78
|
+
li_lines = [
|
79
|
+
_li_line("书名", name_val),
|
80
|
+
_li_line("作者", author),
|
81
|
+
_li_line("分类", subj_val),
|
82
|
+
_li_line("字数", word_count),
|
83
|
+
_li_line("状态", serial_status),
|
84
|
+
]
|
85
|
+
for li in li_lines:
|
86
|
+
if li:
|
87
|
+
lines.append(li)
|
88
|
+
|
89
|
+
lines.append("</ul>")
|
90
|
+
lines.append("</div>")
|
91
|
+
|
92
|
+
if summary:
|
93
|
+
lines.append('<p class="new-page-after"></p>')
|
94
|
+
lines.append("<h2>简介</h2>")
|
95
|
+
lines.append('<div class="intro-summary">')
|
96
|
+
for line in summary.splitlines():
|
97
|
+
s = line.strip()
|
98
|
+
if not s:
|
99
|
+
continue
|
100
|
+
lines.append(f"<p>{escape(s, quote=True)}</p>")
|
101
|
+
lines.append("</div>")
|
102
|
+
|
103
|
+
lines.append("</div>")
|
104
|
+
return "\n".join(lines)
|
105
|
+
|
106
|
+
|
107
|
+
def build_volume_intro(
|
108
|
+
volume_title: str,
|
109
|
+
volume_intro_text: str = "",
|
110
|
+
) -> str:
|
111
|
+
"""
|
112
|
+
Build the HTML snippet for a single-volume introduction.
|
113
|
+
|
114
|
+
This includes:
|
115
|
+
- A decorative border image (top and bottom)
|
116
|
+
- A primary heading (volume main title)
|
117
|
+
- An optional secondary line (subtitle)
|
118
|
+
- One or more paragraphs of intro text
|
119
|
+
|
120
|
+
:param volume_title: e.g. "Volume 1 - The Beginning"
|
121
|
+
:param volume_intro_text: multiline intro text for this volume
|
122
|
+
:return: A HTML string for inclusion in `vol_<n>.xhtml`
|
123
|
+
"""
|
124
|
+
line1, line2 = _split_volume_title(volume_title)
|
125
|
+
|
126
|
+
lines = []
|
127
|
+
lines.append("<div>")
|
128
|
+
lines.append('<div class="vol-header">')
|
129
|
+
lines.append(_vol_border_div_str(flip=False))
|
130
|
+
lines.append(f'<h1 class="vol-title-main">{escape(line1, quote=True)}</h1>')
|
131
|
+
lines.append(_vol_border_div_str(flip=True))
|
132
|
+
if line2:
|
133
|
+
lines.append(f'<h2 class="vol-title-sub">{escape(line2, quote=True)}</h2>')
|
134
|
+
lines.append("</div>")
|
135
|
+
|
136
|
+
if volume_intro_text:
|
137
|
+
lines.append('<p class="new-page-after"></p>')
|
138
|
+
lines.append('<div class="vol-intro-text">')
|
139
|
+
for line in volume_intro_text.splitlines():
|
140
|
+
s = line.strip()
|
141
|
+
if not s:
|
142
|
+
continue
|
143
|
+
lines.append(f"<p>{escape(s, quote=True)}</p>")
|
144
|
+
lines.append("</div>")
|
145
|
+
|
146
|
+
lines.append("</div>")
|
147
|
+
return "\n".join(lines)
|
148
|
+
|
149
|
+
|
150
|
+
def _li_line(label: str, value: str) -> str:
|
151
|
+
if not value:
|
152
|
+
return ""
|
153
|
+
return f"<li>{escape(label, quote=True)}: {escape(value, quote=True)}</li>"
|
154
|
+
|
155
|
+
|
156
|
+
def _vol_border_div_str(flip: bool = False) -> str:
|
157
|
+
classes = "vol-border" + (" flip" if flip else "")
|
158
|
+
return (
|
159
|
+
f'<div class="{classes}">'
|
160
|
+
f'<img src="../{IMAGE_FOLDER}/volume_border.png" alt="">'
|
161
|
+
f"</div>"
|
162
|
+
)
|
163
|
+
|
164
|
+
|
165
|
+
def _split_volume_title(volume_title: str) -> tuple[str, str]:
|
166
|
+
"""
|
167
|
+
Split volume title into two parts for better display.
|
168
|
+
|
169
|
+
:param volume_title: Original volume title string.
|
170
|
+
:return: Tuple of (line1, line2)
|
171
|
+
"""
|
172
|
+
if "-" in volume_title:
|
173
|
+
parts = volume_title.split("-", 1)
|
174
|
+
elif " " in volume_title:
|
175
|
+
parts = volume_title.split(" ", 1)
|
176
|
+
else:
|
177
|
+
return volume_title, ""
|
178
|
+
|
179
|
+
return parts[0], parts[1]
|
@@ -4,39 +4,14 @@ novel_downloader.utils.file_utils
|
|
4
4
|
---------------------------------
|
5
5
|
|
6
6
|
High-level file I/O utility re-exports for convenience.
|
7
|
-
|
8
|
-
This module aggregates commonly used low-level file utilities such as:
|
9
|
-
- Path sanitization (for safe filenames)
|
10
|
-
- Text normalization (e.g. Windows/Linux line endings)
|
11
|
-
- JSON, plain text, and binary file reading/writing
|
12
|
-
|
13
|
-
Included utilities:
|
14
|
-
- sanitize_filename: remove invalid characters from filenames
|
15
|
-
- normalize_txt_line_endings: standardize line endings in text files
|
16
|
-
- save_as_json / save_as_txt: write dict or text to file
|
17
|
-
- read_text_file / read_json_file / read_binary_file: load content from file
|
18
7
|
"""
|
19
8
|
|
20
|
-
from .io import (
|
21
|
-
load_blacklisted_words,
|
22
|
-
load_text_resource,
|
23
|
-
read_binary_file,
|
24
|
-
read_json_file,
|
25
|
-
read_text_file,
|
26
|
-
save_as_json,
|
27
|
-
save_as_txt,
|
28
|
-
)
|
29
|
-
from .normalize import normalize_txt_line_endings
|
30
|
-
from .sanitize import sanitize_filename
|
31
|
-
|
32
9
|
__all__ = [
|
33
10
|
"sanitize_filename",
|
34
|
-
"
|
35
|
-
"save_as_txt",
|
36
|
-
"read_text_file",
|
37
|
-
"read_json_file",
|
38
|
-
"read_binary_file",
|
39
|
-
"load_text_resource",
|
40
|
-
"load_blacklisted_words",
|
11
|
+
"write_file",
|
41
12
|
"normalize_txt_line_endings",
|
42
13
|
]
|
14
|
+
|
15
|
+
from .io import write_file
|
16
|
+
from .normalize import normalize_txt_line_endings
|
17
|
+
from .sanitize import sanitize_filename
|