novel-downloader 1.3.2__py3-none-any.whl → 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/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 -44
- 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 +40 -52
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
- novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
- novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
- 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 +178 -0
- novel_downloader/core/fetchers/linovelib/session.py +178 -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 +24 -17
- 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 +15 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/file_utils/io.py +1 -1
- 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 +53 -39
- 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 +3 -3
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.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 -218
- novel_downloader/core/downloaders/common/common_sync.py +0 -210
- 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 -227
- 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/epub_utils/__init__.py +0 -26
- 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.2.dist-info/RECORD +0 -165
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,227 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.qidian.qidian_sync
|
4
|
-
----------------------------------------------------
|
5
|
-
|
6
|
-
This module defines `QidianDownloader`, a platform-specific downloader
|
7
|
-
implementation for retrieving novels from Qidian (起点中文网).
|
8
|
-
"""
|
9
|
-
|
10
|
-
import json
|
11
|
-
from typing import Any
|
12
|
-
|
13
|
-
from novel_downloader.config import DownloaderConfig
|
14
|
-
from novel_downloader.core.downloaders.base import BaseDownloader
|
15
|
-
from novel_downloader.core.interfaces import (
|
16
|
-
ParserProtocol,
|
17
|
-
SaverProtocol,
|
18
|
-
SyncRequesterProtocol,
|
19
|
-
)
|
20
|
-
from novel_downloader.utils.chapter_storage import ChapterStorage
|
21
|
-
from novel_downloader.utils.file_utils import save_as_json, save_as_txt
|
22
|
-
from novel_downloader.utils.network import download_image_as_bytes
|
23
|
-
from novel_downloader.utils.state import state_mgr
|
24
|
-
from novel_downloader.utils.time_utils import (
|
25
|
-
calculate_time_difference,
|
26
|
-
sleep_with_random_delay,
|
27
|
-
)
|
28
|
-
|
29
|
-
|
30
|
-
class QidianDownloader(BaseDownloader):
|
31
|
-
"""
|
32
|
-
Specialized downloader for Qidian novels.
|
33
|
-
"""
|
34
|
-
|
35
|
-
def __init__(
|
36
|
-
self,
|
37
|
-
requester: SyncRequesterProtocol,
|
38
|
-
parser: ParserProtocol,
|
39
|
-
saver: SaverProtocol,
|
40
|
-
config: DownloaderConfig,
|
41
|
-
):
|
42
|
-
super().__init__(requester, parser, saver, config, "qidian")
|
43
|
-
|
44
|
-
self._site_key = "qidian"
|
45
|
-
self._is_logged_in = self._handle_login()
|
46
|
-
state_mgr.set_manual_login_flag(self._site_key, not self._is_logged_in)
|
47
|
-
|
48
|
-
def download_one(self, book_id: str) -> None:
|
49
|
-
"""
|
50
|
-
The full download logic for a single book.
|
51
|
-
|
52
|
-
:param book_id: The identifier of the book to download.
|
53
|
-
"""
|
54
|
-
if not self._is_logged_in:
|
55
|
-
self.logger.warning(
|
56
|
-
"[%s] login failed, skipping download of %s",
|
57
|
-
self._site_key,
|
58
|
-
book_id,
|
59
|
-
)
|
60
|
-
return
|
61
|
-
|
62
|
-
TAG = "[Downloader]"
|
63
|
-
save_html = self.config.save_html
|
64
|
-
skip_existing = self.config.skip_existing
|
65
|
-
wait_time = self.config.request_interval
|
66
|
-
scroll = self.config.mode == "browser"
|
67
|
-
|
68
|
-
raw_base = self.raw_data_dir / book_id
|
69
|
-
cache_base = self.cache_dir / book_id
|
70
|
-
info_path = raw_base / "book_info.json"
|
71
|
-
chapters_html_dir = cache_base / "html"
|
72
|
-
|
73
|
-
raw_base.mkdir(parents=True, exist_ok=True)
|
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
|
-
)
|
86
|
-
|
87
|
-
book_info: dict[str, Any]
|
88
|
-
|
89
|
-
try:
|
90
|
-
if not info_path.exists():
|
91
|
-
raise FileNotFoundError
|
92
|
-
book_info = json.loads(info_path.read_text(encoding="utf-8"))
|
93
|
-
days, hrs, mins, secs = calculate_time_difference(
|
94
|
-
book_info.get("update_time", ""), "UTC+8"
|
95
|
-
)
|
96
|
-
self.logger.info(
|
97
|
-
"%s Last updated %dd %dh %dm %ds ago", TAG, days, hrs, mins, secs
|
98
|
-
)
|
99
|
-
if days > 1:
|
100
|
-
raise FileNotFoundError # trigger re-fetch
|
101
|
-
except Exception:
|
102
|
-
info_html = self.requester.get_book_info(book_id)
|
103
|
-
if save_html and info_html:
|
104
|
-
info_html_path = chapters_html_dir / "info.html"
|
105
|
-
save_as_txt(info_html[0], info_html_path)
|
106
|
-
book_info = self.parser.parse_book_info(info_html)
|
107
|
-
if (
|
108
|
-
book_info.get("book_name", "") != "未找到书名"
|
109
|
-
and book_info.get("update_time", "") != "未找到更新时间"
|
110
|
-
):
|
111
|
-
save_as_json(book_info, info_path)
|
112
|
-
sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
|
113
|
-
|
114
|
-
# download cover
|
115
|
-
cover_url = book_info.get("cover_url", "")
|
116
|
-
if cover_url:
|
117
|
-
cover_bytes = download_image_as_bytes(cover_url, raw_base)
|
118
|
-
if not cover_bytes:
|
119
|
-
self.logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
120
|
-
|
121
|
-
# enqueue chapters
|
122
|
-
for vol in book_info.get("volumes", []):
|
123
|
-
vol_name = vol.get("volume_name", "")
|
124
|
-
self.logger.info("%s Enqueuing volume: %s", TAG, vol_name)
|
125
|
-
|
126
|
-
for chap in vol.get("chapters", []):
|
127
|
-
cid = chap.get("chapterId")
|
128
|
-
if not cid:
|
129
|
-
self.logger.warning("%s Skipping chapter without chapterId", TAG)
|
130
|
-
continue
|
131
|
-
|
132
|
-
if normal_cs.exists(cid) and skip_existing:
|
133
|
-
self.logger.debug(
|
134
|
-
"%s Chapter already exists, skipping: %s",
|
135
|
-
TAG,
|
136
|
-
cid,
|
137
|
-
)
|
138
|
-
continue
|
139
|
-
|
140
|
-
chap_title = chap.get("title", "")
|
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 not chap_html:
|
144
|
-
continue
|
145
|
-
|
146
|
-
if scroll:
|
147
|
-
self.requester.scroll_page(wait_time * 2) # type: ignore[attr-defined]
|
148
|
-
else:
|
149
|
-
sleep_with_random_delay(
|
150
|
-
wait_time, mul_spread=1.1, max_sleep=wait_time + 2
|
151
|
-
)
|
152
|
-
|
153
|
-
is_encrypted = self.parser.is_encrypted(chap_html[0]) # type: ignore[attr-defined]
|
154
|
-
|
155
|
-
if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
|
156
|
-
self.logger.debug(
|
157
|
-
"%s Chapter already exists, skipping: %s",
|
158
|
-
TAG,
|
159
|
-
cid,
|
160
|
-
)
|
161
|
-
continue
|
162
|
-
|
163
|
-
if save_html and chap_html and not is_vip(chap_html[0]):
|
164
|
-
folder = chapters_html_dir / (
|
165
|
-
"html_encrypted" if is_encrypted else "html_plain"
|
166
|
-
)
|
167
|
-
html_path = folder / f"{cid}.html"
|
168
|
-
save_as_txt(chap_html[0], html_path, on_exist="skip")
|
169
|
-
self.logger.debug(
|
170
|
-
"%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
|
171
|
-
)
|
172
|
-
|
173
|
-
chap_json = self.parser.parse_chapter(chap_html, cid)
|
174
|
-
if not chap_json or not chap_json.get("content"):
|
175
|
-
self.logger.warning(
|
176
|
-
"%s Parsed chapter json is empty, skipping: %s (%s)",
|
177
|
-
TAG,
|
178
|
-
chap_title,
|
179
|
-
cid,
|
180
|
-
)
|
181
|
-
continue
|
182
|
-
|
183
|
-
if is_encrypted:
|
184
|
-
encrypted_cs.save(chap_json)
|
185
|
-
else:
|
186
|
-
normal_cs.save(chap_json)
|
187
|
-
self.logger.info("%s Saved chapter: %s (%s)", TAG, chap_title, cid)
|
188
|
-
|
189
|
-
normal_cs.close()
|
190
|
-
encrypted_cs.close()
|
191
|
-
|
192
|
-
self.saver.save(book_id)
|
193
|
-
|
194
|
-
self.logger.info(
|
195
|
-
"%s Novel '%s' download completed.",
|
196
|
-
TAG,
|
197
|
-
book_info.get("book_name", "unknown"),
|
198
|
-
)
|
199
|
-
return
|
200
|
-
|
201
|
-
def _handle_login(self) -> bool:
|
202
|
-
"""
|
203
|
-
Perform login with automatic fallback to manual:
|
204
|
-
|
205
|
-
1. If manual_flag is False, try automatic login:
|
206
|
-
- On success, return True immediately.
|
207
|
-
2. Always attempt manual login if manual_flag is True.
|
208
|
-
3. Return True if manual login succeeds, False otherwise.
|
209
|
-
"""
|
210
|
-
manual_flag = state_mgr.get_manual_login_flag(self._site_key)
|
211
|
-
|
212
|
-
# First try automatic login
|
213
|
-
if not manual_flag and self._requester.login(manual_login=False):
|
214
|
-
return True
|
215
|
-
|
216
|
-
# try manual login
|
217
|
-
return self._requester.login(manual_login=True)
|
218
|
-
|
219
|
-
|
220
|
-
def is_vip(html_str: str) -> bool:
|
221
|
-
"""
|
222
|
-
Return True if page indicates VIP-only content.
|
223
|
-
|
224
|
-
:param html_str: Raw HTML string.
|
225
|
-
"""
|
226
|
-
markers = ["这是VIP章节", "需要订阅", "订阅后才能阅读"]
|
227
|
-
return any(m in html_str for m in markers)
|
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.sfacg
|
4
|
-
---------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from .sfacg_async import SfacgAsyncDownloader
|
9
|
-
from .sfacg_sync import SfacgDownloader
|
10
|
-
|
11
|
-
__all__ = [
|
12
|
-
"SfacgAsyncDownloader",
|
13
|
-
"SfacgDownloader",
|
14
|
-
]
|
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.sfacg.sfacg_async
|
4
|
-
---------------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import DownloaderConfig
|
9
|
-
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
-
from novel_downloader.core.interfaces import (
|
11
|
-
AsyncRequesterProtocol,
|
12
|
-
ParserProtocol,
|
13
|
-
SaverProtocol,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class SfacgAsyncDownloader(CommonAsyncDownloader):
|
18
|
-
""""""
|
19
|
-
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
requester: AsyncRequesterProtocol,
|
23
|
-
parser: ParserProtocol,
|
24
|
-
saver: SaverProtocol,
|
25
|
-
config: DownloaderConfig,
|
26
|
-
):
|
27
|
-
super().__init__(requester, parser, saver, config, "sfacg")
|
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.sfacg.sfacg_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 SfacgDownloader(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, "sfacg")
|
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.yamibo
|
4
|
-
----------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from .yamibo_async import YamiboAsyncDownloader
|
9
|
-
from .yamibo_sync import YamiboDownloader
|
10
|
-
|
11
|
-
__all__ = [
|
12
|
-
"YamiboAsyncDownloader",
|
13
|
-
"YamiboDownloader",
|
14
|
-
]
|
@@ -1,27 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.downloaders.yamibo.yamibo_async
|
4
|
-
-----------------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import DownloaderConfig
|
9
|
-
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
-
from novel_downloader.core.interfaces import (
|
11
|
-
AsyncRequesterProtocol,
|
12
|
-
ParserProtocol,
|
13
|
-
SaverProtocol,
|
14
|
-
)
|
15
|
-
|
16
|
-
|
17
|
-
class YamiboAsyncDownloader(CommonAsyncDownloader):
|
18
|
-
""""""
|
19
|
-
|
20
|
-
def __init__(
|
21
|
-
self,
|
22
|
-
requester: AsyncRequesterProtocol,
|
23
|
-
parser: ParserProtocol,
|
24
|
-
saver: SaverProtocol,
|
25
|
-
config: DownloaderConfig,
|
26
|
-
):
|
27
|
-
super().__init__(requester, parser, saver, config, "yamibo")
|
@@ -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
|
-
...
|