novel-downloader 2.0.1__py3-none-any.whl → 2.0.2__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 (104) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +11 -8
  3. novel_downloader/cli/export.py +17 -17
  4. novel_downloader/cli/ui.py +28 -1
  5. novel_downloader/config/adapter.py +27 -1
  6. novel_downloader/core/archived/deqixs/fetcher.py +1 -28
  7. novel_downloader/core/downloaders/__init__.py +2 -0
  8. novel_downloader/core/downloaders/base.py +34 -85
  9. novel_downloader/core/downloaders/common.py +147 -171
  10. novel_downloader/core/downloaders/qianbi.py +30 -64
  11. novel_downloader/core/downloaders/qidian.py +157 -184
  12. novel_downloader/core/downloaders/qqbook.py +292 -0
  13. novel_downloader/core/downloaders/registry.py +2 -2
  14. novel_downloader/core/exporters/__init__.py +2 -0
  15. novel_downloader/core/exporters/base.py +37 -59
  16. novel_downloader/core/exporters/common.py +620 -0
  17. novel_downloader/core/exporters/linovelib.py +47 -0
  18. novel_downloader/core/exporters/qidian.py +41 -12
  19. novel_downloader/core/exporters/qqbook.py +28 -0
  20. novel_downloader/core/exporters/registry.py +2 -2
  21. novel_downloader/core/fetchers/__init__.py +4 -2
  22. novel_downloader/core/fetchers/aaatxt.py +2 -22
  23. novel_downloader/core/fetchers/b520.py +3 -23
  24. novel_downloader/core/fetchers/base.py +80 -105
  25. novel_downloader/core/fetchers/biquyuedu.py +2 -22
  26. novel_downloader/core/fetchers/dxmwx.py +10 -22
  27. novel_downloader/core/fetchers/esjzone.py +6 -29
  28. novel_downloader/core/fetchers/guidaye.py +2 -22
  29. novel_downloader/core/fetchers/hetushu.py +9 -29
  30. novel_downloader/core/fetchers/i25zw.py +2 -16
  31. novel_downloader/core/fetchers/ixdzs8.py +2 -16
  32. novel_downloader/core/fetchers/jpxs123.py +2 -16
  33. novel_downloader/core/fetchers/lewenn.py +2 -22
  34. novel_downloader/core/fetchers/linovelib.py +4 -20
  35. novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
  36. novel_downloader/core/fetchers/piaotia.py +2 -16
  37. novel_downloader/core/fetchers/qbtr.py +2 -16
  38. novel_downloader/core/fetchers/qianbi.py +1 -20
  39. novel_downloader/core/fetchers/qidian.py +7 -33
  40. novel_downloader/core/fetchers/qqbook.py +177 -0
  41. novel_downloader/core/fetchers/quanben5.py +9 -29
  42. novel_downloader/core/fetchers/rate_limiter.py +22 -53
  43. novel_downloader/core/fetchers/sfacg.py +3 -16
  44. novel_downloader/core/fetchers/shencou.py +2 -16
  45. novel_downloader/core/fetchers/shuhaige.py +2 -22
  46. novel_downloader/core/fetchers/tongrenquan.py +2 -22
  47. novel_downloader/core/fetchers/ttkan.py +3 -14
  48. novel_downloader/core/fetchers/wanbengo.py +2 -22
  49. novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
  50. novel_downloader/core/fetchers/xiguashuwu.py +4 -20
  51. novel_downloader/core/fetchers/xs63b.py +3 -15
  52. novel_downloader/core/fetchers/xshbook.py +2 -22
  53. novel_downloader/core/fetchers/yamibo.py +4 -28
  54. novel_downloader/core/fetchers/yibige.py +13 -26
  55. novel_downloader/core/interfaces/exporter.py +19 -7
  56. novel_downloader/core/interfaces/fetcher.py +21 -47
  57. novel_downloader/core/parsers/__init__.py +4 -2
  58. novel_downloader/core/parsers/b520.py +2 -2
  59. novel_downloader/core/parsers/base.py +4 -39
  60. novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
  61. novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
  62. novel_downloader/core/parsers/qqbook.py +709 -0
  63. novel_downloader/core/parsers/xiguashuwu.py +3 -4
  64. novel_downloader/core/searchers/__init__.py +2 -2
  65. novel_downloader/core/searchers/b520.py +1 -1
  66. novel_downloader/core/searchers/base.py +2 -2
  67. novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
  68. novel_downloader/models/__init__.py +2 -0
  69. novel_downloader/models/book.py +1 -0
  70. novel_downloader/models/config.py +12 -0
  71. novel_downloader/resources/config/settings.toml +23 -5
  72. novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
  73. novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
  74. novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
  75. novel_downloader/utils/constants.py +6 -0
  76. novel_downloader/utils/crypto_utils/aes_util.py +1 -1
  77. novel_downloader/utils/epub/constants.py +1 -6
  78. novel_downloader/utils/fontocr/core.py +2 -0
  79. novel_downloader/utils/fontocr/loader.py +10 -8
  80. novel_downloader/utils/node_decryptor/__init__.py +13 -0
  81. novel_downloader/utils/node_decryptor/decryptor.py +342 -0
  82. novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
  83. novel_downloader/web/pages/download.py +1 -1
  84. novel_downloader/web/pages/search.py +1 -1
  85. novel_downloader/web/services/task_manager.py +2 -0
  86. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
  87. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
  88. novel_downloader/core/exporters/common/__init__.py +0 -11
  89. novel_downloader/core/exporters/common/epub.py +0 -198
  90. novel_downloader/core/exporters/common/main_exporter.py +0 -64
  91. novel_downloader/core/exporters/common/txt.py +0 -146
  92. novel_downloader/core/exporters/epub_util.py +0 -215
  93. novel_downloader/core/exporters/linovelib/__init__.py +0 -11
  94. novel_downloader/core/exporters/linovelib/epub.py +0 -349
  95. novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
  96. novel_downloader/core/exporters/linovelib/txt.py +0 -139
  97. novel_downloader/core/exporters/txt_util.py +0 -67
  98. novel_downloader/core/parsers/qidian/__init__.py +0 -10
  99. novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
  100. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
  101. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
  102. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
  103. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
  104. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -1,349 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.linovelib.epub
