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.
Files changed (248) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +1 -3
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +26 -21
  5. novel_downloader/cli/download.py +79 -66
  6. novel_downloader/cli/export.py +17 -21
  7. novel_downloader/cli/main.py +1 -1
  8. novel_downloader/cli/search.py +62 -65
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +8 -5
  11. novel_downloader/config/adapter.py +206 -209
  12. novel_downloader/config/{loader.py → file_io.py} +53 -26
  13. novel_downloader/core/__init__.py +5 -5
  14. novel_downloader/core/archived/deqixs/fetcher.py +115 -0
  15. novel_downloader/core/archived/deqixs/parser.py +132 -0
  16. novel_downloader/core/archived/deqixs/searcher.py +89 -0
  17. novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
  18. novel_downloader/core/archived/wanbengo/searcher.py +98 -0
  19. novel_downloader/core/archived/xshbook/searcher.py +93 -0
  20. novel_downloader/core/downloaders/__init__.py +3 -24
  21. novel_downloader/core/downloaders/base.py +49 -23
  22. novel_downloader/core/downloaders/common.py +191 -137
  23. novel_downloader/core/downloaders/qianbi.py +187 -146
  24. novel_downloader/core/downloaders/qidian.py +187 -141
  25. novel_downloader/core/downloaders/registry.py +4 -2
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +3 -20
  28. novel_downloader/core/exporters/base.py +33 -37
  29. novel_downloader/core/exporters/common/__init__.py +1 -2
  30. novel_downloader/core/exporters/common/epub.py +15 -10
  31. novel_downloader/core/exporters/common/main_exporter.py +19 -12
  32. novel_downloader/core/exporters/common/txt.py +17 -12
  33. novel_downloader/core/exporters/epub_util.py +59 -29
  34. novel_downloader/core/exporters/linovelib/__init__.py +1 -0
  35. novel_downloader/core/exporters/linovelib/epub.py +23 -25
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
  37. novel_downloader/core/exporters/linovelib/txt.py +20 -14
  38. novel_downloader/core/exporters/qidian.py +2 -8
  39. novel_downloader/core/exporters/registry.py +4 -2
  40. novel_downloader/core/exporters/txt_util.py +7 -7
  41. novel_downloader/core/fetchers/__init__.py +54 -48
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
  45. novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
  46. novel_downloader/core/fetchers/dxmwx.py +110 -0
  47. novel_downloader/core/fetchers/eightnovel.py +139 -0
  48. novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
  49. novel_downloader/core/fetchers/guidaye.py +85 -0
  50. novel_downloader/core/fetchers/hetushu.py +92 -0
  51. novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/lewenn.py +83 -0
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
  56. novel_downloader/core/fetchers/piaotia.py +105 -0
  57. novel_downloader/core/fetchers/qbtr.py +101 -0
  58. novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
  60. novel_downloader/core/fetchers/quanben5.py +92 -0
  61. novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
  62. novel_downloader/core/fetchers/registry.py +5 -16
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/shuhaige.py +84 -0
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/wanbengo.py +83 -0
  69. novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
  70. novel_downloader/core/fetchers/xiguashuwu.py +177 -0
  71. novel_downloader/core/fetchers/xs63b.py +171 -0
  72. novel_downloader/core/fetchers/xshbook.py +85 -0
  73. novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +1 -9
  76. novel_downloader/core/interfaces/downloader.py +6 -2
  77. novel_downloader/core/interfaces/exporter.py +7 -7
  78. novel_downloader/core/interfaces/fetcher.py +6 -19
  79. novel_downloader/core/interfaces/parser.py +7 -8
  80. novel_downloader/core/interfaces/searcher.py +9 -1
  81. novel_downloader/core/parsers/__init__.py +49 -12
  82. novel_downloader/core/parsers/aaatxt.py +132 -0
  83. novel_downloader/core/parsers/b520.py +116 -0
  84. novel_downloader/core/parsers/base.py +64 -12
  85. novel_downloader/core/parsers/biquyuedu.py +133 -0
  86. novel_downloader/core/parsers/dxmwx.py +162 -0
  87. novel_downloader/core/parsers/eightnovel.py +224 -0
  88. novel_downloader/core/parsers/esjzone.py +64 -69
  89. novel_downloader/core/parsers/guidaye.py +128 -0
  90. novel_downloader/core/parsers/hetushu.py +139 -0
  91. novel_downloader/core/parsers/i25zw.py +137 -0
  92. novel_downloader/core/parsers/ixdzs8.py +186 -0
  93. novel_downloader/core/parsers/jpxs123.py +137 -0
  94. novel_downloader/core/parsers/lewenn.py +142 -0
  95. novel_downloader/core/parsers/linovelib.py +48 -64
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/qianbi.py +48 -50
  99. novel_downloader/core/parsers/qidian/main_parser.py +756 -48
  100. novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
  101. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
  102. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
  103. novel_downloader/core/parsers/quanben5.py +103 -0
  104. novel_downloader/core/parsers/registry.py +5 -16
  105. novel_downloader/core/parsers/sfacg.py +38 -45
  106. novel_downloader/core/parsers/shencou.py +215 -0
  107. novel_downloader/core/parsers/shuhaige.py +111 -0
  108. novel_downloader/core/parsers/tongrenquan.py +116 -0
  109. novel_downloader/core/parsers/ttkan.py +132 -0
  110. novel_downloader/core/parsers/wanbengo.py +191 -0
  111. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  112. novel_downloader/core/parsers/xiguashuwu.py +429 -0
  113. novel_downloader/core/parsers/xs63b.py +161 -0
  114. novel_downloader/core/parsers/xshbook.py +134 -0
  115. novel_downloader/core/parsers/yamibo.py +87 -131
  116. novel_downloader/core/parsers/yibige.py +166 -0
  117. novel_downloader/core/searchers/__init__.py +34 -3
  118. novel_downloader/core/searchers/aaatxt.py +107 -0
  119. novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
  120. novel_downloader/core/searchers/base.py +112 -36
  121. novel_downloader/core/searchers/dxmwx.py +105 -0
  122. novel_downloader/core/searchers/eightnovel.py +84 -0
  123. novel_downloader/core/searchers/esjzone.py +43 -25
  124. novel_downloader/core/searchers/hetushu.py +92 -0
  125. novel_downloader/core/searchers/i25zw.py +93 -0
  126. novel_downloader/core/searchers/ixdzs8.py +107 -0
  127. novel_downloader/core/searchers/jpxs123.py +107 -0
  128. novel_downloader/core/searchers/piaotia.py +100 -0
  129. novel_downloader/core/searchers/qbtr.py +106 -0
  130. novel_downloader/core/searchers/qianbi.py +74 -40
  131. novel_downloader/core/searchers/quanben5.py +144 -0
  132. novel_downloader/core/searchers/registry.py +24 -8
  133. novel_downloader/core/searchers/shuhaige.py +124 -0
  134. novel_downloader/core/searchers/tongrenquan.py +110 -0
  135. novel_downloader/core/searchers/ttkan.py +92 -0
  136. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  137. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  138. novel_downloader/core/searchers/xs63b.py +104 -0
  139. novel_downloader/locales/en.json +34 -85
  140. novel_downloader/locales/zh.json +35 -86
  141. novel_downloader/models/__init__.py +21 -22
  142. novel_downloader/models/book.py +44 -0
  143. novel_downloader/models/config.py +4 -37
  144. novel_downloader/models/login.py +1 -1
  145. novel_downloader/models/search.py +5 -0
  146. novel_downloader/resources/config/settings.toml +8 -70
  147. novel_downloader/resources/json/xiguashuwu.json +718 -0
  148. novel_downloader/utils/__init__.py +13 -24
  149. novel_downloader/utils/chapter_storage.py +5 -5
  150. novel_downloader/utils/constants.py +4 -31
  151. novel_downloader/utils/cookies.py +38 -35
  152. novel_downloader/utils/crypto_utils/__init__.py +7 -0
  153. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  154. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  155. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  156. novel_downloader/utils/crypto_utils/rc4.py +54 -0
  157. novel_downloader/utils/epub/__init__.py +3 -4
  158. novel_downloader/utils/epub/builder.py +6 -6
  159. novel_downloader/utils/epub/constants.py +62 -21
  160. novel_downloader/utils/epub/documents.py +95 -201
  161. novel_downloader/utils/epub/models.py +8 -22
  162. novel_downloader/utils/epub/utils.py +73 -106
  163. novel_downloader/utils/file_utils/__init__.py +2 -23
  164. novel_downloader/utils/file_utils/io.py +53 -188
  165. novel_downloader/utils/file_utils/normalize.py +1 -7
  166. novel_downloader/utils/file_utils/sanitize.py +4 -15
  167. novel_downloader/utils/fontocr/__init__.py +5 -14
  168. novel_downloader/utils/fontocr/core.py +216 -0
  169. novel_downloader/utils/fontocr/loader.py +50 -0
  170. novel_downloader/utils/logger.py +81 -65
  171. novel_downloader/utils/network.py +17 -41
  172. novel_downloader/utils/state.py +4 -90
  173. novel_downloader/utils/text_utils/__init__.py +1 -7
  174. novel_downloader/utils/text_utils/diff_display.py +5 -7
  175. novel_downloader/utils/text_utils/text_cleaner.py +39 -30
  176. novel_downloader/utils/text_utils/truncate_utils.py +3 -14
  177. novel_downloader/utils/time_utils/__init__.py +5 -11
  178. novel_downloader/utils/time_utils/datetime_utils.py +20 -29
  179. novel_downloader/utils/time_utils/sleep_utils.py +55 -49
  180. novel_downloader/web/__init__.py +13 -0
  181. novel_downloader/web/components/__init__.py +11 -0
  182. novel_downloader/web/components/navigation.py +35 -0
  183. novel_downloader/web/main.py +66 -0
  184. novel_downloader/web/pages/__init__.py +17 -0
  185. novel_downloader/web/pages/download.py +78 -0
  186. novel_downloader/web/pages/progress.py +147 -0
  187. novel_downloader/web/pages/search.py +329 -0
  188. novel_downloader/web/services/__init__.py +17 -0
  189. novel_downloader/web/services/client_dialog.py +164 -0
  190. novel_downloader/web/services/cred_broker.py +113 -0
  191. novel_downloader/web/services/cred_models.py +35 -0
  192. novel_downloader/web/services/task_manager.py +264 -0
  193. novel_downloader-2.0.1.dist-info/METADATA +172 -0
  194. novel_downloader-2.0.1.dist-info/RECORD +206 -0
  195. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
  196. novel_downloader/core/downloaders/biquge.py +0 -29
  197. novel_downloader/core/downloaders/esjzone.py +0 -29
  198. novel_downloader/core/downloaders/linovelib.py +0 -29
  199. novel_downloader/core/downloaders/sfacg.py +0 -29
  200. novel_downloader/core/downloaders/yamibo.py +0 -29
  201. novel_downloader/core/exporters/biquge.py +0 -22
  202. novel_downloader/core/exporters/esjzone.py +0 -22
  203. novel_downloader/core/exporters/qianbi.py +0 -22
  204. novel_downloader/core/exporters/sfacg.py +0 -22
  205. novel_downloader/core/exporters/yamibo.py +0 -22
  206. novel_downloader/core/fetchers/base/__init__.py +0 -14
  207. novel_downloader/core/fetchers/base/browser.py +0 -422
  208. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  209. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  210. novel_downloader/core/fetchers/esjzone/browser.py +0 -209
  211. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  212. novel_downloader/core/fetchers/linovelib/browser.py +0 -198
  213. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  214. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  215. novel_downloader/core/fetchers/qidian/browser.py +0 -326
  216. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  217. novel_downloader/core/fetchers/sfacg/browser.py +0 -194
  218. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  219. novel_downloader/core/fetchers/yamibo/browser.py +0 -234
  220. novel_downloader/core/parsers/biquge.py +0 -139
  221. novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
  222. novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
  223. novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
  224. novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
  225. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
  226. novel_downloader/models/chapter.py +0 -25
  227. novel_downloader/models/types.py +0 -13
  228. novel_downloader/tui/__init__.py +0 -7
  229. novel_downloader/tui/app.py +0 -32
  230. novel_downloader/tui/main.py +0 -17
  231. novel_downloader/tui/screens/__init__.py +0 -14
  232. novel_downloader/tui/screens/home.py +0 -198
  233. novel_downloader/tui/screens/login.py +0 -74
  234. novel_downloader/tui/styles/home_layout.tcss +0 -79
  235. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  236. novel_downloader/utils/cache.py +0 -24
  237. novel_downloader/utils/crypto_utils.py +0 -71
  238. novel_downloader/utils/fontocr/hash_store.py +0 -280
  239. novel_downloader/utils/fontocr/hash_utils.py +0 -103
  240. novel_downloader/utils/fontocr/model_loader.py +0 -69
  241. novel_downloader/utils/fontocr/ocr_v1.py +0 -315
  242. novel_downloader/utils/fontocr/ocr_v2.py +0 -764
  243. novel_downloader/utils/fontocr/ocr_v3.py +0 -744
  244. novel_downloader-1.5.0.dist-info/METADATA +0 -196
  245. novel_downloader-1.5.0.dist-info/RECORD +0 -164
  246. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
  247. {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
  248. {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
- This subpackage contains concrete downloader implementations for
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
- "BiqugeDownloader",
25
- "EsjzoneDownloader",
26
- "LinovelibDownloader",
11
+ "CommonDownloader",
27
12
  "QianbiDownloader",
28
13
  "QidianDownloader",
29
- "SfacgDownloader",
30
- "YamiboDownloader",
31
14
  ]
32
15
 
33
- from .biquge import BiqugeDownloader
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
- Defines the abstract base class `BaseDownloader`, which provides a
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 BookConfig, DownloaderConfig
23
- from novel_downloader.utils import calculate_time_difference
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
- DEFAULT_PRIORITIES_MAP = {
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
- ) -> dict[str, Any]:
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
- ) -> dict[str, Any]:
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
- data: dict[str, Any] = json.loads(info_path.read_text(encoding="utf-8"))
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, *_ = calculate_time_difference(
219
- data.get("update_time", ""),
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: dict[str, Any],
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[dict[str, Any]],
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.get("chapters", []):
305
+ for chap in vol["chapters"]:
280
306
  cid = chap.get("chapterId")
281
307
  if not cid:
282
308
  continue