novel-downloader 1.3.3__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 -39
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +22 -22
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
- novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
- novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +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 +20 -14
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +11 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +2 -1
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +1 -1
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +69 -35
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.3.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 -210
- novel_downloader/core/downloaders/common/common_sync.py +0 -202
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -219
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.3.dist-info/RECORD +0 -166
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -1,242 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
novel_downloader.core.requesters.sfacg.session
|
3
|
-
----------------------------------------------
|
4
|
-
|
5
|
-
"""
|
6
|
-
|
7
|
-
from http.cookies import SimpleCookie
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from novel_downloader.config.models import RequesterConfig
|
11
|
-
from novel_downloader.core.requesters.base import BaseSession
|
12
|
-
from novel_downloader.utils.i18n import t
|
13
|
-
from novel_downloader.utils.state import state_mgr
|
14
|
-
|
15
|
-
|
16
|
-
class SfacgSession(BaseSession):
|
17
|
-
"""
|
18
|
-
A session class for interacting with the
|
19
|
-
Sfacg (m.sfacg.com) novel website.
|
20
|
-
"""
|
21
|
-
|
22
|
-
BOOKCASE_URL = "https://m.sfacg.com/sheets/"
|
23
|
-
BOOK_INFO_URL = "https://m.sfacg.com/b/{book_id}/"
|
24
|
-
BOOK_CATALOG_URL = "https://m.sfacg.com/i/{book_id}/"
|
25
|
-
CHAPTER_URL = "https://m.sfacg.com/c/{chapter_id}/"
|
26
|
-
|
27
|
-
def __init__(
|
28
|
-
self,
|
29
|
-
config: RequesterConfig,
|
30
|
-
):
|
31
|
-
super().__init__(config)
|
32
|
-
self._logged_in: bool = False
|
33
|
-
self._retry_times = config.retry_times
|
34
|
-
|
35
|
-
def login(
|
36
|
-
self,
|
37
|
-
username: str = "",
|
38
|
-
password: str = "",
|
39
|
-
manual_login: bool = False,
|
40
|
-
**kwargs: Any,
|
41
|
-
) -> bool:
|
42
|
-
"""
|
43
|
-
Restore cookies persisted by the session-based workflow.
|
44
|
-
"""
|
45
|
-
cookies: dict[str, str] = state_mgr.get_cookies("sfacg")
|
46
|
-
|
47
|
-
self.update_cookies(cookies)
|
48
|
-
for attempt in range(1, self._retry_times + 1):
|
49
|
-
if self._check_login_status():
|
50
|
-
self.logger.debug("[auth] Already logged in.")
|
51
|
-
self._logged_in = True
|
52
|
-
return True
|
53
|
-
|
54
|
-
if attempt == 1:
|
55
|
-
print(t("session_login_prompt_intro"))
|
56
|
-
cookie_str = input(
|
57
|
-
t(
|
58
|
-
"session_login_prompt_paste_cookie",
|
59
|
-
attempt=attempt,
|
60
|
-
max_retries=self._retry_times,
|
61
|
-
)
|
62
|
-
).strip()
|
63
|
-
|
64
|
-
cookies = self._parse_cookie_input(cookie_str)
|
65
|
-
if not cookies:
|
66
|
-
print(t("session_login_prompt_invalid_cookie"))
|
67
|
-
continue
|
68
|
-
|
69
|
-
self.update_cookies(cookies)
|
70
|
-
self._logged_in = self._check_login_status()
|
71
|
-
return self._logged_in
|
72
|
-
|
73
|
-
def get_book_info(
|
74
|
-
self,
|
75
|
-
book_id: str,
|
76
|
-
**kwargs: Any,
|
77
|
-
) -> list[str]:
|
78
|
-
"""
|
79
|
-
Fetch the raw HTML of the book info and catalog pages.
|
80
|
-
|
81
|
-
Order: [info, catalog]
|
82
|
-
|
83
|
-
:param book_id: The book identifier.
|
84
|
-
:return: The page content as a string.
|
85
|
-
"""
|
86
|
-
info_url = self.book_info_url(book_id=book_id)
|
87
|
-
catalog_url = self.book_catalog_url(book_id=book_id)
|
88
|
-
|
89
|
-
pages = []
|
90
|
-
try:
|
91
|
-
resp = self.get(info_url, **kwargs)
|
92
|
-
resp.raise_for_status()
|
93
|
-
pages.append(resp.text)
|
94
|
-
except Exception as exc:
|
95
|
-
self.logger.warning(
|
96
|
-
"[session] get_book_info(info:%s) failed: %s",
|
97
|
-
book_id,
|
98
|
-
exc,
|
99
|
-
)
|
100
|
-
pages.append("")
|
101
|
-
|
102
|
-
try:
|
103
|
-
resp = self.get(catalog_url, **kwargs)
|
104
|
-
resp.raise_for_status()
|
105
|
-
pages.append(resp.text)
|
106
|
-
except Exception as exc:
|
107
|
-
self.logger.warning(
|
108
|
-
"[session] get_book_info(catalog:%s) failed: %s",
|
109
|
-
book_id,
|
110
|
-
exc,
|
111
|
-
)
|
112
|
-
pages.append("")
|
113
|
-
|
114
|
-
return pages
|
115
|
-
|
116
|
-
def get_book_chapter(
|
117
|
-
self,
|
118
|
-
book_id: str,
|
119
|
-
chapter_id: str,
|
120
|
-
**kwargs: Any,
|
121
|
-
) -> list[str]:
|
122
|
-
"""
|
123
|
-
Fetch the HTML of a single chapter.
|
124
|
-
|
125
|
-
:param book_id: The book identifier.
|
126
|
-
:param chapter_id: The chapter identifier.
|
127
|
-
:return: The chapter content as a string.
|
128
|
-
"""
|
129
|
-
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
130
|
-
try:
|
131
|
-
resp = self.get(url, **kwargs)
|
132
|
-
resp.raise_for_status()
|
133
|
-
return [resp.text]
|
134
|
-
except Exception as exc:
|
135
|
-
self.logger.warning(
|
136
|
-
"[session] get_book_chapter(%s) failed: %s",
|
137
|
-
book_id,
|
138
|
-
exc,
|
139
|
-
)
|
140
|
-
return []
|
141
|
-
|
142
|
-
def get_bookcase(
|
143
|
-
self,
|
144
|
-
page: int = 1,
|
145
|
-
**kwargs: Any,
|
146
|
-
) -> list[str]:
|
147
|
-
"""
|
148
|
-
Retrieve the user's *bookcase* page.
|
149
|
-
|
150
|
-
:return: The HTML markup of the bookcase page.
|
151
|
-
"""
|
152
|
-
url = self.bookcase_url()
|
153
|
-
try:
|
154
|
-
resp = self.get(url, **kwargs)
|
155
|
-
resp.raise_for_status()
|
156
|
-
return [resp.text]
|
157
|
-
except Exception as exc:
|
158
|
-
self.logger.warning(
|
159
|
-
"[session] get_bookcase failed: %s",
|
160
|
-
exc,
|
161
|
-
)
|
162
|
-
return []
|
163
|
-
|
164
|
-
@classmethod
|
165
|
-
def bookcase_url(cls) -> str:
|
166
|
-
"""
|
167
|
-
Construct the URL for the user's bookcase page.
|
168
|
-
|
169
|
-
:return: Fully qualified URL of the bookcase.
|
170
|
-
"""
|
171
|
-
return cls.BOOKCASE_URL
|
172
|
-
|
173
|
-
@classmethod
|
174
|
-
def book_info_url(cls, book_id: str) -> str:
|
175
|
-
"""
|
176
|
-
Construct the URL for fetching a book's info page.
|
177
|
-
|
178
|
-
:param book_id: The identifier of the book.
|
179
|
-
:return: Fully qualified URL for the book info page.
|
180
|
-
"""
|
181
|
-
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
182
|
-
|
183
|
-
@classmethod
|
184
|
-
def book_catalog_url(cls, book_id: str) -> str:
|
185
|
-
"""
|
186
|
-
Construct the URL for fetching a book's catalog page.
|
187
|
-
|
188
|
-
:param book_id: The identifier of the book.
|
189
|
-
:return: Fully qualified catalog page URL.
|
190
|
-
"""
|
191
|
-
return cls.BOOK_CATALOG_URL.format(book_id=book_id)
|
192
|
-
|
193
|
-
@classmethod
|
194
|
-
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
195
|
-
"""
|
196
|
-
Construct the URL for fetching a specific chapter.
|
197
|
-
|
198
|
-
:param book_id: The identifier of the book.
|
199
|
-
:param chapter_id: The identifier of the chapter.
|
200
|
-
:return: Fully qualified chapter URL.
|
201
|
-
"""
|
202
|
-
return cls.CHAPTER_URL.format(chapter_id=chapter_id)
|
203
|
-
|
204
|
-
def _check_login_status(self) -> bool:
|
205
|
-
"""
|
206
|
-
Check whether the user is currently logged in by
|
207
|
-
inspecting the bookcase page content.
|
208
|
-
|
209
|
-
:return: True if the user is logged in, False otherwise.
|
210
|
-
"""
|
211
|
-
keywords = [
|
212
|
-
"请输入用户名和密码",
|
213
|
-
"用户未登录",
|
214
|
-
"可输入用户名",
|
215
|
-
]
|
216
|
-
resp_text = self.get_bookcase()
|
217
|
-
if not resp_text:
|
218
|
-
return False
|
219
|
-
return not any(kw in resp_text[0] for kw in keywords)
|
220
|
-
|
221
|
-
@staticmethod
|
222
|
-
def _parse_cookie_input(cookie_str: str) -> dict[str, str]:
|
223
|
-
"""
|
224
|
-
Parse a raw cookie string (e.g. from browser dev tools) into a dict.
|
225
|
-
Returns an empty dict if parsing fails.
|
226
|
-
|
227
|
-
:param cookie_str: The raw cookie header string.
|
228
|
-
:return: Parsed cookie dict.
|
229
|
-
"""
|
230
|
-
filtered = "; ".join(pair for pair in cookie_str.split(";") if "=" in pair)
|
231
|
-
parsed = SimpleCookie()
|
232
|
-
try:
|
233
|
-
parsed.load(filtered)
|
234
|
-
return {k: v.value for k, v in parsed.items()}
|
235
|
-
except Exception:
|
236
|
-
return {}
|
237
|
-
|
238
|
-
def _on_close(self) -> None:
|
239
|
-
"""
|
240
|
-
Save cookies to the state manager before closing.
|
241
|
-
"""
|
242
|
-
state_mgr.set_cookies("sfacg", self.cookies)
|
@@ -1,237 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
novel_downloader.core.requesters.yamibo.session
|
3
|
-
-----------------------------------------------
|
4
|
-
|
5
|
-
"""
|
6
|
-
|
7
|
-
from typing import Any
|
8
|
-
|
9
|
-
from lxml import etree
|
10
|
-
|
11
|
-
from novel_downloader.config.models import RequesterConfig
|
12
|
-
from novel_downloader.core.requesters.base import BaseSession
|
13
|
-
from novel_downloader.utils.i18n import t
|
14
|
-
from novel_downloader.utils.state import state_mgr
|
15
|
-
from novel_downloader.utils.time_utils import sleep_with_random_delay
|
16
|
-
|
17
|
-
|
18
|
-
class YamiboSession(BaseSession):
|
19
|
-
"""
|
20
|
-
A session class for interacting with the
|
21
|
-
yamibo (www.yamibo.com) novel website.
|
22
|
-
"""
|
23
|
-
|
24
|
-
BASE_URL = "https://www.yamibo.com"
|
25
|
-
BOOKCASE_URL = "https://www.yamibo.com/my/fav"
|
26
|
-
BOOK_INFO_URL = "https://www.yamibo.com/novel/{book_id}"
|
27
|
-
CHAPTER_URL = "https://www.yamibo.com/novel/view-chapter?id={chapter_id}"
|
28
|
-
|
29
|
-
LOGIN_URL = "https://www.yamibo.com/user/login"
|
30
|
-
|
31
|
-
def __init__(
|
32
|
-
self,
|
33
|
-
config: RequesterConfig,
|
34
|
-
):
|
35
|
-
super().__init__(config)
|
36
|
-
self._logged_in: bool = False
|
37
|
-
self._request_interval = config.backoff_factor
|
38
|
-
self._retry_times = config.retry_times
|
39
|
-
self._username = config.username
|
40
|
-
self._password = config.password
|
41
|
-
|
42
|
-
def login(
|
43
|
-
self,
|
44
|
-
username: str = "",
|
45
|
-
password: str = "",
|
46
|
-
manual_login: bool = False,
|
47
|
-
**kwargs: Any,
|
48
|
-
) -> bool:
|
49
|
-
"""
|
50
|
-
Restore cookies persisted by the session-based workflow.
|
51
|
-
"""
|
52
|
-
cookies: dict[str, str] = state_mgr.get_cookies("yamibo")
|
53
|
-
username = username or self._username
|
54
|
-
password = password or self._password
|
55
|
-
|
56
|
-
self.update_cookies(cookies)
|
57
|
-
for _ in range(self._retry_times):
|
58
|
-
if self._check_login_status():
|
59
|
-
self.logger.debug("[auth] Already logged in.")
|
60
|
-
self._logged_in = True
|
61
|
-
return True
|
62
|
-
if username and password and not self._api_login(username, password):
|
63
|
-
print(t("session_login_failed", site="esjzone"))
|
64
|
-
sleep_with_random_delay(
|
65
|
-
self._request_interval,
|
66
|
-
mul_spread=1.1,
|
67
|
-
max_sleep=self._request_interval + 2,
|
68
|
-
)
|
69
|
-
|
70
|
-
self._logged_in = self._check_login_status()
|
71
|
-
return self._logged_in
|
72
|
-
|
73
|
-
def get_book_info(
|
74
|
-
self,
|
75
|
-
book_id: str,
|
76
|
-
**kwargs: Any,
|
77
|
-
) -> list[str]:
|
78
|
-
"""
|
79
|
-
Fetch the raw HTML of the book info and catalog pages.
|
80
|
-
|
81
|
-
Order: [info, catalog]
|
82
|
-
|
83
|
-
:param book_id: The book identifier.
|
84
|
-
:return: The page content as a string.
|
85
|
-
"""
|
86
|
-
url = self.book_info_url(book_id=book_id)
|
87
|
-
try:
|
88
|
-
resp = self.get(url, **kwargs)
|
89
|
-
resp.raise_for_status()
|
90
|
-
return [resp.text]
|
91
|
-
except Exception as exc:
|
92
|
-
self.logger.warning(
|
93
|
-
"[session] get_book_info(%s) failed: %s",
|
94
|
-
book_id,
|
95
|
-
exc,
|
96
|
-
)
|
97
|
-
return []
|
98
|
-
|
99
|
-
def get_book_chapter(
|
100
|
-
self,
|
101
|
-
book_id: str,
|
102
|
-
chapter_id: str,
|
103
|
-
**kwargs: Any,
|
104
|
-
) -> list[str]:
|
105
|
-
"""
|
106
|
-
Fetch the HTML of a single chapter.
|
107
|
-
|
108
|
-
:param book_id: The book identifier.
|
109
|
-
:param chapter_id: The chapter identifier.
|
110
|
-
:return: The chapter content as a string.
|
111
|
-
"""
|
112
|
-
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
113
|
-
try:
|
114
|
-
resp = self.get(url, **kwargs)
|
115
|
-
resp.raise_for_status()
|
116
|
-
return [resp.text]
|
117
|
-
except Exception as exc:
|
118
|
-
self.logger.warning(
|
119
|
-
"[session] get_book_chapter(%s) failed: %s",
|
120
|
-
book_id,
|
121
|
-
exc,
|
122
|
-
)
|
123
|
-
return []
|
124
|
-
|
125
|
-
def get_bookcase(
|
126
|
-
self,
|
127
|
-
page: int = 1,
|
128
|
-
**kwargs: Any,
|
129
|
-
) -> list[str]:
|
130
|
-
"""
|
131
|
-
Retrieve the user's *bookcase* page.
|
132
|
-
|
133
|
-
:return: The HTML markup of the bookcase page.
|
134
|
-
"""
|
135
|
-
url = self.bookcase_url()
|
136
|
-
try:
|
137
|
-
resp = self.get(url, **kwargs)
|
138
|
-
resp.raise_for_status()
|
139
|
-
return [resp.text]
|
140
|
-
except Exception as exc:
|
141
|
-
self.logger.warning(
|
142
|
-
"[session] get_bookcase failed: %s",
|
143
|
-
exc,
|
144
|
-
)
|
145
|
-
return []
|
146
|
-
|
147
|
-
@classmethod
|
148
|
-
def bookcase_url(cls) -> str:
|
149
|
-
"""
|
150
|
-
Construct the URL for the user's bookcase page.
|
151
|
-
|
152
|
-
:return: Fully qualified URL of the bookcase.
|
153
|
-
"""
|
154
|
-
return cls.BOOKCASE_URL
|
155
|
-
|
156
|
-
@classmethod
|
157
|
-
def book_info_url(cls, book_id: str) -> str:
|
158
|
-
"""
|
159
|
-
Construct the URL for fetching a book's info page.
|
160
|
-
|
161
|
-
:param book_id: The identifier of the book.
|
162
|
-
:return: Fully qualified URL for the book info page.
|
163
|
-
"""
|
164
|
-
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
165
|
-
|
166
|
-
@classmethod
|
167
|
-
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
168
|
-
"""
|
169
|
-
Construct the URL for fetching a specific chapter.
|
170
|
-
|
171
|
-
:param book_id: The identifier of the book.
|
172
|
-
:param chapter_id: The identifier of the chapter.
|
173
|
-
:return: Fully qualified chapter URL.
|
174
|
-
"""
|
175
|
-
return cls.CHAPTER_URL.format(chapter_id=chapter_id)
|
176
|
-
|
177
|
-
def _api_login(self, username: str, password: str) -> bool:
|
178
|
-
"""
|
179
|
-
Login to the API using a 2-step token-based process.
|
180
|
-
|
181
|
-
Step 1: Get token `_csrf-frontend`.
|
182
|
-
Step 2: Use token and credentials to perform login.
|
183
|
-
Return True if login succeeds, False otherwise.
|
184
|
-
"""
|
185
|
-
try:
|
186
|
-
resp_1 = self.get(self.LOGIN_URL)
|
187
|
-
resp_1.raise_for_status()
|
188
|
-
tree = etree.HTML(resp_1.text)
|
189
|
-
csrf_value = tree.xpath('//input[@name="_csrf-frontend"]/@value')
|
190
|
-
csrf_value = csrf_value[0] if csrf_value else ""
|
191
|
-
if not csrf_value:
|
192
|
-
self.logger.warning("[session] _api_login: CSRF token not found.")
|
193
|
-
return False
|
194
|
-
except Exception as exc:
|
195
|
-
self.logger.warning("[session] _api_login failed at step 1: %s", exc)
|
196
|
-
return False
|
197
|
-
|
198
|
-
data_2 = {
|
199
|
-
"_csrf-frontend": csrf_value,
|
200
|
-
"LoginForm[username]": username,
|
201
|
-
"LoginForm[password]": password,
|
202
|
-
# "LoginForm[rememberMe]": 0,
|
203
|
-
"LoginForm[rememberMe]": 1,
|
204
|
-
"login-button": "",
|
205
|
-
}
|
206
|
-
temp_headers = dict(self.headers)
|
207
|
-
temp_headers["Origin"] = self.BASE_URL
|
208
|
-
temp_headers["Referer"] = self.LOGIN_URL
|
209
|
-
try:
|
210
|
-
resp_2 = self.post(self.LOGIN_URL, data=data_2, headers=temp_headers)
|
211
|
-
resp_2.raise_for_status()
|
212
|
-
return "登录成功" in resp_2.text
|
213
|
-
except Exception as exc:
|
214
|
-
self.logger.warning("[session] _api_login failed at step 2: %s", exc)
|
215
|
-
return False
|
216
|
-
|
217
|
-
def _check_login_status(self) -> bool:
|
218
|
-
"""
|
219
|
-
Check whether the user is currently logged in by
|
220
|
-
inspecting the bookcase page content.
|
221
|
-
|
222
|
-
:return: True if the user is logged in, False otherwise.
|
223
|
-
"""
|
224
|
-
keywords = [
|
225
|
-
"登录 - 百合会",
|
226
|
-
"用户名/邮箱",
|
227
|
-
]
|
228
|
-
resp_text = self.get_bookcase()
|
229
|
-
if not resp_text:
|
230
|
-
return False
|
231
|
-
return not any(kw in resp_text[0] for kw in keywords)
|
232
|
-
|
233
|
-
def _on_close(self) -> None:
|
234
|
-
"""
|
235
|
-
Save cookies to the state manager before closing.
|
236
|
-
"""
|
237
|
-
state_mgr.set_cookies("yamibo", self.cookies)
|
@@ -1,34 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers
|
4
|
-
----------------------------
|
5
|
-
|
6
|
-
This module defines saver classes for different novel platforms.
|
7
|
-
|
8
|
-
Currently supported platforms:
|
9
|
-
- biquge (笔趣阁)
|
10
|
-
- esjzone (ESJ Zone)
|
11
|
-
- qianbi (铅笔小说)
|
12
|
-
- qidian (起点中文网)
|
13
|
-
- sfacg (SF轻小说)
|
14
|
-
- yamibo (百合会)
|
15
|
-
- common (通用架构)
|
16
|
-
"""
|
17
|
-
|
18
|
-
from .biquge import BiqugeSaver
|
19
|
-
from .common import CommonSaver
|
20
|
-
from .esjzone import EsjzoneSaver
|
21
|
-
from .qianbi import QianbiSaver
|
22
|
-
from .qidian import QidianSaver
|
23
|
-
from .sfacg import SfacgSaver
|
24
|
-
from .yamibo import YamiboSaver
|
25
|
-
|
26
|
-
__all__ = [
|
27
|
-
"BiqugeSaver",
|
28
|
-
"CommonSaver",
|
29
|
-
"EsjzoneSaver",
|
30
|
-
"QianbiSaver",
|
31
|
-
"QidianSaver",
|
32
|
-
"SfacgSaver",
|
33
|
-
"YamiboSaver",
|
34
|
-
]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.biquge
|
4
|
-
-----------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import SaverConfig
|
9
|
-
|
10
|
-
from .common import CommonSaver
|
11
|
-
|
12
|
-
|
13
|
-
class BiqugeSaver(CommonSaver):
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
config: SaverConfig,
|
17
|
-
):
|
18
|
-
super().__init__(
|
19
|
-
config,
|
20
|
-
site="biquge",
|
21
|
-
chap_folders=["chapters"],
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
__all__ = ["BiqugeSaver"]
|
@@ -1,12 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.common
|
4
|
-
-----------------------------------
|
5
|
-
|
6
|
-
This module provides the `CommonSaver` class for handling the saving process
|
7
|
-
of novels.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from .main_saver import CommonSaver
|
11
|
-
|
12
|
-
__all__ = ["CommonSaver"]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.esjzone
|
4
|
-
------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import SaverConfig
|
9
|
-
|
10
|
-
from .common import CommonSaver
|
11
|
-
|
12
|
-
|
13
|
-
class EsjzoneSaver(CommonSaver):
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
config: SaverConfig,
|
17
|
-
):
|
18
|
-
super().__init__(
|
19
|
-
config,
|
20
|
-
site="esjzone",
|
21
|
-
chap_folders=["chapters"],
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
__all__ = ["EsjzoneSaver"]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.qianbi
|
4
|
-
-----------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import SaverConfig
|
9
|
-
|
10
|
-
from .common import CommonSaver
|
11
|
-
|
12
|
-
|
13
|
-
class QianbiSaver(CommonSaver):
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
config: SaverConfig,
|
17
|
-
):
|
18
|
-
super().__init__(
|
19
|
-
config,
|
20
|
-
site="qianbi",
|
21
|
-
chap_folders=["chapters"],
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
__all__ = ["QianbiSaver"]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.sfacg
|
4
|
-
----------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import SaverConfig
|
9
|
-
|
10
|
-
from .common import CommonSaver
|
11
|
-
|
12
|
-
|
13
|
-
class SfacgSaver(CommonSaver):
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
config: SaverConfig,
|
17
|
-
):
|
18
|
-
super().__init__(
|
19
|
-
config,
|
20
|
-
site="sfacg",
|
21
|
-
chap_folders=["chapters"],
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
__all__ = ["SfacgSaver"]
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.savers.yamibo
|
4
|
-
-----------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.config.models import SaverConfig
|
9
|
-
|
10
|
-
from .common import CommonSaver
|
11
|
-
|
12
|
-
|
13
|
-
class YamiboSaver(CommonSaver):
|
14
|
-
def __init__(
|
15
|
-
self,
|
16
|
-
config: SaverConfig,
|
17
|
-
):
|
18
|
-
super().__init__(
|
19
|
-
config,
|
20
|
-
site="yamibo",
|
21
|
-
chap_folders=["chapters"],
|
22
|
-
)
|
23
|
-
|
24
|
-
|
25
|
-
__all__ = ["YamiboSaver"]
|