novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +79 -66
- novel_downloader/cli/export.py +17 -21
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- 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/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +17 -12
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +20 -14
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- 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} +19 -12
- 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} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- 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} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
- 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 +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- 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} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +64 -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.py +64 -69
- 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.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- 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 +429 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- 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 +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- 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 +34 -85
- novel_downloader/locales/zh.json +35 -86
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -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/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +55 -49
- 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.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- 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 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- 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/crypto_utils.py +0 -71
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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
|
|
@@ -15,11 +15,10 @@ from novel_downloader.models import FetcherConfig
|
|
15
15
|
|
16
16
|
@register_fetcher(
|
17
17
|
site_keys=["qianbi"],
|
18
|
-
backends=["session"],
|
19
18
|
)
|
20
19
|
class QianbiSession(BaseSession):
|
21
20
|
"""
|
22
|
-
A session class for interacting with the
|
21
|
+
A session class for interacting with the 铅笔小说 (www.23qb.com) novel website.
|
23
22
|
"""
|
24
23
|
|
25
24
|
BASE_URLS = [
|
@@ -50,7 +49,7 @@ class QianbiSession(BaseSession):
|
|
50
49
|
Order: [info, catalog]
|
51
50
|
|
52
51
|
:param book_id: The book identifier.
|
53
|
-
:return: The page content as
|
52
|
+
:return: The page content as string list.
|
54
53
|
"""
|
55
54
|
info_url = self.book_info_url(book_id=book_id)
|
56
55
|
catalog_url = self.book_catalog_url(book_id=book_id)
|
@@ -72,7 +71,7 @@ class QianbiSession(BaseSession):
|
|
72
71
|
|
73
72
|
:param book_id: The book identifier.
|
74
73
|
:param chapter_id: The chapter identifier.
|
75
|
-
:return: The
|
74
|
+
:return: The page content as string list.
|
76
75
|
"""
|
77
76
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
78
77
|
return [await self.fetch(url, **kwargs)]
|
@@ -107,7 +106,3 @@ class QianbiSession(BaseSession):
|
|
107
106
|
:return: Fully qualified chapter URL.
|
108
107
|
"""
|
109
108
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
110
|
-
|
111
|
-
@property
|
112
|
-
def hostname(self) -> str:
|
113
|
-
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,6 +10,7 @@ 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
|
@@ -17,24 +18,20 @@ import aiohttp
|
|
17
18
|
from novel_downloader.core.fetchers.base import BaseSession
|
18
19
|
from novel_downloader.core.fetchers.registry import register_fetcher
|
19
20
|
from novel_downloader.models import FetcherConfig, LoginField
|
20
|
-
from novel_downloader.utils import
|
21
|
-
|
22
|
-
rc4_crypt,
|
23
|
-
)
|
21
|
+
from novel_downloader.utils import async_jitter_sleep
|
22
|
+
from novel_downloader.utils.crypto_utils.rc4 import rc4_init, rc4_stream
|
24
23
|
|
25
24
|
|
26
25
|
@register_fetcher(
|
27
26
|
site_keys=["qidian", "qd"],
|
28
|
-
backends=["session"],
|
29
27
|
)
|
30
28
|
class QidianSession(BaseSession):
|
31
29
|
"""
|
32
|
-
A session class for interacting with the
|
30
|
+
A session class for interacting with the 起点中文网 (www.qidian.com) novel website.
|
33
31
|
"""
|
34
32
|
|
35
33
|
HOMEPAGE_URL = "https://www.qidian.com/"
|
36
34
|
BOOKCASE_URL = "https://my.qidian.com/bookcase/"
|
37
|
-
# BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
|
38
35
|
BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
|
39
36
|
CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
|
40
37
|
|
@@ -55,11 +52,13 @@ class QidianSession(BaseSession):
|
|
55
52
|
**kwargs: Any,
|
56
53
|
) -> None:
|
57
54
|
super().__init__("qidian", config, cookies, **kwargs)
|
58
|
-
self.
|
59
|
-
self.
|
60
|
-
self.
|
61
|
-
self.
|
62
|
-
self.
|
55
|
+
self._s_init = rc4_init(self._d2("dGcwOUl0Myo5aA=="))
|
56
|
+
self._cookie_key = self._d("d190c2Zw")
|
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")
|
63
62
|
self._fp_val: str = ""
|
64
63
|
self._ab_val: str = ""
|
65
64
|
|
@@ -90,7 +89,7 @@ class QidianSession(BaseSession):
|
|
90
89
|
Fetch the raw HTML of the book info page asynchronously.
|
91
90
|
|
92
91
|
:param book_id: The book identifier.
|
93
|
-
:return: The page content as
|
92
|
+
:return: The page content as string list.
|
94
93
|
"""
|
95
94
|
url = self.book_info_url(book_id=book_id)
|
96
95
|
return [await self.fetch(url, **kwargs)]
|
@@ -106,7 +105,7 @@ class QidianSession(BaseSession):
|
|
106
105
|
|
107
106
|
:param book_id: The book identifier.
|
108
107
|
:param chapter_id: The chapter identifier.
|
109
|
-
:return: The
|
108
|
+
:return: The page content as string list.
|
110
109
|
"""
|
111
110
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
112
111
|
return [await self.fetch(url, **kwargs)]
|
@@ -159,19 +158,17 @@ class QidianSession(BaseSession):
|
|
159
158
|
a cookie-based token used for request validation.
|
160
159
|
|
161
160
|
The method:
|
162
|
-
|
163
|
-
|
164
|
-
|
161
|
+
1. Reads the existing cookie (if any);
|
162
|
+
2. Generates a new value tied to *url*;
|
163
|
+
3. Updates the live ``requests.Session``;
|
165
164
|
"""
|
166
165
|
if self._rate_limiter:
|
167
166
|
await self._rate_limiter.wait()
|
168
167
|
|
169
|
-
cookie_key = _d("d190c2Zw")
|
170
|
-
|
171
168
|
for attempt in range(self.retry_times + 1):
|
172
169
|
try:
|
173
170
|
refreshed_token = self._build_payload_token(url)
|
174
|
-
self.update_cookies({
|
171
|
+
self.update_cookies({self._cookie_key: refreshed_token})
|
175
172
|
|
176
173
|
async with self.session.get(url, **kwargs) as resp:
|
177
174
|
resp.raise_for_status()
|
@@ -179,7 +176,7 @@ class QidianSession(BaseSession):
|
|
179
176
|
return text
|
180
177
|
except aiohttp.ClientError:
|
181
178
|
if attempt < self.retry_times:
|
182
|
-
await
|
179
|
+
await async_jitter_sleep(
|
183
180
|
self.backoff_factor,
|
184
181
|
mul_spread=1.1,
|
185
182
|
max_sleep=self.backoff_factor + 2,
|
@@ -228,47 +225,30 @@ class QidianSession(BaseSession):
|
|
228
225
|
"""
|
229
226
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
230
227
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
self,
|
237
|
-
*,
|
238
|
-
key: str = "",
|
239
|
-
) -> None:
|
240
|
-
""""""
|
241
|
-
enc_token = self.get_cookie_value(_d("d190c2Zw"))
|
228
|
+
def _update_fp_val(self) -> None:
|
229
|
+
"""
|
230
|
+
Decrypt the payload from cookie and update `_fp_val` and `_ab_val`.
|
231
|
+
"""
|
232
|
+
enc_token = self._get_cookie_value(self._cookie_key)
|
242
233
|
if not enc_token:
|
243
234
|
return
|
244
|
-
|
245
|
-
|
246
|
-
|
235
|
+
|
236
|
+
cipher_bytes = base64.b64decode(enc_token)
|
237
|
+
plain_bytes = rc4_stream(self._s_init, cipher_bytes)
|
238
|
+
decrypted_json = plain_bytes.decode("utf-8", errors="replace")
|
247
239
|
payload: dict[str, Any] = json.loads(decrypted_json)
|
248
240
|
self._fp_val = payload.get(self._fp_key, "")
|
249
241
|
self._ab_val = payload.get(self._ab_key, "0" * 32)
|
250
242
|
|
251
|
-
def _build_payload_token(
|
252
|
-
self,
|
253
|
-
new_uri: str,
|
254
|
-
*,
|
255
|
-
key: str = "",
|
256
|
-
) -> str:
|
243
|
+
def _build_payload_token(self, new_uri: str) -> str:
|
257
244
|
"""
|
258
245
|
Patch a timestamp-bearing token with fresh timing and checksum info.
|
259
246
|
|
260
247
|
:param new_uri: URI used in checksum generation.
|
261
|
-
:type new_uri: str
|
262
|
-
:param key: RC4 key extracted from front-end JavaScript (optional).
|
263
|
-
:type key: str, optional
|
264
|
-
|
265
248
|
:return: Updated token with new timing and checksum values.
|
266
|
-
:rtype: str
|
267
249
|
"""
|
268
250
|
if not self._fp_val or not self._ab_val:
|
269
251
|
self._update_fp_val()
|
270
|
-
if not key:
|
271
|
-
key = _get_key()
|
272
252
|
|
273
253
|
# rebuild timing fields
|
274
254
|
loadts = int(time.time() * 1000) # ms since epoch
|
@@ -286,9 +266,9 @@ class QidianSession(BaseSession):
|
|
286
266
|
self._ab_key: self._ab_val,
|
287
267
|
self._ck_key: ck_val,
|
288
268
|
}
|
289
|
-
|
290
|
-
|
291
|
-
)
|
269
|
+
plain_bytes = json.dumps(new_payload, separators=(",", ":")).encode("utf-8")
|
270
|
+
cipher_bytes = rc4_stream(self._s_init, plain_bytes)
|
271
|
+
return base64.b64encode(cipher_bytes).decode("utf-8")
|
292
272
|
|
293
273
|
async def _check_login_status(self) -> bool:
|
294
274
|
"""
|
@@ -311,25 +291,37 @@ class QidianSession(BaseSession):
|
|
311
291
|
"""
|
312
292
|
Check if the provided cookies contain all required keys.
|
313
293
|
|
314
|
-
Logs any missing keys as warnings.
|
315
|
-
|
316
294
|
:param cookies: The cookie dictionary to validate.
|
317
295
|
:return: True if all required keys are present, False otherwise.
|
318
296
|
"""
|
319
|
-
required = {_d(k) for k in self._cookie_keys}
|
297
|
+
required = {self._d(k) for k in self._cookie_keys}
|
320
298
|
actual = set(cookies)
|
321
299
|
missing = required - actual
|
322
300
|
if missing:
|
323
301
|
self.logger.warning("Missing required cookies: %s", ", ".join(missing))
|
324
302
|
return not missing
|
325
303
|
|
304
|
+
def _get_cookie_value(self, key: str) -> str | None:
|
305
|
+
for cookie in self.session.cookie_jar:
|
306
|
+
if cookie.key == key:
|
307
|
+
return str(cookie.value)
|
308
|
+
return None
|
309
|
+
|
310
|
+
@staticmethod
|
311
|
+
def _filter_cookies(
|
312
|
+
raw_cookies: list[Mapping[str, Any]],
|
313
|
+
) -> dict[str, str]:
|
314
|
+
ALLOWED_DOMAINS = {".qidian.com", "www.qidian.com", ""}
|
315
|
+
return {
|
316
|
+
c["name"]: c["value"]
|
317
|
+
for c in raw_cookies
|
318
|
+
if c.get("domain", "") in ALLOWED_DOMAINS
|
319
|
+
}
|
326
320
|
|
327
|
-
|
328
|
-
|
329
|
-
|
321
|
+
@staticmethod
|
322
|
+
def _d(b: str) -> str:
|
323
|
+
return base64.b64decode(b).decode()
|
330
324
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
key = "".join([chr(b ^ 0x5A) for b in decoded])
|
335
|
-
return key
|
325
|
+
@staticmethod
|
326
|
+
def _d2(b: str) -> bytes:
|
327
|
+
return base64.b64decode(b)
|
@@ -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
|
+
)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
novel_downloader.core.fetchers.registry
|
4
4
|
---------------------------------------
|
5
5
|
|
6
|
+
Registry and factory helpers for creating site-specific fetchers.
|
6
7
|
"""
|
7
8
|
|
8
9
|
__all__ = ["register_fetcher", "get_fetcher"]
|
@@ -16,27 +17,24 @@ from novel_downloader.models import FetcherConfig
|
|
16
17
|
FetcherBuilder = Callable[[FetcherConfig], FetcherProtocol]
|
17
18
|
|
18
19
|
F = TypeVar("F", bound=FetcherProtocol)
|
19
|
-
_FETCHER_MAP: dict[str,
|
20
|
+
_FETCHER_MAP: dict[str, FetcherBuilder] = {}
|
20
21
|
|
21
22
|
|
22
23
|
def register_fetcher(
|
23
24
|
site_keys: Sequence[str],
|
24
|
-
backends: Sequence[str],
|
25
25
|
) -> Callable[[type[F]], type[F]]:
|
26
26
|
"""
|
27
27
|
Decorator to register a fetcher class under given keys.
|
28
28
|
|
29
29
|
:param site_keys: Sequence of site identifiers
|
30
|
-
:param backends:
|
30
|
+
:param backends: Sequence of backend types
|
31
31
|
:return: A class decorator that populates _FETCHER_MAP.
|
32
32
|
"""
|
33
33
|
|
34
34
|
def decorator(cls: type[F]) -> type[F]:
|
35
35
|
for site in site_keys:
|
36
36
|
site_lower = site.lower()
|
37
|
-
|
38
|
-
for backend in backends:
|
39
|
-
bucket[backend] = cls
|
37
|
+
_FETCHER_MAP[site_lower] = cls
|
40
38
|
return cls
|
41
39
|
|
42
40
|
return decorator
|
@@ -55,17 +53,8 @@ def get_fetcher(
|
|
55
53
|
"""
|
56
54
|
site_key = site.lower()
|
57
55
|
try:
|
58
|
-
|
56
|
+
fetcher_cls = _FETCHER_MAP[site_key]
|
59
57
|
except KeyError as err:
|
60
58
|
raise ValueError(f"Unsupported site: {site!r}") from err
|
61
59
|
|
62
|
-
mode = config.mode
|
63
|
-
try:
|
64
|
-
fetcher_cls = backend_map[mode]
|
65
|
-
except KeyError as err:
|
66
|
-
raise ValueError(
|
67
|
-
f"Unsupported fetcher mode {mode!r} for site {site!r}. "
|
68
|
-
f"Available modes: {list(backend_map)}"
|
69
|
-
) from err
|
70
|
-
|
71
60
|
return fetcher_cls(config)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.fetchers.sfacg
|
4
|
-
|
3
|
+
novel_downloader.core.fetchers.sfacg
|
4
|
+
------------------------------------
|
5
5
|
|
6
6
|
"""
|
7
7
|
|
@@ -14,11 +14,10 @@ from novel_downloader.models import FetcherConfig, LoginField
|
|
14
14
|
|
15
15
|
@register_fetcher(
|
16
16
|
site_keys=["sfacg"],
|
17
|
-
backends=["session"],
|
18
17
|
)
|
19
18
|
class SfacgSession(BaseSession):
|
20
19
|
"""
|
21
|
-
A session class for interacting with the
|
20
|
+
A session class for interacting with the SF轻小说 (m.sfacg.com) novel website.
|
22
21
|
"""
|
23
22
|
|
24
23
|
LOGIN_URL = "https://m.sfacg.com/login"
|
@@ -65,8 +64,10 @@ class SfacgSession(BaseSession):
|
|
65
64
|
"""
|
66
65
|
Fetch the raw HTML of the book info page asynchronously.
|
67
66
|
|
67
|
+
Order: [info, catalog]
|
68
|
+
|
68
69
|
:param book_id: The book identifier.
|
69
|
-
:return: The page content as
|
70
|
+
:return: The page content as string list.
|
70
71
|
"""
|
71
72
|
info_url = self.book_info_url(book_id=book_id)
|
72
73
|
catalog_url = self.book_catalog_url(book_id=book_id)
|
@@ -87,7 +88,7 @@ class SfacgSession(BaseSession):
|
|
87
88
|
|
88
89
|
:param book_id: The book identifier.
|
89
90
|
:param chapter_id: The chapter identifier.
|
90
|
-
:return: The
|
91
|
+
:return: The page content as string list.
|
91
92
|
"""
|
92
93
|
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
93
94
|
return [await self.fetch(url, **kwargs)]
|
@@ -157,10 +158,6 @@ class SfacgSession(BaseSession):
|
|
157
158
|
"""
|
158
159
|
return cls.CHAPTER_URL.format(chapter_id=chapter_id)
|
159
160
|
|
160
|
-
@property
|
161
|
-
def hostname(self) -> str:
|
162
|
-
return "m.sfacg.com"
|
163
|
-
|
164
161
|
async def _check_login_status(self) -> bool:
|
165
162
|
"""
|
166
163
|
Check whether the user is currently logged in by
|