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
@@ -3,18 +3,23 @@
|
|
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
|
|
8
|
-
from
|
9
|
-
from typing import Any
|
9
|
+
from pathlib import Path
|
10
10
|
|
11
11
|
from novel_downloader.core.exporters.base import BaseExporter
|
12
|
+
from novel_downloader.core.exporters.registry import register_exporter
|
12
13
|
from novel_downloader.models import ExporterConfig
|
13
|
-
from novel_downloader.utils.chapter_storage import ChapterStorage
|
14
14
|
|
15
|
+
from .epub import (
|
16
|
+
export_by_volume,
|
17
|
+
export_whole_book,
|
18
|
+
)
|
15
19
|
from .txt import linovelib_export_as_txt
|
16
20
|
|
17
21
|
|
22
|
+
@register_exporter(site_keys=["linovelib"])
|
18
23
|
class LinovelibExporter(BaseExporter):
|
19
24
|
""""""
|
20
25
|
|
@@ -29,10 +34,8 @@ class LinovelibExporter(BaseExporter):
|
|
29
34
|
save paths, formats, and options.
|
30
35
|
"""
|
31
36
|
super().__init__(config, "linovelib")
|
32
|
-
self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
|
33
|
-
self._chap_folders: list[str] = ["chapters"]
|
34
37
|
|
35
|
-
def export_as_txt(self, book_id: str) -> None:
|
38
|
+
def export_as_txt(self, book_id: str) -> Path | None:
|
36
39
|
"""
|
37
40
|
Compile and export a novel as a single .txt file.
|
38
41
|
|
@@ -41,23 +44,13 @@ class LinovelibExporter(BaseExporter):
|
|
41
44
|
self._init_chapter_storages(book_id)
|
42
45
|
return linovelib_export_as_txt(self, book_id)
|
43
46
|
|
44
|
-
def export_as_epub(self, book_id: str) -> None:
|
47
|
+
def export_as_epub(self, book_id: str) -> Path | None:
|
45
48
|
"""
|
46
49
|
Persist the assembled book as a EPUB (.epub) file.
|
47
50
|
|
48
51
|
:param book_id: The book identifier.
|
49
52
|
:raises NotImplementedError: If the method is not overridden.
|
50
53
|
"""
|
51
|
-
try:
|
52
|
-
from .epub import (
|
53
|
-
export_by_volume,
|
54
|
-
export_whole_book,
|
55
|
-
)
|
56
|
-
except ImportError as err:
|
57
|
-
raise NotImplementedError(
|
58
|
-
"EPUB export not supported. Please install 'ebooklib'"
|
59
|
-
) from err
|
60
|
-
|
61
54
|
self._init_chapter_storages(book_id)
|
62
55
|
|
63
56
|
exporters = {
|
@@ -71,57 +64,3 @@ class LinovelibExporter(BaseExporter):
|
|
71
64
|
f"Unsupported split_mode: {self._config.split_mode!r}"
|
72
65
|
) from err
|
73
66
|
return export_fn(self, book_id)
|
74
|
-
|
75
|
-
@property
|
76
|
-
def site(self) -> str:
|
77
|
-
"""
|
78
|
-
Get the site identifier.
|
79
|
-
|
80
|
-
:return: The site string.
|
81
|
-
"""
|
82
|
-
return self._site
|
83
|
-
|
84
|
-
@site.setter
|
85
|
-
def site(self, value: str) -> None:
|
86
|
-
"""
|
87
|
-
Set the site identifier.
|
88
|
-
|
89
|
-
:param value: New site string to set.
|
90
|
-
"""
|
91
|
-
self._site = value
|
92
|
-
|
93
|
-
def _get_chapter(
|
94
|
-
self,
|
95
|
-
book_id: str,
|
96
|
-
chap_id: str,
|
97
|
-
) -> Mapping[str, Any]:
|
98
|
-
for storage in self._chapter_storage_cache[book_id]:
|
99
|
-
data = storage.get(chap_id)
|
100
|
-
if data:
|
101
|
-
return data
|
102
|
-
return {}
|
103
|
-
|
104
|
-
def _init_chapter_storages(self, book_id: str) -> None:
|
105
|
-
if book_id in self._chapter_storage_cache:
|
106
|
-
return
|
107
|
-
raw_base = self._raw_data_dir / book_id
|
108
|
-
self._chapter_storage_cache[book_id] = [
|
109
|
-
ChapterStorage(
|
110
|
-
raw_base=raw_base,
|
111
|
-
namespace=ns,
|
112
|
-
backend_type=self._config.storage_backend,
|
113
|
-
)
|
114
|
-
for ns in self._chap_folders
|
115
|
-
]
|
116
|
-
|
117
|
-
def _on_close(self) -> None:
|
118
|
-
"""
|
119
|
-
Close all ChapterStorage connections in the cache.
|
120
|
-
"""
|
121
|
-
for storages in self._chapter_storage_cache.values():
|
122
|
-
for storage in storages:
|
123
|
-
try:
|
124
|
-
storage.close()
|
125
|
-
except Exception as e:
|
126
|
-
self.logger.warning("Failed to close storage %s: %s", storage, e)
|
127
|
-
self._chapter_storage_cache.clear()
|
@@ -3,22 +3,20 @@
|
|
3
3
|
novel_downloader.core.exporters.linovelib.txt
|
4
4
|
---------------------------------------------
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
This module defines `linovelib_export_as_txt` function, which assembles and formats
|
9
|
-
a novel based on metadata and chapter files found in the raw data directory.
|
10
|
-
It is intended to be used by `LinovelibExporter` as part of the save/export process.
|
6
|
+
Defines `linovelib_export_as_txt` to assemble and export a Linovelib novel
|
7
|
+
into a single `.txt` file. Intended for use by `LinovelibExporter`.
|
11
8
|
"""
|
12
9
|
|
13
10
|
from __future__ import annotations
|
14
11
|
|
15
|
-
import
|
12
|
+
from pathlib import Path
|
16
13
|
from typing import TYPE_CHECKING
|
17
14
|
|
18
|
-
from novel_downloader.
|
19
|
-
|
20
|
-
|
15
|
+
from novel_downloader.core.exporters.txt_util import (
|
16
|
+
build_txt_chapter,
|
17
|
+
build_txt_header,
|
21
18
|
)
|
19
|
+
from novel_downloader.utils import get_cleaner, write_file
|
22
20
|
|
23
21
|
if TYPE_CHECKING:
|
24
22
|
from .main_exporter import LinovelibExporter
|
@@ -27,58 +25,71 @@ if TYPE_CHECKING:
|
|
27
25
|
def linovelib_export_as_txt(
|
28
26
|
exporter: LinovelibExporter,
|
29
27
|
book_id: str,
|
30
|
-
) -> None:
|
28
|
+
) -> Path | None:
|
31
29
|
"""
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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).
|
42
44
|
"""
|
43
45
|
TAG = "[exporter]"
|
44
46
|
# --- Paths & options ---
|
45
|
-
raw_base = exporter._raw_data_dir / book_id
|
46
47
|
out_dir = exporter.output_dir
|
47
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
|
+
)
|
48
53
|
|
49
54
|
# --- Load book_info.json ---
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
book_info = json.loads(info_text)
|
54
|
-
except Exception as e:
|
55
|
-
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
56
|
-
return
|
55
|
+
book_info = exporter._load_book_info(book_id)
|
56
|
+
if not book_info:
|
57
|
+
return None
|
57
58
|
|
58
59
|
# --- Compile chapters ---
|
59
60
|
parts: list[str] = []
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
|
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
69
|
if vol_intro:
|
70
70
|
parts.append(f"{vol_intro}\n\n")
|
71
|
-
|
72
|
-
|
73
|
-
|
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")
|
74
82
|
if not chap_id:
|
75
|
-
exporter.logger.warning(
|
83
|
+
exporter.logger.warning(
|
84
|
+
"%s Missing chapterId, skipping: %s", TAG, chap_meta
|
85
|
+
)
|
76
86
|
continue
|
77
87
|
|
78
|
-
|
79
|
-
|
88
|
+
chap_title = chap_meta.get("title", "")
|
89
|
+
data = chap_map.get(chap_id)
|
90
|
+
if not data:
|
80
91
|
exporter.logger.info(
|
81
|
-
"%s Missing chapter
|
92
|
+
"%s Missing chapter: %s (%s), skipping.",
|
82
93
|
TAG,
|
83
94
|
chap_title,
|
84
95
|
chap_id,
|
@@ -86,33 +97,27 @@ def linovelib_export_as_txt(
|
|
86
97
|
continue
|
87
98
|
|
88
99
|
# Extract structured fields
|
89
|
-
title =
|
90
|
-
content =
|
100
|
+
title = cleaner.clean_title(data.get("title", chap_title))
|
101
|
+
content = cleaner.clean_content(data.get("content", ""))
|
91
102
|
|
92
|
-
parts.append(
|
103
|
+
parts.append(build_txt_chapter(title=title, paragraphs=content, extras={}))
|
93
104
|
|
94
105
|
# --- Build header ---
|
95
|
-
name = book_info.get("book_name")
|
96
|
-
author = book_info.get("author")
|
97
|
-
words = book_info.get("word_count")
|
98
|
-
updated = book_info.get("update_time")
|
99
|
-
summary = book_info.get("summary")
|
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 ""
|
100
111
|
|
101
|
-
|
112
|
+
header_fields = [
|
102
113
|
("书名", name),
|
103
114
|
("作者", author),
|
104
115
|
("总字数", words),
|
105
116
|
("更新日期", updated),
|
117
|
+
("内容简介", summary),
|
106
118
|
]
|
107
|
-
header_lines = [f"{label}: {value}" for label, value in fields if value]
|
108
|
-
|
109
|
-
if summary:
|
110
|
-
header_lines.append("内容简介:")
|
111
|
-
header_lines.append(summary)
|
112
|
-
|
113
|
-
header_lines += ["", "-" * 10, ""]
|
114
119
|
|
115
|
-
header =
|
120
|
+
header = build_txt_header(header_fields)
|
116
121
|
|
117
122
|
final_text = header + "\n\n" + "\n\n".join(parts).strip()
|
118
123
|
|
@@ -121,9 +126,14 @@ def linovelib_export_as_txt(
|
|
121
126
|
out_path = out_dir / out_name
|
122
127
|
|
123
128
|
# --- Save final text ---
|
124
|
-
|
125
|
-
|
129
|
+
result = write_file(
|
130
|
+
content=final_text,
|
131
|
+
filepath=out_path,
|
132
|
+
write_mode="w",
|
133
|
+
on_exist="overwrite",
|
134
|
+
)
|
135
|
+
if result:
|
126
136
|
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
127
|
-
|
128
|
-
exporter.logger.error("%s Failed to
|
129
|
-
return
|
137
|
+
else:
|
138
|
+
exporter.logger.error("%s Failed to write novel to %s", TAG, out_path)
|
139
|
+
return result
|
@@ -3,26 +3,30 @@
|
|
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
|
|
9
|
+
__all__ = ["QidianExporter"]
|
10
|
+
|
11
|
+
from novel_downloader.core.exporters.registry import register_exporter
|
11
12
|
from novel_downloader.models import ExporterConfig
|
12
13
|
|
13
14
|
from .common import CommonExporter
|
14
15
|
|
15
16
|
|
17
|
+
@register_exporter(site_keys=["qidian", "qd"])
|
16
18
|
class QidianExporter(CommonExporter):
|
19
|
+
""" """
|
20
|
+
|
21
|
+
DEFAULT_SOURCE_ID = 0
|
22
|
+
ENCRYPTED_SOURCE_ID = 1
|
23
|
+
PRIORITIES_MAP = {
|
24
|
+
DEFAULT_SOURCE_ID: 0,
|
25
|
+
ENCRYPTED_SOURCE_ID: 1,
|
26
|
+
}
|
27
|
+
|
17
28
|
def __init__(
|
18
29
|
self,
|
19
30
|
config: ExporterConfig,
|
20
31
|
):
|
21
|
-
super().__init__(
|
22
|
-
config,
|
23
|
-
site="qidian",
|
24
|
-
chap_folders=["chapters", "encrypted_chapters"],
|
25
|
-
)
|
26
|
-
|
27
|
-
|
28
|
-
__all__ = ["QidianExporter"]
|
32
|
+
super().__init__(config, site="qidian")
|
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.registry
|
4
|
+
----------------------------------------
|
5
|
+
|
6
|
+
Registry and factory helpers for creating site-specific or common exporters.
|
7
|
+
"""
|
8
|
+
|
9
|
+
__all__ = ["register_exporter", "get_exporter"]
|
10
|
+
|
11
|
+
from collections.abc import Callable, Sequence
|
12
|
+
from typing import TypeVar
|
13
|
+
|
14
|
+
from novel_downloader.core.exporters.common import CommonExporter
|
15
|
+
from novel_downloader.core.interfaces import ExporterProtocol
|
16
|
+
from novel_downloader.models import ExporterConfig
|
17
|
+
|
18
|
+
ExporterBuilder = Callable[[ExporterConfig], ExporterProtocol]
|
19
|
+
|
20
|
+
E = TypeVar("E", bound=ExporterProtocol)
|
21
|
+
_EXPORTER_MAP: dict[str, ExporterBuilder] = {}
|
22
|
+
|
23
|
+
|
24
|
+
def register_exporter(
|
25
|
+
site_keys: Sequence[str],
|
26
|
+
) -> Callable[[type[E]], type[E]]:
|
27
|
+
"""
|
28
|
+
Decorator to register a exporter class under given keys.
|
29
|
+
|
30
|
+
:param site_keys: Sequence of site identifiers
|
31
|
+
:return: A class decorator that populates _EXPORTER_MAP.
|
32
|
+
"""
|
33
|
+
|
34
|
+
def decorator(cls: type[E]) -> type[E]:
|
35
|
+
for key in site_keys:
|
36
|
+
_EXPORTER_MAP[key.lower()] = cls
|
37
|
+
return cls
|
38
|
+
|
39
|
+
return decorator
|
40
|
+
|
41
|
+
|
42
|
+
def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
|
43
|
+
"""
|
44
|
+
Returns a site-specific exporter instance.
|
45
|
+
|
46
|
+
:param site: Site name (e.g., 'qidian')
|
47
|
+
:param config: Configuration for the exporter
|
48
|
+
:return: An instance of a exporter class
|
49
|
+
"""
|
50
|
+
site_key = site.lower()
|
51
|
+
try:
|
52
|
+
exporter_cls = _EXPORTER_MAP[site_key]
|
53
|
+
except KeyError:
|
54
|
+
return CommonExporter(config, site_key)
|
55
|
+
return exporter_cls(config)
|
@@ -0,0 +1,67 @@
|
|
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)
|
@@ -3,70 +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 (百合会)
|
19
|
-
- common (通用架构)
|
6
|
+
Fetcher implementations for retrieving raw data and HTML from various novel sources
|
20
7
|
"""
|
21
8
|
|
22
|
-
from .biquge import (
|
23
|
-
BiqugeBrowser,
|
24
|
-
BiqugeSession,
|
25
|
-
)
|
26
|
-
from .common import (
|
27
|
-
CommonBrowser,
|
28
|
-
CommonSession,
|
29
|
-
)
|
30
|
-
from .esjzone import (
|
31
|
-
EsjzoneBrowser,
|
32
|
-
EsjzoneSession,
|
33
|
-
)
|
34
|
-
from .linovelib import (
|
35
|
-
LinovelibBrowser,
|
36
|
-
LinovelibSession,
|
37
|
-
)
|
38
|
-
from .qianbi import (
|
39
|
-
QianbiBrowser,
|
40
|
-
QianbiSession,
|
41
|
-
)
|
42
|
-
from .qidian import (
|
43
|
-
QidianBrowser,
|
44
|
-
QidianSession,
|
45
|
-
)
|
46
|
-
from .sfacg import (
|
47
|
-
SfacgBrowser,
|
48
|
-
SfacgSession,
|
49
|
-
)
|
50
|
-
from .yamibo import (
|
51
|
-
YamiboBrowser,
|
52
|
-
YamiboSession,
|
53
|
-
)
|
54
|
-
|
55
9
|
__all__ = [
|
56
|
-
"
|
10
|
+
"get_fetcher",
|
11
|
+
"AaatxtSession",
|
57
12
|
"BiqugeSession",
|
58
|
-
"
|
59
|
-
"
|
60
|
-
"
|
13
|
+
"BiquyueduSession",
|
14
|
+
"DxmwxSession",
|
15
|
+
"EightnovelSession",
|
61
16
|
"EsjzoneSession",
|
62
|
-
"
|
17
|
+
"GuidayeSession",
|
18
|
+
"HetushuSession",
|
19
|
+
"I25zwSession",
|
20
|
+
"Ixdzs8Session",
|
21
|
+
"Jpxs123Session",
|
22
|
+
"LewennSession",
|
63
23
|
"LinovelibSession",
|
64
|
-
"
|
24
|
+
"PiaotiaSession",
|
25
|
+
"QbtrSession",
|
65
26
|
"QianbiSession",
|
66
|
-
"QidianBrowser",
|
67
27
|
"QidianSession",
|
68
|
-
"
|
28
|
+
"Quanben5Session",
|
69
29
|
"SfacgSession",
|
70
|
-
"
|
30
|
+
"ShencouSession",
|
31
|
+
"ShuhaigeSession",
|
32
|
+
"TongrenquanSession",
|
33
|
+
"TtkanSession",
|
34
|
+
"WanbengoSession",
|
35
|
+
"XiaoshuowuSession",
|
36
|
+
"XiguashuwuSession",
|
37
|
+
"Xs63bSession",
|
38
|
+
"XshbookSession",
|
71
39
|
"YamiboSession",
|
40
|
+
"YibigeSession",
|
72
41
|
]
|
42
|
+
|
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
|
61
|
+
from .registry import get_fetcher
|
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
|