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
@@ -1,194 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.fetchers.sfacg.browser
|
4
|
-
--------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from novel_downloader.core.fetchers.base import BaseBrowser
|
11
|
-
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
|
-
from novel_downloader.models import FetcherConfig, LoginField
|
13
|
-
from novel_downloader.utils.i18n import t
|
14
|
-
|
15
|
-
|
16
|
-
@register_fetcher(
|
17
|
-
site_keys=["sfacg"],
|
18
|
-
backends=["browser"],
|
19
|
-
)
|
20
|
-
class SfacgBrowser(BaseBrowser):
|
21
|
-
"""
|
22
|
-
A browser class for interacting with the Sfacg (m.sfacg.com) novel website.
|
23
|
-
"""
|
24
|
-
|
25
|
-
LOGIN_URL = "https://m.sfacg.com/login"
|
26
|
-
BOOKCASE_URL = "https://m.sfacg.com/sheets/"
|
27
|
-
BOOK_INFO_URL = "https://m.sfacg.com/b/{book_id}/"
|
28
|
-
BOOK_CATALOG_URL = "https://m.sfacg.com/i/{book_id}/"
|
29
|
-
CHAPTER_URL = "https://m.sfacg.com/c/{chapter_id}/"
|
30
|
-
|
31
|
-
def __init__(
|
32
|
-
self,
|
33
|
-
config: FetcherConfig,
|
34
|
-
reuse_page: bool = False,
|
35
|
-
**kwargs: Any,
|
36
|
-
) -> None:
|
37
|
-
super().__init__("sfacg", config, reuse_page, **kwargs)
|
38
|
-
|
39
|
-
async def login(
|
40
|
-
self,
|
41
|
-
username: str = "",
|
42
|
-
password: str = "",
|
43
|
-
cookies: dict[str, str] | None = None,
|
44
|
-
attempt: int = 1,
|
45
|
-
**kwargs: Any,
|
46
|
-
) -> bool:
|
47
|
-
self._is_logged_in = await self._check_login_status()
|
48
|
-
return self._is_logged_in
|
49
|
-
|
50
|
-
async def get_book_info(
|
51
|
-
self,
|
52
|
-
book_id: str,
|
53
|
-
**kwargs: Any,
|
54
|
-
) -> list[str]:
|
55
|
-
"""
|
56
|
-
Fetch the raw HTML of the book info page asynchronously.
|
57
|
-
|
58
|
-
Order: [info, catalog]
|
59
|
-
|
60
|
-
:param book_id: The book identifier.
|
61
|
-
:return: The page content as a string.
|
62
|
-
"""
|
63
|
-
info_url = self.book_info_url(book_id=book_id)
|
64
|
-
catalog_url = self.book_catalog_url(book_id=book_id)
|
65
|
-
|
66
|
-
info_html = await self.fetch(info_url, **kwargs)
|
67
|
-
catalog_html = await self.fetch(catalog_url, **kwargs)
|
68
|
-
|
69
|
-
return [info_html, catalog_html]
|
70
|
-
|
71
|
-
async def get_book_chapter(
|
72
|
-
self,
|
73
|
-
book_id: str,
|
74
|
-
chapter_id: str,
|
75
|
-
**kwargs: Any,
|
76
|
-
) -> list[str]:
|
77
|
-
"""
|
78
|
-
Fetch the raw HTML of a single chapter asynchronously.
|
79
|
-
|
80
|
-
:param book_id: The book identifier.
|
81
|
-
:param chapter_id: The chapter identifier.
|
82
|
-
:return: The chapter content as a string.
|
83
|
-
"""
|
84
|
-
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
85
|
-
return [await self.fetch(url, **kwargs)]
|
86
|
-
|
87
|
-
async def get_bookcase(
|
88
|
-
self,
|
89
|
-
**kwargs: Any,
|
90
|
-
) -> list[str]:
|
91
|
-
"""
|
92
|
-
Retrieve the user's *bookcase* page.
|
93
|
-
|
94
|
-
:return: The HTML markup of the bookcase page.
|
95
|
-
"""
|
96
|
-
url = self.bookcase_url()
|
97
|
-
return [await self.fetch(url, **kwargs)]
|
98
|
-
|
99
|
-
async def set_interactive_mode(self, enable: bool) -> bool:
|
100
|
-
"""
|
101
|
-
Enable or disable interactive mode for manual login.
|
102
|
-
|
103
|
-
:param enable: True to enable, False to disable interactive mode.
|
104
|
-
:return: True if operation or login check succeeded, False otherwise.
|
105
|
-
"""
|
106
|
-
if enable:
|
107
|
-
if self.headless:
|
108
|
-
await self._restart_browser(headless=False)
|
109
|
-
if self._manual_page is None:
|
110
|
-
self._manual_page = await self.context.new_page()
|
111
|
-
await self._manual_page.goto(self.LOGIN_URL)
|
112
|
-
return True
|
113
|
-
|
114
|
-
# restore
|
115
|
-
if self._manual_page:
|
116
|
-
await self._manual_page.close()
|
117
|
-
self._manual_page = None
|
118
|
-
if self.headless:
|
119
|
-
await self._restart_browser(headless=True)
|
120
|
-
self._is_logged_in = await self._check_login_status()
|
121
|
-
return self.is_logged_in
|
122
|
-
|
123
|
-
@property
|
124
|
-
def login_fields(self) -> list[LoginField]:
|
125
|
-
return [
|
126
|
-
LoginField(
|
127
|
-
name="manual_login",
|
128
|
-
label="手动登录",
|
129
|
-
type="manual_login",
|
130
|
-
required=True,
|
131
|
-
description=t("login_prompt_intro"),
|
132
|
-
)
|
133
|
-
]
|
134
|
-
|
135
|
-
@classmethod
|
136
|
-
def bookcase_url(cls) -> str:
|
137
|
-
"""
|
138
|
-
Construct the URL for the user's bookcase page.
|
139
|
-
|
140
|
-
:return: Fully qualified URL of the bookcase.
|
141
|
-
"""
|
142
|
-
return cls.BOOKCASE_URL
|
143
|
-
|
144
|
-
@classmethod
|
145
|
-
def book_info_url(cls, book_id: str) -> str:
|
146
|
-
"""
|
147
|
-
Construct the URL for fetching a book's info page.
|
148
|
-
|
149
|
-
:param book_id: The identifier of the book.
|
150
|
-
:return: Fully qualified URL for the book info page.
|
151
|
-
"""
|
152
|
-
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
153
|
-
|
154
|
-
@classmethod
|
155
|
-
def book_catalog_url(cls, book_id: str) -> str:
|
156
|
-
"""
|
157
|
-
Construct the URL for fetching a book's catalog page.
|
158
|
-
|
159
|
-
:param book_id: The identifier of the book.
|
160
|
-
:return: Fully qualified catalog page URL.
|
161
|
-
"""
|
162
|
-
return cls.BOOK_CATALOG_URL.format(book_id=book_id)
|
163
|
-
|
164
|
-
@classmethod
|
165
|
-
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
166
|
-
"""
|
167
|
-
Construct the URL for fetching a specific chapter.
|
168
|
-
|
169
|
-
:param book_id: The identifier of the book.
|
170
|
-
:param chapter_id: The identifier of the chapter.
|
171
|
-
:return: Fully qualified chapter URL.
|
172
|
-
"""
|
173
|
-
return cls.CHAPTER_URL.format(chapter_id=chapter_id)
|
174
|
-
|
175
|
-
@property
|
176
|
-
def hostname(self) -> str:
|
177
|
-
return "m.sfacg.com"
|
178
|
-
|
179
|
-
async def _check_login_status(self) -> bool:
|
180
|
-
"""
|
181
|
-
Check whether the user is currently logged in by
|
182
|
-
inspecting the bookcase page content.
|
183
|
-
|
184
|
-
:return: True if the user is logged in, False otherwise.
|
185
|
-
"""
|
186
|
-
keywords = [
|
187
|
-
"请输入用户名和密码",
|
188
|
-
"用户未登录",
|
189
|
-
"可输入用户名",
|
190
|
-
]
|
191
|
-
resp_text = await self.get_bookcase()
|
192
|
-
if not resp_text:
|
193
|
-
return False
|
194
|
-
return not any(kw in resp_text[0] for kw in keywords)
|
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.fetchers.yamibo
|
4
|
-
-------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
__all__ = [
|
9
|
-
"YamiboBrowser",
|
10
|
-
"YamiboSession",
|
11
|
-
]
|
12
|
-
|
13
|
-
from .browser import YamiboBrowser
|
14
|
-
from .session import YamiboSession
|
@@ -1,234 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.fetchers.yamibo.browser
|
4
|
-
---------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from novel_downloader.core.fetchers.base import BaseBrowser
|
11
|
-
from novel_downloader.core.fetchers.registry import register_fetcher
|
12
|
-
from novel_downloader.models import FetcherConfig, LoginField
|
13
|
-
|
14
|
-
|
15
|
-
@register_fetcher(
|
16
|
-
site_keys=["yamibo"],
|
17
|
-
backends=["browser"],
|
18
|
-
)
|
19
|
-
class YamiboBrowser(BaseBrowser):
|
20
|
-
"""
|
21
|
-
A browser class for interacting with the Yamibo (www.yamibo.com) novel website.
|
22
|
-
"""
|
23
|
-
|
24
|
-
BASE_URL = "https://www.yamibo.com"
|
25
|
-
BOOKCASE_URL = "https://www.yamibo.com/my/fav"
|
26
|
-
BOOK_INFO_URL = "https://www.yamibo.com/novel/{book_id}"
|
27
|
-
CHAPTER_URL = "https://www.yamibo.com/novel/view-chapter?id={chapter_id}"
|
28
|
-
|
29
|
-
LOGIN_URL = "https://www.yamibo.com/user/login"
|
30
|
-
|
31
|
-
def __init__(
|
32
|
-
self,
|
33
|
-
config: FetcherConfig,
|
34
|
-
reuse_page: bool = False,
|
35
|
-
**kwargs: Any,
|
36
|
-
) -> None:
|
37
|
-
super().__init__("yamibo", config, reuse_page, **kwargs)
|
38
|
-
|
39
|
-
async def login(
|
40
|
-
self,
|
41
|
-
username: str = "",
|
42
|
-
password: str = "",
|
43
|
-
cookies: dict[str, str] | None = None,
|
44
|
-
attempt: int = 1,
|
45
|
-
**kwargs: Any,
|
46
|
-
) -> bool:
|
47
|
-
self._is_logged_in = await self._check_login_status()
|
48
|
-
if self._is_logged_in:
|
49
|
-
return True
|
50
|
-
|
51
|
-
if not (username and password):
|
52
|
-
self.logger.warning("[auth] No credentials provided.")
|
53
|
-
return False
|
54
|
-
|
55
|
-
for i in range(1, attempt + 1):
|
56
|
-
login_page = await self.context.new_page()
|
57
|
-
try:
|
58
|
-
await login_page.goto(self.LOGIN_URL, wait_until="networkidle")
|
59
|
-
|
60
|
-
await login_page.fill("#loginform-username", username)
|
61
|
-
await login_page.fill("#loginform-password", password)
|
62
|
-
|
63
|
-
before_url = login_page.url
|
64
|
-
await login_page.click('button[name="login-button"]')
|
65
|
-
|
66
|
-
try:
|
67
|
-
await login_page.wait_for_url(
|
68
|
-
lambda url, before_url=before_url: url_changed(url, before_url),
|
69
|
-
timeout=15000,
|
70
|
-
)
|
71
|
-
except Exception as e:
|
72
|
-
self.logger.debug(
|
73
|
-
f"[auth] No URL change after login attempt {i}: {e}"
|
74
|
-
)
|
75
|
-
|
76
|
-
self._is_logged_in = await self._check_login_status()
|
77
|
-
if self._is_logged_in:
|
78
|
-
self.logger.info(f"[auth] Login successful on attempt {i}.")
|
79
|
-
return True
|
80
|
-
else:
|
81
|
-
self.logger.warning(
|
82
|
-
f"[auth] Login check failed after attempt {i}. Retrying..."
|
83
|
-
)
|
84
|
-
|
85
|
-
except Exception as e:
|
86
|
-
self.logger.error(
|
87
|
-
f"[auth] Unexpected error during login attempt {i}: {e}"
|
88
|
-
)
|
89
|
-
finally:
|
90
|
-
await login_page.close()
|
91
|
-
|
92
|
-
self.logger.error(f"[auth] Login failed after {attempt} attempt(s).")
|
93
|
-
return False
|
94
|
-
|
95
|
-
async def get_book_info(
|
96
|
-
self,
|
97
|
-
book_id: str,
|
98
|
-
**kwargs: Any,
|
99
|
-
) -> list[str]:
|
100
|
-
"""
|
101
|
-
Fetch the raw HTML of the book info page asynchronously.
|
102
|
-
|
103
|
-
:param book_id: The book identifier.
|
104
|
-
:return: The page content as a string.
|
105
|
-
"""
|
106
|
-
url = self.book_info_url(book_id=book_id)
|
107
|
-
return [await self.fetch(url, **kwargs)]
|
108
|
-
|
109
|
-
async def get_book_chapter(
|
110
|
-
self,
|
111
|
-
book_id: str,
|
112
|
-
chapter_id: str,
|
113
|
-
**kwargs: Any,
|
114
|
-
) -> list[str]:
|
115
|
-
"""
|
116
|
-
Fetch the raw HTML of a single chapter asynchronously.
|
117
|
-
|
118
|
-
:param book_id: The book identifier.
|
119
|
-
:param chapter_id: The chapter identifier.
|
120
|
-
:return: The chapter content as a string.
|
121
|
-
"""
|
122
|
-
url = self.chapter_url(book_id=book_id, chapter_id=chapter_id)
|
123
|
-
return [await self.fetch(url, **kwargs)]
|
124
|
-
|
125
|
-
async def get_bookcase(
|
126
|
-
self,
|
127
|
-
**kwargs: Any,
|
128
|
-
) -> list[str]:
|
129
|
-
"""
|
130
|
-
Retrieve the user's *bookcase* page.
|
131
|
-
|
132
|
-
:return: The HTML markup of the bookcase page.
|
133
|
-
"""
|
134
|
-
url = self.bookcase_url()
|
135
|
-
return [await self.fetch(url, **kwargs)]
|
136
|
-
|
137
|
-
async def set_interactive_mode(self, enable: bool) -> bool:
|
138
|
-
"""
|
139
|
-
Enable or disable interactive mode for manual login.
|
140
|
-
|
141
|
-
:param enable: True to enable, False to disable interactive mode.
|
142
|
-
:return: True if operation or login check succeeded, False otherwise.
|
143
|
-
"""
|
144
|
-
if enable:
|
145
|
-
if self.headless:
|
146
|
-
await self._restart_browser(headless=False)
|
147
|
-
if self._manual_page is None:
|
148
|
-
self._manual_page = await self.context.new_page()
|
149
|
-
await self._manual_page.goto(self.LOGIN_URL)
|
150
|
-
return True
|
151
|
-
|
152
|
-
# restore
|
153
|
-
if self._manual_page:
|
154
|
-
await self._manual_page.close()
|
155
|
-
self._manual_page = None
|
156
|
-
if self.headless:
|
157
|
-
await self._restart_browser(headless=True)
|
158
|
-
self._is_logged_in = await self._check_login_status()
|
159
|
-
return self.is_logged_in
|
160
|
-
|
161
|
-
@property
|
162
|
-
def login_fields(self) -> list[LoginField]:
|
163
|
-
return [
|
164
|
-
LoginField(
|
165
|
-
name="username",
|
166
|
-
label="用户名",
|
167
|
-
type="text",
|
168
|
-
required=True,
|
169
|
-
placeholder="请输入你的用户名",
|
170
|
-
description="用于登录 www.yamibo.com 的用户名",
|
171
|
-
),
|
172
|
-
LoginField(
|
173
|
-
name="password",
|
174
|
-
label="密码",
|
175
|
-
type="password",
|
176
|
-
required=True,
|
177
|
-
placeholder="请输入你的密码",
|
178
|
-
description="用于登录 www.yamibo.com 的密码",
|
179
|
-
),
|
180
|
-
]
|
181
|
-
|
182
|
-
@classmethod
|
183
|
-
def bookcase_url(cls) -> str:
|
184
|
-
"""
|
185
|
-
Construct the URL for the user's bookcase page.
|
186
|
-
|
187
|
-
:return: Fully qualified URL of the bookcase.
|
188
|
-
"""
|
189
|
-
return cls.BOOKCASE_URL
|
190
|
-
|
191
|
-
@classmethod
|
192
|
-
def book_info_url(cls, book_id: str) -> str:
|
193
|
-
"""
|
194
|
-
Construct the URL for fetching a book's info page.
|
195
|
-
|
196
|
-
:param book_id: The identifier of the book.
|
197
|
-
:return: Fully qualified URL for the book info page.
|
198
|
-
"""
|
199
|
-
return cls.BOOK_INFO_URL.format(book_id=book_id)
|
200
|
-
|
201
|
-
@classmethod
|
202
|
-
def chapter_url(cls, book_id: str, chapter_id: str) -> str:
|
203
|
-
"""
|
204
|
-
Construct the URL for fetching a specific chapter.
|
205
|
-
|
206
|
-
:param book_id: The identifier of the book.
|
207
|
-
:param chapter_id: The identifier of the chapter.
|
208
|
-
:return: Fully qualified chapter URL.
|
209
|
-
"""
|
210
|
-
return cls.CHAPTER_URL.format(book_id=book_id, chapter_id=chapter_id)
|
211
|
-
|
212
|
-
@property
|
213
|
-
def hostname(self) -> str:
|
214
|
-
return "www.yamibo.com"
|
215
|
-
|
216
|
-
async def _check_login_status(self) -> bool:
|
217
|
-
"""
|
218
|
-
Check whether the user is currently logged in by
|
219
|
-
inspecting the bookcase page content.
|
220
|
-
|
221
|
-
:return: True if the user is logged in, False otherwise.
|
222
|
-
"""
|
223
|
-
keywords = [
|
224
|
-
"登录 - 百合会",
|
225
|
-
"用户名/邮箱",
|
226
|
-
]
|
227
|
-
resp_text = await self.get_bookcase()
|
228
|
-
if not resp_text:
|
229
|
-
return False
|
230
|
-
return not any(kw in resp_text[0] for kw in keywords)
|
231
|
-
|
232
|
-
|
233
|
-
def url_changed(url: str, before: str) -> bool:
|
234
|
-
return url != before
|
@@ -1,139 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.biquge
|
4
|
-
------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
import re
|
9
|
-
from typing import Any
|
10
|
-
|
11
|
-
from lxml import html
|
12
|
-
|
13
|
-
from novel_downloader.core.parsers.base import BaseParser
|
14
|
-
from novel_downloader.core.parsers.registry import register_parser
|
15
|
-
from novel_downloader.models import ChapterDict
|
16
|
-
|
17
|
-
|
18
|
-
@register_parser(
|
19
|
-
site_keys=["biquge", "bqg"],
|
20
|
-
backends=["session", "browser"],
|
21
|
-
)
|
22
|
-
class BiqugeParser(BaseParser):
|
23
|
-
""" """
|
24
|
-
|
25
|
-
def parse_book_info(
|
26
|
-
self,
|
27
|
-
html_list: list[str],
|
28
|
-
**kwargs: Any,
|
29
|
-
) -> dict[str, Any]:
|
30
|
-
"""
|
31
|
-
Parse a book info page and extract metadata and chapter structure.
|
32
|
-
|
33
|
-
:param html_list: Raw HTML of the book info page.
|
34
|
-
:return: Parsed metadata and chapter structure as a dictionary.
|
35
|
-
"""
|
36
|
-
if not html_list:
|
37
|
-
return {}
|
38
|
-
tree = html.fromstring(html_list[0])
|
39
|
-
result: dict[str, Any] = {}
|
40
|
-
|
41
|
-
def extract_text(elem: html.HtmlElement | None) -> str:
|
42
|
-
if elem is None:
|
43
|
-
return ""
|
44
|
-
return "".join(elem.itertext(tag=None)).strip()
|
45
|
-
|
46
|
-
# 书名
|
47
|
-
book_name_elem = tree.xpath('//div[@id="info"]/h1')
|
48
|
-
result["book_name"] = extract_text(book_name_elem[0]) if book_name_elem else ""
|
49
|
-
|
50
|
-
# 作者
|
51
|
-
author_elem = tree.xpath('//div[@id="info"]/p[1]')
|
52
|
-
if author_elem:
|
53
|
-
author_text = extract_text(author_elem[0]).replace("\u00a0", "")
|
54
|
-
match = re.search(r"作\s*者[::]?\s*(\S+)", author_text)
|
55
|
-
result["author"] = match.group(1).strip() if match else ""
|
56
|
-
else:
|
57
|
-
result["author"] = ""
|
58
|
-
|
59
|
-
# 封面
|
60
|
-
cover_elem = tree.xpath('//div[@id="fmimg"]/img/@src')
|
61
|
-
result["cover_url"] = cover_elem[0].strip() if cover_elem else ""
|
62
|
-
|
63
|
-
# 最后更新时间
|
64
|
-
update_elem = tree.xpath('//div[@id="info"]/p[3]')
|
65
|
-
if update_elem:
|
66
|
-
update_text = extract_text(update_elem[0])
|
67
|
-
match = re.search(r"最后更新[::]\s*(\S+)", update_text)
|
68
|
-
result["update_time"] = match.group(1).strip() if match else ""
|
69
|
-
else:
|
70
|
-
result["update_time"] = ""
|
71
|
-
|
72
|
-
# 简介
|
73
|
-
intro_elem = tree.xpath('//div[@id="intro"]')
|
74
|
-
result["summary"] = extract_text(intro_elem[0]) if intro_elem else ""
|
75
|
-
|
76
|
-
# 卷和章节
|
77
|
-
chapters = []
|
78
|
-
in_main_volume = False
|
79
|
-
|
80
|
-
list_dl = tree.xpath('//div[@id="list"]/dl')[0]
|
81
|
-
for elem in list_dl:
|
82
|
-
if elem.tag == "dt":
|
83
|
-
text = "".join(elem.itertext()).strip()
|
84
|
-
in_main_volume = "正文" in text
|
85
|
-
elif in_main_volume and elem.tag == "dd":
|
86
|
-
a: list[html.HtmlElement] = elem.xpath("./a")
|
87
|
-
if a:
|
88
|
-
title = "".join(a[0].itertext(tag=None)).strip()
|
89
|
-
url = a[0].get("href", "").strip()
|
90
|
-
href_cleaned = url.replace(".html", "")
|
91
|
-
chapter_id_match = re.search(r"/(\d+)$", href_cleaned)
|
92
|
-
chapter_id = chapter_id_match.group(1) if chapter_id_match else ""
|
93
|
-
chapters.append(
|
94
|
-
{"title": title, "url": url, "chapterId": chapter_id}
|
95
|
-
)
|
96
|
-
|
97
|
-
result["volumes"] = [{"volume_name": "正文", "chapters": chapters}]
|
98
|
-
|
99
|
-
return result
|
100
|
-
|
101
|
-
def parse_chapter(
|
102
|
-
self,
|
103
|
-
html_list: list[str],
|
104
|
-
chapter_id: str,
|
105
|
-
**kwargs: Any,
|
106
|
-
) -> ChapterDict | None:
|
107
|
-
"""
|
108
|
-
Parse a single chapter page and extract clean text or simplified HTML.
|
109
|
-
|
110
|
-
:param html_list: Raw HTML of the chapter page.
|
111
|
-
:param chapter_id: Identifier of the chapter being parsed.
|
112
|
-
:return: Cleaned chapter content as plain text or minimal HTML.
|
113
|
-
"""
|
114
|
-
if not html_list:
|
115
|
-
return None
|
116
|
-
tree = html.fromstring(html_list[0], parser=None)
|
117
|
-
|
118
|
-
# 提取标题
|
119
|
-
title_elem = tree.xpath('//div[@class="bookname"]/h1')
|
120
|
-
title = "".join(title_elem[0].itertext()).strip() if title_elem else ""
|
121
|
-
if not title:
|
122
|
-
title = f"第 {chapter_id} 章"
|
123
|
-
|
124
|
-
# 提取内容
|
125
|
-
content_elem = tree.xpath('//div[@id="content"]')
|
126
|
-
paragraphs = content_elem[0].xpath(".//p") if content_elem else []
|
127
|
-
paragraph_texts = [
|
128
|
-
"".join(p.itertext()).strip() for p in paragraphs if p is not None
|
129
|
-
]
|
130
|
-
content = "\n\n".join([p for p in paragraph_texts if p])
|
131
|
-
if not content.strip():
|
132
|
-
return None
|
133
|
-
|
134
|
-
return {
|
135
|
-
"id": chapter_id,
|
136
|
-
"title": title,
|
137
|
-
"content": content,
|
138
|
-
"extra": {"site": "biquge"},
|
139
|
-
}
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.models.chapter
|
4
|
-
-------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any, TypedDict
|
9
|
-
|
10
|
-
|
11
|
-
class ChapterDict(TypedDict, total=True):
|
12
|
-
"""
|
13
|
-
TypedDict for a novel chapter.
|
14
|
-
|
15
|
-
Fields:
|
16
|
-
id -- Unique chapter identifier
|
17
|
-
title -- Chapter title
|
18
|
-
content -- Chapter text
|
19
|
-
extra -- Arbitrary metadata (e.g. author remarks, timestamps)
|
20
|
-
"""
|
21
|
-
|
22
|
-
id: str
|
23
|
-
title: str
|
24
|
-
content: str
|
25
|
-
extra: dict[str, Any]
|
novel_downloader/models/types.py
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.models.types
|
4
|
-
-----------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Literal
|
9
|
-
|
10
|
-
ModeType = Literal["browser", "session"]
|
11
|
-
SplitMode = Literal["book", "volume"]
|
12
|
-
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
|
13
|
-
BrowserType = Literal["chromium", "firefox", "webkit"]
|
novel_downloader/tui/__init__.py
DELETED
novel_downloader/tui/app.py
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.app
|
4
|
-
------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from textual.app import App, ComposeResult
|
11
|
-
from textual.containers import Container
|
12
|
-
from textual.widgets import Footer, Header
|
13
|
-
|
14
|
-
from novel_downloader.config import load_config
|
15
|
-
from novel_downloader.tui.screens import HomeScreen
|
16
|
-
|
17
|
-
|
18
|
-
class NovelDownloaderTUI(App): # type: ignore[misc]
|
19
|
-
TITLE = "Novel Downloader TUI"
|
20
|
-
SCREENS = {
|
21
|
-
"home": HomeScreen,
|
22
|
-
}
|
23
|
-
config: dict[str, Any]
|
24
|
-
|
25
|
-
def compose(self) -> ComposeResult:
|
26
|
-
yield Header()
|
27
|
-
yield Container(id="main_area")
|
28
|
-
yield Footer()
|
29
|
-
|
30
|
-
def on_mount(self) -> None:
|
31
|
-
self.config = load_config()
|
32
|
-
self.push_screen("home")
|
novel_downloader/tui/main.py
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.main
|
4
|
-
-------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.tui.app import NovelDownloaderTUI
|
9
|
-
|
10
|
-
|
11
|
-
def tui_main() -> None:
|
12
|
-
app = NovelDownloaderTUI()
|
13
|
-
app.run()
|
14
|
-
|
15
|
-
|
16
|
-
if __name__ == "__main__":
|
17
|
-
tui_main()
|