novel-downloader 1.3.3__py3-none-any.whl → 1.4.1__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/clean.py +97 -78
- novel_downloader/cli/config.py +177 -0
- novel_downloader/cli/download.py +132 -87
- novel_downloader/cli/export.py +77 -0
- novel_downloader/cli/main.py +21 -28
- novel_downloader/config/__init__.py +1 -25
- novel_downloader/config/adapter.py +32 -31
- novel_downloader/config/loader.py +3 -3
- novel_downloader/config/site_rules.py +1 -2
- novel_downloader/core/__init__.py +3 -6
- novel_downloader/core/downloaders/__init__.py +10 -13
- novel_downloader/core/downloaders/base.py +233 -0
- novel_downloader/core/downloaders/biquge.py +27 -0
- novel_downloader/core/downloaders/common.py +414 -0
- novel_downloader/core/downloaders/esjzone.py +27 -0
- novel_downloader/core/downloaders/linovelib.py +27 -0
- novel_downloader/core/downloaders/qianbi.py +27 -0
- novel_downloader/core/downloaders/qidian.py +352 -0
- novel_downloader/core/downloaders/sfacg.py +27 -0
- novel_downloader/core/downloaders/yamibo.py +27 -0
- novel_downloader/core/exporters/__init__.py +37 -0
- novel_downloader/core/{savers → exporters}/base.py +73 -39
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +22 -22
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
- novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
- novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +193 -0
- novel_downloader/core/fetchers/linovelib/session.py +193 -0
- novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
- novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
- novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
- novel_downloader/core/fetchers/qidian/__init__.py +14 -0
- novel_downloader/core/fetchers/qidian/browser.py +266 -0
- novel_downloader/core/fetchers/qidian/session.py +326 -0
- novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
- novel_downloader/core/fetchers/sfacg/browser.py +189 -0
- novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
- novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
- novel_downloader/core/fetchers/yamibo/browser.py +229 -0
- novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
- novel_downloader/core/interfaces/__init__.py +8 -12
- novel_downloader/core/interfaces/downloader.py +54 -0
- novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
- novel_downloader/core/interfaces/fetcher.py +162 -0
- novel_downloader/core/interfaces/parser.py +6 -7
- novel_downloader/core/parsers/__init__.py +5 -6
- novel_downloader/core/parsers/base.py +9 -13
- novel_downloader/core/parsers/biquge/main_parser.py +12 -13
- novel_downloader/core/parsers/common/helper.py +3 -3
- novel_downloader/core/parsers/common/main_parser.py +39 -34
- novel_downloader/core/parsers/esjzone/main_parser.py +20 -14
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +11 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +2 -1
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +1 -1
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/METADATA +69 -35
- novel_downloader-1.4.1.dist-info/RECORD +170 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/entry_points.txt +1 -0
- novel_downloader/cli/interactive.py +0 -66
- novel_downloader/cli/settings.py +0 -177
- novel_downloader/config/models.py +0 -187
- novel_downloader/core/downloaders/base/__init__.py +0 -14
- novel_downloader/core/downloaders/base/base_async.py +0 -153
- novel_downloader/core/downloaders/base/base_sync.py +0 -208
- novel_downloader/core/downloaders/biquge/__init__.py +0 -14
- novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
- novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
- novel_downloader/core/downloaders/common/__init__.py +0 -14
- novel_downloader/core/downloaders/common/common_async.py +0 -210
- novel_downloader/core/downloaders/common/common_sync.py +0 -202
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -219
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.3.dist-info/RECORD +0 -166
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.yamibo.yamibo_sync
|
4
|
-
----------------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import DownloaderConfig
|
9
|
-
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
-
from novel_downloader.core.interfaces import (
|
11
|
-
ParserProtocol,
|
12
|
-
SaverProtocol,
|
13
|
-
SyncRequesterProtocol,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class YamiboDownloader(CommonDownloader):
|
18
|
-
""""""
|
19
|
-
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
requester: SyncRequesterProtocol,
|
23
|
-
parser: ParserProtocol,
|
24
|
-
saver: SaverProtocol,
|
25
|
-
config: DownloaderConfig,
|
26
|
-
):
|
27
|
-
super().__init__(requester, parser, saver, config, "yamibo")
|
@@ -1,144 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.factory.requester_factory
|
4
|
-
-----------------------------------------------
|
5
|
-
|
6
|
-
This module implements a factory function for retrieving requester instances
|
7
|
-
based on the target novel platform (site).
|
8
|
-
"""
|
9
|
-
|
10
|
-
from collections.abc import Callable
|
11
|
-
|
12
|
-
from novel_downloader.config import RequesterConfig, load_site_rules
|
13
|
-
from novel_downloader.core.interfaces import (
|
14
|
-
AsyncRequesterProtocol,
|
15
|
-
SyncRequesterProtocol,
|
16
|
-
)
|
17
|
-
from novel_downloader.core.requesters import (
|
18
|
-
BiqugeAsyncSession,
|
19
|
-
BiqugeSession,
|
20
|
-
CommonAsyncSession,
|
21
|
-
CommonSession,
|
22
|
-
EsjzoneAsyncSession,
|
23
|
-
EsjzoneSession,
|
24
|
-
QianbiAsyncSession,
|
25
|
-
QianbiSession,
|
26
|
-
QidianBrowser,
|
27
|
-
QidianSession,
|
28
|
-
SfacgAsyncSession,
|
29
|
-
SfacgSession,
|
30
|
-
YamiboAsyncSession,
|
31
|
-
YamiboSession,
|
32
|
-
)
|
33
|
-
|
34
|
-
AsyncRequesterBuilder = Callable[[RequesterConfig], AsyncRequesterProtocol]
|
35
|
-
SyncRequesterBuilder = Callable[[RequesterConfig], SyncRequesterProtocol]
|
36
|
-
|
37
|
-
|
38
|
-
_async_site_map: dict[str, AsyncRequesterBuilder] = {
|
39
|
-
"biquge": BiqugeAsyncSession,
|
40
|
-
"esjzone": EsjzoneAsyncSession,
|
41
|
-
"qianbi": QianbiAsyncSession,
|
42
|
-
"sfacg": SfacgAsyncSession,
|
43
|
-
"yamibo": YamiboAsyncSession,
|
44
|
-
}
|
45
|
-
_sync_site_map: dict[
|
46
|
-
str,
|
47
|
-
dict[str, SyncRequesterBuilder],
|
48
|
-
] = {
|
49
|
-
"biquge": {
|
50
|
-
"session": BiqugeSession,
|
51
|
-
},
|
52
|
-
"esjzone": {
|
53
|
-
"session": EsjzoneSession,
|
54
|
-
},
|
55
|
-
"qianbi": {
|
56
|
-
"session": QianbiSession,
|
57
|
-
},
|
58
|
-
"qidian": {
|
59
|
-
"session": QidianSession,
|
60
|
-
"browser": QidianBrowser,
|
61
|
-
},
|
62
|
-
"sfacg": {
|
63
|
-
"session": SfacgSession,
|
64
|
-
},
|
65
|
-
"yamibo": {
|
66
|
-
"session": YamiboSession,
|
67
|
-
},
|
68
|
-
}
|
69
|
-
|
70
|
-
|
71
|
-
def get_async_requester(
|
72
|
-
site: str,
|
73
|
-
config: RequesterConfig,
|
74
|
-
) -> AsyncRequesterProtocol:
|
75
|
-
"""
|
76
|
-
Returns an AsyncRequesterProtocol for the given site.
|
77
|
-
|
78
|
-
:param site: Site name (e.g., 'qidian')
|
79
|
-
:param config: Configuration for the requester
|
80
|
-
:return: An instance of a requester class
|
81
|
-
"""
|
82
|
-
site_key = site.lower()
|
83
|
-
|
84
|
-
# site-specific
|
85
|
-
if site_key in _async_site_map:
|
86
|
-
return _async_site_map[site_key](config)
|
87
|
-
|
88
|
-
# fallback
|
89
|
-
site_rules = load_site_rules()
|
90
|
-
site_rule = site_rules.get(site_key)
|
91
|
-
if site_rule is None:
|
92
|
-
raise ValueError(f"Unsupported site: {site}")
|
93
|
-
profile = site_rule["profile"]
|
94
|
-
return CommonAsyncSession(config, site_key, profile)
|
95
|
-
|
96
|
-
|
97
|
-
def get_sync_requester(
|
98
|
-
site: str,
|
99
|
-
config: RequesterConfig,
|
100
|
-
) -> SyncRequesterProtocol:
|
101
|
-
"""
|
102
|
-
Returns a RequesterProtocol for the given site.
|
103
|
-
|
104
|
-
:param site: Site name (e.g., 'qidian')
|
105
|
-
:param config: Configuration for the requester
|
106
|
-
:return: An instance of a requester class
|
107
|
-
"""
|
108
|
-
site_key = site.lower()
|
109
|
-
site_entry = _sync_site_map.get(site_key)
|
110
|
-
|
111
|
-
# site-specific
|
112
|
-
if site_entry:
|
113
|
-
cls = site_entry.get(config.mode)
|
114
|
-
if cls:
|
115
|
-
return cls(config)
|
116
|
-
|
117
|
-
# fallback
|
118
|
-
site_rules = load_site_rules()
|
119
|
-
site_rule = site_rules.get(site_key)
|
120
|
-
if site_rule is None:
|
121
|
-
raise ValueError(f"Unsupported site: {site}")
|
122
|
-
profile = site_rule["profile"]
|
123
|
-
return CommonSession(config, site_key, profile)
|
124
|
-
|
125
|
-
|
126
|
-
def get_requester(
|
127
|
-
site: str,
|
128
|
-
config: RequesterConfig,
|
129
|
-
) -> AsyncRequesterProtocol | SyncRequesterProtocol:
|
130
|
-
"""
|
131
|
-
Dispatches to either get_async_requester or get_sync_requester
|
132
|
-
based on config.mode. Treats 'browser' and 'async' as async modes,
|
133
|
-
'session' as sync; anything else is an error.
|
134
|
-
|
135
|
-
:param site: Site name (e.g., 'qidian')
|
136
|
-
:param config: Configuration for the requester
|
137
|
-
:return: An instance of a requester class
|
138
|
-
"""
|
139
|
-
mode = config.mode.lower()
|
140
|
-
if mode == "async":
|
141
|
-
return get_async_requester(site, config)
|
142
|
-
if mode in ("browser", "session"):
|
143
|
-
return get_sync_requester(site, config)
|
144
|
-
raise ValueError(f"Unknown mode '{config.mode}' for site '{site}'")
|
@@ -1,56 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.factory.parser_factory
|
4
|
-
--------------------------------------------
|
5
|
-
|
6
|
-
This module implements a factory function for creating saver instances
|
7
|
-
based on the site name and parser mode specified in the configuration.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from collections.abc import Callable
|
11
|
-
|
12
|
-
from novel_downloader.config import SaverConfig, load_site_rules
|
13
|
-
from novel_downloader.core.interfaces import SaverProtocol
|
14
|
-
from novel_downloader.core.savers import (
|
15
|
-
BiqugeSaver,
|
16
|
-
CommonSaver,
|
17
|
-
EsjzoneSaver,
|
18
|
-
QianbiSaver,
|
19
|
-
QidianSaver,
|
20
|
-
SfacgSaver,
|
21
|
-
YamiboSaver,
|
22
|
-
)
|
23
|
-
|
24
|
-
SaverBuilder = Callable[[SaverConfig], SaverProtocol]
|
25
|
-
|
26
|
-
_site_map: dict[str, SaverBuilder] = {
|
27
|
-
"biquge": BiqugeSaver,
|
28
|
-
"esjzone": EsjzoneSaver,
|
29
|
-
"qianbi": QianbiSaver,
|
30
|
-
"qidian": QidianSaver,
|
31
|
-
"sfacg": SfacgSaver,
|
32
|
-
"yamibo": YamiboSaver,
|
33
|
-
}
|
34
|
-
|
35
|
-
|
36
|
-
def get_saver(site: str, config: SaverConfig) -> SaverProtocol:
|
37
|
-
"""
|
38
|
-
Returns a site-specific saver instance.
|
39
|
-
|
40
|
-
:param site: Site name (e.g., 'qidian')
|
41
|
-
:param config: Configuration for the saver
|
42
|
-
:return: An instance of a saver class
|
43
|
-
"""
|
44
|
-
site_key = site.lower()
|
45
|
-
|
46
|
-
# site-specific
|
47
|
-
saver_class = _site_map.get(site_key)
|
48
|
-
if saver_class:
|
49
|
-
return saver_class(config)
|
50
|
-
|
51
|
-
# Fallback
|
52
|
-
site_rules = load_site_rules()
|
53
|
-
if site_key not in site_rules:
|
54
|
-
raise ValueError(f"Unsupported site: {site}")
|
55
|
-
|
56
|
-
return CommonSaver(config, site_key)
|
@@ -1,36 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.interfaces.async_downloader
|
4
|
-
----------------------------------------------------------
|
5
|
-
|
6
|
-
This module defines the AsyncDownloaderProtocol, a structural interface
|
7
|
-
that outlines the expected behavior of any downloader class.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from typing import Protocol
|
11
|
-
|
12
|
-
|
13
|
-
class AsyncDownloaderProtocol(Protocol):
|
14
|
-
"""
|
15
|
-
Protocol for fully-asynchronous downloader classes.
|
16
|
-
|
17
|
-
Defines the expected interface for any downloader implementation,
|
18
|
-
including both batch and single book downloads,
|
19
|
-
as well as optional pre-download hooks.
|
20
|
-
"""
|
21
|
-
|
22
|
-
async def download(self, book_ids: list[str]) -> None:
|
23
|
-
"""
|
24
|
-
Batch download entry point.
|
25
|
-
|
26
|
-
:param book_ids: List of book IDs to download.
|
27
|
-
"""
|
28
|
-
...
|
29
|
-
|
30
|
-
async def download_one(self, book_id: str) -> None:
|
31
|
-
"""
|
32
|
-
Download logic for a single book.
|
33
|
-
|
34
|
-
:param book_id: The identifier of the book.
|
35
|
-
"""
|
36
|
-
...
|
@@ -1,84 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.interfaces.async_requester
|
4
|
-
--------------------------------------------------------
|
5
|
-
|
6
|
-
Defines the AsyncRequesterProtocol interface for fetching raw HTML or JSON
|
7
|
-
for book info pages, individual chapters, managing request lifecycle,
|
8
|
-
and optionally retrieving a user's authenticated bookcase.
|
9
|
-
"""
|
10
|
-
|
11
|
-
from typing import Any, Literal, Protocol, runtime_checkable
|
12
|
-
|
13
|
-
|
14
|
-
@runtime_checkable
|
15
|
-
class AsyncRequesterProtocol(Protocol):
|
16
|
-
"""
|
17
|
-
An async requester must be able to fetch raw HTML/data for:
|
18
|
-
- a book's info page,
|
19
|
-
- a specific chapter page,
|
20
|
-
and manage login/shutdown asynchronously.
|
21
|
-
"""
|
22
|
-
|
23
|
-
def is_async(self) -> Literal[True]:
|
24
|
-
...
|
25
|
-
|
26
|
-
async def login(
|
27
|
-
self,
|
28
|
-
username: str = "",
|
29
|
-
password: str = "",
|
30
|
-
manual_login: bool = False,
|
31
|
-
**kwargs: Any,
|
32
|
-
) -> bool:
|
33
|
-
"""
|
34
|
-
Attempt to log in asynchronously.
|
35
|
-
:returns: True if login succeeded.
|
36
|
-
"""
|
37
|
-
...
|
38
|
-
|
39
|
-
async def get_book_info(
|
40
|
-
self,
|
41
|
-
book_id: str,
|
42
|
-
**kwargs: Any,
|
43
|
-
) -> list[str]:
|
44
|
-
"""
|
45
|
-
Fetch the raw HTML (or JSON) of the book info page asynchronously.
|
46
|
-
|
47
|
-
:param book_id: The book identifier.
|
48
|
-
:return: The page content as a string.
|
49
|
-
"""
|
50
|
-
...
|
51
|
-
|
52
|
-
async def get_book_chapter(
|
53
|
-
self,
|
54
|
-
book_id: str,
|
55
|
-
chapter_id: str,
|
56
|
-
**kwargs: Any,
|
57
|
-
) -> list[str]:
|
58
|
-
"""
|
59
|
-
Fetch the raw HTML (or JSON) of a single chapter asynchronously.
|
60
|
-
|
61
|
-
:param book_id: The book identifier.
|
62
|
-
:param chapter_id: The chapter identifier.
|
63
|
-
:return: The chapter content as a string.
|
64
|
-
"""
|
65
|
-
...
|
66
|
-
|
67
|
-
async def get_bookcase(
|
68
|
-
self,
|
69
|
-
page: int = 1,
|
70
|
-
**kwargs: Any,
|
71
|
-
) -> list[str]:
|
72
|
-
"""
|
73
|
-
Optional: Retrieve the HTML content of the authenticated
|
74
|
-
user's bookcase page asynchronously.
|
75
|
-
|
76
|
-
:return: The HTML markup of the bookcase page.
|
77
|
-
"""
|
78
|
-
...
|
79
|
-
|
80
|
-
async def close(self) -> None:
|
81
|
-
"""
|
82
|
-
Shutdown and clean up any resources (e.g., close aiohttp session).
|
83
|
-
"""
|
84
|
-
...
|
@@ -1,36 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.interfaces.sync_downloader
|
4
|
-
------------------------------------------------
|
5
|
-
|
6
|
-
This module defines the SyncDownloaderProtocol, a structural interface
|
7
|
-
that outlines the expected behavior of any downloader class.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from typing import Protocol
|
11
|
-
|
12
|
-
|
13
|
-
class SyncDownloaderProtocol(Protocol):
|
14
|
-
"""
|
15
|
-
Protocol for downloader classes.
|
16
|
-
|
17
|
-
Defines the expected interface for any downloader implementation,
|
18
|
-
including both batch and single book downloads,
|
19
|
-
as well as optional pre-download hooks.
|
20
|
-
"""
|
21
|
-
|
22
|
-
def download(self, book_ids: list[str]) -> None:
|
23
|
-
"""
|
24
|
-
Batch download entry point.
|
25
|
-
|
26
|
-
:param book_ids: List of book IDs to download.
|
27
|
-
"""
|
28
|
-
...
|
29
|
-
|
30
|
-
def download_one(self, book_id: str) -> None:
|
31
|
-
"""
|
32
|
-
Download logic for a single book.
|
33
|
-
|
34
|
-
:param book_id: The identifier of the book.
|
35
|
-
"""
|
36
|
-
...
|
@@ -1,82 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.interfaces.sync_requester
|
4
|
-
-----------------------------------------------
|
5
|
-
|
6
|
-
Defines the RequesterProtocol interface for fetching raw HTML or JSON
|
7
|
-
for book info pages, individual chapters, managing request lifecycle,
|
8
|
-
and optionally retrieving a user's authenticated bookcase.
|
9
|
-
"""
|
10
|
-
|
11
|
-
from typing import Any, Literal, Protocol, runtime_checkable
|
12
|
-
|
13
|
-
|
14
|
-
@runtime_checkable
|
15
|
-
class SyncRequesterProtocol(Protocol):
|
16
|
-
"""
|
17
|
-
A requester must be able to fetch raw HTML/data for:
|
18
|
-
- a book's info page,
|
19
|
-
- a specific chapter page.
|
20
|
-
"""
|
21
|
-
|
22
|
-
def is_async(self) -> Literal[False]:
|
23
|
-
...
|
24
|
-
|
25
|
-
def login(
|
26
|
-
self,
|
27
|
-
username: str = "",
|
28
|
-
password: str = "",
|
29
|
-
manual_login: bool = False,
|
30
|
-
**kwargs: Any,
|
31
|
-
) -> bool:
|
32
|
-
"""
|
33
|
-
Attempt to log in
|
34
|
-
"""
|
35
|
-
...
|
36
|
-
|
37
|
-
def get_book_info(
|
38
|
-
self,
|
39
|
-
book_id: str,
|
40
|
-
**kwargs: Any,
|
41
|
-
) -> list[str]:
|
42
|
-
"""
|
43
|
-
Fetch the raw HTML (or JSON) of the book info page.
|
44
|
-
|
45
|
-
:param book_id: The book identifier.
|
46
|
-
:return: The page content as a string.
|
47
|
-
"""
|
48
|
-
...
|
49
|
-
|
50
|
-
def get_book_chapter(
|
51
|
-
self,
|
52
|
-
book_id: str,
|
53
|
-
chapter_id: str,
|
54
|
-
**kwargs: Any,
|
55
|
-
) -> list[str]:
|
56
|
-
"""
|
57
|
-
Fetch the raw HTML (or JSON) of a single chapter.
|
58
|
-
|
59
|
-
:param book_id: The book identifier.
|
60
|
-
:param chapter_id: The chapter identifier.
|
61
|
-
:return: The chapter content as a string.
|
62
|
-
"""
|
63
|
-
...
|
64
|
-
|
65
|
-
def get_bookcase(
|
66
|
-
self,
|
67
|
-
page: int = 1,
|
68
|
-
**kwargs: Any,
|
69
|
-
) -> list[str]:
|
70
|
-
"""
|
71
|
-
Optional: Retrieve the HTML content of the authenticated user's bookcase page.
|
72
|
-
|
73
|
-
:param page: Page idx
|
74
|
-
:return: The HTML markup of the bookcase page.
|
75
|
-
"""
|
76
|
-
...
|
77
|
-
|
78
|
-
def close(self) -> None:
|
79
|
-
"""
|
80
|
-
Shutdown and cleans up resources.
|
81
|
-
"""
|
82
|
-
...
|
@@ -1,12 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.browser
|
4
|
-
--------------------------------------------
|
5
|
-
|
6
|
-
This package provides parsing components for handling Qidian
|
7
|
-
pages that have been rendered by a browser engine.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from .main_parser import QidianBrowserParser
|
11
|
-
|
12
|
-
__all__ = ["QidianBrowserParser"]
|
@@ -1,93 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.browser.chapter_normal
|
4
|
-
-----------------------------------------------------------
|
5
|
-
|
6
|
-
Parser logic for extracting readable text from Qidian chapters
|
7
|
-
that use plain (non-encrypted) browser-rendered HTML.
|
8
|
-
"""
|
9
|
-
|
10
|
-
import logging
|
11
|
-
|
12
|
-
from bs4 import BeautifulSoup
|
13
|
-
|
14
|
-
from novel_downloader.utils.chapter_storage import ChapterDict
|
15
|
-
|
16
|
-
from ..shared import (
|
17
|
-
extract_chapter_info,
|
18
|
-
find_ssr_page_context,
|
19
|
-
)
|
20
|
-
|
21
|
-
logger = logging.getLogger(__name__)
|
22
|
-
|
23
|
-
|
24
|
-
def parse_normal_chapter(
|
25
|
-
soup: BeautifulSoup,
|
26
|
-
chapter_id: str,
|
27
|
-
) -> ChapterDict | None:
|
28
|
-
"""
|
29
|
-
Extract and format the chapter text from a normal Qidian page.
|
30
|
-
Returns empty string if VIP/encrypted.
|
31
|
-
|
32
|
-
This method performs the following steps:
|
33
|
-
1. Parses HTML into soup.
|
34
|
-
2. Skips parsing if VIP or encrypted.
|
35
|
-
3. Locates main content container.
|
36
|
-
4. Extracts SSR-rendered chapter info (title, author note).
|
37
|
-
5. Removes review spans.
|
38
|
-
6. Extracts paragraph texts and formats them.
|
39
|
-
|
40
|
-
:param html_str: Raw HTML content of the chapter page.
|
41
|
-
:return: Formatted chapter text or empty string if not parsable.
|
42
|
-
"""
|
43
|
-
try:
|
44
|
-
main = soup.select_one("div#app div#reader-content main")
|
45
|
-
if not main:
|
46
|
-
logger.warning("[Parser] Main content not found for chapter")
|
47
|
-
return None
|
48
|
-
|
49
|
-
ssr_data = find_ssr_page_context(soup)
|
50
|
-
chapter_info = extract_chapter_info(ssr_data)
|
51
|
-
if not chapter_info:
|
52
|
-
logger.warning(
|
53
|
-
"[Parser] ssr_chapterInfo not found for chapter '%s'", chapter_id
|
54
|
-
)
|
55
|
-
return None
|
56
|
-
|
57
|
-
title = chapter_info.get("chapterName", "Untitled")
|
58
|
-
chapter_id = chapter_info.get("chapterId", "")
|
59
|
-
author_say = chapter_info.get("authorSay", "")
|
60
|
-
update_time = chapter_info.get("updateTime", "")
|
61
|
-
update_timestamp = chapter_info.get("updateTimestamp", 0)
|
62
|
-
modify_time = chapter_info.get("modifyTime", 0)
|
63
|
-
word_count = chapter_info.get("wordsCount", 0)
|
64
|
-
seq = chapter_info.get("seq", None)
|
65
|
-
volume = chapter_info.get("extra", {}).get("volumeName", "")
|
66
|
-
|
67
|
-
# remove review spans
|
68
|
-
for span in main.select("span.review"):
|
69
|
-
span.decompose()
|
70
|
-
|
71
|
-
paras = [p.get_text(strip=True) for p in main.find_all("p")]
|
72
|
-
chapter_text = "\n\n".join(paras)
|
73
|
-
|
74
|
-
return {
|
75
|
-
"id": str(chapter_id),
|
76
|
-
"title": title,
|
77
|
-
"content": chapter_text,
|
78
|
-
"extra": {
|
79
|
-
"author_say": author_say.strip() if author_say else "",
|
80
|
-
"updated_at": update_time,
|
81
|
-
"update_timestamp": update_timestamp,
|
82
|
-
"modify_time": modify_time,
|
83
|
-
"word_count": word_count,
|
84
|
-
"seq": seq,
|
85
|
-
"volume": volume,
|
86
|
-
},
|
87
|
-
}
|
88
|
-
|
89
|
-
except Exception as e:
|
90
|
-
logger.warning(
|
91
|
-
"[Parser] parse error for normal chapter '%s': %s", chapter_id, e
|
92
|
-
)
|
93
|
-
return None
|
@@ -1,71 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.browser.chapter_router
|
4
|
-
-----------------------------------------------------------
|
5
|
-
|
6
|
-
Routing logic for selecting the correct chapter parser for Qidian browser pages.
|
7
|
-
|
8
|
-
This module acts as a dispatcher that analyzes a chapter's HTML content and
|
9
|
-
routes the parsing task to either the encrypted or normal chapter parser.
|
10
|
-
"""
|
11
|
-
|
12
|
-
from __future__ import annotations
|
13
|
-
|
14
|
-
import logging
|
15
|
-
from typing import TYPE_CHECKING
|
16
|
-
|
17
|
-
from novel_downloader.utils.chapter_storage import ChapterDict
|
18
|
-
|
19
|
-
from ..shared import (
|
20
|
-
can_view_chapter,
|
21
|
-
html_to_soup,
|
22
|
-
is_encrypted,
|
23
|
-
)
|
24
|
-
from .chapter_normal import parse_normal_chapter
|
25
|
-
|
26
|
-
if TYPE_CHECKING:
|
27
|
-
from .main_parser import QidianBrowserParser
|
28
|
-
|
29
|
-
logger = logging.getLogger(__name__)
|
30
|
-
|
31
|
-
|
32
|
-
def parse_chapter(
|
33
|
-
parser: QidianBrowserParser,
|
34
|
-
html_str: str,
|
35
|
-
chapter_id: str,
|
36
|
-
) -> ChapterDict | None:
|
37
|
-
"""
|
38
|
-
Extract and return the formatted textual content of chapter.
|
39
|
-
|
40
|
-
:param parser: Instance of QidianBrowserParser.
|
41
|
-
:param html_str: Raw HTML content of the chapter page.
|
42
|
-
:param chapter_id: Identifier of the chapter being parsed.
|
43
|
-
:return: Formatted chapter text or empty string if not parsable.
|
44
|
-
"""
|
45
|
-
try:
|
46
|
-
soup = html_to_soup(html_str)
|
47
|
-
|
48
|
-
if not can_view_chapter(soup):
|
49
|
-
logger.warning(
|
50
|
-
"[Parser] Chapter '%s' is not purchased or inaccessible.", chapter_id
|
51
|
-
)
|
52
|
-
return None
|
53
|
-
|
54
|
-
if is_encrypted(soup):
|
55
|
-
if not parser._decode_font:
|
56
|
-
return None
|
57
|
-
try:
|
58
|
-
from .chapter_encrypted import parse_encrypted_chapter
|
59
|
-
|
60
|
-
return parse_encrypted_chapter(parser, soup, chapter_id)
|
61
|
-
except ImportError:
|
62
|
-
logger.warning(
|
63
|
-
"[Parser] Encrypted chapter '%s' requires extra dependencies.",
|
64
|
-
chapter_id,
|
65
|
-
)
|
66
|
-
return None
|
67
|
-
|
68
|
-
return parse_normal_chapter(soup, chapter_id)
|
69
|
-
except Exception as e:
|
70
|
-
logger.warning("[Parser] parse error for chapter '%s': %s", chapter_id, e)
|
71
|
-
return None
|
@@ -1,12 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.session
|
4
|
-
--------------------------------------------
|
5
|
-
|
6
|
-
This package provides parsing components for handling Qidian
|
7
|
-
pages that have been rendered by a session.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from .main_parser import QidianSessionParser
|
11
|
-
|
12
|
-
__all__ = ["QidianSessionParser"]
|