novel-downloader 1.4.5__py3-none-any.whl → 1.5.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.
Files changed (165) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -2
  3. novel_downloader/cli/config.py +1 -83
  4. novel_downloader/cli/download.py +4 -5
  5. novel_downloader/cli/export.py +4 -1
  6. novel_downloader/cli/main.py +2 -0
  7. novel_downloader/cli/search.py +123 -0
  8. novel_downloader/config/__init__.py +3 -10
  9. novel_downloader/config/adapter.py +190 -54
  10. novel_downloader/config/loader.py +2 -3
  11. novel_downloader/core/__init__.py +13 -13
  12. novel_downloader/core/downloaders/__init__.py +10 -11
  13. novel_downloader/core/downloaders/base.py +152 -26
  14. novel_downloader/core/downloaders/biquge.py +5 -1
  15. novel_downloader/core/downloaders/common.py +157 -378
  16. novel_downloader/core/downloaders/esjzone.py +5 -1
  17. novel_downloader/core/downloaders/linovelib.py +5 -1
  18. novel_downloader/core/downloaders/qianbi.py +291 -4
  19. novel_downloader/core/downloaders/qidian.py +199 -285
  20. novel_downloader/core/downloaders/registry.py +67 -0
  21. novel_downloader/core/downloaders/sfacg.py +5 -1
  22. novel_downloader/core/downloaders/yamibo.py +5 -1
  23. novel_downloader/core/exporters/__init__.py +10 -11
  24. novel_downloader/core/exporters/base.py +87 -7
  25. novel_downloader/core/exporters/biquge.py +5 -8
  26. novel_downloader/core/exporters/common/__init__.py +2 -2
  27. novel_downloader/core/exporters/common/epub.py +82 -166
  28. novel_downloader/core/exporters/common/main_exporter.py +0 -60
  29. novel_downloader/core/exporters/common/txt.py +82 -83
  30. novel_downloader/core/exporters/epub_util.py +157 -1330
  31. novel_downloader/core/exporters/esjzone.py +5 -8
  32. novel_downloader/core/exporters/linovelib/__init__.py +2 -2
  33. novel_downloader/core/exporters/linovelib/epub.py +157 -212
  34. novel_downloader/core/exporters/linovelib/main_exporter.py +2 -59
  35. novel_downloader/core/exporters/linovelib/txt.py +67 -63
  36. novel_downloader/core/exporters/qianbi.py +5 -8
  37. novel_downloader/core/exporters/qidian.py +14 -4
  38. novel_downloader/core/exporters/registry.py +53 -0
  39. novel_downloader/core/exporters/sfacg.py +5 -8
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/exporters/yamibo.py +5 -8
  42. novel_downloader/core/fetchers/__init__.py +19 -24
  43. novel_downloader/core/fetchers/base/__init__.py +3 -3
  44. novel_downloader/core/fetchers/base/browser.py +23 -4
  45. novel_downloader/core/fetchers/base/session.py +30 -5
  46. novel_downloader/core/fetchers/biquge/__init__.py +3 -3
  47. novel_downloader/core/fetchers/biquge/browser.py +5 -0
  48. novel_downloader/core/fetchers/biquge/session.py +6 -1
  49. novel_downloader/core/fetchers/esjzone/__init__.py +3 -3
  50. novel_downloader/core/fetchers/esjzone/browser.py +5 -0
  51. novel_downloader/core/fetchers/esjzone/session.py +6 -1
  52. novel_downloader/core/fetchers/linovelib/__init__.py +3 -3
  53. novel_downloader/core/fetchers/linovelib/browser.py +6 -1
  54. novel_downloader/core/fetchers/linovelib/session.py +6 -1
  55. novel_downloader/core/fetchers/qianbi/__init__.py +3 -3
  56. novel_downloader/core/fetchers/qianbi/browser.py +5 -0
  57. novel_downloader/core/fetchers/qianbi/session.py +5 -0
  58. novel_downloader/core/fetchers/qidian/__init__.py +3 -3
  59. novel_downloader/core/fetchers/qidian/browser.py +12 -4
  60. novel_downloader/core/fetchers/qidian/session.py +11 -3
  61. novel_downloader/core/fetchers/registry.py +71 -0
  62. novel_downloader/core/fetchers/sfacg/__init__.py +3 -3
  63. novel_downloader/core/fetchers/sfacg/browser.py +5 -0
  64. novel_downloader/core/fetchers/sfacg/session.py +5 -0
  65. novel_downloader/core/fetchers/yamibo/__init__.py +3 -3
  66. novel_downloader/core/fetchers/yamibo/browser.py +5 -0
  67. novel_downloader/core/fetchers/yamibo/session.py +6 -1
  68. novel_downloader/core/interfaces/__init__.py +7 -5
  69. novel_downloader/core/interfaces/searcher.py +18 -0
  70. novel_downloader/core/parsers/__init__.py +10 -11
  71. novel_downloader/core/parsers/{biquge/main_parser.py → biquge.py} +7 -2
  72. novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +7 -2
  73. novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +7 -2
  74. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +7 -2
  75. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  76. novel_downloader/core/parsers/qidian/chapter_encrypted.py +23 -21
  77. novel_downloader/core/parsers/qidian/chapter_normal.py +1 -1
  78. novel_downloader/core/parsers/qidian/main_parser.py +10 -21
  79. novel_downloader/core/parsers/qidian/utils/__init__.py +11 -11
  80. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +5 -6
  81. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  82. novel_downloader/core/parsers/registry.py +68 -0
  83. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +7 -2
  84. novel_downloader/core/parsers/{yamibo/main_parser.py → yamibo.py} +7 -2
  85. novel_downloader/core/searchers/__init__.py +20 -0
  86. novel_downloader/core/searchers/base.py +92 -0
  87. novel_downloader/core/searchers/biquge.py +83 -0
  88. novel_downloader/core/searchers/esjzone.py +84 -0
  89. novel_downloader/core/searchers/qianbi.py +131 -0
  90. novel_downloader/core/searchers/qidian.py +87 -0
  91. novel_downloader/core/searchers/registry.py +63 -0
  92. novel_downloader/locales/en.json +12 -4
  93. novel_downloader/locales/zh.json +12 -4
  94. novel_downloader/models/__init__.py +4 -30
  95. novel_downloader/models/config.py +12 -6
  96. novel_downloader/models/search.py +16 -0
  97. novel_downloader/models/types.py +0 -2
  98. novel_downloader/resources/config/settings.toml +31 -4
  99. novel_downloader/resources/css_styles/intro.css +83 -0
  100. novel_downloader/resources/css_styles/main.css +30 -89
  101. novel_downloader/utils/__init__.py +52 -0
  102. novel_downloader/utils/chapter_storage.py +244 -224
  103. novel_downloader/utils/constants.py +1 -21
  104. novel_downloader/utils/epub/__init__.py +34 -0
  105. novel_downloader/utils/epub/builder.py +377 -0
  106. novel_downloader/utils/epub/constants.py +77 -0
  107. novel_downloader/utils/epub/documents.py +403 -0
  108. novel_downloader/utils/epub/models.py +134 -0
  109. novel_downloader/utils/epub/utils.py +212 -0
  110. novel_downloader/utils/file_utils/__init__.py +10 -14
  111. novel_downloader/utils/file_utils/io.py +20 -51
  112. novel_downloader/utils/file_utils/normalize.py +2 -2
  113. novel_downloader/utils/file_utils/sanitize.py +2 -3
  114. novel_downloader/utils/fontocr/__init__.py +5 -5
  115. novel_downloader/utils/{hash_store.py → fontocr/hash_store.py} +4 -3
  116. novel_downloader/utils/{hash_utils.py → fontocr/hash_utils.py} +2 -2
  117. novel_downloader/utils/fontocr/ocr_v1.py +13 -1
  118. novel_downloader/utils/fontocr/ocr_v2.py +13 -1
  119. novel_downloader/utils/fontocr/ocr_v3.py +744 -0
  120. novel_downloader/utils/i18n.py +2 -0
  121. novel_downloader/utils/logger.py +2 -0
  122. novel_downloader/utils/network.py +110 -251
  123. novel_downloader/utils/state.py +1 -0
  124. novel_downloader/utils/text_utils/__init__.py +18 -17
  125. novel_downloader/utils/text_utils/diff_display.py +4 -5
  126. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  127. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  128. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  129. novel_downloader/utils/time_utils/__init__.py +3 -3
  130. novel_downloader/utils/time_utils/datetime_utils.py +4 -5
  131. novel_downloader/utils/time_utils/sleep_utils.py +2 -3
  132. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/METADATA +2 -2
  133. novel_downloader-1.5.0.dist-info/RECORD +164 -0
  134. novel_downloader/config/site_rules.py +0 -94
  135. novel_downloader/core/factory/__init__.py +0 -20
  136. novel_downloader/core/factory/downloader.py +0 -73
  137. novel_downloader/core/factory/exporter.py +0 -58
  138. novel_downloader/core/factory/fetcher.py +0 -96
  139. novel_downloader/core/factory/parser.py +0 -86
  140. novel_downloader/core/fetchers/common/__init__.py +0 -14
  141. novel_downloader/core/fetchers/common/browser.py +0 -79
  142. novel_downloader/core/fetchers/common/session.py +0 -79
  143. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  144. novel_downloader/core/parsers/common/__init__.py +0 -13
  145. novel_downloader/core/parsers/common/helper.py +0 -323
  146. novel_downloader/core/parsers/common/main_parser.py +0 -106
  147. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  148. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  149. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  150. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  151. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  152. novel_downloader/models/browser.py +0 -21
  153. novel_downloader/models/site_rules.py +0 -99
  154. novel_downloader/models/tasks.py +0 -33
  155. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  156. novel_downloader/resources/json/replace_word_map.json +0 -4
  157. novel_downloader/resources/text/blacklist.txt +0 -22
  158. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  159. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  160. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  161. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  162. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/WHEEL +0 -0
  163. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/entry_points.txt +0 -0
  164. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/licenses/LICENSE +0 -0
  165. {novel_downloader-1.4.5.dist-info → novel_downloader-1.5.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.builder
4
+ -----------------------------------
5
+
6
+ Orchestrates the end-to-end EPUB build process by:
7
+ - Managing metadata (title, author, description, language, etc.)
8
+ - Collecting and deduplicating resources (chapters, images, stylesheets)
9
+ - Registering everything in the OPF manifest and spine
10
+ - Generating nav.xhtml, toc.ncx, content.opf, and the zipped .epub file
11
+
12
+ Provides:
13
+ - methods to add chapters, volumes, images, and styles
14
+ - a clean `export()` entry point that writes the final EPUB archive
15
+ """
16
+
17
+ import zipfile
18
+ from pathlib import Path
19
+ from zipfile import ZIP_DEFLATED, ZIP_STORED
20
+
21
+ from novel_downloader.utils.constants import (
22
+ CSS_INTRO_PATH,
23
+ VOLUME_BORDER_IMAGE_PATH,
24
+ )
25
+
26
+ from .constants import (
27
+ COVER_IMAGE_TEMPLATE,
28
+ CSS_FOLDER,
29
+ IMAGE_FOLDER,
30
+ IMAGE_MEDIA_TYPES,
31
+ ROOT_PATH,
32
+ TEXT_FOLDER,
33
+ )
34
+ from .documents import (
35
+ NavDocument,
36
+ NCXDocument,
37
+ OpfDocument,
38
+ )
39
+ from .models import (
40
+ Chapter,
41
+ ChapterEntry,
42
+ EpubResource,
43
+ ImageResource,
44
+ StyleSheet,
45
+ Volume,
46
+ )
47
+ from .utils import (
48
+ build_book_intro,
49
+ build_container_xml,
50
+ build_volume_intro,
51
+ hash_file,
52
+ )
53
+
54
+
55
+ class EpubBuilder:
56
+ def __init__(
57
+ self,
58
+ title: str,
59
+ author: str = "",
60
+ description: str = "",
61
+ cover_path: Path | None = None,
62
+ subject: list[str] | None = None,
63
+ serial_status: str = "",
64
+ word_count: str = "0",
65
+ uid: str = "",
66
+ language: str = "zh-CN",
67
+ ):
68
+ # metadata
69
+ self.title = title
70
+ self.author = author
71
+ self.description = description
72
+ self.language = language
73
+ self.subject = subject or []
74
+ self.serial_status = serial_status
75
+ self.word_count = word_count
76
+ self.uid = uid
77
+
78
+ # builder state
79
+ self.chapters: list[Chapter] = []
80
+ self.images: list[ImageResource] = []
81
+ self.styles: list[StyleSheet] = []
82
+ self._img_map: dict[str, str] = {}
83
+ self._img_idx = 0
84
+ self._vol_idx = 0
85
+
86
+ # core EPUB documents
87
+ self.nav = NavDocument(title=title, language=language)
88
+ self.ncx = NCXDocument(title=title, uid=uid)
89
+ self.opf = OpfDocument(
90
+ title=title,
91
+ author=author,
92
+ description=description,
93
+ uid=uid,
94
+ subject=self.subject,
95
+ language=language,
96
+ )
97
+
98
+ # register the nav & ncx items
99
+ self.opf.add_manifest_item(
100
+ "nav",
101
+ "nav.xhtml",
102
+ self.nav.media_type,
103
+ properties="nav",
104
+ )
105
+ self.opf.add_manifest_item("ncx", "toc.ncx", self.ncx.media_type)
106
+
107
+ self._init_styles()
108
+ self._init_cover(cover_path)
109
+ self._init_intro()
110
+
111
+ def add_image(self, image_path: Path) -> str:
112
+ """
113
+ Add an image resource (deduped by hash) and register it.
114
+ """
115
+ if not (image_path.exists() and image_path.is_file()):
116
+ return ""
117
+ h = hash_file(image_path)
118
+ if h in self._img_map:
119
+ return self._img_map[h]
120
+
121
+ ext = image_path.suffix.lower().lstrip(".")
122
+ mtype = IMAGE_MEDIA_TYPES.get(ext)
123
+ if not mtype:
124
+ return ""
125
+
126
+ res_id = f"img_{self._img_idx}"
127
+ filename = f"{res_id}.{ext}"
128
+ data = image_path.read_bytes()
129
+ img = ImageResource(id=res_id, data=data, media_type=mtype, filename=filename)
130
+ self.images.append(img)
131
+ self._register(img, folder=IMAGE_FOLDER, in_spine=False)
132
+
133
+ self._img_map[h] = filename
134
+ self._img_idx += 1
135
+ return filename
136
+
137
+ def add_chapter(self, chap: Chapter) -> None:
138
+ self.chapters.append(chap)
139
+ self._register(chap, folder=TEXT_FOLDER)
140
+ self.nav.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
141
+ self.ncx.add_chapter(chap.id, chap.title, f"{TEXT_FOLDER}/{chap.filename}")
142
+
143
+ def add_volume(self, volume: Volume) -> None:
144
+ """Add a volume cover, intro, and all its chapters to the EPUB."""
145
+ # volume-specific cover
146
+ if volume.cover:
147
+ filename = self.add_image(volume.cover)
148
+ cover_html = f'<img class="width100" src="../{IMAGE_FOLDER}/{filename}"/>'
149
+ cover_chap = Chapter(
150
+ id=f"vol_{self._vol_idx}_cover",
151
+ title=volume.title,
152
+ content=cover_html,
153
+ filename=f"vol_{self._vol_idx}_cover.xhtml",
154
+ )
155
+ self.chapters.append(cover_chap)
156
+ self._register(
157
+ cover_chap,
158
+ folder=TEXT_FOLDER,
159
+ properties="duokan-page-fullscreen",
160
+ )
161
+
162
+ # volume intro page
163
+ intro_content = build_volume_intro(volume.title, volume.intro)
164
+ vol_intro = Chapter(
165
+ id=f"vol_{self._vol_idx}",
166
+ title=volume.title,
167
+ content=intro_content,
168
+ css=[self.intro_css],
169
+ filename=f"vol_{self._vol_idx}.xhtml",
170
+ )
171
+ self.chapters.append(vol_intro)
172
+ self._register(vol_intro, folder=TEXT_FOLDER)
173
+
174
+ # nested chapters
175
+ entries: list[ChapterEntry] = []
176
+ for chap in volume.chapters:
177
+ self.chapters.append(chap)
178
+ self._register(chap, folder=TEXT_FOLDER)
179
+ entries.append(
180
+ ChapterEntry(
181
+ id=chap.id,
182
+ label=chap.title,
183
+ src=f"{TEXT_FOLDER}/{chap.filename}",
184
+ )
185
+ )
186
+
187
+ # TOC updates
188
+ self.ncx.add_volume(
189
+ id=f"vol_{self._vol_idx}",
190
+ label=volume.title,
191
+ src=f"{TEXT_FOLDER}/{vol_intro.filename}",
192
+ chapters=entries,
193
+ )
194
+ self.nav.add_volume(
195
+ id=f"vol_{self._vol_idx}",
196
+ label=volume.title,
197
+ src=f"{TEXT_FOLDER}/{vol_intro.filename}",
198
+ chapters=entries,
199
+ )
200
+
201
+ self._vol_idx += 1
202
+
203
+ def add_stylesheet(self, css: StyleSheet) -> None:
204
+ """
205
+ Register an external CSS file in the EPUB.
206
+ """
207
+ self.styles.append(css)
208
+ self._register(css, folder=CSS_FOLDER, in_spine=False)
209
+
210
+ def export(self, output_path: str | Path) -> Path:
211
+ """
212
+ Build and export the current book as an EPUB file.
213
+
214
+ :param output_path: Path to save the final .epub file.
215
+ """
216
+ return self._build_epub(output_path=Path(output_path))
217
+
218
+ def _register(
219
+ self,
220
+ res: EpubResource,
221
+ folder: str,
222
+ in_spine: bool = True,
223
+ properties: str | None = None,
224
+ ) -> None:
225
+ """
226
+ Add resource to the manifest—and optionally to the spine.
227
+ """
228
+ href = f"{folder}/{res.filename}"
229
+ self.opf.add_manifest_item(res.id, href, res.media_type, properties)
230
+ if in_spine:
231
+ self.opf.add_spine_item(res.id, properties)
232
+
233
+ def _init_styles(self) -> None:
234
+ # volume border & intro CSS
235
+ self.intro_css = StyleSheet(
236
+ id="intro_style",
237
+ content=CSS_INTRO_PATH.read_text("utf-8"),
238
+ filename="intro_style.css",
239
+ )
240
+ self.styles.append(self.intro_css)
241
+ self._register(self.intro_css, folder=CSS_FOLDER, in_spine=False)
242
+
243
+ try:
244
+ border_bytes = VOLUME_BORDER_IMAGE_PATH.read_bytes()
245
+ except FileNotFoundError:
246
+ return
247
+ border = ImageResource(
248
+ id="img-volume-border",
249
+ data=border_bytes,
250
+ media_type="image/png",
251
+ filename="volume_border.png",
252
+ )
253
+ self.images.append(border)
254
+ self._register(border, folder=IMAGE_FOLDER, in_spine=False)
255
+
256
+ def _init_cover(self, cover_path: Path | None) -> None:
257
+ if not cover_path or not cover_path.is_file():
258
+ return
259
+ ext = cover_path.suffix.lower().lstrip(".")
260
+ mtype = IMAGE_MEDIA_TYPES.get(ext)
261
+ if not mtype:
262
+ return
263
+
264
+ data = cover_path.read_bytes()
265
+ cover_img = ImageResource(
266
+ id="cover-img",
267
+ data=data,
268
+ media_type=mtype,
269
+ filename=f"cover.{ext}",
270
+ )
271
+ self.images.append(cover_img)
272
+ self._register(
273
+ cover_img,
274
+ folder=IMAGE_FOLDER,
275
+ in_spine=False,
276
+ properties="cover-image",
277
+ )
278
+
279
+ cover_chapter = Chapter(
280
+ id="cover",
281
+ title="Cover",
282
+ content=COVER_IMAGE_TEMPLATE.format(ext=ext),
283
+ filename="cover.xhtml",
284
+ )
285
+ self.chapters.append(cover_chapter)
286
+ self._register(
287
+ cover_chapter,
288
+ folder=TEXT_FOLDER,
289
+ properties="duokan-page-fullscreen",
290
+ )
291
+ self.nav.add_chapter(
292
+ cover_chapter.id,
293
+ cover_chapter.title,
294
+ f"{TEXT_FOLDER}/{cover_chapter.filename}",
295
+ )
296
+ self.ncx.add_chapter(
297
+ cover_chapter.id,
298
+ cover_chapter.title,
299
+ f"{TEXT_FOLDER}/{cover_chapter.filename}",
300
+ )
301
+ self.opf.include_cover = True
302
+
303
+ def _init_intro(self) -> None:
304
+ intro_html = build_book_intro(
305
+ book_name=self.title,
306
+ author=self.author,
307
+ serial_status=self.serial_status,
308
+ subject=self.subject,
309
+ word_count=self.word_count,
310
+ summary=self.description,
311
+ )
312
+ intro = Chapter(
313
+ id="intro",
314
+ title="书籍简介",
315
+ content=intro_html,
316
+ filename="intro.xhtml",
317
+ css=[self.intro_css],
318
+ )
319
+ self.chapters.append(intro)
320
+ self._register(intro, folder=TEXT_FOLDER)
321
+ self.nav.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
322
+ self.ncx.add_chapter(intro.id, intro.title, f"{TEXT_FOLDER}/{intro.filename}")
323
+
324
+ def _build_epub(self, output_path: Path) -> Path:
325
+ """
326
+ Write out the .epub ZIP file.
327
+ """
328
+ output_path.parent.mkdir(parents=True, exist_ok=True)
329
+
330
+ with zipfile.ZipFile(output_path, "w") as epub:
331
+ # must be first and uncompressed
332
+ epub.writestr(
333
+ "mimetype",
334
+ "application/epub+zip",
335
+ compress_type=ZIP_STORED,
336
+ )
337
+
338
+ # container
339
+ epub.writestr(
340
+ "META-INF/container.xml",
341
+ build_container_xml(),
342
+ compress_type=ZIP_DEFLATED,
343
+ )
344
+
345
+ # core documents
346
+ epub.writestr(
347
+ f"{ROOT_PATH}/nav.xhtml",
348
+ self.nav.to_xhtml(),
349
+ compress_type=ZIP_DEFLATED,
350
+ )
351
+ epub.writestr(
352
+ f"{ROOT_PATH}/toc.ncx",
353
+ self.ncx.to_xml(),
354
+ compress_type=ZIP_DEFLATED,
355
+ )
356
+ epub.writestr(
357
+ f"{ROOT_PATH}/content.opf",
358
+ self.opf.to_xml(),
359
+ compress_type=ZIP_DEFLATED,
360
+ )
361
+
362
+ # stylesheets
363
+ for css in self.styles:
364
+ path = f"{ROOT_PATH}/{CSS_FOLDER}/{css.filename}"
365
+ epub.writestr(path, css.content, compress_type=ZIP_DEFLATED)
366
+
367
+ # chapters
368
+ for chap in self.chapters:
369
+ path = f"{ROOT_PATH}/{TEXT_FOLDER}/{chap.filename}"
370
+ epub.writestr(path, chap.to_xhtml(), compress_type=ZIP_DEFLATED)
371
+
372
+ # images
373
+ for img in self.images:
374
+ path = f"{ROOT_PATH}/{IMAGE_FOLDER}/{img.filename}"
375
+ epub.writestr(path, img.data, compress_type=ZIP_DEFLATED)
376
+
377
+ return output_path
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.utils.epub.constants
4
+ -------------------------------------
5
+
6
+ EPUB-specific constants used by the builder, including:
7
+ - Directory names for OEBPS structure
8
+ - XML namespace URIs
9
+ - Package attributes and document-type declarations
10
+ - Media type mappings for images
11
+ - Template strings for container.xml and cover image HTML
12
+ """
13
+
14
+ PRETTY_PRINT_FLAG = True
15
+ ROOT_PATH = "OEBPS"
16
+ IMAGE_FOLDER = "Images"
17
+ TEXT_FOLDER = "Text"
18
+ CSS_FOLDER = "Styles"
19
+
20
+ XHTML_NS = "http://www.w3.org/1999/xhtml"
21
+ EPUB_NS = "http://www.idpf.org/2007/ops"
22
+ XML_NS = "http://www.w3.org/XML/1998/namespace"
23
+ NCX_NS = "http://www.daisy.org/z3986/2005/ncx/"
24
+ OPF_NS = "http://www.idpf.org/2007/opf"
25
+ DC_NS = "http://purl.org/dc/elements/1.1/"
26
+
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
+ IMAGE_MEDIA_TYPES: dict[str, str] = {
40
+ "png": "image/png",
41
+ "jpg": "image/jpeg",
42
+ "jpeg": "image/jpeg",
43
+ "gif": "image/gif",
44
+ "svg": "image/svg+xml",
45
+ "webp": "image/webp",
46
+ }
47
+
48
+ CONTAINER_TEMPLATE = """<?xml version="1.0" encoding="UTF-8"?>
49
+ <container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
50
+ <rootfiles>
51
+ <rootfile full-path="{root_path}/content.opf"
52
+ media-type="application/oebps-package+xml"/>
53
+ </rootfiles>
54
+ </container>"""
55
+
56
+ COVER_IMAGE_TEMPLATE = (
57
+ f'<div style="text-align: center; margin: 0; padding: 0;">'
58
+ f'<img src="../{IMAGE_FOLDER}/cover.{{ext}}" alt="cover" '
59
+ f'style="max-width: 100%; height: auto;" />'
60
+ f"</div>"
61
+ )
62
+
63
+ CSS_TMPLATE = (
64
+ f'<link href="../{CSS_FOLDER}/{{filename}}" '
65
+ f'rel="stylesheet" type="{{media_type}}"/>'
66
+ )
67
+
68
+ CHAP_TMPLATE = f"""\
69
+ {CHAP_DOC_TYPE}
70
+ <html xmlns="{XHTML_NS}" xmlns:epub="{EPUB_NS}" lang="{{lang}}" xml:lang="{{lang}}">
71
+ <head>
72
+ <title>{{title}}</title>
73
+ {{xlinks}}
74
+ </head>
75
+ <body>{{content}}</body>
76
+ </html>
77
+ """