novel-downloader 1.3.1__py3-none-any.whl → 1.3.3__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/download.py +1 -1
- novel_downloader/config/adapter.py +3 -0
- novel_downloader/config/models.py +3 -0
- novel_downloader/core/downloaders/__init__.py +23 -1
- novel_downloader/core/downloaders/biquge/__init__.py +2 -0
- novel_downloader/core/downloaders/biquge/biquge_async.py +27 -0
- novel_downloader/core/downloaders/biquge/biquge_sync.py +5 -3
- novel_downloader/core/downloaders/common/common_async.py +5 -11
- novel_downloader/core/downloaders/common/common_sync.py +18 -18
- novel_downloader/core/downloaders/esjzone/__init__.py +14 -0
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +27 -0
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +27 -0
- novel_downloader/core/downloaders/qianbi/__init__.py +14 -0
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +27 -0
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +27 -0
- novel_downloader/core/downloaders/qidian/qidian_sync.py +9 -14
- novel_downloader/core/downloaders/sfacg/__init__.py +14 -0
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +27 -0
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +27 -0
- novel_downloader/core/downloaders/yamibo/__init__.py +14 -0
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +27 -0
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +27 -0
- novel_downloader/core/factory/downloader.py +35 -7
- novel_downloader/core/factory/parser.py +23 -2
- novel_downloader/core/factory/requester.py +32 -7
- novel_downloader/core/factory/saver.py +14 -2
- novel_downloader/core/interfaces/async_requester.py +3 -3
- novel_downloader/core/interfaces/parser.py +7 -2
- novel_downloader/core/interfaces/sync_requester.py +3 -3
- novel_downloader/core/parsers/__init__.py +15 -5
- novel_downloader/core/parsers/base.py +7 -2
- novel_downloader/core/parsers/biquge/main_parser.py +13 -4
- novel_downloader/core/parsers/common/main_parser.py +13 -4
- novel_downloader/core/parsers/esjzone/__init__.py +10 -0
- novel_downloader/core/parsers/esjzone/main_parser.py +220 -0
- novel_downloader/core/parsers/qianbi/__init__.py +10 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +142 -0
- novel_downloader/core/parsers/qidian/browser/main_parser.py +13 -4
- novel_downloader/core/parsers/qidian/session/main_parser.py +13 -4
- novel_downloader/core/parsers/sfacg/__init__.py +10 -0
- novel_downloader/core/parsers/sfacg/main_parser.py +166 -0
- novel_downloader/core/parsers/yamibo/__init__.py +10 -0
- novel_downloader/core/parsers/yamibo/main_parser.py +194 -0
- novel_downloader/core/requesters/__init__.py +33 -3
- novel_downloader/core/requesters/base/async_session.py +14 -10
- novel_downloader/core/requesters/base/browser.py +4 -7
- novel_downloader/core/requesters/base/session.py +25 -11
- novel_downloader/core/requesters/biquge/__init__.py +2 -0
- novel_downloader/core/requesters/biquge/async_session.py +71 -0
- novel_downloader/core/requesters/biquge/session.py +6 -6
- novel_downloader/core/requesters/common/async_session.py +4 -4
- novel_downloader/core/requesters/common/session.py +6 -6
- novel_downloader/core/requesters/esjzone/__init__.py +13 -0
- novel_downloader/core/requesters/esjzone/async_session.py +211 -0
- novel_downloader/core/requesters/esjzone/session.py +235 -0
- novel_downloader/core/requesters/qianbi/__init__.py +13 -0
- novel_downloader/core/requesters/qianbi/async_session.py +96 -0
- novel_downloader/core/requesters/qianbi/session.py +125 -0
- novel_downloader/core/requesters/qidian/broswer.py +9 -9
- novel_downloader/core/requesters/qidian/session.py +14 -11
- novel_downloader/core/requesters/sfacg/__init__.py +13 -0
- novel_downloader/core/requesters/sfacg/async_session.py +204 -0
- novel_downloader/core/requesters/sfacg/session.py +242 -0
- novel_downloader/core/requesters/yamibo/__init__.py +13 -0
- novel_downloader/core/requesters/yamibo/async_session.py +211 -0
- novel_downloader/core/requesters/yamibo/session.py +237 -0
- novel_downloader/core/savers/__init__.py +15 -3
- novel_downloader/core/savers/base.py +3 -7
- novel_downloader/core/savers/common/epub.py +21 -33
- novel_downloader/core/savers/common/main_saver.py +3 -1
- novel_downloader/core/savers/common/txt.py +1 -2
- novel_downloader/core/savers/epub_utils/__init__.py +14 -5
- novel_downloader/core/savers/epub_utils/css_builder.py +1 -0
- novel_downloader/core/savers/epub_utils/image_loader.py +89 -0
- novel_downloader/core/savers/epub_utils/initializer.py +1 -0
- novel_downloader/core/savers/epub_utils/text_to_html.py +48 -1
- novel_downloader/core/savers/epub_utils/volume_intro.py +1 -0
- novel_downloader/core/savers/esjzone.py +25 -0
- novel_downloader/core/savers/qianbi.py +25 -0
- novel_downloader/core/savers/sfacg.py +25 -0
- novel_downloader/core/savers/yamibo.py +25 -0
- novel_downloader/locales/en.json +1 -0
- novel_downloader/locales/zh.json +1 -0
- novel_downloader/resources/config/settings.toml +40 -4
- novel_downloader/utils/constants.py +4 -0
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/network.py +51 -38
- novel_downloader/utils/time_utils/__init__.py +2 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -1
- novel_downloader/utils/time_utils/sleep_utils.py +44 -2
- {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/METADATA +29 -24
- novel_downloader-1.3.3.dist-info/RECORD +166 -0
- novel_downloader-1.3.1.dist-info/RECORD +0 -127
- {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/WHEEL +0 -0
- {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
novel_downloader/cli/download.py
CHANGED
@@ -119,7 +119,7 @@ def download_cli(ctx: Context, book_ids: list[str], site: str) -> None:
|
|
119
119
|
config=downloader_cfg,
|
120
120
|
)
|
121
121
|
|
122
|
-
for book_id in
|
122
|
+
for book_id in valid_book_ids:
|
123
123
|
click.echo(t("download_downloading", book_id=book_id, site=site))
|
124
124
|
sync_downloader.download_one(book_id)
|
125
125
|
|
@@ -90,6 +90,8 @@ class ConfigAdapter:
|
|
90
90
|
disable_images=req.get("disable_images", True),
|
91
91
|
mute_audio=req.get("mute_audio", True),
|
92
92
|
mode=site_cfg.get("mode", "session"),
|
93
|
+
username=site_cfg.get("username", ""),
|
94
|
+
password=site_cfg.get("password", ""),
|
93
95
|
)
|
94
96
|
|
95
97
|
def get_downloader_config(self) -> DownloaderConfig:
|
@@ -150,6 +152,7 @@ class ConfigAdapter:
|
|
150
152
|
naming = out.get("naming", {})
|
151
153
|
epub_opts = out.get("epub", {})
|
152
154
|
return SaverConfig(
|
155
|
+
cache_dir=gen.get("cache_dir", "./novel_cache"),
|
153
156
|
raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
|
154
157
|
output_dir=gen.get("output_dir", "./downloads"),
|
155
158
|
storage_backend=gen.get("storage_backend", "json"),
|
@@ -38,6 +38,8 @@ class RequesterConfig:
|
|
38
38
|
mode: ModeType = "session"
|
39
39
|
max_connections: int = 10
|
40
40
|
max_rps: float | None = None # Maximum requests per second
|
41
|
+
username: str = ""
|
42
|
+
password: str = ""
|
41
43
|
|
42
44
|
|
43
45
|
# === Downloaders ===
|
@@ -78,6 +80,7 @@ class ParserConfig:
|
|
78
80
|
# === Savers ===
|
79
81
|
@dataclass
|
80
82
|
class SaverConfig:
|
83
|
+
cache_dir: str = "./novel_cache"
|
81
84
|
raw_data_dir: str = "./raw_data"
|
82
85
|
output_dir: str = "./downloads"
|
83
86
|
storage_backend: StorageBackend = "json"
|
@@ -8,15 +8,37 @@ specific novel platforms.
|
|
8
8
|
|
9
9
|
Each downloader is responsible for orchestrating the full lifecycle
|
10
10
|
of retrieving, parsing, and saving novel content for a given source.
|
11
|
+
|
12
|
+
Currently supported platforms:
|
13
|
+
- biquge (笔趣阁)
|
14
|
+
- esjzone (ESJ Zone)
|
15
|
+
- qianbi (铅笔小说)
|
16
|
+
- qidian (起点中文网)
|
17
|
+
- sfacg (SF轻小说)
|
18
|
+
- yamibo (百合会)
|
19
|
+
- common (通用架构)
|
11
20
|
"""
|
12
21
|
|
13
|
-
from .biquge import BiqugeDownloader
|
22
|
+
from .biquge import BiqugeAsyncDownloader, BiqugeDownloader
|
14
23
|
from .common import CommonAsyncDownloader, CommonDownloader
|
24
|
+
from .esjzone import EsjzoneAsyncDownloader, EsjzoneDownloader
|
25
|
+
from .qianbi import QianbiAsyncDownloader, QianbiDownloader
|
15
26
|
from .qidian import QidianDownloader
|
27
|
+
from .sfacg import SfacgAsyncDownloader, SfacgDownloader
|
28
|
+
from .yamibo import YamiboAsyncDownloader, YamiboDownloader
|
16
29
|
|
17
30
|
__all__ = [
|
31
|
+
"BiqugeAsyncDownloader",
|
18
32
|
"BiqugeDownloader",
|
19
33
|
"CommonAsyncDownloader",
|
20
34
|
"CommonDownloader",
|
35
|
+
"EsjzoneAsyncDownloader",
|
36
|
+
"EsjzoneDownloader",
|
37
|
+
"QianbiAsyncDownloader",
|
38
|
+
"QianbiDownloader",
|
21
39
|
"QidianDownloader",
|
40
|
+
"SfacgAsyncDownloader",
|
41
|
+
"SfacgDownloader",
|
42
|
+
"YamiboAsyncDownloader",
|
43
|
+
"YamiboDownloader",
|
22
44
|
]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.biquge.biquge_async
|
4
|
+
-----------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
AsyncRequesterProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
SaverProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class BiqugeAsyncDownloader(CommonAsyncDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: AsyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "biquge")
|
@@ -7,9 +7,11 @@ novel_downloader.core.downloaders.biquge.biquge_sync
|
|
7
7
|
|
8
8
|
from novel_downloader.config.models import DownloaderConfig
|
9
9
|
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
-
from novel_downloader.core.interfaces
|
11
|
-
|
12
|
-
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
ParserProtocol,
|
12
|
+
SaverProtocol,
|
13
|
+
SyncRequesterProtocol,
|
14
|
+
)
|
13
15
|
|
14
16
|
|
15
17
|
class BiqugeDownloader(CommonDownloader):
|
@@ -20,7 +20,6 @@ from novel_downloader.core.interfaces import (
|
|
20
20
|
)
|
21
21
|
from novel_downloader.utils.chapter_storage import ChapterDict, ChapterStorage
|
22
22
|
from novel_downloader.utils.file_utils import save_as_json, save_as_txt
|
23
|
-
from novel_downloader.utils.network import download_image_as_bytes
|
24
23
|
from novel_downloader.utils.time_utils import calculate_time_difference
|
25
24
|
|
26
25
|
logger = logging.getLogger(__name__)
|
@@ -48,7 +47,7 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
|
|
48
47
|
Perform login
|
49
48
|
"""
|
50
49
|
if self.login_required and not self._is_logged_in:
|
51
|
-
success = await self.requester.login(
|
50
|
+
success = await self.requester.login()
|
52
51
|
if not success:
|
53
52
|
raise RuntimeError("Login failed")
|
54
53
|
self._is_logged_in = True
|
@@ -60,6 +59,7 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
|
|
60
59
|
:param book_id: The identifier of the book to download.
|
61
60
|
"""
|
62
61
|
assert isinstance(self.requester, AsyncRequesterProtocol)
|
62
|
+
await self.prepare()
|
63
63
|
|
64
64
|
TAG = "[AsyncDownloader]"
|
65
65
|
wait_time = self.config.request_interval
|
@@ -95,7 +95,8 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
|
|
95
95
|
if re_fetch:
|
96
96
|
info_html = await self.requester.get_book_info(book_id)
|
97
97
|
if self.save_html:
|
98
|
-
|
98
|
+
for i, html in enumerate(info_html):
|
99
|
+
save_as_txt(html, chapters_html_dir / f"info_{i}.html")
|
99
100
|
book_info = self.parser.parse_book_info(info_html)
|
100
101
|
if book_info.get("book_name") != "未找到书名":
|
101
102
|
save_as_json(book_info, info_path)
|
@@ -105,16 +106,9 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
|
|
105
106
|
else:
|
106
107
|
book_info = json.loads(info_path.read_text("utf-8"))
|
107
108
|
|
108
|
-
# download cover
|
109
|
-
cover_url = book_info.get("cover_url", "")
|
110
|
-
if cover_url:
|
111
|
-
await asyncio.get_running_loop().run_in_executor(
|
112
|
-
None, download_image_as_bytes, cover_url, raw_base
|
113
|
-
)
|
114
|
-
|
115
109
|
# setup queue, semaphore, executor
|
116
110
|
semaphore = asyncio.Semaphore(self.download_workers)
|
117
|
-
queue: asyncio.Queue[tuple[str, str]] = asyncio.Queue()
|
111
|
+
queue: asyncio.Queue[tuple[str, list[str]]] = asyncio.Queue()
|
118
112
|
save_queue: asyncio.Queue[ChapterDict] = asyncio.Queue()
|
119
113
|
loop = asyncio.get_running_loop()
|
120
114
|
executor = (
|
@@ -19,7 +19,6 @@ from novel_downloader.core.interfaces import (
|
|
19
19
|
)
|
20
20
|
from novel_downloader.utils.chapter_storage import ChapterStorage
|
21
21
|
from novel_downloader.utils.file_utils import save_as_json, save_as_txt
|
22
|
-
from novel_downloader.utils.network import download_image_as_bytes
|
23
22
|
from novel_downloader.utils.time_utils import (
|
24
23
|
calculate_time_difference,
|
25
24
|
sleep_with_random_delay,
|
@@ -52,6 +51,17 @@ class CommonDownloader(BaseDownloader):
|
|
52
51
|
"""
|
53
52
|
super().__init__(requester, parser, saver, config, site)
|
54
53
|
self._site = site
|
54
|
+
self._is_logged_in = False
|
55
|
+
|
56
|
+
def prepare(self) -> None:
|
57
|
+
"""
|
58
|
+
Perform login
|
59
|
+
"""
|
60
|
+
if self.login_required and not self._is_logged_in:
|
61
|
+
success = self.requester.login()
|
62
|
+
if not success:
|
63
|
+
raise RuntimeError("Login failed")
|
64
|
+
self._is_logged_in = True
|
55
65
|
|
56
66
|
def download_one(self, book_id: str) -> None:
|
57
67
|
"""
|
@@ -59,6 +69,8 @@ class CommonDownloader(BaseDownloader):
|
|
59
69
|
|
60
70
|
:param book_id: The identifier of the book to download.
|
61
71
|
"""
|
72
|
+
self.prepare()
|
73
|
+
|
62
74
|
TAG = "[Downloader]"
|
63
75
|
save_html = self.config.save_html
|
64
76
|
skip_existing = self.config.skip_existing
|
@@ -96,8 +108,8 @@ class CommonDownloader(BaseDownloader):
|
|
96
108
|
except Exception:
|
97
109
|
info_html = self.requester.get_book_info(book_id)
|
98
110
|
if save_html:
|
99
|
-
|
100
|
-
|
111
|
+
for i, html in enumerate(info_html):
|
112
|
+
save_as_txt(html, chapters_html_dir / f"info_{i}.html")
|
101
113
|
book_info = self.parser.parse_book_info(info_html)
|
102
114
|
if (
|
103
115
|
book_info.get("book_name", "") != "未找到书名"
|
@@ -106,13 +118,6 @@ class CommonDownloader(BaseDownloader):
|
|
106
118
|
save_as_json(book_info, info_path)
|
107
119
|
sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
|
108
120
|
|
109
|
-
# download cover
|
110
|
-
cover_url = book_info.get("cover_url", "")
|
111
|
-
if cover_url:
|
112
|
-
cover_bytes = download_image_as_bytes(cover_url, raw_base)
|
113
|
-
if not cover_bytes:
|
114
|
-
logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
115
|
-
|
116
121
|
# enqueue chapters
|
117
122
|
for vol in book_info.get("volumes", []):
|
118
123
|
vol_name = vol.get("volume_name", "")
|
@@ -138,14 +143,9 @@ class CommonDownloader(BaseDownloader):
|
|
138
143
|
chap_html = self.requester.get_book_chapter(book_id, cid)
|
139
144
|
|
140
145
|
if save_html:
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
"%s Saved raw HTML for chapter %s to %s",
|
145
|
-
TAG,
|
146
|
-
cid,
|
147
|
-
html_path,
|
148
|
-
)
|
146
|
+
for i, html in enumerate(chap_html):
|
147
|
+
html_path = chapters_html_dir / f"{cid}_{i}.html"
|
148
|
+
save_as_txt(html, html_path, on_exist="skip")
|
149
149
|
|
150
150
|
chap_json = self.parser.parse_chapter(chap_html, cid)
|
151
151
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.esjzone
|
4
|
+
-----------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .esjzone_async import EsjzoneAsyncDownloader
|
9
|
+
from .esjzone_sync import EsjzoneDownloader
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"EsjzoneAsyncDownloader",
|
13
|
+
"EsjzoneDownloader",
|
14
|
+
]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.esjzone.esjzone_async
|
4
|
+
-------------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
AsyncRequesterProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
SaverProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class EsjzoneAsyncDownloader(CommonAsyncDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: AsyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "esjzone")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.esjzone.esjzone_sync
|
4
|
+
------------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
ParserProtocol,
|
12
|
+
SaverProtocol,
|
13
|
+
SyncRequesterProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class EsjzoneDownloader(CommonDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: SyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "esjzone")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.qianbi
|
4
|
+
----------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .qianbi_async import QianbiAsyncDownloader
|
9
|
+
from .qianbi_sync import QianbiDownloader
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"QianbiAsyncDownloader",
|
13
|
+
"QianbiDownloader",
|
14
|
+
]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.qianbi.qianbi_async
|
4
|
+
-----------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
AsyncRequesterProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
SaverProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class QianbiAsyncDownloader(CommonAsyncDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: AsyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "qianbi")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.qianbi.qianbi_sync
|
4
|
+
----------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
ParserProtocol,
|
12
|
+
SaverProtocol,
|
13
|
+
SyncRequesterProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class QianbiDownloader(CommonDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: SyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "qianbi")
|
@@ -19,7 +19,6 @@ from novel_downloader.core.interfaces import (
|
|
19
19
|
)
|
20
20
|
from novel_downloader.utils.chapter_storage import ChapterStorage
|
21
21
|
from novel_downloader.utils.file_utils import save_as_json, save_as_txt
|
22
|
-
from novel_downloader.utils.network import download_image_as_bytes
|
23
22
|
from novel_downloader.utils.state import state_mgr
|
24
23
|
from novel_downloader.utils.time_utils import (
|
25
24
|
calculate_time_difference,
|
@@ -100,9 +99,9 @@ class QidianDownloader(BaseDownloader):
|
|
100
99
|
raise FileNotFoundError # trigger re-fetch
|
101
100
|
except Exception:
|
102
101
|
info_html = self.requester.get_book_info(book_id)
|
103
|
-
if save_html:
|
102
|
+
if save_html and info_html:
|
104
103
|
info_html_path = chapters_html_dir / "info.html"
|
105
|
-
save_as_txt(info_html, info_html_path)
|
104
|
+
save_as_txt(info_html[0], info_html_path)
|
106
105
|
book_info = self.parser.parse_book_info(info_html)
|
107
106
|
if (
|
108
107
|
book_info.get("book_name", "") != "未找到书名"
|
@@ -111,13 +110,6 @@ class QidianDownloader(BaseDownloader):
|
|
111
110
|
save_as_json(book_info, info_path)
|
112
111
|
sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
|
113
112
|
|
114
|
-
# download cover
|
115
|
-
cover_url = book_info.get("cover_url", "")
|
116
|
-
if cover_url:
|
117
|
-
cover_bytes = download_image_as_bytes(cover_url, raw_base)
|
118
|
-
if not cover_bytes:
|
119
|
-
self.logger.warning("%s Failed to download cover: %s", TAG, cover_url)
|
120
|
-
|
121
113
|
# enqueue chapters
|
122
114
|
for vol in book_info.get("volumes", []):
|
123
115
|
vol_name = vol.get("volume_name", "")
|
@@ -140,6 +132,9 @@ class QidianDownloader(BaseDownloader):
|
|
140
132
|
chap_title = chap.get("title", "")
|
141
133
|
self.logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
|
142
134
|
chap_html = self.requester.get_book_chapter(book_id, cid)
|
135
|
+
if not chap_html:
|
136
|
+
continue
|
137
|
+
|
143
138
|
if scroll:
|
144
139
|
self.requester.scroll_page(wait_time * 2) # type: ignore[attr-defined]
|
145
140
|
else:
|
@@ -147,7 +142,7 @@ class QidianDownloader(BaseDownloader):
|
|
147
142
|
wait_time, mul_spread=1.1, max_sleep=wait_time + 2
|
148
143
|
)
|
149
144
|
|
150
|
-
is_encrypted = self.parser.is_encrypted(chap_html) # type: ignore[attr-defined]
|
145
|
+
is_encrypted = self.parser.is_encrypted(chap_html[0]) # type: ignore[attr-defined]
|
151
146
|
|
152
147
|
if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
|
153
148
|
self.logger.debug(
|
@@ -157,18 +152,18 @@ class QidianDownloader(BaseDownloader):
|
|
157
152
|
)
|
158
153
|
continue
|
159
154
|
|
160
|
-
if save_html and not is_vip(chap_html):
|
155
|
+
if save_html and chap_html and not is_vip(chap_html[0]):
|
161
156
|
folder = chapters_html_dir / (
|
162
157
|
"html_encrypted" if is_encrypted else "html_plain"
|
163
158
|
)
|
164
159
|
html_path = folder / f"{cid}.html"
|
165
|
-
save_as_txt(chap_html, html_path, on_exist="skip")
|
160
|
+
save_as_txt(chap_html[0], html_path, on_exist="skip")
|
166
161
|
self.logger.debug(
|
167
162
|
"%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
|
168
163
|
)
|
169
164
|
|
170
165
|
chap_json = self.parser.parse_chapter(chap_html, cid)
|
171
|
-
if not chap_json:
|
166
|
+
if not chap_json or not chap_json.get("content"):
|
172
167
|
self.logger.warning(
|
173
168
|
"%s Parsed chapter json is empty, skipping: %s (%s)",
|
174
169
|
TAG,
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.sfacg
|
4
|
+
---------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .sfacg_async import SfacgAsyncDownloader
|
9
|
+
from .sfacg_sync import SfacgDownloader
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"SfacgAsyncDownloader",
|
13
|
+
"SfacgDownloader",
|
14
|
+
]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.sfacg.sfacg_async
|
4
|
+
---------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
AsyncRequesterProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
SaverProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class SfacgAsyncDownloader(CommonAsyncDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: AsyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "sfacg")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.sfacg.sfacg_sync
|
4
|
+
--------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
ParserProtocol,
|
12
|
+
SaverProtocol,
|
13
|
+
SyncRequesterProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class SfacgDownloader(CommonDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: SyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "sfacg")
|
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.yamibo
|
4
|
+
----------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .yamibo_async import YamiboAsyncDownloader
|
9
|
+
from .yamibo_sync import YamiboDownloader
|
10
|
+
|
11
|
+
__all__ = [
|
12
|
+
"YamiboAsyncDownloader",
|
13
|
+
"YamiboDownloader",
|
14
|
+
]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.yamibo.yamibo_async
|
4
|
+
-----------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonAsyncDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
AsyncRequesterProtocol,
|
12
|
+
ParserProtocol,
|
13
|
+
SaverProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class YamiboAsyncDownloader(CommonAsyncDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: AsyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "yamibo")
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.downloaders.yamibo.yamibo_sync
|
4
|
+
----------------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.config.models import DownloaderConfig
|
9
|
+
from novel_downloader.core.downloaders.common import CommonDownloader
|
10
|
+
from novel_downloader.core.interfaces import (
|
11
|
+
ParserProtocol,
|
12
|
+
SaverProtocol,
|
13
|
+
SyncRequesterProtocol,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class YamiboDownloader(CommonDownloader):
|
18
|
+
""""""
|
19
|
+
|
20
|
+
def __init__(
|
21
|
+
self,
|
22
|
+
requester: SyncRequesterProtocol,
|
23
|
+
parser: ParserProtocol,
|
24
|
+
saver: SaverProtocol,
|
25
|
+
config: DownloaderConfig,
|
26
|
+
):
|
27
|
+
super().__init__(requester, parser, saver, config, "yamibo")
|