novel-downloader 1.5.0__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 +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +77 -64
- novel_downloader/cli/export.py +16 -20
- 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 +65 -105
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +1 -0
- 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 +14 -9
- 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 +17 -11
- 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} +46 -39
- 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 +4 -17
- novel_downloader/core/interfaces/parser.py +5 -6
- 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 +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.py +61 -66
- 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/book_info_parser.py +58 -59
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +272 -330
- novel_downloader/core/parsers/qidian/chapter_normal.py +24 -55
- novel_downloader/core/parsers/qidian/main_parser.py +11 -38
- novel_downloader/core/parsers/qidian/utils/__init__.py +1 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- 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/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 +435 -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 +31 -82
- novel_downloader/locales/zh.json +32 -83
- 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 -22
- novel_downloader/utils/chapter_storage.py +3 -2
- novel_downloader/utils/constants.py +4 -29
- 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 +1 -1
- novel_downloader/utils/epub/constants.py +57 -16
- novel_downloader/utils/epub/documents.py +88 -194
- novel_downloader/utils/epub/models.py +0 -14
- novel_downloader/utils/epub/utils.py +63 -96
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +3 -113
- novel_downloader/utils/file_utils/sanitize.py +0 -4
- novel_downloader/utils/fontocr.py +207 -0
- novel_downloader/utils/logger.py +8 -16
- novel_downloader/utils/network.py +2 -2
- 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/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +4 -8
- 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.5.0.dist-info → novel_downloader-2.0.0.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/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/fontocr/__init__.py +0 -22
- 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.0.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.0.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
|
@@ -18,23 +19,21 @@ 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
21
|
from novel_downloader.utils import (
|
21
|
-
|
22
|
+
async_jitter_sleep,
|
22
23
|
rc4_crypt,
|
23
24
|
)
|
24
25
|
|
25
26
|
|
26
27
|
@register_fetcher(
|
27
28
|
site_keys=["qidian", "qd"],
|
28
|
-
backends=["session"],
|
29
29
|
)
|
30
30
|
class QidianSession(BaseSession):
|
31
31
|
"""
|
32
|
-
A session class for interacting with the
|
32
|
+
A session class for interacting with the 起点中文网 (www.qidian.com) novel website.
|
33
33
|
"""
|
34
34
|
|
35
35
|
HOMEPAGE_URL = "https://www.qidian.com/"
|
36
36
|
BOOKCASE_URL = "https://my.qidian.com/bookcase/"
|
37
|
-
# BOOK_INFO_URL = "https://book.qidian.com/info/{book_id}/"
|
38
37
|
BOOK_INFO_URL = "https://www.qidian.com/book/{book_id}/"
|
39
38
|
CHAPTER_URL = "https://www.qidian.com/chapter/{book_id}/{chapter_id}/"
|
40
39
|
|
@@ -55,11 +54,11 @@ class QidianSession(BaseSession):
|
|
55
54
|
**kwargs: Any,
|
56
55
|
) -> None:
|
57
56
|
super().__init__("qidian", config, cookies, **kwargs)
|
58
|
-
self._fp_key = _d("ZmluZ2VycHJpbnQ=")
|
59
|
-
self._ab_key = _d("YWJub3JtYWw=")
|
60
|
-
self._ck_key = _d("Y2hlY2tzdW0=")
|
61
|
-
self._lt_key = _d("bG9hZHRz")
|
62
|
-
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")
|
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,14 +158,14 @@ 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")
|
168
|
+
cookie_key = self._d("d190c2Zw")
|
170
169
|
|
171
170
|
for attempt in range(self.retry_times + 1):
|
172
171
|
try:
|
@@ -179,7 +178,7 @@ class QidianSession(BaseSession):
|
|
179
178
|
return text
|
180
179
|
except aiohttp.ClientError:
|
181
180
|
if attempt < self.retry_times:
|
182
|
-
await
|
181
|
+
await async_jitter_sleep(
|
183
182
|
self.backoff_factor,
|
184
183
|
mul_spread=1.1,
|
185
184
|
max_sleep=self.backoff_factor + 2,
|
@@ -228,21 +227,17 @@ class QidianSession(BaseSession):
|
|
228
227
|
"""
|
229
228
|
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
230
229
|
|
231
|
-
@property
|
232
|
-
def hostname(self) -> str:
|
233
|
-
return "www.qidian.com"
|
234
|
-
|
235
230
|
def _update_fp_val(
|
236
231
|
self,
|
237
232
|
*,
|
238
233
|
key: str = "",
|
239
234
|
) -> None:
|
240
235
|
""""""
|
241
|
-
enc_token = self.
|
236
|
+
enc_token = self._get_cookie_value(self._d("d190c2Zw"))
|
242
237
|
if not enc_token:
|
243
238
|
return
|
244
239
|
if not key:
|
245
|
-
key = _get_key()
|
240
|
+
key = self._get_key()
|
246
241
|
decrypted_json: str = rc4_crypt(key, enc_token, mode="decrypt")
|
247
242
|
payload: dict[str, Any] = json.loads(decrypted_json)
|
248
243
|
self._fp_val = payload.get(self._fp_key, "")
|
@@ -258,17 +253,14 @@ class QidianSession(BaseSession):
|
|
258
253
|
Patch a timestamp-bearing token with fresh timing and checksum info.
|
259
254
|
|
260
255
|
:param new_uri: URI used in checksum generation.
|
261
|
-
:type new_uri: str
|
262
256
|
:param key: RC4 key extracted from front-end JavaScript (optional).
|
263
|
-
:type key: str, optional
|
264
257
|
|
265
258
|
:return: Updated token with new timing and checksum values.
|
266
|
-
:rtype: str
|
267
259
|
"""
|
268
260
|
if not self._fp_val or not self._ab_val:
|
269
261
|
self._update_fp_val()
|
270
262
|
if not key:
|
271
|
-
key = _get_key()
|
263
|
+
key = self._get_key()
|
272
264
|
|
273
265
|
# rebuild timing fields
|
274
266
|
loadts = int(time.time() * 1000) # ms since epoch
|
@@ -311,25 +303,40 @@ class QidianSession(BaseSession):
|
|
311
303
|
"""
|
312
304
|
Check if the provided cookies contain all required keys.
|
313
305
|
|
314
|
-
Logs any missing keys as warnings.
|
315
|
-
|
316
306
|
:param cookies: The cookie dictionary to validate.
|
317
307
|
:return: True if all required keys are present, False otherwise.
|
318
308
|
"""
|
319
|
-
required = {_d(k) for k in self._cookie_keys}
|
309
|
+
required = {self._d(k) for k in self._cookie_keys}
|
320
310
|
actual = set(cookies)
|
321
311
|
missing = required - actual
|
322
312
|
if missing:
|
323
313
|
self.logger.warning("Missing required cookies: %s", ", ".join(missing))
|
324
314
|
return not missing
|
325
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
|
+
}
|
326
332
|
|
327
|
-
|
328
|
-
|
329
|
-
|
333
|
+
@staticmethod
|
334
|
+
def _d(b: str) -> str:
|
335
|
+
return base64.b64decode(b).decode()
|
330
336
|
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
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
|
+
)
|
@@ -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
|
@@ -0,0 +1,106 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.fetchers.shencou
|
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=["shencou"],
|
18
|
+
)
|
19
|
+
class ShencouSession(BaseSession):
|
20
|
+
"""
|
21
|
+
A session class for interacting with the 神凑轻小说 (www.shencou.com) novel website.
|
22
|
+
"""
|
23
|
+
|
24
|
+
BOOK_INFO_URL = "https://www.shencou.com/books/read_{book_id}.html"
|
25
|
+
BOOK_CATALOG_URL = "https://www.shencou.com/read/{book_id}/index.html"
|
26
|
+
CHAPTER_URL = "https://www.shencou.com/read/{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__("shencou", 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
|
+
clean_id = book_id.rsplit("/", 1)[-1]
|
85
|
+
return cls.BOOK_INFO_URL.format(book_id=clean_id)
|
86
|
+
|
87
|
+
@classmethod
|
88
|
+
def book_catalog_url(cls, book_id: str) -> str:
|
89
|
+
"""
|
90
|
+
Construct the URL for fetching a book's catalog page.
|
91
|
+
|
92
|
+
:param book_id: The identifier of the book.
|
93
|
+
:return: Fully qualified catalog page URL.
|
94
|
+
"""
|
95
|
+
return cls.BOOK_CATALOG_URL.format(book_id=book_id)
|
96
|
+
|
97
|
+
@classmethod
|
98
|
+
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
99
|
+
"""
|
100
|
+
Construct the URL for fetching a specific chapter.
|
101
|
+
|
102
|
+
:param book_id: The identifier of the book.
|
103
|
+
:param chapter_id: The identifier of the chapter.
|
104
|
+
:return: Fully qualified chapter URL.
|
105
|
+
"""
|
106
|
+
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|