novel-downloader 1.2.1__py3-none-any.whl → 1.3.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 -2
- novel_downloader/cli/__init__.py +0 -1
- novel_downloader/cli/clean.py +2 -10
- novel_downloader/cli/download.py +18 -22
- novel_downloader/cli/interactive.py +0 -1
- novel_downloader/cli/main.py +1 -3
- novel_downloader/cli/settings.py +8 -8
- novel_downloader/config/__init__.py +0 -1
- novel_downloader/config/adapter.py +48 -18
- novel_downloader/config/loader.py +116 -108
- novel_downloader/config/models.py +41 -32
- novel_downloader/config/site_rules.py +2 -4
- novel_downloader/core/__init__.py +0 -1
- novel_downloader/core/downloaders/__init__.py +4 -4
- novel_downloader/core/downloaders/base/__init__.py +14 -0
- novel_downloader/core/downloaders/{base_async_downloader.py → base/base_async.py} +49 -53
- novel_downloader/core/downloaders/{base_downloader.py → base/base_sync.py} +64 -43
- novel_downloader/core/downloaders/biquge/__init__.py +12 -0
- novel_downloader/core/downloaders/biquge/biquge_sync.py +25 -0
- novel_downloader/core/downloaders/common/__init__.py +14 -0
- novel_downloader/core/downloaders/{common_asynb_downloader.py → common/common_async.py} +42 -33
- novel_downloader/core/downloaders/{common_downloader.py → common/common_sync.py} +34 -23
- novel_downloader/core/downloaders/qidian/__init__.py +10 -0
- novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +80 -64
- novel_downloader/core/factory/__init__.py +4 -5
- novel_downloader/core/factory/{downloader_factory.py → downloader.py} +36 -35
- novel_downloader/core/factory/{parser_factory.py → parser.py} +12 -14
- novel_downloader/core/factory/{requester_factory.py → requester.py} +29 -16
- novel_downloader/core/factory/{saver_factory.py → saver.py} +4 -9
- novel_downloader/core/interfaces/__init__.py +8 -9
- novel_downloader/core/interfaces/{async_downloader_protocol.py → async_downloader.py} +4 -5
- novel_downloader/core/interfaces/{async_requester_protocol.py → async_requester.py} +26 -12
- novel_downloader/core/interfaces/{parser_protocol.py → parser.py} +11 -6
- novel_downloader/core/interfaces/{saver_protocol.py → saver.py} +2 -3
- novel_downloader/core/interfaces/{downloader_protocol.py → sync_downloader.py} +6 -7
- novel_downloader/core/interfaces/{requester_protocol.py → sync_requester.py} +34 -17
- novel_downloader/core/parsers/__init__.py +5 -4
- novel_downloader/core/parsers/{base_parser.py → base.py} +20 -11
- novel_downloader/core/parsers/biquge/__init__.py +10 -0
- novel_downloader/core/parsers/biquge/main_parser.py +126 -0
- novel_downloader/core/parsers/{common_parser → common}/__init__.py +2 -3
- novel_downloader/core/parsers/{common_parser → common}/helper.py +20 -18
- novel_downloader/core/parsers/{common_parser → common}/main_parser.py +15 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_encrypted.py +41 -49
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_normal.py +17 -21
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/browser/main_parser.py +16 -12
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +37 -45
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_normal.py +19 -23
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_router.py +10 -9
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/main_parser.py +16 -12
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/node_decryptor.py +7 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/__init__.py +2 -3
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +150 -0
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +9 -10
- novel_downloader/core/requesters/__init__.py +9 -5
- novel_downloader/core/requesters/base/__init__.py +16 -0
- novel_downloader/core/requesters/{base_async_session.py → base/async_session.py} +180 -73
- novel_downloader/core/requesters/base/browser.py +340 -0
- novel_downloader/core/requesters/base/session.py +364 -0
- novel_downloader/core/requesters/biquge/__init__.py +12 -0
- novel_downloader/core/requesters/biquge/session.py +90 -0
- novel_downloader/core/requesters/{common_requester → common}/__init__.py +4 -5
- novel_downloader/core/requesters/common/async_session.py +96 -0
- novel_downloader/core/requesters/common/session.py +113 -0
- novel_downloader/core/requesters/qidian/__init__.py +21 -0
- novel_downloader/core/requesters/qidian/broswer.py +306 -0
- novel_downloader/core/requesters/qidian/session.py +287 -0
- novel_downloader/core/savers/__init__.py +5 -3
- novel_downloader/core/savers/{base_saver.py → base.py} +12 -13
- novel_downloader/core/savers/biquge.py +25 -0
- novel_downloader/core/savers/{common_saver → common}/__init__.py +2 -3
- novel_downloader/core/savers/{common_saver/common_epub.py → common/epub.py} +24 -52
- novel_downloader/core/savers/{common_saver → common}/main_saver.py +43 -9
- novel_downloader/core/savers/{common_saver/common_txt.py → common/txt.py} +16 -46
- novel_downloader/core/savers/epub_utils/__init__.py +0 -1
- novel_downloader/core/savers/epub_utils/css_builder.py +13 -7
- novel_downloader/core/savers/epub_utils/initializer.py +4 -5
- novel_downloader/core/savers/epub_utils/text_to_html.py +2 -3
- novel_downloader/core/savers/epub_utils/volume_intro.py +1 -3
- novel_downloader/core/savers/{qidian_saver.py → qidian.py} +12 -6
- novel_downloader/locales/en.json +12 -4
- novel_downloader/locales/zh.json +9 -1
- novel_downloader/resources/config/settings.toml +88 -0
- novel_downloader/utils/cache.py +2 -2
- novel_downloader/utils/chapter_storage.py +340 -0
- novel_downloader/utils/constants.py +8 -5
- novel_downloader/utils/crypto_utils.py +3 -3
- novel_downloader/utils/file_utils/__init__.py +0 -1
- novel_downloader/utils/file_utils/io.py +12 -17
- novel_downloader/utils/file_utils/normalize.py +1 -3
- novel_downloader/utils/file_utils/sanitize.py +2 -9
- novel_downloader/utils/fontocr/__init__.py +0 -1
- novel_downloader/utils/fontocr/ocr_v1.py +19 -22
- novel_downloader/utils/fontocr/ocr_v2.py +147 -60
- novel_downloader/utils/hash_store.py +19 -20
- novel_downloader/utils/hash_utils.py +0 -1
- novel_downloader/utils/i18n.py +3 -4
- novel_downloader/utils/logger.py +5 -6
- novel_downloader/utils/model_loader.py +5 -8
- novel_downloader/utils/network.py +9 -10
- novel_downloader/utils/state.py +6 -7
- novel_downloader/utils/text_utils/__init__.py +0 -1
- novel_downloader/utils/text_utils/chapter_formatting.py +2 -7
- novel_downloader/utils/text_utils/diff_display.py +0 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -4
- novel_downloader/utils/text_utils/text_cleaning.py +0 -1
- novel_downloader/utils/time_utils/__init__.py +0 -1
- novel_downloader/utils/time_utils/datetime_utils.py +9 -11
- novel_downloader/utils/time_utils/sleep_utils.py +27 -13
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/METADATA +14 -17
- novel_downloader-1.3.0.dist-info/RECORD +127 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/WHEEL +1 -1
- novel_downloader/core/parsers/qidian_parser/shared/book_info_parser.py +0 -95
- novel_downloader/core/requesters/base_browser.py +0 -210
- novel_downloader/core/requesters/base_session.py +0 -243
- novel_downloader/core/requesters/common_requester/common_async_session.py +0 -98
- novel_downloader/core/requesters/common_requester/common_session.py +0 -126
- novel_downloader/core/requesters/qidian_requester/__init__.py +0 -22
- novel_downloader/core/requesters/qidian_requester/qidian_broswer.py +0 -377
- novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
- novel_downloader/resources/config/settings.yaml +0 -76
- novel_downloader-1.2.1.dist-info/RECORD +0 -115
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.2.1.dist-info → novel_downloader-1.3.0.dist-info}/top_level.txt +0 -0
@@ -1,31 +1,30 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
|
-
novel_downloader.core.downloaders.
|
5
|
-
|
3
|
+
novel_downloader.core.downloaders.qidian.qidian_sync
|
4
|
+
----------------------------------------------------
|
6
5
|
|
7
6
|
This module defines `QidianDownloader`, a platform-specific downloader
|
8
7
|
implementation for retrieving novels from Qidian (起点中文网).
|
9
8
|
"""
|
10
9
|
|
11
10
|
import json
|
12
|
-
import
|
13
|
-
from typing import Any, Dict
|
11
|
+
from typing import Any
|
14
12
|
|
15
13
|
from novel_downloader.config import DownloaderConfig
|
14
|
+
from novel_downloader.core.downloaders.base import BaseDownloader
|
16
15
|
from novel_downloader.core.interfaces import (
|
17
16
|
ParserProtocol,
|
18
|
-
RequesterProtocol,
|
19
17
|
SaverProtocol,
|
18
|
+
SyncRequesterProtocol,
|
20
19
|
)
|
20
|
+
from novel_downloader.utils.chapter_storage import ChapterStorage
|
21
21
|
from novel_downloader.utils.file_utils import save_as_json, save_as_txt
|
22
22
|
from novel_downloader.utils.network import download_image_as_bytes
|
23
23
|
from novel_downloader.utils.state import state_mgr
|
24
|
-
from novel_downloader.utils.time_utils import
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
logger = logging.getLogger(__name__)
|
24
|
+
from novel_downloader.utils.time_utils import (
|
25
|
+
calculate_time_difference,
|
26
|
+
sleep_with_random_delay,
|
27
|
+
)
|
29
28
|
|
30
29
|
|
31
30
|
class QidianDownloader(BaseDownloader):
|
@@ -35,36 +34,17 @@ class QidianDownloader(BaseDownloader):
|
|
35
34
|
|
36
35
|
def __init__(
|
37
36
|
self,
|
38
|
-
requester:
|
37
|
+
requester: SyncRequesterProtocol,
|
39
38
|
parser: ParserProtocol,
|
40
39
|
saver: SaverProtocol,
|
41
40
|
config: DownloaderConfig,
|
42
41
|
):
|
43
|
-
super().__init__(requester, parser, saver, config)
|
42
|
+
super().__init__(requester, parser, saver, config, "qidian")
|
44
43
|
|
45
44
|
self._site_key = "qidian"
|
46
45
|
self._is_logged_in = self._handle_login()
|
47
46
|
state_mgr.set_manual_login_flag(self._site_key, not self._is_logged_in)
|
48
47
|
|
49
|
-
def _handle_login(self) -> bool:
|
50
|
-
"""
|
51
|
-
Perform login with automatic fallback to manual:
|
52
|
-
|
53
|
-
1. If manual_flag is False, try automatic login:
|
54
|
-
- On success, return True immediately.
|
55
|
-
2. Always attempt manual login if manual_flag is True.
|
56
|
-
3. Return True if manual login succeeds, False otherwise.
|
57
|
-
"""
|
58
|
-
manual_flag = state_mgr.get_manual_login_flag(self._site_key)
|
59
|
-
|
60
|
-
# First try automatic login
|
61
|
-
if not manual_flag:
|
62
|
-
if self._requester.login(manual_login=False):
|
63
|
-
return True
|
64
|
-
|
65
|
-
# try manual login
|
66
|
-
return self._requester.login(manual_login=True)
|
67
|
-
|
68
48
|
def download_one(self, book_id: str) -> None:
|
69
49
|
"""
|
70
50
|
The full download logic for a single book.
|
@@ -72,8 +52,10 @@ class QidianDownloader(BaseDownloader):
|
|
72
52
|
:param book_id: The identifier of the book to download.
|
73
53
|
"""
|
74
54
|
if not self._is_logged_in:
|
75
|
-
logger.warning(
|
76
|
-
|
55
|
+
self.logger.warning(
|
56
|
+
"[%s] login failed, skipping download of %s",
|
57
|
+
self._site_key,
|
58
|
+
book_id,
|
77
59
|
)
|
78
60
|
return
|
79
61
|
|
@@ -81,20 +63,28 @@ class QidianDownloader(BaseDownloader):
|
|
81
63
|
save_html = self.config.save_html
|
82
64
|
skip_existing = self.config.skip_existing
|
83
65
|
wait_time = self.config.request_interval
|
66
|
+
scroll = self.config.mode == "browser"
|
84
67
|
|
85
|
-
raw_base = self.raw_data_dir /
|
86
|
-
cache_base = self.cache_dir /
|
68
|
+
raw_base = self.raw_data_dir / book_id
|
69
|
+
cache_base = self.cache_dir / book_id
|
87
70
|
info_path = raw_base / "book_info.json"
|
88
|
-
|
89
|
-
encrypted_chapter_dir = raw_base / "encrypted_chapters"
|
90
|
-
if save_html:
|
91
|
-
chapters_html_dir = cache_base / "html"
|
71
|
+
chapters_html_dir = cache_base / "html"
|
92
72
|
|
93
73
|
raw_base.mkdir(parents=True, exist_ok=True)
|
94
|
-
|
95
|
-
|
74
|
+
normal_cs = ChapterStorage(
|
75
|
+
raw_base=raw_base,
|
76
|
+
namespace="chapters",
|
77
|
+
backend_type=self._config.storage_backend,
|
78
|
+
batch_size=self._config.storage_batch_size,
|
79
|
+
)
|
80
|
+
encrypted_cs = ChapterStorage(
|
81
|
+
raw_base=raw_base,
|
82
|
+
namespace="encrypted_chapters",
|
83
|
+
backend_type=self._config.storage_backend,
|
84
|
+
batch_size=self._config.storage_batch_size,
|
85
|
+
)
|
96
86
|
|
97
|
-
book_info:
|
87
|
+
book_info: dict[str, Any]
|
98
88
|
|
99
89
|
try:
|
100
90
|
if not info_path.exists():
|
@@ -103,13 +93,13 @@ class QidianDownloader(BaseDownloader):
|
|
103
93
|
days, hrs, mins, secs = calculate_time_difference(
|
104
94
|
book_info.get("update_time", ""), "UTC+8"
|
105
95
|
)
|
106
|
-
logger.info(
|
96
|
+
self.logger.info(
|
107
97
|
"%s Last updated %dd %dh %dm %ds ago", TAG, days, hrs, mins, secs
|
108
98
|
)
|
109
99
|
if days > 1:
|
110
100
|
raise FileNotFoundError # trigger re-fetch
|
111
101
|
except Exception:
|
112
|
-
info_html = self.requester.get_book_info(book_id
|
102
|
+
info_html = self.requester.get_book_info(book_id)
|
113
103
|
if save_html:
|
114
104
|
info_html_path = chapters_html_dir / "info.html"
|
115
105
|
save_as_txt(info_html, info_html_path)
|
@@ -119,29 +109,28 @@ class QidianDownloader(BaseDownloader):
|
|
119
109
|
and book_info.get("update_time", "") != "未找到更新时间"
|
120
110
|
):
|
121
111
|
save_as_json(book_info, info_path)
|
112
|
+
sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
|
122
113
|
|
123
114
|
# download cover
|
124
115
|
cover_url = book_info.get("cover_url", "")
|
125
116
|
if cover_url:
|
126
117
|
cover_bytes = download_image_as_bytes(cover_url, raw_base)
|
127
118
|
if not cover_bytes:
|
128
|
-
logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
119
|
+
self.logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
129
120
|
|
130
121
|
# enqueue chapters
|
131
122
|
for vol in book_info.get("volumes", []):
|
132
123
|
vol_name = vol.get("volume_name", "")
|
133
|
-
logger.info("%s Enqueuing volume: %s", TAG, vol_name)
|
124
|
+
self.logger.info("%s Enqueuing volume: %s", TAG, vol_name)
|
134
125
|
|
135
126
|
for chap in vol.get("chapters", []):
|
136
127
|
cid = chap.get("chapterId")
|
137
128
|
if not cid:
|
138
|
-
logger.warning("%s Skipping chapter without chapterId", TAG)
|
129
|
+
self.logger.warning("%s Skipping chapter without chapterId", TAG)
|
139
130
|
continue
|
140
131
|
|
141
|
-
|
142
|
-
|
143
|
-
if chap_path.exists() and skip_existing:
|
144
|
-
logger.debug(
|
132
|
+
if normal_cs.exists(cid) and skip_existing:
|
133
|
+
self.logger.debug(
|
145
134
|
"%s Chapter already exists, skipping: %s",
|
146
135
|
TAG,
|
147
136
|
cid,
|
@@ -149,16 +138,19 @@ class QidianDownloader(BaseDownloader):
|
|
149
138
|
continue
|
150
139
|
|
151
140
|
chap_title = chap.get("title", "")
|
152
|
-
logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
|
153
|
-
chap_html = self.requester.get_book_chapter(book_id, cid
|
141
|
+
self.logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
|
142
|
+
chap_html = self.requester.get_book_chapter(book_id, cid)
|
143
|
+
if scroll:
|
144
|
+
self.requester.scroll_page(wait_time * 2) # type: ignore[attr-defined]
|
145
|
+
else:
|
146
|
+
sleep_with_random_delay(
|
147
|
+
wait_time, mul_spread=1.1, max_sleep=wait_time + 2
|
148
|
+
)
|
154
149
|
|
155
150
|
is_encrypted = self.parser.is_encrypted(chap_html) # type: ignore[attr-defined]
|
156
151
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
if chap_path.exists() and skip_existing:
|
161
|
-
logger.debug(
|
152
|
+
if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
|
153
|
+
self.logger.debug(
|
162
154
|
"%s Chapter already exists, skipping: %s",
|
163
155
|
TAG,
|
164
156
|
cid,
|
@@ -171,13 +163,13 @@ class QidianDownloader(BaseDownloader):
|
|
171
163
|
)
|
172
164
|
html_path = folder / f"{cid}.html"
|
173
165
|
save_as_txt(chap_html, html_path, on_exist="skip")
|
174
|
-
logger.debug(
|
166
|
+
self.logger.debug(
|
175
167
|
"%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
|
176
168
|
)
|
177
169
|
|
178
170
|
chap_json = self.parser.parse_chapter(chap_html, cid)
|
179
171
|
if not chap_json:
|
180
|
-
logger.warning(
|
172
|
+
self.logger.warning(
|
181
173
|
"%s Parsed chapter json is empty, skipping: %s (%s)",
|
182
174
|
TAG,
|
183
175
|
chap_title,
|
@@ -185,18 +177,42 @@ class QidianDownloader(BaseDownloader):
|
|
185
177
|
)
|
186
178
|
continue
|
187
179
|
|
188
|
-
|
189
|
-
|
180
|
+
if is_encrypted:
|
181
|
+
encrypted_cs.save(chap_json)
|
182
|
+
else:
|
183
|
+
normal_cs.save(chap_json)
|
184
|
+
self.logger.info("%s Saved chapter: %s (%s)", TAG, chap_title, cid)
|
185
|
+
|
186
|
+
normal_cs.close()
|
187
|
+
encrypted_cs.close()
|
190
188
|
|
191
189
|
self.saver.save(book_id)
|
192
190
|
|
193
|
-
logger.info(
|
191
|
+
self.logger.info(
|
194
192
|
"%s Novel '%s' download completed.",
|
195
193
|
TAG,
|
196
194
|
book_info.get("book_name", "unknown"),
|
197
195
|
)
|
198
196
|
return
|
199
197
|
|
198
|
+
def _handle_login(self) -> bool:
|
199
|
+
"""
|
200
|
+
Perform login with automatic fallback to manual:
|
201
|
+
|
202
|
+
1. If manual_flag is False, try automatic login:
|
203
|
+
- On success, return True immediately.
|
204
|
+
2. Always attempt manual login if manual_flag is True.
|
205
|
+
3. Return True if manual login succeeds, False otherwise.
|
206
|
+
"""
|
207
|
+
manual_flag = state_mgr.get_manual_login_flag(self._site_key)
|
208
|
+
|
209
|
+
# First try automatic login
|
210
|
+
if not manual_flag and self._requester.login(manual_login=False):
|
211
|
+
return True
|
212
|
+
|
213
|
+
# try manual login
|
214
|
+
return self._requester.login(manual_login=True)
|
215
|
+
|
200
216
|
|
201
217
|
def is_vip(html_str: str) -> bool:
|
202
218
|
"""
|
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.factory
|
5
4
|
-----------------------------
|
@@ -8,18 +7,18 @@ This package provides factory methods for dynamically retrieving components
|
|
8
7
|
based on runtime parameters such as site name or content type.
|
9
8
|
"""
|
10
9
|
|
11
|
-
from .
|
10
|
+
from .downloader import (
|
12
11
|
get_async_downloader,
|
13
12
|
get_downloader,
|
14
13
|
get_sync_downloader,
|
15
14
|
)
|
16
|
-
from .
|
17
|
-
from .
|
15
|
+
from .parser import get_parser
|
16
|
+
from .requester import (
|
18
17
|
get_async_requester,
|
19
18
|
get_requester,
|
20
19
|
get_sync_requester,
|
21
20
|
)
|
22
|
-
from .
|
21
|
+
from .saver import get_saver
|
23
22
|
|
24
23
|
__all__ = [
|
25
24
|
"get_async_downloader",
|
@@ -1,23 +1,17 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.factory.downloader_factory
|
5
4
|
------------------------------------------------
|
6
5
|
|
7
6
|
This module implements a factory function for creating downloader instances
|
8
7
|
based on the site name and parser mode specified in the configuration.
|
9
|
-
|
10
|
-
- get_async_downloader -> always returns a CommonAsyncDownloader
|
11
|
-
- get_sync_downloader -> returns a site-specific downloader or CommonDownloader
|
12
|
-
- get_downloader -> dispatches to one of the above based on config.mode
|
13
|
-
|
14
|
-
To add support for new sites or modes, extend the `_site_map` accordingly.
|
15
8
|
"""
|
16
9
|
|
17
|
-
from typing import
|
10
|
+
from typing import cast
|
18
11
|
|
19
12
|
from novel_downloader.config import DownloaderConfig, load_site_rules
|
20
13
|
from novel_downloader.core.downloaders import (
|
14
|
+
BiqugeDownloader,
|
21
15
|
CommonAsyncDownloader,
|
22
16
|
CommonDownloader,
|
23
17
|
QidianDownloader,
|
@@ -25,15 +19,18 @@ from novel_downloader.core.downloaders import (
|
|
25
19
|
from novel_downloader.core.interfaces import (
|
26
20
|
AsyncDownloaderProtocol,
|
27
21
|
AsyncRequesterProtocol,
|
28
|
-
DownloaderProtocol,
|
29
22
|
ParserProtocol,
|
30
|
-
RequesterProtocol,
|
31
23
|
SaverProtocol,
|
24
|
+
SyncDownloaderProtocol,
|
25
|
+
SyncRequesterProtocol,
|
32
26
|
)
|
33
27
|
|
34
|
-
|
28
|
+
# _async_site_map = {
|
29
|
+
# # "biquge": ...
|
30
|
+
# }
|
31
|
+
_sync_site_map = {
|
35
32
|
"qidian": QidianDownloader,
|
36
|
-
|
33
|
+
"biquge": BiqugeDownloader,
|
37
34
|
}
|
38
35
|
|
39
36
|
|
@@ -56,14 +53,18 @@ def get_async_downloader(
|
|
56
53
|
:return: An instance of a downloader class
|
57
54
|
|
58
55
|
:raises ValueError: If a site-specific downloader does not support async mode.
|
59
|
-
:raises TypeError: If the provided requester does not match the required protocol
|
60
|
-
for the chosen mode (sync vs async).
|
56
|
+
:raises TypeError: If the provided requester does not match the required protocol.
|
61
57
|
"""
|
62
58
|
site_key = site.lower()
|
63
59
|
|
64
60
|
if not isinstance(requester, AsyncRequesterProtocol):
|
65
61
|
raise TypeError("Async mode requires an AsyncRequesterProtocol")
|
66
62
|
|
63
|
+
# site-specific
|
64
|
+
# if site_key in _async_site_map:
|
65
|
+
# return _async_site_map[site_key](requester, parser, saver, config)
|
66
|
+
|
67
|
+
# fallback
|
67
68
|
site_rules = load_site_rules()
|
68
69
|
site_rule = site_rules.get(site_key)
|
69
70
|
if site_rule is None:
|
@@ -73,12 +74,12 @@ def get_async_downloader(
|
|
73
74
|
|
74
75
|
|
75
76
|
def get_sync_downloader(
|
76
|
-
requester:
|
77
|
+
requester: SyncRequesterProtocol,
|
77
78
|
parser: ParserProtocol,
|
78
79
|
saver: SaverProtocol,
|
79
80
|
site: str,
|
80
81
|
config: DownloaderConfig,
|
81
|
-
) ->
|
82
|
+
) -> SyncDownloaderProtocol:
|
82
83
|
"""
|
83
84
|
Returns a DownloaderProtocol for the given site.
|
84
85
|
First tries a site-specific downloader (e.g. QidianDownloader),
|
@@ -93,17 +94,16 @@ def get_sync_downloader(
|
|
93
94
|
:return: An instance of a downloader class
|
94
95
|
|
95
96
|
:raises ValueError: If a site-specific downloader does not support async mode.
|
96
|
-
:raises TypeError: If the provided requester does not match the required protocol
|
97
|
-
for the chosen mode (sync vs async).
|
97
|
+
:raises TypeError: If the provided requester does not match the required protocol.
|
98
98
|
"""
|
99
99
|
site_key = site.lower()
|
100
100
|
|
101
|
-
if not isinstance(requester,
|
101
|
+
if not isinstance(requester, SyncRequesterProtocol):
|
102
102
|
raise TypeError("Sync mode requires a RequesterProtocol")
|
103
103
|
|
104
104
|
# site-specific
|
105
|
-
if site_key in
|
106
|
-
return
|
105
|
+
if site_key in _sync_site_map:
|
106
|
+
return _sync_site_map[site_key](requester, parser, saver, config)
|
107
107
|
|
108
108
|
# fallback
|
109
109
|
site_rules = load_site_rules()
|
@@ -115,12 +115,12 @@ def get_sync_downloader(
|
|
115
115
|
|
116
116
|
|
117
117
|
def get_downloader(
|
118
|
-
requester:
|
118
|
+
requester: AsyncRequesterProtocol | SyncRequesterProtocol,
|
119
119
|
parser: ParserProtocol,
|
120
120
|
saver: SaverProtocol,
|
121
121
|
site: str,
|
122
122
|
config: DownloaderConfig,
|
123
|
-
) ->
|
123
|
+
) -> AsyncDownloaderProtocol | SyncDownloaderProtocol:
|
124
124
|
"""
|
125
125
|
Dispatches to get_async_downloader if config.mode == 'async',
|
126
126
|
otherwise to get_sync_downloader.
|
@@ -134,16 +134,17 @@ def get_downloader(
|
|
134
134
|
:return: An instance of a downloader class
|
135
135
|
|
136
136
|
:raises ValueError: If a site-specific downloader does not support async mode.
|
137
|
-
:raises TypeError: If the provided requester does not match the required protocol
|
138
|
-
for the chosen mode (sync vs async).
|
137
|
+
:raises TypeError: If the provided requester does not match the required protocol.
|
139
138
|
"""
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
return get_async_downloader(
|
145
|
-
|
146
|
-
if not
|
147
|
-
raise TypeError(
|
148
|
-
|
149
|
-
|
139
|
+
if requester.is_async():
|
140
|
+
if config.mode.lower() != "async":
|
141
|
+
raise TypeError("Requester is async, but config.mode is not 'async'")
|
142
|
+
async_requester = cast(AsyncRequesterProtocol, requester)
|
143
|
+
return get_async_downloader(async_requester, parser, saver, site, config)
|
144
|
+
else:
|
145
|
+
if config.mode.lower() not in ("browser", "session"):
|
146
|
+
raise TypeError(
|
147
|
+
"Requester is sync, but config.mode is not 'browser' or 'session'"
|
148
|
+
)
|
149
|
+
sync_requester = cast(SyncRequesterProtocol, requester)
|
150
|
+
return get_sync_downloader(sync_requester, parser, saver, site, config)
|
@@ -1,35 +1,33 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.factory.parser_factory
|
5
4
|
--------------------------------------------
|
6
5
|
|
7
6
|
This module implements a factory function for creating parser instances
|
8
7
|
based on the site name and parser mode specified in the configuration.
|
9
|
-
|
10
|
-
Currently supported:
|
11
|
-
- Site: 'qidian'
|
12
|
-
- Modes:
|
13
|
-
- 'browser': QidianBrowserParser
|
14
|
-
- 'session': (Not implemented yet)
|
15
|
-
|
16
|
-
To add support for new sites or modes, extend the `_site_map` accordingly.
|
17
8
|
"""
|
18
9
|
|
10
|
+
from collections.abc import Callable
|
11
|
+
|
19
12
|
from novel_downloader.config import ParserConfig, load_site_rules
|
20
13
|
from novel_downloader.core.interfaces import ParserProtocol
|
21
14
|
from novel_downloader.core.parsers import (
|
15
|
+
BiqugeParser,
|
22
16
|
CommonParser,
|
23
17
|
QidianBrowserParser,
|
24
18
|
QidianSessionParser,
|
25
19
|
)
|
26
20
|
|
27
|
-
|
21
|
+
ParserBuilder = Callable[[ParserConfig], ParserProtocol]
|
22
|
+
|
23
|
+
_site_map: dict[str, dict[str, ParserBuilder]] = {
|
28
24
|
"qidian": {
|
29
25
|
"browser": QidianBrowserParser,
|
30
26
|
"session": QidianSessionParser,
|
31
27
|
},
|
32
|
-
|
28
|
+
"biquge": {
|
29
|
+
"session": BiqugeParser,
|
30
|
+
},
|
33
31
|
}
|
34
32
|
|
35
33
|
|
@@ -47,11 +45,11 @@ def get_parser(site: str, config: ParserConfig) -> ParserProtocol:
|
|
47
45
|
site_entry = _site_map[site_key]
|
48
46
|
if isinstance(site_entry, dict):
|
49
47
|
parser_class = site_entry.get(config.mode)
|
50
|
-
if parser_class is None:
|
51
|
-
raise ValueError(f"Unsupported mode '{config.mode}' for site '{site}'")
|
52
48
|
else:
|
53
49
|
parser_class = site_entry
|
54
|
-
|
50
|
+
|
51
|
+
if parser_class:
|
52
|
+
return parser_class(config)
|
55
53
|
|
56
54
|
# Fallback: site not mapped specially, try to load rule
|
57
55
|
site_rules = load_site_rules()
|
@@ -1,38 +1,45 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.factory.requester_factory
|
5
4
|
-----------------------------------------------
|
6
5
|
|
7
6
|
This module implements a factory function for retrieving requester instances
|
8
7
|
based on the target novel platform (site).
|
9
|
-
|
10
|
-
- get_async_requester -> returns AsyncRequesterProtocol
|
11
|
-
- get_sync_requester -> returns RequesterProtocol
|
12
|
-
- get_requester -> dispatches to one of the above based on config.mode
|
13
|
-
|
14
|
-
To add support for new sites or modes, extend the `_site_map` accordingly.
|
15
8
|
"""
|
16
9
|
|
17
|
-
from
|
10
|
+
from collections.abc import Callable
|
18
11
|
|
19
12
|
from novel_downloader.config import RequesterConfig, load_site_rules
|
20
|
-
from novel_downloader.core.interfaces import
|
13
|
+
from novel_downloader.core.interfaces import (
|
14
|
+
AsyncRequesterProtocol,
|
15
|
+
SyncRequesterProtocol,
|
16
|
+
)
|
21
17
|
from novel_downloader.core.requesters import (
|
18
|
+
BiqugeSession,
|
22
19
|
CommonAsyncSession,
|
23
20
|
CommonSession,
|
24
21
|
QidianBrowser,
|
25
22
|
QidianSession,
|
26
23
|
)
|
27
24
|
|
28
|
-
|
25
|
+
AsyncRequesterBuilder = Callable[[RequesterConfig], AsyncRequesterProtocol]
|
26
|
+
SyncRequesterBuilder = Callable[[RequesterConfig], SyncRequesterProtocol]
|
27
|
+
|
28
|
+
|
29
|
+
# _async_site_map: dict[str, AsyncRequesterBuilder] = {
|
30
|
+
# # "biquge": ...
|
31
|
+
# }
|
32
|
+
_sync_site_map: dict[
|
29
33
|
str,
|
30
|
-
dict[str,
|
34
|
+
dict[str, SyncRequesterBuilder],
|
31
35
|
] = {
|
32
36
|
"qidian": {
|
33
37
|
"session": QidianSession,
|
34
38
|
"browser": QidianBrowser,
|
35
39
|
},
|
40
|
+
"biquge": {
|
41
|
+
"session": BiqugeSession,
|
42
|
+
},
|
36
43
|
}
|
37
44
|
|
38
45
|
|
@@ -48,6 +55,12 @@ def get_async_requester(
|
|
48
55
|
:return: An instance of a requester class
|
49
56
|
"""
|
50
57
|
site_key = site.lower()
|
58
|
+
|
59
|
+
# site-specific
|
60
|
+
# if site_key in _async_site_map:
|
61
|
+
# return _async_site_map[site_key](config)
|
62
|
+
|
63
|
+
# fallback
|
51
64
|
site_rules = load_site_rules()
|
52
65
|
site_rule = site_rules.get(site_key)
|
53
66
|
if site_rule is None:
|
@@ -59,7 +72,7 @@ def get_async_requester(
|
|
59
72
|
def get_sync_requester(
|
60
73
|
site: str,
|
61
74
|
config: RequesterConfig,
|
62
|
-
) ->
|
75
|
+
) -> SyncRequesterProtocol:
|
63
76
|
"""
|
64
77
|
Returns a RequesterProtocol for the given site.
|
65
78
|
|
@@ -68,15 +81,15 @@ def get_sync_requester(
|
|
68
81
|
:return: An instance of a requester class
|
69
82
|
"""
|
70
83
|
site_key = site.lower()
|
71
|
-
site_entry =
|
84
|
+
site_entry = _sync_site_map.get(site_key)
|
72
85
|
|
73
|
-
# site-specific
|
86
|
+
# site-specific
|
74
87
|
if site_entry:
|
75
88
|
cls = site_entry.get(config.mode)
|
76
89
|
if cls:
|
77
90
|
return cls(config)
|
78
91
|
|
79
|
-
# fallback
|
92
|
+
# fallback
|
80
93
|
site_rules = load_site_rules()
|
81
94
|
site_rule = site_rules.get(site_key)
|
82
95
|
if site_rule is None:
|
@@ -88,7 +101,7 @@ def get_sync_requester(
|
|
88
101
|
def get_requester(
|
89
102
|
site: str,
|
90
103
|
config: RequesterConfig,
|
91
|
-
) ->
|
104
|
+
) -> AsyncRequesterProtocol | SyncRequesterProtocol:
|
92
105
|
"""
|
93
106
|
Dispatches to either get_async_requester or get_sync_requester
|
94
107
|
based on config.mode. Treats 'browser' and 'async' as async modes,
|
@@ -1,29 +1,23 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.factory.parser_factory
|
5
4
|
--------------------------------------------
|
6
5
|
|
7
6
|
This module implements a factory function for creating saver instances
|
8
7
|
based on the site name and parser mode specified in the configuration.
|
9
|
-
|
10
|
-
Currently supported:
|
11
|
-
- Site: 'qidian'
|
12
|
-
- QidianSaver
|
13
|
-
|
14
|
-
To add support for new sites or modes, extend the `_site_map` accordingly.
|
15
8
|
"""
|
16
9
|
|
17
10
|
from novel_downloader.config import SaverConfig, load_site_rules
|
18
11
|
from novel_downloader.core.interfaces import SaverProtocol
|
19
12
|
from novel_downloader.core.savers import (
|
13
|
+
BiqugeSaver,
|
20
14
|
CommonSaver,
|
21
15
|
QidianSaver,
|
22
16
|
)
|
23
17
|
|
24
18
|
_site_map = {
|
25
19
|
"qidian": QidianSaver,
|
26
|
-
|
20
|
+
"biquge": BiqugeSaver,
|
27
21
|
}
|
28
22
|
|
29
23
|
|
@@ -37,11 +31,12 @@ def get_saver(site: str, config: SaverConfig) -> SaverProtocol:
|
|
37
31
|
"""
|
38
32
|
site_key = site.lower()
|
39
33
|
|
34
|
+
# site-specific
|
40
35
|
saver_class = _site_map.get(site_key)
|
41
36
|
if saver_class:
|
42
37
|
return saver_class(config)
|
43
38
|
|
44
|
-
# Fallback
|
39
|
+
# Fallback
|
45
40
|
site_rules = load_site_rules()
|
46
41
|
if site_key not in site_rules:
|
47
42
|
raise ValueError(f"Unsupported site: {site}")
|