4
- ----------------------------------------------
5
-
6
- Contains the logic for exporting novel content as a single `.epub` file.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from pathlib import Path
12
- from typing import TYPE_CHECKING
13
-
14
- from novel_downloader.core.exporters.epub_util import (
15
- build_epub_chapter,
16
- download_cover,
17
- finalize_export,
18
- inline_remote_images,
19
- prepare_builder,
20
- remove_all_images,
21
- )
22
- from novel_downloader.utils import (
23
- download,
24
- get_cleaner,
25
- )
26
- from novel_downloader.utils.constants import (
27
- DEFAULT_HEADERS,
28
- DEFAULT_IMAGE_SUFFIX,
29
- )
30
- from novel_downloader.utils.epub import (
31
- Chapter,
32
- Volume,
33
- )
34
-
35
- if TYPE_CHECKING:
36
- from .main_exporter import LinovelibExporter
37
-
38
- _IMG_HEADERS = DEFAULT_HEADERS.copy()
39
- _IMG_HEADERS["Referer"] = "https://www.linovelib.com/"
40
-
41
-
42
- def export_whole_book(
43
- exporter: LinovelibExporter,
44
- book_id: str,
45
- ) -> Path | None:
46
- """
47
- Export a single novel (identified by `book_id`) to an EPUB file.
48
-
49
- This function will:
50
- 1. Load `book_info.json` for metadata.
51
- 2. Generate introductory HTML and optionally include the cover image.
52
- 3. Initialize the EPUB container.
53
- 4. Iterate through volumes and chapters in volume-batches, convert each to XHTML.
54
- 5. Assemble the spine, TOC, CSS and write out the final `.epub`.
55
-
56
- :param exporter: The exporter instance, carrying config and path info.
57
- :param book_id: Identifier of the novel (used as subdirectory name).
58
- """
59
- TAG = "[exporter]"
60
- config = exporter._config
61
-
62
- raw_base = exporter._raw_data_dir / book_id
63
- img_dir = raw_base / "images"
64
- out_dir = exporter.output_dir
65
-
66
- img_dir.mkdir(parents=True, exist_ok=True)
67
- out_dir.mkdir(parents=True, exist_ok=True)
68
-
69
- cleaner = get_cleaner(
70
- enabled=config.clean_text,
71
- config=config.cleaner_cfg,
72
- )
73
-
74
- # --- Load book_info.json ---
75
- book_info = exporter._load_book_info(book_id)
76
- if not book_info:
77
- return None
78
-
79
- book_name = book_info.get("book_name", book_id)
80
- book_author = book_info.get("author", "")
81
-
82
- exporter.logger.info(
83
- "%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
84
- )
85
-
86
- # --- Generate intro + cover ---
87
- cover_path = download_cover(
88
- book_info.get("cover_url", ""),
89
- raw_base,
90
- config.include_cover,
91
- exporter.logger,
92
- TAG,
93
- headers=_IMG_HEADERS,
94
- )
95
-
96
- # --- Initialize EPUB ---
97
- book, main_css = prepare_builder(
98
- site_name=exporter.site,
99
- book_id=book_id,
100
- title=book_name,
101
- author=book_author,
102
- description=book_info.get("summary", ""),
103
- subject=book_info.get("tags", []),
104
- serial_status=book_info.get("serial_status", ""),
105
- word_count=book_info.get("word_count", ""),
106
- cover_path=cover_path,
107
- )
108
-
109
- # --- Compile chapters ---
110
- volumes = book_info.get("volumes", [])
111
- if not volumes:
112
- exporter.logger.warning("%s No volumes found in metadata.", TAG)
113
-
114
- for vol_index, vol in enumerate(volumes, start=1):
115
- raw_name = vol.get("volume_name", "")
116
- raw_name = raw_name.replace(book_name, "").strip()
117
- vol_name = raw_name or f"Volume {vol_index}"
118
- exporter.logger.info("Processing volume %d: %s", vol_index, vol_name)
119
-
120
- # Batch-fetch chapters for this volume
121
- chap_ids = [
122
- chap["chapterId"]
123
- for chap in vol.get("chapters", [])
124
- if chap.get("chapterId")
125
- ]
126
- chap_map = exporter._get_chapters(book_id, chap_ids)
127
-
128
- vol_cover: Path | None = None
129
- vol_cover_url = vol.get("volume_cover", "")
130
- if vol_cover_url:
131
- vol_cover = download(
132
- vol_cover_url,
133
- img_dir,
134
- on_exist="skip",
135
- default_suffix=DEFAULT_IMAGE_SUFFIX,
136
- headers=_IMG_HEADERS,
137
- )
138
-
139
- curr_vol = Volume(
140
- id=f"vol_{vol_index}",
141
- title=vol_name,
142
- intro=cleaner.clean_content(vol.get("volume_intro", "")),
143
- cover=vol_cover,
144
- )
145
-
146
- for chap_meta in vol.get("chapters", []):
147
- chap_id = chap_meta.get("chapterId")
148
- if not chap_id:
149
- exporter.logger.warning(
150
- "%s Missing chapterId, skipping: %s",
151
- TAG,
152
- chap_meta,
153
- )
154
- continue
155
-
156
- chap_title = chap_meta.get("title", "")
157
- data = chap_map.get(chap_id)
158
- if not data:
159
- exporter.logger.info(
160
- "%s Missing chapter: %s (%s), skipping.",
161
- TAG,
162
- chap_title,
163
- chap_id,
164
- )
165
- continue
166
-
167
- title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
168
- content = cleaner.clean_content(data.get("content", ""))
169
- content = (
170
- inline_remote_images(book, content, img_dir, headers=_IMG_HEADERS)
171
- if config.include_picture
172
- else remove_all_images(content)
173
- )
174
-
175
- chap_html = build_epub_chapter(
176
- title=title,
177
- paragraphs=content,
178
- extras={},
179
- )
180
- curr_vol.chapters.append(
181
- Chapter(
182
- id=f"c_{chap_id}",
183
- filename=f"c{chap_id}.xhtml",
184
- title=title,
185
- content=chap_html,
186
- css=[main_css],
187
- )
188
- )
189
-
190
- book.add_volume(curr_vol)
191
-
192
- # --- 5. Finalize EPUB ---
193
- out_name = exporter.get_filename(
194
- title=book_name,
195
- author=book_info.get("author"),
196
- ext="epub",
197
- )
198
- return finalize_export(
199
- book=book,
200
- out_dir=out_dir,
201
- filename=out_name,
202
- logger=exporter.logger,
203
- tag=TAG,
204
- )
205
-
206
-
207
- def export_by_volume(
208
- exporter: LinovelibExporter,
209
- book_id: str,
210
- ) -> Path | None:
211
- """
212
- Export each volume of a novel as a separate EPUB file.
213
-
214
- Steps:
215
- 1. Load metadata from `book_info.json`.
216
- 2. For each volume:
217
- a. Clean the volume title and determine output filename.
218
- b. Batch-fetch all chapters in this volume to minimize SQLite overhead.
219
- c. Initialize an EPUB builder for the volume, including cover and intro.
220
- d. For each chapter: clean title & content, inline remote images.
221
- e. Finalize and write the volume EPUB.
222
-
223
- :param book_id: Identifier of the novel (used as subdirectory name).
224
- """
225
- TAG = "[exporter]"
226
- config = exporter._config
227
-
228
- raw_base = exporter._raw_data_dir / book_id
229
- img_dir = raw_base / "images"
230
- out_dir = exporter.output_dir
231
-
232
- img_dir.mkdir(parents=True, exist_ok=True)
233
- out_dir.mkdir(parents=True, exist_ok=True)
234
-
235
- cleaner = get_cleaner(
236
- enabled=config.clean_text,
237
- config=config.cleaner_cfg,
238
- )
239
-
240
- # --- Load book_info.json ---
241
- book_info = exporter._load_book_info(book_id)
242
- if not book_info:
243
- return None
244
-
245
- book_name = book_info.get("book_name", book_id)
246
- book_author = book_info.get("author", "")
247
- book_summary = book_info.get("summary", "")
248
-
249
- exporter.logger.info(
250
- "%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
251
- )
252
-
253
- # --- Compile columes ---
254
- volumes = book_info.get("volumes", [])
255
- if not volumes:
256
- exporter.logger.warning("%s No volumes found in metadata.", TAG)
257
-
258
- for vol_index, vol in enumerate(volumes, start=1):
259
- raw_name = vol.get("volume_name", "")
260
- raw_name = cleaner.clean_title(raw_name.replace(book_name, ""))
261
- vol_name = raw_name or f"Volume {vol_index}"
262
-
263
- # Batch-fetch chapters for this volume
264
- chap_ids = [
265
- chap["chapterId"]
266
- for chap in vol.get("chapters", [])
267
- if chap.get("chapterId")
268
- ]
269
- chap_map = exporter._get_chapters(book_id, chap_ids)
270
-
271
- vol_cover: Path | None = None
272
- vol_cover_url = vol.get("volume_cover", "")
273
- if config.include_cover and vol_cover_url:
274
- vol_cover = download(
275
- vol_cover_url,
276
- img_dir,
277
- headers=_IMG_HEADERS,
278
- on_exist="skip",
279
- default_suffix=DEFAULT_IMAGE_SUFFIX,
280
- )
281
-
282
- book, main_css = prepare_builder(
283
- site_name=exporter.site,
284
- book_id=book_id,
285
- title=book_name,
286
- author=book_author,
287
- description=vol.get("volume_intro") or book_summary,
288
- subject=book_info.get("tags", []),
289
- serial_status=book_info.get("serial_status", ""),
290
- word_count=vol.get("word_count", ""),
291
- cover_path=vol_cover,
292
- )
293
-
294
- for chap_meta in vol.get("chapters", []):
295
- chap_id = chap_meta.get("chapterId")
296
- if not chap_id:
297
- exporter.logger.warning(
298
- "%s Missing chapterId, skipping: %s",
299
- TAG,
300
- chap_meta,
301
- )
302
- continue
303
-
304
- chap_title = chap_meta.get("title", "")
305
- data = chap_map.get(chap_id)
306
- if not data:
307
- exporter.logger.info(
308
- "%s Missing chapter: %s (%s), skipping.",
309
- TAG,
310
- chap_title,
311
- chap_id,
312
- )
313
- continue
314
-
315
- title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
316
- content = cleaner.clean_content(data.get("content", ""))
317
- content = (
318
- inline_remote_images(book, content, img_dir, headers=_IMG_HEADERS)
319
- if config.include_picture
320
- else remove_all_images(content)
321
- )
322
- chap_html = build_epub_chapter(
323
- title=title,
324
- paragraphs=content,
325
- extras={},
326
- )
327
- book.add_chapter(
328
- Chapter(
329
- id=f"c_{chap_id}",
330
- filename=f"c{chap_id}.xhtml",
331
- title=title,
332
- content=chap_html,
333
- css=[main_css],
334
- )
335
- )
336
-
337
- out_name = exporter.get_filename(
338
- title=vol_name,
339
- author=book_info.get("author"),
340
- ext="epub",
341
- )
342
- finalize_export(
343
- book=book,
344
- out_dir=out_dir,
345
- filename=out_name,
346
- logger=exporter.logger,
347
- tag=TAG,
348
- )
349
- return None
@@ -1,66 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.linovelib.main_exporter
4
- -------------------------------------------------------
5
-
6
- Exporter implementation for Linovelib novels, supporting TXT and EPUB outputs.
7
- """
8
-
9
- from pathlib import Path
10
-
11
- from novel_downloader.core.exporters.base import BaseExporter
12
- from novel_downloader.core.exporters.registry import register_exporter
13
- from novel_downloader.models import ExporterConfig
14
-
15
- from .epub import (
16
- export_by_volume,
17
- export_whole_book,
18
- )
19
- from .txt import linovelib_export_as_txt
20
-
21
-
22
- @register_exporter(site_keys=["linovelib"])
23
- class LinovelibExporter(BaseExporter):
24
- """"""
25
-
26
- def __init__(
27
- self,
28
- config: ExporterConfig,
29
- ):
30
- """
31
- Initialize the linovelib exporter.
32
-
33
- :param config: A ExporterConfig object that defines
34
- save paths, formats, and options.
35
- """
36
- super().__init__(config, "linovelib")
37
-
38
- def export_as_txt(self, book_id: str) -> Path | None:
39
- """
40
- Compile and export a novel as a single .txt file.
41
-
42
- :param book_id: The book identifier (used to locate raw data)
43
- """
44
- self._init_chapter_storages(book_id)
45
- return linovelib_export_as_txt(self, book_id)
46
-
47
- def export_as_epub(self, book_id: str) -> Path | None:
48
- """
49
- Persist the assembled book as a EPUB (.epub) file.
50
-
51
- :param book_id: The book identifier.
52
- :raises NotImplementedError: If the method is not overridden.
53
- """
54
- self._init_chapter_storages(book_id)
55
-
56
- exporters = {
57
- "volume": export_by_volume,
58
- "book": export_whole_book,
59
- }
60
- try:
61
- export_fn = exporters[self._config.split_mode]
62
- except KeyError as err:
63
- raise ValueError(
64
- f"Unsupported split_mode: {self._config.split_mode!r}"
65
- ) from err
66
- return export_fn(self, book_id)
@@ -1,139 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.linovelib.txt
4
- ---------------------------------------------
5
-
6
- Defines `linovelib_export_as_txt` to assemble and export a Linovelib novel
7
- into a single `.txt` file. Intended for use by `LinovelibExporter`.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from pathlib import Path
13
- from typing import TYPE_CHECKING
14
-
15
- from novel_downloader.core.exporters.txt_util import (
16
- build_txt_chapter,
17
- build_txt_header,
18
- )
19
- from novel_downloader.utils import get_cleaner, write_file
20
-
21
- if TYPE_CHECKING:
22
- from .main_exporter import LinovelibExporter
23
-
24
-
25
- def linovelib_export_as_txt(
26
- exporter: LinovelibExporter,
27
- book_id: str,
28
- ) -> Path | None:
29
- """
30
- Export a novel as a single text file by merging all chapter data.
31
-
32
- Steps:
33
- 1. Read metadata from `book_info.json`.
34
- 2. For each volume:
35
- * Clean & append the volume title.
36
- * Clean & append optional volume intro.
37
- * Batch-fetch all chapters in this volume to minimize SQLite overhead.
38
- * For each chapter: clean title & content, then append.
39
- 3. Build a header block with metadata.
40
- 4. Concatenate header + all chapter blocks, then save as `{book_name}.txt`.
41
-
42
- :param exporter: The LinovelibExporter instance.
43
- :param book_id: Identifier of the novel (subdirectory under raw data).
44
- """
45
- TAG = "[exporter]"
46
- # --- Paths & options ---
47
- out_dir = exporter.output_dir
48
- out_dir.mkdir(parents=True, exist_ok=True)
49
- cleaner = get_cleaner(
50
- enabled=exporter._config.clean_text,
51
- config=exporter._config.cleaner_cfg,
52
- )
53
-
54
- # --- Load book_info.json ---
55
- book_info = exporter._load_book_info(book_id)
56
- if not book_info:
57
- return None
58
-
59
- # --- Compile chapters ---
60
- parts: list[str] = []
61
-
62
- for vol in book_info.get("volumes", []):
63
- vol_title = cleaner.clean_title(vol.get("volume_name", ""))
64
- if vol_title:
65
- parts.append(f"\n\n{'=' * 6} {vol_title} {'=' * 6}\n\n")
66
- exporter.logger.info("%s Processing volume: %s", TAG, vol_title)
67
-
68
- vol_intro = cleaner.clean_content(vol.get("volume_intro", ""))
69
- if vol_intro:
70
- parts.append(f"{vol_intro}\n\n")
71
-
72
- # Batch-fetch chapters for this volume
73
- chap_ids = [
74
- chap["chapterId"]
75
- for chap in vol.get("chapters", [])
76
- if chap.get("chapterId")
77
- ]
78
- chap_map = exporter._get_chapters(book_id, chap_ids)
79
-
80
- for chap_meta in vol.get("chapters", []):
81
- chap_id = chap_meta.get("chapterId")
82
- if not chap_id:
83
- exporter.logger.warning(
84
- "%s Missing chapterId, skipping: %s", TAG, chap_meta
85
- )
86
- continue
87
-
88
- chap_title = chap_meta.get("title", "")
89
- data = chap_map.get(chap_id)
90
- if not data:
91
- exporter.logger.info(
92
- "%s Missing chapter: %s (%s), skipping.",
93
- TAG,
94
- chap_title,
95
- chap_id,
96
- )
97
- continue
98
-
99
- # Extract structured fields
100
- title = cleaner.clean_title(data.get("title", chap_title))
101
- content = cleaner.clean_content(data.get("content", ""))
102
-
103
- parts.append(build_txt_chapter(title=title, paragraphs=content, extras={}))
104
-
105
- # --- Build header ---
106
- name = book_info.get("book_name") or ""
107
- author = book_info.get("author") or ""
108
- words = book_info.get("word_count") or ""
109
- updated = book_info.get("update_time") or ""
110
- summary = book_info.get("summary") or ""
111
-
112
- header_fields = [
113
- ("书名", name),
114
- ("作者", author),
115
- ("总字数", words),
116
- ("更新日期", updated),
117
- ("内容简介", summary),
118
- ]
119
-
120
- header = build_txt_header(header_fields)
121
-
122
- final_text = header + "\n\n" + "\n\n".join(parts).strip()
123
-
124
- # --- Determine output file path ---
125
- out_name = exporter.get_filename(title=name, author=author, ext="txt")
126
- out_path = out_dir / out_name
127
-
128
- # --- Save final text ---
129
- try:
130
- result = write_file(
131
- content=final_text,
132
- filepath=out_path,
133
- on_exist="overwrite",
134
- )
135
- exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
136
- except Exception as e:
137
- exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
138
- return None
139
- return result
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.txt_util
4
- ----------------------------------------
5
-
6
- Utilities for generating plain-text exports of novel content.
7
- """
8
-
9
- __all__ = [
10
- "build_txt_header",
11
- "build_txt_chapter",
12
- ]
13
-
14
- import re
15
-
16
- _IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
17
-
18
-
19
- def build_txt_header(fields: list[tuple[str, str]]) -> str:
20
- """
21
- Build a simple text header from label-value pairs, followed by a dashed separator.
22
-
23
- :param fields: List of (label, value) pairs.
24
- :return: A single string containing the formatted header.
25
- """
26
- header_lines = [f"{label}: {value}" for label, value in fields if value]
27
- header_lines += ["", "-" * 10, ""]
28
- return "\n".join(header_lines)
29
-
30
-
31
- def build_txt_chapter(
32
- title: str,
33
- paragraphs: str,
34
- extras: dict[str, str] | None = None,
35
- ) -> str:
36
- """
37
- Build a formatted chapter text block including title, body paragraphs,
38
- and optional extra sections.
39
-
40
- * Strips any `<img...>` tags from paragraphs.
41
- * Title appears first (stripped of surrounding whitespace).
42
- * Each non-blank line in `paragraphs` becomes its own paragraph.
43
-
44
- :param title: Chapter title.
45
- :param paragraphs: Raw multi-line string. Blank lines are ignored.
46
- :param extras: Optional dict mapping section titles to multi-line strings.
47
- :return: A string where title, paragraphs, and extras are joined by lines.
48
- """
49
- parts: list[str] = [title.strip()]
50
-
51
- # add each nonempty paragraph line
52
- paragraphs = _IMG_TAG_RE.sub("", paragraphs)
53
- for ln in paragraphs.splitlines():
54
- line = ln.strip()
55
- if line:
56
- parts.append(line)
57
-
58
- if extras:
59
- for title, text in extras.items():
60
- lines = [ln.strip() for ln in text.splitlines() if ln.strip()]
61
- if not lines:
62
- continue
63
- parts.append("---")
64
- parts.append(title.strip())
65
- parts.extend(lines)
66
-
67
- return "\n\n".join(parts)
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.parsers.qidian
4
- ------------------------------------
5
-
6
- """
7
-
8
- __all__ = ["QidianParser"]
9
-
10
- from .main_parser import QidianParser
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.parsers.qidian.utils
4
- ------------------------------------------
5
-
6
- Utility functions and helpers for parsing and decrypting Qidian novel pages
7
- """
8
-
9
- __all__ = ["get_decryptor"]
10
-
11
- from .node_decryptor import get_decryptor