novel-downloader 1.4.5__py3-none-any.whl → 2.0.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/__init__.py +2 -4
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +27 -104
- novel_downloader/cli/download.py +78 -66
- novel_downloader/cli/export.py +20 -21
- novel_downloader/cli/main.py +3 -1
- novel_downloader/cli/search.py +120 -0
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +10 -14
- novel_downloader/config/adapter.py +195 -99
- novel_downloader/config/{loader.py → file_io.py} +53 -27
- novel_downloader/core/__init__.py +14 -13
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/archived/qidian/searcher.py +79 -0
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +8 -30
- novel_downloader/core/downloaders/base.py +182 -30
- novel_downloader/core/downloaders/common.py +217 -384
- novel_downloader/core/downloaders/qianbi.py +332 -4
- novel_downloader/core/downloaders/qidian.py +250 -290
- novel_downloader/core/downloaders/registry.py +69 -0
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +8 -26
- novel_downloader/core/exporters/base.py +107 -31
- novel_downloader/core/exporters/common/__init__.py +3 -4
- novel_downloader/core/exporters/common/epub.py +92 -171
- novel_downloader/core/exporters/common/main_exporter.py +14 -67
- novel_downloader/core/exporters/common/txt.py +90 -86
- novel_downloader/core/exporters/epub_util.py +184 -1327
- novel_downloader/core/exporters/linovelib/__init__.py +3 -2
- novel_downloader/core/exporters/linovelib/epub.py +165 -222
- novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
- novel_downloader/core/exporters/linovelib/txt.py +76 -66
- novel_downloader/core/exporters/qidian.py +15 -11
- novel_downloader/core/exporters/registry.py +55 -0
- novel_downloader/core/exporters/txt_util.py +67 -0
- novel_downloader/core/fetchers/__init__.py +57 -56
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
- novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
- novel_downloader/core/fetchers/biquyuedu.py +83 -0
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +23 -11
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +22 -26
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +9 -9
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +60 -0
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +23 -11
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +8 -14
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- novel_downloader/core/interfaces/searcher.py +26 -0
- novel_downloader/core/parsers/__init__.py +58 -22
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +63 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/{esjzone/main_parser.py → esjzone.py} +67 -67
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/{linovelib/main_parser.py → linovelib.py} +54 -65
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
- novel_downloader/core/parsers/qidian/__init__.py +2 -2
- novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
- novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
- novel_downloader/core/parsers/qidian/main_parser.py +19 -57
- novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
- novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +57 -0
- novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +435 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +155 -0
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +51 -0
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/b520.py +84 -0
- novel_downloader/core/searchers/base.py +168 -0
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +102 -0
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +165 -0
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +79 -0
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +36 -79
- novel_downloader/locales/zh.json +37 -80
- novel_downloader/models/__init__.py +23 -50
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +16 -43
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +21 -0
- novel_downloader/resources/config/settings.toml +39 -74
- novel_downloader/resources/css_styles/intro.css +83 -0
- novel_downloader/resources/css_styles/main.css +30 -89
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +43 -0
- novel_downloader/utils/chapter_storage.py +247 -226
- novel_downloader/utils/constants.py +5 -50
- novel_downloader/utils/cookies.py +6 -18
- novel_downloader/utils/crypto_utils/__init__.py +13 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
- novel_downloader/utils/epub/__init__.py +34 -0
- novel_downloader/utils/epub/builder.py +377 -0
- novel_downloader/utils/epub/constants.py +118 -0
- novel_downloader/utils/epub/documents.py +297 -0
- novel_downloader/utils/epub/models.py +120 -0
- novel_downloader/utils/epub/utils.py +179 -0
- novel_downloader/utils/file_utils/__init__.py +5 -30
- novel_downloader/utils/file_utils/io.py +9 -150
- novel_downloader/utils/file_utils/normalize.py +2 -2
- novel_downloader/utils/file_utils/sanitize.py +2 -7
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/i18n.py +2 -0
- novel_downloader/utils/logger.py +10 -16
- novel_downloader/utils/network.py +111 -252
- novel_downloader/utils/state.py +5 -90
- novel_downloader/utils/text_utils/__init__.py +16 -21
- novel_downloader/utils/text_utils/diff_display.py +6 -9
- novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
- novel_downloader/utils/text_utils/text_cleaner.py +179 -0
- novel_downloader/utils/text_utils/truncate_utils.py +62 -0
- novel_downloader/utils/time_utils/__init__.py +6 -12
- novel_downloader/utils/time_utils/datetime_utils.py +23 -33
- novel_downloader/utils/time_utils/sleep_utils.py +5 -10
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.0.dist-info/METADATA +171 -0
- novel_downloader-2.0.0.dist-info/RECORD +210 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
- novel_downloader/config/site_rules.py +0 -94
- novel_downloader/core/downloaders/biquge.py +0 -25
- novel_downloader/core/downloaders/esjzone.py +0 -25
- novel_downloader/core/downloaders/linovelib.py +0 -25
- novel_downloader/core/downloaders/sfacg.py +0 -25
- novel_downloader/core/downloaders/yamibo.py +0 -25
- novel_downloader/core/exporters/biquge.py +0 -25
- novel_downloader/core/exporters/esjzone.py +0 -25
- novel_downloader/core/exporters/qianbi.py +0 -25
- novel_downloader/core/exporters/sfacg.py +0 -25
- novel_downloader/core/exporters/yamibo.py +0 -25
- novel_downloader/core/factory/__init__.py +0 -20
- novel_downloader/core/factory/downloader.py +0 -73
- novel_downloader/core/factory/exporter.py +0 -58
- novel_downloader/core/factory/fetcher.py +0 -96
- novel_downloader/core/factory/parser.py +0 -86
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -403
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/common/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -204
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -193
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -318
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -189
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -229
- novel_downloader/core/parsers/biquge/__init__.py +0 -10
- novel_downloader/core/parsers/biquge/main_parser.py +0 -134
- novel_downloader/core/parsers/common/__init__.py +0 -13
- novel_downloader/core/parsers/common/helper.py +0 -323
- novel_downloader/core/parsers/common/main_parser.py +0 -106
- novel_downloader/core/parsers/esjzone/__init__.py +0 -10
- novel_downloader/core/parsers/linovelib/__init__.py +0 -10
- novel_downloader/core/parsers/qianbi/__init__.py +0 -10
- novel_downloader/core/parsers/sfacg/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/__init__.py +0 -10
- novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
- novel_downloader/models/browser.py +0 -21
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/site_rules.py +0 -99
- novel_downloader/models/tasks.py +0 -33
- novel_downloader/models/types.py +0 -15
- novel_downloader/resources/css_styles/volume-intro.css +0 -56
- novel_downloader/resources/json/replace_word_map.json +0 -4
- novel_downloader/resources/text/blacklist.txt +0 -22
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/fontocr/__init__.py +0 -22
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -303
- novel_downloader/utils/fontocr/ocr_v2.py +0 -752
- novel_downloader/utils/hash_store.py +0 -279
- novel_downloader/utils/hash_utils.py +0 -103
- novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
- novel_downloader/utils/text_utils/font_mapping.py +0 -28
- novel_downloader/utils/text_utils/text_cleaning.py +0 -107
- novel_downloader-1.4.5.dist-info/METADATA +0 -196
- novel_downloader-1.4.5.dist-info/RECORD +0 -165
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.fetchers.linovelib
|
4
|
-
|
3
|
+
novel_downloader.core.fetchers.linovelib
|
4
|
+
----------------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -9,13 +9,17 @@ import re
|
|
9
9
|
from typing import Any
|
10
10
|
|
11
11
|
from novel_downloader.core.fetchers.base import BaseSession
|
12
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
13
|
from novel_downloader.models import FetcherConfig
|
13
|
-
from novel_downloader.utils
|
14
|
+
from novel_downloader.utils import async_jitter_sleep
|
14
15
|
|
15
16
|
|
17
|
+
@register_fetcher(
|
18
|
+
site_keys=["linovelib"],
|
19
|
+
)
|
16
20
|
class LinovelibSession(BaseSession):
|
17
21
|
"""
|
18
|
-
A session class for interacting with
|
22
|
+
A session class for interacting with 哔哩轻小说 (www.linovelib.com) novel website.
|
19
23
|
"""
|
20
24
|
|
21
25
|
BASE_URL = "https://www.linovelib.com"
|
@@ -42,8 +46,10 @@ class LinovelibSession(BaseSession):
|
|
42
46
|
"""
|
43
47
|
Fetch the raw HTML of the book info page.
|
44
48
|
|
49
|
+
Order: [info, vol1_html, ..., volN_html]
|
50
|
+
|
45
51
|
:param book_id: The book identifier.
|
46
|
-
:return:
|
52
|
+
:return: The page content as string list.
|
47
53
|
"""
|
48
54
|
url = self.book_info_url(book_id=book_id)
|
49
55
|
info_html = await self.fetch(url, **kwargs)
|
@@ -57,7 +63,7 @@ class LinovelibSession(BaseSession):
|
|
57
63
|
|
58
64
|
vol_htmls = []
|
59
65
|
for vol_id in vol_ids:
|
60
|
-
await
|
66
|
+
await async_jitter_sleep(
|
61
67
|
self.request_interval,
|
62
68
|
mul_spread=1.1,
|
63
69
|
max_sleep=self.request_interval + 2,
|
@@ -93,9 +99,11 @@ class LinovelibSession(BaseSession):
|
|
93
99
|
"""
|
94
100
|
Fetch the raw HTML of a single chapter asynchronously.
|
95
101
|
|
102
|
+
Order: [page1, ..., pageN]
|
103
|
+
|
96
104
|
:param book_id: The book identifier.
|
97
105
|
:param chapter_id: The chapter identifier.
|
98
|
-
:return: The
|
106
|
+
:return: The page content as string list.
|
99
107
|
"""
|
100
108
|
html_pages: list[str] = []
|
101
109
|
idx = 1
|
@@ -121,7 +129,7 @@ class LinovelibSession(BaseSession):
|
|
121
129
|
|
122
130
|
html_pages.append(html)
|
123
131
|
idx += 1
|
124
|
-
await
|
132
|
+
await async_jitter_sleep(
|
125
133
|
self.request_interval,
|
126
134
|
mul_spread=1.1,
|
127
135
|
max_sleep=self.request_interval + 2,
|
@@ -178,10 +186,6 @@ class LinovelibSession(BaseSession):
|
|
178
186
|
"""
|
179
187
|
return f"/novel/{book_id}/{chapter_id}.html"
|
180
188
|
|
181
|
-
@property
|
182
|
-
def hostname(self) -> str:
|
183
|
-
return "www.linovelib.com"
|
184
|
-
|
185
189
|
def _extract_vol_ids(self, html_str: str) -> list[str]:
|
186
190
|
"""
|
187
191
|
Extract volume IDs (like 'vol_12345') from the info HTML.
|
@@ -0,0 +1,105 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.fetchers.piaotia
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
from typing import Any
|
10
|
+
|
11
|
+
from novel_downloader.core.fetchers.base import BaseSession
|
12
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
13
|
+
from novel_downloader.models import FetcherConfig
|
14
|
+
|
15
|
+
|
16
|
+
@register_fetcher(
|
17
|
+
site_keys=["piaotia"],
|
18
|
+
)
|
19
|
+
class PiaotiaSession(BaseSession):
|
20
|
+
"""
|
21
|
+
A session class for interacting with the 飘天文学网 (www.piaotia.com) novel website.
|
22
|
+
"""
|
23
|
+
|
24
|
+
BOOK_INFO_URL = "https://www.piaotia.com/bookinfo/{book_id}.html"
|
25
|
+
BOOK_CATALOG_URL = "https://www.piaotia.com/html/{book_id}/index.html"
|
26
|
+
CHAPTER_URL = "https://www.piaotia.com/html/{book_id}/{chapter_id}.html"
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
config: FetcherConfig,
|
31
|
+
cookies: dict[str, str] | None = None,
|
32
|
+
**kwargs: Any,
|
33
|
+
) -> None:
|
34
|
+
super().__init__("piaotia", config, cookies, **kwargs)
|
35
|
+
|
36
|
+
async def get_book_info(
|
37
|
+
self,
|
38
|
+
book_id: str,
|
39
|
+
**kwargs: Any,
|
40
|
+
) -> list[str]:
|
41
|
+
"""
|
42
|
+
Fetch the raw HTML of the book info page asynchronously.
|
43
|
+
|
44
|
+
Order: [info, catalog]
|
45
|
+
|
46
|
+
:param book_id: The book identifier.
|
47
|
+
:return: The page content as string list.
|
48
|
+
"""
|
49
|
+
book_id = book_id.replace("-", "/")
|
50
|
+
info_url = self.book_info_url(book_id=book_id)
|
51
|
+
catalog_url = self.book_catalog_url(book_id=book_id)
|
52
|
+
|
53
|
+
info_html, catalog_html = await asyncio.gather(
|
54
|
+
self.fetch(info_url, **kwargs),
|
55
|
+
self.fetch(catalog_url, **kwargs),
|
56
|
+
)
|
57
|
+
return [info_html, catalog_html]
|
58
|
+
|
59
|
+
async def get_book_chapter(
|
60
|
+
self,
|
61
|
+
book_id: str,
|
62
|
+
chapter_id: str,
|
63
|
+
**kwargs: Any,
|
64
|
+
) -> list[str]:
|
65
|
+
"""
|
66
|
+
Fetch the raw HTML of a single chapter asynchronously.
|
67
|
+
|
68
|
+
:param book_id: The book identifier.
|
69
|
+
:param chapter_id: The chapter identifier.
|
70
|
+
:return: The page content as string list.
|
71
|
+
"""
|
72
|
+
book_id = book_id.replace("-", "/")
|
73
|
+
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
74
|
+
return [await self.fetch(url, **kwargs)]
|
75
|
+
|
76
|
+
@classmethod
|
77
|
+
def book_info_url(cls, book_id: str) -> str:
|
78
|
+
"""
|
79
|
+
Construct the URL for fetching a book's info page.
|
80
|
+
|
81
|
+
:param book_id: The identifier of the book.
|
82
|
+
:return: Fully qualified URL for the book info page.
|
83
|
+
"""
|
84
|
+
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
85
|
+
|
86
|
+
@classmethod
|
87
|
+
def book_catalog_url(cls, book_id: str) -> str:
|
88
|
+
"""
|
89
|
+
Construct the URL for fetching a book's catalog page.
|
90
|
+
|
91
|
+
:param book_id: The identifier of the book.
|
92
|
+
:return: Fully qualified catalog page URL.
|
93
|
+
"""
|
94
|
+
return cls.BOOK_CATALOG_URL.format(book_id=book_id)
|
95
|
+
|
96
|
+
@classmethod
|
97
|
+
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
98
|
+
"""
|
99
|
+
Construct the URL for fetching a specific chapter.
|
100
|
+
|
101
|
+
:param book_id: The identifier of the book.
|
102
|
+
:param chapter_id: The identifier of the chapter.
|
103
|
+
:return: Fully qualified chapter URL.
|
104
|
+
"""
|
105
|
+
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.fetchers.qbtr
|
4
|
+
-----------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from lxml import html
|
11
|
+
|
12
|
+
from novel_downloader.core.fetchers.base import BaseSession
|
13
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
14
|
+
from novel_downloader.models import FetcherConfig
|
15
|
+
|
16
|
+
|
17
|
+
@register_fetcher(
|
18
|
+
site_keys=["qbtr"],
|
19
|
+
)
|
20
|
+
class QbtrSession(BaseSession):
|
21
|
+
"""
|
22
|
+
A session class for interacting with the 全本同人小说 (www.qbtr.cc) novel website.
|
23
|
+
"""
|
24
|
+
|
25
|
+
BASE_URL = "https://www.qbtr.cc"
|
26
|
+
BOOK_INFO_URL = "https://www.qbtr.cc/{book_id}.html"
|
27
|
+
CHAPTER_URL = "https://www.qbtr.cc/{book_id}/{chapter_id}.html"
|
28
|
+
|
29
|
+
def __init__(
|
30
|
+
self,
|
31
|
+
config: FetcherConfig,
|
32
|
+
cookies: dict[str, str] | None = None,
|
33
|
+
**kwargs: Any,
|
34
|
+
) -> None:
|
35
|
+
super().__init__("qbtr", config, cookies, **kwargs)
|
36
|
+
|
37
|
+
async def get_book_info(
|
38
|
+
self,
|
39
|
+
book_id: str,
|
40
|
+
**kwargs: Any,
|
41
|
+
) -> list[str]:
|
42
|
+
"""
|
43
|
+
Fetch the raw HTML of the book info page asynchronously.
|
44
|
+
|
45
|
+
Order: [info, download]
|
46
|
+
|
47
|
+
:param book_id: The book identifier.
|
48
|
+
:return: The page content as string list.
|
49
|
+
"""
|
50
|
+
book_id = book_id.replace("-", "/")
|
51
|
+
url = self.book_info_url(book_id=book_id)
|
52
|
+
info_html = await self.fetch(url, **kwargs)
|
53
|
+
try:
|
54
|
+
info_tree = html.fromstring(info_html)
|
55
|
+
txt_link = info_tree.xpath(
|
56
|
+
'//div[@class="booktips"]/h3/a[contains(text(), "txt下载")]/@href'
|
57
|
+
)
|
58
|
+
download_url = f"{self.BASE_URL}{txt_link[0]}" if txt_link else None
|
59
|
+
except Exception:
|
60
|
+
download_url = None
|
61
|
+
|
62
|
+
download_html = await self.fetch(download_url, **kwargs) if download_url else ""
|
63
|
+
return [info_html, download_html]
|
64
|
+
|
65
|
+
async def get_book_chapter(
|
66
|
+
self,
|
67
|
+
book_id: str,
|
68
|
+
chapter_id: str,
|
69
|
+
**kwargs: Any,
|
70
|
+
) -> list[str]:
|
71
|
+
"""
|
72
|
+
Fetch the raw HTML of a single chapter asynchronously.
|
73
|
+
|
74
|
+
:param book_id: The book identifier.
|
75
|
+
:param chapter_id: The chapter identifier.
|
76
|
+
:return: The page content as string list.
|
77
|
+
"""
|
78
|
+
book_id = book_id.replace("-", "/")
|
79
|
+
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
80
|
+
return [await self.fetch(url, **kwargs)]
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def book_info_url(cls, book_id: str) -> str:
|
84
|
+
"""
|
85
|
+
Construct the URL for fetching a book's info page.
|
86
|
+
|
87
|
+
:param book_id: The identifier of the book.
|
88
|
+
:return: Fully qualified URL for the book info page.
|
89
|
+
"""
|
90
|
+
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
94
|
+
"""
|
95
|
+
Construct the URL for fetching a specific chapter.
|
96
|
+
|
97
|
+
:param book_id: The identifier of the book.
|
98
|
+
:param chapter_id: The identifier of the chapter.
|
99
|
+
:return: Fully qualified chapter URL.
|
100
|
+
"""
|
101
|
+
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.fetchers.qianbi
|
4
|
-
|
3
|
+
novel_downloader.core.fetchers.qianbi
|
4
|
+
-------------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -9,12 +9,16 @@ import asyncio
|
|
9
9
|
from typing import Any
|
10
10
|
|
11
11
|
from novel_downloader.core.fetchers.base import BaseSession
|
12
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
13
|
from novel_downloader.models import FetcherConfig
|
13
14
|
|
14
15
|
|
16
|
+
@register_fetcher(
|
17
|
+
site_keys=["qianbi"],
|
18
|
+
)
|
15
19
|
class QianbiSession(BaseSession):
|
16
20
|
"""
|
17
|
-
A session class for interacting with the
|
21
|
+
A session class for interacting with the 铅笔小说 (www.23qb.com) novel website.
|
18
22
|
"""
|
19
23
|
|
20
24
|
BASE_URLS = [
|
@@ -45,7 +49,7 @@ class QianbiSession(BaseSession):
|
|
45
49
|
Order: [info, catalog]
|
46
50
|
|
47
51
|
:param book_id: The book identifier.
|
48
|
-
:return: The page content as
|
52
|
+
:return: The page content as string list.
|
49
53
|
"""
|
50
54
|
info_url = self.book_info_url(book_id=book_id)
|
51
55
|
catalog_url = self.book_catalog_url(book_id=book_id)
|
@@ -67,7 +71,7 @@ class QianbiSession(BaseSession):
|
|
67
71
|
|
68
72
|
:param book_id: The book identifier.
|
69
73
|
:param chapter_id: The chapter identifier.
|
70
|
-
:return: The
|
74
|
+
:return: The page content as string list.
|
71
75
|
"""
|
72
76
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
73
77
|
return [await self.fetch(url, **kwargs)]
|
@@ -102,7 +106,3 @@ class QianbiSession(BaseSession):
|
|
102
106
|
:return: Fully qualified chapter URL.
|
103
107
|
"""
|
104
108
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
105
|
-
|
106
|
-
@property
|
107
|
-
def hostname(self) -> str:
|
108
|
-
return "www.23qb.com"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.fetchers.qidian
|
4
|
-
|
3
|
+
novel_downloader.core.fetchers.qidian
|
4
|
+
-------------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -10,24 +10,30 @@ import hashlib
|
|
10
10
|
import json
|
11
11
|
import random
|
12
12
|
import time
|
13
|
+
from collections.abc import Mapping
|
13
14
|
from typing import Any, ClassVar
|
14
15
|
|
15
16
|
import aiohttp
|
16
17
|
|
17
18
|
from novel_downloader.core.fetchers.base import BaseSession
|
19
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
18
20
|
from novel_downloader.models import FetcherConfig, LoginField
|
19
|
-
from novel_downloader.utils
|
20
|
-
|
21
|
+
from novel_downloader.utils import (
|
22
|
+
async_jitter_sleep,
|
23
|
+
rc4_crypt,
|
24
|
+
)
|
21
25
|
|
22
26
|
|
27
|
+
@register_fetcher(
|
28
|
+
site_keys=["qidian", "qd"],
|
29
|
+
)
|
23
30
|
class QidianSession(BaseSession):
|
24
31
|
"""
|
25
|
-
A session class for interacting with the
|
32
|
+
A session class for interacting with the 起点中文网 (www.qidian.com) novel website.
|
26
33
|
"""
|
27
34
|
|
28
35
|
HOMEPAGE_URL = "https://www.qidian.com/"
|
29
36
|
BOOKCASE_URL = "https://my.qidian.com/bookcase/"
|
30
|
-
# BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
|
31
37
|
BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
|
32
38
|
CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
|
33
39
|
|
@@ -48,11 +54,11 @@ class QidianSession(BaseSession):
|
|
48
54
|
**kwargs: Any,
|
49
55
|
) -> None:
|
50
56
|
super().__init__("qidian", config, cookies, **kwargs)
|
51
|
-
self._fp_key = _d("ZmluZ2VycHJpbnQ=")
|
52
|
-
self._ab_key = _d("YWJub3JtYWw=")
|
53
|
-
self._ck_key = _d("Y2hlY2tzdW0=")
|
54
|
-
self._lt_key = _d("bG9hZHRz")
|
55
|
-
self._ts_key = _d("dGltZXN0YW1w")
|
57
|
+
self._fp_key = self._d("ZmluZ2VycHJpbnQ=")
|
58
|
+
self._ab_key = self._d("YWJub3JtYWw=")
|
59
|
+
self._ck_key = self._d("Y2hlY2tzdW0=")
|
60
|
+
self._lt_key = self._d("bG9hZHRz")
|
61
|
+
self._ts_key = self._d("dGltZXN0YW1w")
|
56
62
|
self._fp_val: str = ""
|
57
63
|
self._ab_val: str = ""
|
58
64
|
|
@@ -83,7 +89,7 @@ class QidianSession(BaseSession):
|
|
83
89
|
Fetch the raw HTML of the book info page asynchronously.
|
84
90
|
|
85
91
|
:param book_id: The book identifier.
|
86
|
-
:return: The page content as
|
92
|
+
:return: The page content as string list.
|
87
93
|
"""
|
88
94
|
url = self.book_info_url(book_id=book_id)
|
89
95
|
return [await self.fetch(url, **kwargs)]
|
@@ -99,7 +105,7 @@ class QidianSession(BaseSession):
|
|
99
105
|
|
100
106
|
:param book_id: The book identifier.
|
101
107
|
:param chapter_id: The chapter identifier.
|
102
|
-
:return: The
|
108
|
+
:return: The page content as string list.
|
103
109
|
"""
|
104
110
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
105
111
|
return [await self.fetch(url, **kwargs)]
|
@@ -144,6 +150,7 @@ class QidianSession(BaseSession):
|
|
144
150
|
async def fetch(
|
145
151
|
self,
|
146
152
|
url: str,
|
153
|
+
encoding: str | None = None,
|
147
154
|
**kwargs: Any,
|
148
155
|
) -> str:
|
149
156
|
"""
|
@@ -151,14 +158,14 @@ class QidianSession(BaseSession):
|
|
151
158
|
a cookie-based token used for request validation.
|
152
159
|
|
153
160
|
The method:
|
154
|
-
|
155
|
-
|
156
|
-
|
161
|
+
1. Reads the existing cookie (if any);
|
162
|
+
2. Generates a new value tied to *url*;
|
163
|
+
3. Updates the live ``requests.Session``;
|
157
164
|
"""
|
158
165
|
if self._rate_limiter:
|
159
166
|
await self._rate_limiter.wait()
|
160
167
|
|
161
|
-
cookie_key = _d("d190c2Zw")
|
168
|
+
cookie_key = self._d("d190c2Zw")
|
162
169
|
|
163
170
|
for attempt in range(self.retry_times + 1):
|
164
171
|
try:
|
@@ -167,11 +174,11 @@ class QidianSession(BaseSession):
|
|
167
174
|
|
168
175
|
async with self.session.get(url, **kwargs) as resp:
|
169
176
|
resp.raise_for_status()
|
170
|
-
text: str = await resp.text()
|
177
|
+
text: str = await resp.text(encoding=encoding)
|
171
178
|
return text
|
172
179
|
except aiohttp.ClientError:
|
173
180
|
if attempt < self.retry_times:
|
174
|
-
await
|
181
|
+
await async_jitter_sleep(
|
175
182
|
self.backoff_factor,
|
176
183
|
mul_spread=1.1,
|
177
184
|
max_sleep=self.backoff_factor + 2,
|
@@ -220,21 +227,17 @@ class QidianSession(BaseSession):
|
|
220
227
|
"""
|
221
228
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
222
229
|
|
223
|
-
@property
|
224
|
-
def hostname(self) -> str:
|
225
|
-
return "www.qidian.com"
|
226
|
-
|
227
230
|
def _update_fp_val(
|
228
231
|
self,
|
229
232
|
*,
|
230
233
|
key: str = "",
|
231
234
|
) -> None:
|
232
235
|
""""""
|
233
|
-
enc_token = self.
|
236
|
+
enc_token = self._get_cookie_value(self._d("d190c2Zw"))
|
234
237
|
if not enc_token:
|
235
238
|
return
|
236
239
|
if not key:
|
237
|
-
key = _get_key()
|
240
|
+
key = self._get_key()
|
238
241
|
decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
|
239
242
|
payload: dict[str, Any] = json.loads(decrypted_json)
|
240
243
|
self._fp_val = payload.get(self._fp_key, "")
|
@@ -250,17 +253,14 @@ class QidianSession(BaseSession):
|
|
250
253
|
Patch a timestamp-bearing token with fresh timing and checksum info.
|
251
254
|
|
252
255
|
:param new_uri: URI used in checksum generation.
|
253
|
-
:type new_uri: str
|
254
256
|
:param key: RC4 key extracted from front-end JavaScript (optional).
|
255
|
-
:type key: str, optional
|
256
257
|
|
257
258
|
:return: Updated token with new timing and checksum values.
|
258
|
-
:rtype: str
|
259
259
|
"""
|
260
260
|
if not self._fp_val or not self._ab_val:
|
261
261
|
self._update_fp_val()
|
262
262
|
if not key:
|
263
|
-
key = _get_key()
|
263
|
+
key = self._get_key()
|
264
264
|
|
265
265
|
# rebuild timing fields
|
266
266
|
loadts = int(time.time() * 1000) # ms since epoch
|
@@ -303,25 +303,40 @@ class QidianSession(BaseSession):
|
|
303
303
|
"""
|
304
304
|
Check if the provided cookies contain all required keys.
|
305
305
|
|
306
|
-
Logs any missing keys as warnings.
|
307
|
-
|
308
306
|
:param cookies: The cookie dictionary to validate.
|
309
307
|
:return: True if all required keys are present, False otherwise.
|
310
308
|
"""
|
311
|
-
required = {_d(k) for k in self._cookie_keys}
|
309
|
+
required = {self._d(k) for k in self._cookie_keys}
|
312
310
|
actual = set(cookies)
|
313
311
|
missing = required - actual
|
314
312
|
if missing:
|
315
313
|
self.logger.warning("Missing required cookies: %s", ", ".join(missing))
|
316
314
|
return not missing
|
317
315
|
|
316
|
+
def _get_cookie_value(self, key: str) -> str | None:
|
317
|
+
for cookie in self.session.cookie_jar:
|
318
|
+
if cookie.key == key:
|
319
|
+
return str(cookie.value)
|
320
|
+
return None
|
321
|
+
|
322
|
+
@staticmethod
|
323
|
+
def _filter_cookies(
|
324
|
+
raw_cookies: list[Mapping[str, Any]],
|
325
|
+
) -> dict[str, str]:
|
326
|
+
ALLOWED_DOMAINS = {".qidian.com", "www.qidian.com", ""}
|
327
|
+
return {
|
328
|
+
c["name"]: c["value"]
|
329
|
+
for c in raw_cookies
|
330
|
+
if c.get("domain", "") in ALLOWED_DOMAINS
|
331
|
+
}
|
318
332
|
|
319
|
-
|
320
|
-
|
321
|
-
|
333
|
+
@staticmethod
|
334
|
+
def _d(b: str) -> str:
|
335
|
+
return base64.b64decode(b).decode()
|
322
336
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
337
|
+
@staticmethod
|
338
|
+
def _get_key() -> str:
|
339
|
+
encoded = "Lj1qYxMuaXBjMg=="
|
340
|
+
decoded = base64.b64decode(encoded)
|
341
|
+
key = "".join([chr(b ^ 0x5A) for b in decoded])
|
342
|
+
return key
|
@@ -0,0 +1,92 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.fetchers.quanben5
|
4
|
+
---------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from novel_downloader.core.fetchers.base import BaseSession
|
11
|
+
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
|
+
from novel_downloader.models import FetcherConfig
|
13
|
+
|
14
|
+
|
15
|
+
@register_fetcher(
|
16
|
+
site_keys=["quanben5"],
|
17
|
+
)
|
18
|
+
class Quanben5Session(BaseSession):
|
19
|
+
"""
|
20
|
+
A session class for interacting with the 全本小说网 (quanben5.com) novel website.
|
21
|
+
"""
|
22
|
+
|
23
|
+
BOOK_INFO_URL = "https://{base_url}/n/{book_id}/xiaoshuo.html"
|
24
|
+
CHAPTER_URL = "https://{base_url}/n/{book_id}/{chapter_id}.html"
|
25
|
+
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
config: FetcherConfig,
|
29
|
+
cookies: dict[str, str] | None = None,
|
30
|
+
**kwargs: Any,
|
31
|
+
) -> None:
|
32
|
+
super().__init__("quanben5", config, cookies, **kwargs)
|
33
|
+
self.base_url = (
|
34
|
+
"quanben5.com"
|
35
|
+
if config.locale_style == "simplified"
|
36
|
+
else "big5.quanben5.com"
|
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 of the book info page asynchronously.
|
46
|
+
|
47
|
+
:param book_id: The book identifier.
|
48
|
+
:return: The page content as string list.
|
49
|
+
"""
|
50
|
+
url = self.book_info_url(base_url=self.base_url, book_id=book_id)
|
51
|
+
return [await self.fetch(url, **kwargs)]
|
52
|
+
|
53
|
+
async def get_book_chapter(
|
54
|
+
self,
|
55
|
+
book_id: str,
|
56
|
+
chapter_id: str,
|
57
|
+
**kwargs: Any,
|
58
|
+
) -> list[str]:
|
59
|
+
"""
|
60
|
+
Fetch the raw HTML of a single chapter asynchronously.
|
61
|
+
|
62
|
+
:param book_id: The book identifier.
|
63
|
+
:param chapter_id: The chapter identifier.
|
64
|
+
:return: The page content as string list.
|
65
|
+
"""
|
66
|
+
url = self.chapter_url(
|
67
|
+
base_url=self.base_url, book_id=book_id, chapter_id=chapter_id
|
68
|
+
)
|
69
|
+
return [await self.fetch(url, **kwargs)]
|
70
|
+
|
71
|
+
@classmethod
|
72
|
+
def book_info_url(cls, base_url: str, book_id: str) -> str:
|
73
|
+
"""
|
74
|
+
Construct the URL for fetching a book's info page.
|
75
|
+
|
76
|
+
:param book_id: The identifier of the book.
|
77
|
+
:return: Fully qualified URL for the book info page.
|
78
|
+
"""
|
79
|
+
return cls.BOOK_INFO_URL.format(base_url=base_url, book_id=book_id)
|
80
|
+
|
81
|
+
@classmethod
|
82
|
+
def chapter_url(cls, base_url: str, book_id: str, chapter_id: str) -> str:
|
83
|
+
"""
|
84
|
+
Construct the URL for fetching a specific chapter.
|
85
|
+
|
86
|
+
:param book_id: The identifier of the book.
|
87
|
+
:param chapter_id: The identifier of the chapter.
|
88
|
+
:return: Fully qualified chapter URL.
|
89
|
+
"""
|
90
|
+
return cls.CHAPTER_URL.format(
|
91
|
+
base_url=base_url, book_id=book_id, chapter_id=chapter_id
|
92
|
+
)
|