novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novel_downloader/__init__.py +1 -1
- novel_downloader/cli/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +79 -66
- novel_downloader/cli/export.py +17 -21
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +17 -12
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +20 -14
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +64 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/esjzone.py +64 -69
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/linovelib.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +429 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +34 -85
- novel_downloader/locales/zh.json +35 -86
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/crypto_utils/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +55 -49
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/crypto_utils.py +0 -71
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,93 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.archived.xshbook.searcher
|
4
|
+
-----------------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
|
10
|
+
from lxml import html
|
11
|
+
from novel_downloader.core.searchers.base import BaseSearcher
|
12
|
+
from novel_downloader.models import SearchResult
|
13
|
+
|
14
|
+
# from novel_downloader.core.searchers.registry import register_searcher
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
# @register_searcher(
|
20
|
+
# site_keys=["xshbook"],
|
21
|
+
# )
|
22
|
+
class XshbookSearcher(BaseSearcher):
|
23
|
+
site_name = "xshbook"
|
24
|
+
priority = 30
|
25
|
+
BASE_URL = "https://www.xshbook.com"
|
26
|
+
SEARCH_URL = "https://www.sososhu.com/"
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
async def _fetch_html(cls, keyword: str) -> str:
|
30
|
+
params = {
|
31
|
+
"q": keyword,
|
32
|
+
"site": "xshbook",
|
33
|
+
}
|
34
|
+
try:
|
35
|
+
async with (await cls._http_get(cls.SEARCH_URL, params=params)) as resp:
|
36
|
+
return await cls._response_to_str(resp)
|
37
|
+
except Exception:
|
38
|
+
logger.error(
|
39
|
+
"Failed to fetch HTML for keyword '%s' from '%s'",
|
40
|
+
keyword,
|
41
|
+
cls.SEARCH_URL,
|
42
|
+
)
|
43
|
+
return ""
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def _parse_html(cls, html_str: str, limit: int | None = None) -> list[SearchResult]:
|
47
|
+
doc = html.fromstring(html_str)
|
48
|
+
rows = doc.xpath(
|
49
|
+
"//div[contains(@class,'so_list')]//div[contains(@class,'hot')]//div[contains(@class,'item')]"
|
50
|
+
)
|
51
|
+
results: list[SearchResult] = []
|
52
|
+
|
53
|
+
for idx, row in enumerate(rows):
|
54
|
+
if limit is not None and idx >= limit:
|
55
|
+
break
|
56
|
+
a_nodes = row.xpath(".//dl/dt/a[1]")
|
57
|
+
a = a_nodes[0] if a_nodes else None
|
58
|
+
href = a.get("href") if a is not None else ""
|
59
|
+
book_url = cls._abs_url(href)
|
60
|
+
book_id = cls._book_id_from_url(book_url) if book_url else ""
|
61
|
+
if not book_id:
|
62
|
+
continue
|
63
|
+
|
64
|
+
title = (a.text_content() if a is not None else "").strip()
|
65
|
+
author = cls._first_str(row.xpath(".//dl/dt/span[1]/text()"))
|
66
|
+
cover_url = cls._first_str(
|
67
|
+
row.xpath(".//div[contains(@class,'image')]//img/@src")
|
68
|
+
)
|
69
|
+
|
70
|
+
# Compute priority
|
71
|
+
prio = cls.priority + idx
|
72
|
+
|
73
|
+
results.append(
|
74
|
+
SearchResult(
|
75
|
+
site=cls.site_name,
|
76
|
+
book_id=book_id,
|
77
|
+
book_url=book_url,
|
78
|
+
cover_url=cover_url,
|
79
|
+
title=title,
|
80
|
+
author=author,
|
81
|
+
latest_chapter="-",
|
82
|
+
update_date="-",
|
83
|
+
word_count="-",
|
84
|
+
priority=prio,
|
85
|
+
)
|
86
|
+
)
|
87
|
+
return results
|
88
|
+
|
89
|
+
@staticmethod
|
90
|
+
def _book_id_from_url(url: str) -> str:
|
91
|
+
tail = url.split("xshbook.com", 1)[-1]
|
92
|
+
tail = tail.strip("/")
|
93
|
+
return tail.replace("/", "-")
|
@@ -3,38 +3,17 @@
|
|
3
3
|
novel_downloader.core.downloaders
|
4
4
|
---------------------------------
|
5
5
|
|
6
|
-
|
7
|
-
specific novel platforms.
|
8
|
-
|
9
|
-
Each downloader is responsible for orchestrating the full lifecycle
|
10
|
-
of retrieving, parsing, and saving novel content for a given source.
|
11
|
-
|
12
|
-
Currently supported platforms:
|
13
|
-
- biquge (笔趣阁)
|
14
|
-
- esjzone (ESJ Zone)
|
15
|
-
- linovelib (哔哩轻小说)
|
16
|
-
- qianbi (铅笔小说)
|
17
|
-
- qidian (起点中文网)
|
18
|
-
- sfacg (SF轻小说)
|
19
|
-
- yamibo (百合会)
|
6
|
+
Downloader implementations for retrieving novels from different sources
|
20
7
|
"""
|
21
8
|
|
22
9
|
__all__ = [
|
23
10
|
"get_downloader",
|
24
|
-
"
|
25
|
-
"EsjzoneDownloader",
|
26
|
-
"LinovelibDownloader",
|
11
|
+
"CommonDownloader",
|
27
12
|
"QianbiDownloader",
|
28
13
|
"QidianDownloader",
|
29
|
-
"SfacgDownloader",
|
30
|
-
"YamiboDownloader",
|
31
14
|
]
|
32
15
|
|
33
|
-
from .
|
34
|
-
from .esjzone import EsjzoneDownloader
|
35
|
-
from .linovelib import LinovelibDownloader
|
16
|
+
from .common import CommonDownloader
|
36
17
|
from .qianbi import QianbiDownloader
|
37
18
|
from .qidian import QidianDownloader
|
38
19
|
from .registry import get_downloader
|
39
|
-
from .sfacg import SfacgDownloader
|
40
|
-
from .yamibo import YamiboDownloader
|
@@ -3,24 +3,29 @@
|
|
3
3
|
novel_downloader.core.downloaders.base
|
4
4
|
--------------------------------------
|
5
5
|
|
6
|
-
|
7
|
-
common interface and reusable logic for all downloader implementations.
|
6
|
+
Abstract base class providing common workflow and utilities for novel downloaders
|
8
7
|
"""
|
9
8
|
|
10
9
|
import abc
|
10
|
+
import asyncio
|
11
11
|
import json
|
12
12
|
import logging
|
13
13
|
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
|
14
14
|
from pathlib import Path
|
15
|
-
from typing import Any
|
15
|
+
from typing import Any, cast
|
16
16
|
|
17
17
|
from novel_downloader.core.interfaces import (
|
18
18
|
DownloaderProtocol,
|
19
19
|
FetcherProtocol,
|
20
20
|
ParserProtocol,
|
21
21
|
)
|
22
|
-
from novel_downloader.models import
|
23
|
-
|
22
|
+
from novel_downloader.models import (
|
23
|
+
BookConfig,
|
24
|
+
BookInfoDict,
|
25
|
+
DownloaderConfig,
|
26
|
+
VolumeInfoDict,
|
27
|
+
)
|
28
|
+
from novel_downloader.utils import time_diff
|
24
29
|
|
25
30
|
|
26
31
|
class BaseDownloader(DownloaderProtocol, abc.ABC):
|
@@ -35,7 +40,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
35
40
|
"""
|
36
41
|
|
37
42
|
DEFAULT_SOURCE_ID = 0
|
38
|
-
|
43
|
+
PRIORITIES_MAP = {
|
39
44
|
DEFAULT_SOURCE_ID: 0,
|
40
45
|
}
|
41
46
|
|
@@ -45,7 +50,6 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
45
50
|
parser: ParserProtocol,
|
46
51
|
config: DownloaderConfig,
|
47
52
|
site: str,
|
48
|
-
priorities: dict[int, int] | None = None,
|
49
53
|
):
|
50
54
|
"""
|
51
55
|
Initialize the downloader for a specific site.
|
@@ -54,15 +58,11 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
54
58
|
:param parser: Parser component for extracting chapter content.
|
55
59
|
:param config: Downloader configuration settings.
|
56
60
|
:param site: Identifier for the target website or source.
|
57
|
-
:param priorities: Mapping of source_id to priority value.
|
58
|
-
Lower numbers indicate higher priority.
|
59
|
-
E.X. {0: 10, 1: 100} means source 0 is preferred.
|
60
61
|
"""
|
61
62
|
self._fetcher = fetcher
|
62
63
|
self._parser = parser
|
63
64
|
self._config = config
|
64
65
|
self._site = site
|
65
|
-
self._priorities = priorities or self.DEFAULT_PRIORITIES_MAP
|
66
66
|
|
67
67
|
self._raw_data_dir = Path(config.raw_data_dir) / site
|
68
68
|
self._raw_data_dir.mkdir(parents=True, exist_ok=True)
|
@@ -76,6 +76,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
76
76
|
books: list[BookConfig],
|
77
77
|
*,
|
78
78
|
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
79
|
+
cancel_event: asyncio.Event | None = None,
|
79
80
|
**kwargs: Any,
|
80
81
|
) -> None:
|
81
82
|
"""
|
@@ -84,6 +85,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
84
85
|
:param books: List of BookConfig entries.
|
85
86
|
:param progress_hook: Optional async callback after each chapter.
|
86
87
|
args: completed_count, total_count.
|
88
|
+
:param cancel_event: Optional asyncio.Event to allow cancellation.
|
87
89
|
"""
|
88
90
|
if not await self._ensure_ready():
|
89
91
|
book_ids = [b["book_id"] for b in books]
|
@@ -95,10 +97,20 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
95
97
|
return
|
96
98
|
|
97
99
|
for book in books:
|
100
|
+
# stop early if cancellation requested
|
101
|
+
if cancel_event and cancel_event.is_set():
|
102
|
+
self.logger.info(
|
103
|
+
"[%s] download cancelled before book: %s",
|
104
|
+
self._site,
|
105
|
+
book["book_id"],
|
106
|
+
)
|
107
|
+
break
|
108
|
+
|
98
109
|
try:
|
99
110
|
await self._download_one(
|
100
111
|
book,
|
101
112
|
progress_hook=progress_hook,
|
113
|
+
cancel_event=cancel_event,
|
102
114
|
**kwargs,
|
103
115
|
)
|
104
116
|
except Exception as e:
|
@@ -111,6 +123,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
111
123
|
book: BookConfig,
|
112
124
|
*,
|
113
125
|
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
126
|
+
cancel_event: asyncio.Event | None = None,
|
114
127
|
**kwargs: Any,
|
115
128
|
) -> None:
|
116
129
|
"""
|
@@ -119,6 +132,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
119
132
|
:param book: BookConfig with at least 'book_id'.
|
120
133
|
:param progress_hook: Optional async callback after each chapter.
|
121
134
|
args: completed_count, total_count.
|
135
|
+
:param cancel_event: Optional asyncio.Event to allow cancellation.
|
122
136
|
"""
|
123
137
|
if not await self._ensure_ready():
|
124
138
|
self.logger.warning(
|
@@ -129,10 +143,20 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
129
143
|
book.get("end_id", "-"),
|
130
144
|
)
|
131
145
|
|
146
|
+
# if already cancelled before starting
|
147
|
+
if cancel_event and cancel_event.is_set():
|
148
|
+
self.logger.info(
|
149
|
+
"[%s] download cancelled before start of book: %s",
|
150
|
+
self._site,
|
151
|
+
book["book_id"],
|
152
|
+
)
|
153
|
+
return
|
154
|
+
|
132
155
|
try:
|
133
156
|
await self._download_one(
|
134
157
|
book,
|
135
158
|
progress_hook=progress_hook,
|
159
|
+
cancel_event=cancel_event,
|
136
160
|
**kwargs,
|
137
161
|
)
|
138
162
|
except Exception as e:
|
@@ -144,7 +168,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
144
168
|
self,
|
145
169
|
book_id: str,
|
146
170
|
html_dir: Path,
|
147
|
-
) ->
|
171
|
+
) -> BookInfoDict | None:
|
148
172
|
book_info = self._load_book_info(
|
149
173
|
book_id=book_id,
|
150
174
|
max_age_days=1,
|
@@ -168,6 +192,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
168
192
|
book: BookConfig,
|
169
193
|
*,
|
170
194
|
progress_hook: Callable[[int, int], Awaitable[None]] | None = None,
|
195
|
+
cancel_event: asyncio.Event | None = None,
|
171
196
|
**kwargs: Any,
|
172
197
|
) -> None:
|
173
198
|
"""
|
@@ -197,7 +222,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
197
222
|
book_id: str,
|
198
223
|
*,
|
199
224
|
max_age_days: int | None = None,
|
200
|
-
) ->
|
225
|
+
) -> BookInfoDict | None:
|
201
226
|
"""
|
202
227
|
Attempt to read and parse the book_info.json for a given book_id.
|
203
228
|
|
@@ -207,27 +232,28 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
207
232
|
"""
|
208
233
|
info_path = self._raw_data_dir / book_id / "book_info.json"
|
209
234
|
if not info_path.is_file():
|
210
|
-
return
|
235
|
+
return None
|
211
236
|
|
212
237
|
try:
|
213
|
-
|
238
|
+
raw: dict[str, Any] = json.loads(info_path.read_text(encoding="utf-8"))
|
214
239
|
except json.JSONDecodeError:
|
215
|
-
return
|
240
|
+
return None
|
216
241
|
|
217
242
|
if max_age_days is not None:
|
218
|
-
days, *_ =
|
219
|
-
|
243
|
+
days, *_ = time_diff(
|
244
|
+
raw.get("update_time", ""),
|
220
245
|
"UTC+8",
|
221
246
|
)
|
222
247
|
if days > max_age_days:
|
223
|
-
return
|
248
|
+
return None
|
224
249
|
|
225
|
-
return data
|
250
|
+
# return data
|
251
|
+
return cast(BookInfoDict, raw)
|
226
252
|
|
227
253
|
def _save_book_info(
|
228
254
|
self,
|
229
255
|
book_id: str,
|
230
|
-
book_info:
|
256
|
+
book_info: BookInfoDict,
|
231
257
|
) -> None:
|
232
258
|
"""
|
233
259
|
Serialize and save the book_info dict as json.
|
@@ -267,7 +293,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
267
293
|
|
268
294
|
@staticmethod
|
269
295
|
async def _chapter_ids(
|
270
|
-
volumes: list[
|
296
|
+
volumes: list[VolumeInfoDict],
|
271
297
|
start_id: str | None,
|
272
298
|
end_id: str | None,
|
273
299
|
) -> AsyncIterator[str]:
|
@@ -276,7 +302,7 @@ class BaseDownloader(DownloaderProtocol, abc.ABC):
|
|
276
302
|
"""
|
277
303
|
seen_start = start_id is None
|
278
304
|
for vol in volumes:
|
279
|
-
for chap in vol
|
305
|
+
for chap in vol["chapters"]:
|
280
306
|
cid = chap.get("chapterId")
|
281
307
|
if not cid:
|
282
308
|
continue
|