novel-downloader 1.2.2__py3-none-any.whl → 1.3.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 -2
- novel_downloader/cli/__init__.py +0 -1
- novel_downloader/cli/clean.py +2 -10
- novel_downloader/cli/download.py +16 -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 +32 -27
- novel_downloader/config/loader.py +116 -108
- novel_downloader/config/models.py +35 -29
- 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} +33 -21
- novel_downloader/core/downloaders/qidian/__init__.py +10 -0
- novel_downloader/core/downloaders/{qidian_downloader.py → qidian/qidian_sync.py} +79 -62
- novel_downloader/core/factory/__init__.py +4 -5
- novel_downloader/core/factory/{downloader_factory.py → downloader.py} +25 -26
- 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} +23 -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} +31 -17
- novel_downloader/core/parsers/__init__.py +5 -4
- novel_downloader/core/parsers/{base_parser.py → base.py} +18 -9
- 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 +13 -13
- 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 +40 -48
- 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 +14 -10
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/__init__.py +2 -3
- novel_downloader/core/parsers/{qidian_parser → qidian}/session/chapter_encrypted.py +36 -44
- 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 +14 -10
- 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_parser → qidian}/shared/book_info_parser.py +5 -6
- novel_downloader/core/parsers/{qidian_parser → qidian}/shared/helpers.py +7 -8
- 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} +177 -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 +307 -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} +23 -51
- 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 +8 -4
- novel_downloader/locales/zh.json +5 -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 +6 -4
- 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 +8 -10
- novel_downloader/utils/time_utils/sleep_utils.py +1 -3
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/METADATA +14 -17
- novel_downloader-1.3.1.dist-info/RECORD +127 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/WHEEL +1 -1
- novel_downloader/core/requesters/base_browser.py +0 -214
- novel_downloader/core/requesters/base_session.py +0 -246
- 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 -396
- novel_downloader/core/requesters/qidian_requester/qidian_session.py +0 -202
- novel_downloader/resources/config/settings.yaml +0 -76
- novel_downloader-1.2.2.dist-info/RECORD +0 -115
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.2.2.dist-info → novel_downloader-1.3.1.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,19 +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
|
-
chapter_dir = raw_base / "chapters"
|
89
|
-
encrypted_chapter_dir = raw_base / "encrypted_chapters"
|
90
71
|
chapters_html_dir = cache_base / "html"
|
91
72
|
|
92
73
|
raw_base.mkdir(parents=True, exist_ok=True)
|
93
|
-
|
94
|
-
|
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
|
+
)
|
95
86
|
|
96
|
-
book_info:
|
87
|
+
book_info: dict[str, Any]
|
97
88
|
|
98
89
|
try:
|
99
90
|
if not info_path.exists():
|
@@ -102,13 +93,13 @@ class QidianDownloader(BaseDownloader):
|
|
102
93
|
days, hrs, mins, secs = calculate_time_difference(
|
103
94
|
book_info.get("update_time", ""), "UTC+8"
|
104
95
|
)
|
105
|
-
logger.info(
|
96
|
+
self.logger.info(
|
106
97
|
"%s Last updated %dd %dh %dm %ds ago", TAG, days, hrs, mins, secs
|
107
98
|
)
|
108
99
|
if days > 1:
|
109
100
|
raise FileNotFoundError # trigger re-fetch
|
110
101
|
except Exception:
|
111
|
-
info_html = self.requester.get_book_info(book_id
|
102
|
+
info_html = self.requester.get_book_info(book_id)
|
112
103
|
if save_html:
|
113
104
|
info_html_path = chapters_html_dir / "info.html"
|
114
105
|
save_as_txt(info_html, info_html_path)
|
@@ -118,29 +109,28 @@ class QidianDownloader(BaseDownloader):
|
|
118
109
|
and book_info.get("update_time", "") != "未找到更新时间"
|
119
110
|
):
|
120
111
|
save_as_json(book_info, info_path)
|
112
|
+
sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
|
121
113
|
|
122
114
|
# download cover
|
123
115
|
cover_url = book_info.get("cover_url", "")
|
124
116
|
if cover_url:
|
125
117
|
cover_bytes = download_image_as_bytes(cover_url, raw_base)
|
126
118
|
if not cover_bytes:
|
127
|
-
logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
119
|
+
self.logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
128
120
|
|
129
121
|
# enqueue chapters
|
130
122
|
for vol in book_info.get("volumes", []):
|
131
123
|
vol_name = vol.get("volume_name", "")
|
132
|
-
logger.info("%s Enqueuing volume: %s", TAG, vol_name)
|
124
|
+
self.logger.info("%s Enqueuing volume: %s", TAG, vol_name)
|
133
125
|
|
134
126
|
for chap in vol.get("chapters", []):
|
135
127
|
cid = chap.get("chapterId")
|
136
128
|
if not cid:
|
137
|
-
logger.warning("%s Skipping chapter without chapterId", TAG)
|
129
|
+
self.logger.warning("%s Skipping chapter without chapterId", TAG)
|
138
130
|
continue
|
139
131
|
|
140
|
-
|
141
|
-
|
142
|
-
if chap_path.exists() and skip_existing:
|
143
|
-
logger.debug(
|
132
|
+
if normal_cs.exists(cid) and skip_existing:
|
133
|
+
self.logger.debug(
|
144
134
|
"%s Chapter already exists, skipping: %s",
|
145
135
|
TAG,
|
146
136
|
cid,
|
@@ -148,16 +138,19 @@ class QidianDownloader(BaseDownloader):
|
|
148
138
|
continue
|
149
139
|
|
150
140
|
chap_title = chap.get("title", "")
|
151
|
-
logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
|
152
|
-
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
|
+
)
|
153
149
|
|
154
150
|
is_encrypted = self.parser.is_encrypted(chap_html) # type: ignore[attr-defined]
|
155
151
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
if chap_path.exists() and skip_existing:
|
160
|
-
logger.debug(
|
152
|
+
if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
|
153
|
+
self.logger.debug(
|
161
154
|
"%s Chapter already exists, skipping: %s",
|
162
155
|
TAG,
|
163
156
|
cid,
|
@@ -170,13 +163,13 @@ class QidianDownloader(BaseDownloader):
|
|
170
163
|
)
|
171
164
|
html_path = folder / f"{cid}.html"
|
172
165
|
save_as_txt(chap_html, html_path, on_exist="skip")
|
173
|
-
logger.debug(
|
166
|
+
self.logger.debug(
|
174
167
|
"%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
|
175
168
|
)
|
176
169
|
|
177
170
|
chap_json = self.parser.parse_chapter(chap_html, cid)
|
178
171
|
if not chap_json:
|
179
|
-
logger.warning(
|
172
|
+
self.logger.warning(
|
180
173
|
"%s Parsed chapter json is empty, skipping: %s (%s)",
|
181
174
|
TAG,
|
182
175
|
chap_title,
|
@@ -184,18 +177,42 @@ class QidianDownloader(BaseDownloader):
|
|
184
177
|
)
|
185
178
|
continue
|
186
179
|
|
187
|
-
|
188
|
-
|
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()
|
189
188
|
|
190
189
|
self.saver.save(book_id)
|
191
190
|
|
192
|
-
logger.info(
|
191
|
+
self.logger.info(
|
193
192
|
"%s Novel '%s' download completed.",
|
194
193
|
TAG,
|
195
194
|
book_info.get("book_name", "unknown"),
|
196
195
|
)
|
197
196
|
return
|
198
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
|
+
|
199
216
|
|
200
217
|
def is_vip(html_str: str) -> bool:
|
201
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,8 +134,7 @@ 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
139
|
if requester.is_async():
|
141
140
|
if config.mode.lower() != "async":
|
@@ -147,5 +146,5 @@ def get_downloader(
|
|
147
146
|
raise TypeError(
|
148
147
|
"Requester is sync, but config.mode is not 'browser' or 'session'"
|
149
148
|
)
|
150
|
-
sync_requester = cast(
|
149
|
+
sync_requester = cast(SyncRequesterProtocol, requester)
|
151
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}")
|
@@ -1,5 +1,4 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
|
-
# -*- coding: utf-8 -*-
|
3
2
|
"""
|
4
3
|
novel_downloader.core.interfaces
|
5
4
|
--------------------------------
|
@@ -15,18 +14,18 @@ Included protocols:
|
|
15
14
|
- SaverProtocol
|
16
15
|
"""
|
17
16
|
|
18
|
-
from .
|
19
|
-
from .
|
20
|
-
from .
|
21
|
-
from .
|
22
|
-
from .
|
23
|
-
from .
|
17
|
+
from .async_downloader import AsyncDownloaderProtocol
|
18
|
+
from .async_requester import AsyncRequesterProtocol
|
19
|
+
from .parser import ParserProtocol
|
20
|
+
from .saver import SaverProtocol
|
21
|
+
from .sync_downloader import SyncDownloaderProtocol
|
22
|
+
from .sync_requester import SyncRequesterProtocol
|
24
23
|
|
25
24
|
__all__ = [
|
26
25
|
"AsyncDownloaderProtocol",
|
27
26
|
"AsyncRequesterProtocol",
|
28
|
-
"DownloaderProtocol",
|
29
27
|
"ParserProtocol",
|
30
|
-
"RequesterProtocol",
|
31
28
|
"SaverProtocol",
|
29
|
+
"SyncDownloaderProtocol",
|
30
|
+
"SyncRequesterProtocol",
|
32
31
|
]
|