novel-downloader 1.5.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +77 -64
- novel_downloader/cli/export.py +16 -20
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +65 -105
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +1 -0
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +14 -9
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +17 -11
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +46 -39
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/esjzone.py +61 -66
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/linovelib.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
- novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
- novel_downloader/core/parsers/qidian/main_parser.py +11 -38
- novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +31 -82
- novel_downloader/locales/zh.json +32 -83
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -22
- novel_downloader/utils/chapter_storage.py +3 -2
- novel_downloader/utils/constants.py +4 -29
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +1 -1
- novel_downloader/utils/epub/constants.py +57 -16
- novel_downloader/utils/epub/documents.py +88 -194
- novel_downloader/utils/epub/models.py +0 -14
- novel_downloader/utils/epub/utils.py +63 -96
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +3 -113
- novel_downloader/utils/file_utils/sanitize.py +0 -4
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/logger.py +8 -16
- novel_downloader/utils/network.py +2 -2
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +4 -8
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -17,6 +17,7 @@ from novel_downloader.core.exporters.epub_util import (
|
|
17
17
|
finalize_export,
|
18
18
|
inline_remote_images,
|
19
19
|
prepare_builder,
|
20
|
+
remove_all_images,
|
20
21
|
)
|
21
22
|
from novel_downloader.utils import (
|
22
23
|
download,
|
@@ -41,7 +42,7 @@ _IMG_HEADERS["Referer"] = "https://www.linovelib.com/"
|
|
41
42
|
def export_whole_book(
|
42
43
|
exporter: LinovelibExporter,
|
43
44
|
book_id: str,
|
44
|
-
) -> None:
|
45
|
+
) -> Path | None:
|
45
46
|
"""
|
46
47
|
Export a single novel (identified by `book_id`) to an EPUB file.
|
47
48
|
|
@@ -73,7 +74,7 @@ def export_whole_book(
|
|
73
74
|
# --- Load book_info.json ---
|
74
75
|
book_info = exporter._load_book_info(book_id)
|
75
76
|
if not book_info:
|
76
|
-
return
|
77
|
+
return None
|
77
78
|
|
78
79
|
book_name = book_info.get("book_name", book_id)
|
79
80
|
book_author = book_info.get("author", "")
|
@@ -99,7 +100,7 @@ def export_whole_book(
|
|
99
100
|
title=book_name,
|
100
101
|
author=book_author,
|
101
102
|
description=book_info.get("summary", ""),
|
102
|
-
subject=book_info.get("
|
103
|
+
subject=book_info.get("tags", []),
|
103
104
|
serial_status=book_info.get("serial_status", ""),
|
104
105
|
word_count=book_info.get("word_count", ""),
|
105
106
|
cover_path=cover_path,
|
@@ -118,7 +119,7 @@ def export_whole_book(
|
|
118
119
|
|
119
120
|
# Batch-fetch chapters for this volume
|
120
121
|
chap_ids = [
|
121
|
-
chap
|
122
|
+
chap["chapterId"]
|
122
123
|
for chap in vol.get("chapters", [])
|
123
124
|
if chap.get("chapterId")
|
124
125
|
]
|
@@ -152,7 +153,7 @@ def export_whole_book(
|
|
152
153
|
)
|
153
154
|
continue
|
154
155
|
|
155
|
-
chap_title =
|
156
|
+
chap_title = chap_meta.get("title", "")
|
156
157
|
data = chap_map.get(chap_id)
|
157
158
|
if not data:
|
158
159
|
exporter.logger.info(
|
@@ -165,11 +166,10 @@ def export_whole_book(
|
|
165
166
|
|
166
167
|
title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
|
167
168
|
content = cleaner.clean_content(data.get("content", ""))
|
168
|
-
content =
|
169
|
-
book,
|
170
|
-
|
171
|
-
|
172
|
-
headers=_IMG_HEADERS,
|
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
173
|
)
|
174
174
|
|
175
175
|
chap_html = build_epub_chapter(
|
@@ -177,7 +177,7 @@ def export_whole_book(
|
|
177
177
|
paragraphs=content,
|
178
178
|
extras={},
|
179
179
|
)
|
180
|
-
curr_vol.
|
180
|
+
curr_vol.chapters.append(
|
181
181
|
Chapter(
|
182
182
|
id=f"c_{chap_id}",
|
183
183
|
filename=f"c{chap_id}.xhtml",
|
@@ -195,20 +195,19 @@ def export_whole_book(
|
|
195
195
|
author=book_info.get("author"),
|
196
196
|
ext="epub",
|
197
197
|
)
|
198
|
-
finalize_export(
|
198
|
+
return finalize_export(
|
199
199
|
book=book,
|
200
200
|
out_dir=out_dir,
|
201
201
|
filename=out_name,
|
202
202
|
logger=exporter.logger,
|
203
203
|
tag=TAG,
|
204
204
|
)
|
205
|
-
return
|
206
205
|
|
207
206
|
|
208
207
|
def export_by_volume(
|
209
208
|
exporter: LinovelibExporter,
|
210
209
|
book_id: str,
|
211
|
-
) -> None:
|
210
|
+
) -> Path | None:
|
212
211
|
"""
|
213
212
|
Export each volume of a novel as a separate EPUB file.
|
214
213
|
|
@@ -241,7 +240,7 @@ def export_by_volume(
|
|
241
240
|
# --- Load book_info.json ---
|
242
241
|
book_info = exporter._load_book_info(book_id)
|
243
242
|
if not book_info:
|
244
|
-
return
|
243
|
+
return None
|
245
244
|
|
246
245
|
book_name = book_info.get("book_name", book_id)
|
247
246
|
book_author = book_info.get("author", "")
|
@@ -263,7 +262,7 @@ def export_by_volume(
|
|
263
262
|
|
264
263
|
# Batch-fetch chapters for this volume
|
265
264
|
chap_ids = [
|
266
|
-
chap
|
265
|
+
chap["chapterId"]
|
267
266
|
for chap in vol.get("chapters", [])
|
268
267
|
if chap.get("chapterId")
|
269
268
|
]
|
@@ -286,8 +285,8 @@ def export_by_volume(
|
|
286
285
|
title=book_name,
|
287
286
|
author=book_author,
|
288
287
|
description=vol.get("volume_intro") or book_summary,
|
289
|
-
subject=book_info.get("
|
290
|
-
serial_status=
|
288
|
+
subject=book_info.get("tags", []),
|
289
|
+
serial_status=book_info.get("serial_status", ""),
|
291
290
|
word_count=vol.get("word_count", ""),
|
292
291
|
cover_path=vol_cover,
|
293
292
|
)
|
@@ -302,7 +301,7 @@ def export_by_volume(
|
|
302
301
|
)
|
303
302
|
continue
|
304
303
|
|
305
|
-
chap_title =
|
304
|
+
chap_title = chap_meta.get("title", "")
|
306
305
|
data = chap_map.get(chap_id)
|
307
306
|
if not data:
|
308
307
|
exporter.logger.info(
|
@@ -315,11 +314,10 @@ def export_by_volume(
|
|
315
314
|
|
316
315
|
title = cleaner.clean_title(data.get("title", chap_title)) or chap_id
|
317
316
|
content = cleaner.clean_content(data.get("content", ""))
|
318
|
-
content =
|
319
|
-
book,
|
320
|
-
|
321
|
-
|
322
|
-
headers=_IMG_HEADERS,
|
317
|
+
content = (
|
318
|
+
inline_remote_images(book, content, img_dir, headers=_IMG_HEADERS)
|
319
|
+
if config.include_picture
|
320
|
+
else remove_all_images(content)
|
323
321
|
)
|
324
322
|
chap_html = build_epub_chapter(
|
325
323
|
title=title,
|
@@ -348,4 +346,4 @@ def export_by_volume(
|
|
348
346
|
logger=exporter.logger,
|
349
347
|
tag=TAG,
|
350
348
|
)
|
351
|
-
return
|
349
|
+
return None
|
@@ -3,13 +3,19 @@
|
|
3
3
|
novel_downloader.core.exporters.linovelib.main_exporter
|
4
4
|
-------------------------------------------------------
|
5
5
|
|
6
|
+
Exporter implementation for Linovelib novels, supporting TXT and EPUB outputs.
|
6
7
|
"""
|
7
8
|
|
9
|
+
from pathlib import Path
|
8
10
|
|
9
11
|
from novel_downloader.core.exporters.base import BaseExporter
|
10
12
|
from novel_downloader.core.exporters.registry import register_exporter
|
11
13
|
from novel_downloader.models import ExporterConfig
|
12
14
|
|
15
|
+
from .epub import (
|
16
|
+
export_by_volume,
|
17
|
+
export_whole_book,
|
18
|
+
)
|
13
19
|
from .txt import linovelib_export_as_txt
|
14
20
|
|
15
21
|
|
@@ -29,7 +35,7 @@ class LinovelibExporter(BaseExporter):
|
|
29
35
|
"""
|
30
36
|
super().__init__(config, "linovelib")
|
31
37
|
|
32
|
-
def export_as_txt(self, book_id: str) -> None:
|
38
|
+
def export_as_txt(self, book_id: str) -> Path | None:
|
33
39
|
"""
|
34
40
|
Compile and export a novel as a single .txt file.
|
35
41
|
|
@@ -38,23 +44,13 @@ class LinovelibExporter(BaseExporter):
|
|
38
44
|
self._init_chapter_storages(book_id)
|
39
45
|
return linovelib_export_as_txt(self, book_id)
|
40
46
|
|
41
|
-
def export_as_epub(self, book_id: str) -> None:
|
47
|
+
def export_as_epub(self, book_id: str) -> Path | None:
|
42
48
|
"""
|
43
49
|
Persist the assembled book as a EPUB (.epub) file.
|
44
50
|
|
45
51
|
:param book_id: The book identifier.
|
46
52
|
:raises NotImplementedError: If the method is not overridden.
|
47
53
|
"""
|
48
|
-
try:
|
49
|
-
from .epub import (
|
50
|
-
export_by_volume,
|
51
|
-
export_whole_book,
|
52
|
-
)
|
53
|
-
except ImportError as err:
|
54
|
-
raise NotImplementedError(
|
55
|
-
"EPUB export not supported. Please install 'ebooklib'"
|
56
|
-
) from err
|
57
|
-
|
58
54
|
self._init_chapter_storages(book_id)
|
59
55
|
|
60
56
|
exporters = {
|
@@ -9,13 +9,14 @@ into a single `.txt` file. Intended for use by `LinovelibExporter`.
|
|
9
9
|
|
10
10
|
from __future__ import annotations
|
11
11
|
|
12
|
+
from pathlib import Path
|
12
13
|
from typing import TYPE_CHECKING
|
13
14
|
|
14
15
|
from novel_downloader.core.exporters.txt_util import (
|
15
16
|
build_txt_chapter,
|
16
17
|
build_txt_header,
|
17
18
|
)
|
18
|
-
from novel_downloader.utils import get_cleaner,
|
19
|
+
from novel_downloader.utils import get_cleaner, write_file
|
19
20
|
|
20
21
|
if TYPE_CHECKING:
|
21
22
|
from .main_exporter import LinovelibExporter
|
@@ -24,17 +25,17 @@ if TYPE_CHECKING:
|
|
24
25
|
def linovelib_export_as_txt(
|
25
26
|
exporter: LinovelibExporter,
|
26
27
|
book_id: str,
|
27
|
-
) -> None:
|
28
|
+
) -> Path | None:
|
28
29
|
"""
|
29
30
|
Export a novel as a single text file by merging all chapter data.
|
30
31
|
|
31
32
|
Steps:
|
32
33
|
1. Read metadata from `book_info.json`.
|
33
34
|
2. For each volume:
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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.
|
38
39
|
3. Build a header block with metadata.
|
39
40
|
4. Concatenate header + all chapter blocks, then save as `{book_name}.txt`.
|
40
41
|
|
@@ -53,7 +54,7 @@ def linovelib_export_as_txt(
|
|
53
54
|
# --- Load book_info.json ---
|
54
55
|
book_info = exporter._load_book_info(book_id)
|
55
56
|
if not book_info:
|
56
|
-
return
|
57
|
+
return None
|
57
58
|
|
58
59
|
# --- Compile chapters ---
|
59
60
|
parts: list[str] = []
|
@@ -70,7 +71,7 @@ def linovelib_export_as_txt(
|
|
70
71
|
|
71
72
|
# Batch-fetch chapters for this volume
|
72
73
|
chap_ids = [
|
73
|
-
chap
|
74
|
+
chap["chapterId"]
|
74
75
|
for chap in vol.get("chapters", [])
|
75
76
|
if chap.get("chapterId")
|
76
77
|
]
|
@@ -84,7 +85,7 @@ def linovelib_export_as_txt(
|
|
84
85
|
)
|
85
86
|
continue
|
86
87
|
|
87
|
-
chap_title =
|
88
|
+
chap_title = chap_meta.get("title", "")
|
88
89
|
data = chap_map.get(chap_id)
|
89
90
|
if not data:
|
90
91
|
exporter.logger.info(
|
@@ -125,9 +126,14 @@ def linovelib_export_as_txt(
|
|
125
126
|
out_path = out_dir / out_name
|
126
127
|
|
127
128
|
# --- Save final text ---
|
128
|
-
result =
|
129
|
+
result = write_file(
|
130
|
+
content=final_text,
|
131
|
+
filepath=out_path,
|
132
|
+
write_mode="w",
|
133
|
+
on_exist="overwrite",
|
134
|
+
)
|
129
135
|
if result:
|
130
136
|
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
131
137
|
else:
|
132
138
|
exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
|
133
|
-
return
|
139
|
+
return result
|
@@ -3,9 +3,7 @@
|
|
3
3
|
novel_downloader.core.exporters.qidian
|
4
4
|
--------------------------------------
|
5
5
|
|
6
|
-
|
7
|
-
of novels sourced from Qidian (起点中文网). It implements the platform-specific
|
8
|
-
logic required to structure and export novel content into desired formats.
|
6
|
+
Exporter implementation for Qidian novels, supporting plain and encrypted sources.
|
9
7
|
"""
|
10
8
|
|
11
9
|
__all__ = ["QidianExporter"]
|
@@ -31,8 +29,4 @@ class QidianExporter(CommonExporter):
|
|
31
29
|
self,
|
32
30
|
config: ExporterConfig,
|
33
31
|
):
|
34
|
-
super().__init__(
|
35
|
-
config,
|
36
|
-
site="qidian",
|
37
|
-
priorities=self.PRIORITIES_MAP,
|
38
|
-
)
|
32
|
+
super().__init__(config, site="qidian")
|
@@ -3,6 +3,7 @@
|
|
3
3
|
novel_downloader.core.exporters.registry
|
4
4
|
----------------------------------------
|
5
5
|
|
6
|
+
Registry and factory helpers for creating site-specific or common exporters.
|
6
7
|
"""
|
7
8
|
|
8
9
|
__all__ = ["register_exporter", "get_exporter"]
|
@@ -10,6 +11,7 @@ __all__ = ["register_exporter", "get_exporter"]
|
|
10
11
|
from collections.abc import Callable, Sequence
|
11
12
|
from typing import TypeVar
|
12
13
|
|
14
|
+
from novel_downloader.core.exporters.common import CommonExporter
|
13
15
|
from novel_downloader.core.interfaces import ExporterProtocol
|
14
16
|
from novel_downloader.models import ExporterConfig
|
15
17
|
|
@@ -48,6 +50,6 @@ def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
|
|
48
50
|
site_key = site.lower()
|
49
51
|
try:
|
50
52
|
exporter_cls = _EXPORTER_MAP[site_key]
|
51
|
-
except KeyError
|
52
|
-
|
53
|
+
except KeyError:
|
54
|
+
return CommonExporter(config, site_key)
|
53
55
|
return exporter_cls(config)
|
@@ -13,7 +13,7 @@ __all__ = [
|
|
13
13
|
|
14
14
|
import re
|
15
15
|
|
16
|
-
_IMG_TAG_RE = re.compile(r"<img[^>]*>")
|
16
|
+
_IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
|
17
17
|
|
18
18
|
|
19
19
|
def build_txt_header(fields: list[tuple[str, str]]) -> str:
|
@@ -37,14 +37,14 @@ def build_txt_chapter(
|
|
37
37
|
Build a formatted chapter text block including title, body paragraphs,
|
38
38
|
and optional extra sections.
|
39
39
|
|
40
|
-
|
41
|
-
|
42
|
-
|
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
43
|
|
44
|
-
:param title:
|
44
|
+
:param title: Chapter title.
|
45
45
|
:param paragraphs: Raw multi-line string. Blank lines are ignored.
|
46
|
-
:param extras:
|
47
|
-
:return:
|
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
48
|
"""
|
49
49
|
parts: list[str] = [title.strip()]
|
50
50
|
|
@@ -3,65 +3,71 @@
|
|
3
3
|
novel_downloader.core.fetchers
|
4
4
|
------------------------------
|
5
5
|
|
6
|
-
|
7
|
-
Each submodule corresponds to a specific site and encapsulates the logic needed
|
8
|
-
to perform network interactions, such as logging in, sending requests,
|
9
|
-
or interacting with browser/session-based sources.
|
10
|
-
|
11
|
-
Subpackages:
|
12
|
-
- biquge (笔趣阁)
|
13
|
-
- esjzone (ESJ Zone)
|
14
|
-
- linovelib (哔哩轻小说)
|
15
|
-
- qianbi (铅笔小说)
|
16
|
-
- qidian (起点中文网)
|
17
|
-
- sfacg (SF轻小说)
|
18
|
-
- yamibo (百合会)
|
6
|
+
Fetcher implementations for retrieving raw data and HTML from various novel sources
|
19
7
|
"""
|
20
8
|
|
21
9
|
__all__ = [
|
22
10
|
"get_fetcher",
|
23
|
-
"
|
11
|
+
"AaatxtSession",
|
24
12
|
"BiqugeSession",
|
25
|
-
"
|
13
|
+
"BiquyueduSession",
|
14
|
+
"DxmwxSession",
|
15
|
+
"EightnovelSession",
|
26
16
|
"EsjzoneSession",
|
27
|
-
"
|
17
|
+
"GuidayeSession",
|
18
|
+
"HetushuSession",
|
19
|
+
"I25zwSession",
|
20
|
+
"Ixdzs8Session",
|
21
|
+
"Jpxs123Session",
|
22
|
+
"LewennSession",
|
28
23
|
"LinovelibSession",
|
29
|
-
"
|
24
|
+
"PiaotiaSession",
|
25
|
+
"QbtrSession",
|
30
26
|
"QianbiSession",
|
31
|
-
"QidianBrowser",
|
32
27
|
"QidianSession",
|
33
|
-
"
|
28
|
+
"Quanben5Session",
|
34
29
|
"SfacgSession",
|
35
|
-
"
|
30
|
+
"ShencouSession",
|
31
|
+
"ShuhaigeSession",
|
32
|
+
"TongrenquanSession",
|
33
|
+
"TtkanSession",
|
34
|
+
"WanbengoSession",
|
35
|
+
"XiaoshuowuSession",
|
36
|
+
"XiguashuwuSession",
|
37
|
+
"Xs63bSession",
|
38
|
+
"XshbookSession",
|
36
39
|
"YamiboSession",
|
40
|
+
"YibigeSession",
|
37
41
|
]
|
38
42
|
|
39
|
-
from .
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
from .
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
from .
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
from .
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
from .qidian import
|
56
|
-
|
57
|
-
QidianSession,
|
58
|
-
)
|
43
|
+
from .aaatxt import AaatxtSession
|
44
|
+
from .b520 import BiqugeSession
|
45
|
+
from .biquyuedu import BiquyueduSession
|
46
|
+
from .dxmwx import DxmwxSession
|
47
|
+
from .eightnovel import EightnovelSession
|
48
|
+
from .esjzone import EsjzoneSession
|
49
|
+
from .guidaye import GuidayeSession
|
50
|
+
from .hetushu import HetushuSession
|
51
|
+
from .i25zw import I25zwSession
|
52
|
+
from .ixdzs8 import Ixdzs8Session
|
53
|
+
from .jpxs123 import Jpxs123Session
|
54
|
+
from .lewenn import LewennSession
|
55
|
+
from .linovelib import LinovelibSession
|
56
|
+
from .piaotia import PiaotiaSession
|
57
|
+
from .qbtr import QbtrSession
|
58
|
+
from .qianbi import QianbiSession
|
59
|
+
from .qidian import QidianSession
|
60
|
+
from .quanben5 import Quanben5Session
|
59
61
|
from .registry import get_fetcher
|
60
|
-
from .sfacg import
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
from .
|
65
|
-
|
66
|
-
|
67
|
-
|
62
|
+
from .sfacg import SfacgSession
|
63
|
+
from .shencou import ShencouSession
|
64
|
+
from .shuhaige import ShuhaigeSession
|
65
|
+
from .tongrenquan import TongrenquanSession
|
66
|
+
from .ttkan import TtkanSession
|
67
|
+
from .wanbengo import WanbengoSession
|
68
|
+
from .xiaoshuowu import XiaoshuowuSession
|
69
|
+
from .xiguashuwu import XiguashuwuSession
|
70
|
+
from .xs63b import Xs63bSession
|
71
|
+
from .xshbook import XshbookSession
|
72
|
+
from .yamibo import YamiboSession
|
73
|
+
from .yibige import YibigeSession
|
@@ -0,0 +1,83 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.fetchers.aaatxt
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from novel_downloader.core.fetchers.base import BaseSession
|
11
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
|
+
from novel_downloader.models import FetcherConfig
|
13
|
+
|
14
|
+
|
15
|
+
@register_fetcher(
|
16
|
+
site_keys=["aaatxt"],
|
17
|
+
)
|
18
|
+
class AaatxtSession(BaseSession):
|
19
|
+
"""
|
20
|
+
A session class for interacting with the 3A电子书 (www.aaatxt.com) novel website.
|
21
|
+
"""
|
22
|
+
|
23
|
+
BOOK_INFO_URL = "http://www.aaatxt.com/shu/{book_id}.html"
|
24
|
+
CHAPTER_URL = "http://www.aaatxt.com/yuedu/{chapter_id}.html"
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
config: FetcherConfig,
|
29
|
+
cookies: dict[str, str] | None = None,
|
30
|
+
**kwargs: Any,
|
31
|
+
) -> None:
|
32
|
+
super().__init__("aaatxt", config, cookies, **kwargs)
|
33
|
+
|
34
|
+
async def get_book_info(
|
35
|
+
self,
|
36
|
+
book_id: str,
|
37
|
+
**kwargs: Any,
|
38
|
+
) -> list[str]:
|
39
|
+
"""
|
40
|
+
Fetch the raw HTML of the book info page asynchronously.
|
41
|
+
|
42
|
+
:param book_id: The book identifier.
|
43
|
+
:return: The page content as string list.
|
44
|
+
"""
|
45
|
+
url = self.book_info_url(book_id=book_id)
|
46
|
+
return [await self.fetch(url, **kwargs)]
|
47
|
+
|
48
|
+
async def get_book_chapter(
|
49
|
+
self,
|
50
|
+
book_id: str,
|
51
|
+
chapter_id: str,
|
52
|
+
**kwargs: Any,
|
53
|
+
) -> list[str]:
|
54
|
+
"""
|
55
|
+
Fetch the raw HTML of a single chapter asynchronously.
|
56
|
+
|
57
|
+
:param book_id: The book identifier.
|
58
|
+
:param chapter_id: The chapter identifier.
|
59
|
+
:return: The page content as string list.
|
60
|
+
"""
|
61
|
+
url = self.chapter_url(chapter_id=chapter_id)
|
62
|
+
return [await self.fetch(url, encoding="gb2312", **kwargs)]
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def book_info_url(cls, book_id: str) -> str:
|
66
|
+
"""
|
67
|
+
Construct the URL for fetching a book's info page.
|
68
|
+
|
69
|
+
:param book_id: The identifier of the book.
|
70
|
+
:return: Fully qualified URL for the book info page.
|
71
|
+
"""
|
72
|
+
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
73
|
+
|
74
|
+
@classmethod
|
75
|
+
def chapter_url(cls, chapter_id: str) -> str:
|
76
|
+
"""
|
77
|
+
Construct the URL for fetching a specific chapter.
|
78
|
+
|
79
|
+
:param book_id: The identifier of the book.
|
80
|
+
:param chapter_id: The identifier of the chapter.
|
81
|
+
:return: Fully qualified chapter URL.
|
82
|
+
"""
|
83
|
+
return cls.CHAPTER_URL.format(chapter_id=chapter_id)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.fetchers.
|
4
|
-
|
3
|
+
novel_downloader.core.fetchers.b520
|
4
|
+
-----------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -13,12 +13,11 @@ from novel_downloader.models import FetcherConfig
|
|
13
13
|
|
14
14
|
|
15
15
|
@register_fetcher(
|
16
|
-
site_keys=["biquge", "bqg"],
|
17
|
-
backends=["session"],
|
16
|
+
site_keys=["biquge", "bqg", "b520"],
|
18
17
|
)
|
19
18
|
class BiqugeSession(BaseSession):
|
20
19
|
"""
|
21
|
-
A session class for interacting with the
|
20
|
+
A session class for interacting with the 笔趣阁 (www.b520.cc) novel website.
|
22
21
|
"""
|
23
22
|
|
24
23
|
BOOK_INFO_URL = "http://www.b520.cc/{book_id}/"
|
@@ -41,7 +40,7 @@ class BiqugeSession(BaseSession):
|
|
41
40
|
Fetch the raw HTML of the book info page asynchronously.
|
42
41
|
|
43
42
|
:param book_id: The book identifier.
|
44
|
-
:return: The page content as
|
43
|
+
:return: The page content as string list.
|
45
44
|
"""
|
46
45
|
url = self.book_info_url(book_id=book_id)
|
47
46
|
return [await self.fetch(url, **kwargs)]
|
@@ -57,7 +56,7 @@ class BiqugeSession(BaseSession):
|
|
57
56
|
|
58
57
|
:param book_id: The book identifier.
|
59
58
|
:param chapter_id: The chapter identifier.
|
60
|
-
:return: The
|
59
|
+
:return: The page content as string list.
|
61
60
|
"""
|
62
61
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
63
62
|
return [await self.fetch(url, encoding="gbk", **kwargs)]
|
@@ -82,7 +81,3 @@ class BiqugeSession(BaseSession):
|
|
82
81
|
:return: Fully qualified chapter URL.
|
83
82
|
"""
|
84
83
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
85
|
-
|
86
|
-
@property
|
87
|
-
def hostname(self) -> str:
|
88
|
-
return "www.b520.cc"
|