novel-downloader 1.4.5__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.
Files changed (276) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/__init__.py +2 -4
  3. novel_downloader/cli/clean.py +21 -88
  4. novel_downloader/cli/config.py +27 -104
  5. novel_downloader/cli/download.py +78 -66
  6. novel_downloader/cli/export.py +20 -21
  7. novel_downloader/cli/main.py +3 -1
  8. novel_downloader/cli/search.py +120 -0
  9. novel_downloader/cli/ui.py +156 -0
  10. novel_downloader/config/__init__.py +10 -14
  11. novel_downloader/config/adapter.py +195 -99
  12. novel_downloader/config/{loader.py → file_io.py} +53 -27
  13. novel_downloader/core/__init__.py +14 -13
  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/archived/qidian/searcher.py +79 -0
  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 +8 -30
  21. novel_downloader/core/downloaders/base.py +182 -30
  22. novel_downloader/core/downloaders/common.py +217 -384
  23. novel_downloader/core/downloaders/qianbi.py +332 -4
  24. novel_downloader/core/downloaders/qidian.py +250 -290
  25. novel_downloader/core/downloaders/registry.py +69 -0
  26. novel_downloader/core/downloaders/signals.py +46 -0
  27. novel_downloader/core/exporters/__init__.py +8 -26
  28. novel_downloader/core/exporters/base.py +107 -31
  29. novel_downloader/core/exporters/common/__init__.py +3 -4
  30. novel_downloader/core/exporters/common/epub.py +92 -171
  31. novel_downloader/core/exporters/common/main_exporter.py +14 -67
  32. novel_downloader/core/exporters/common/txt.py +90 -86
  33. novel_downloader/core/exporters/epub_util.py +184 -1327
  34. novel_downloader/core/exporters/linovelib/__init__.py +3 -2
  35. novel_downloader/core/exporters/linovelib/epub.py +165 -222
  36. novel_downloader/core/exporters/linovelib/main_exporter.py +10 -71
  37. novel_downloader/core/exporters/linovelib/txt.py +76 -66
  38. novel_downloader/core/exporters/qidian.py +15 -11
  39. novel_downloader/core/exporters/registry.py +55 -0
  40. novel_downloader/core/exporters/txt_util.py +67 -0
  41. novel_downloader/core/fetchers/__init__.py +57 -56
  42. novel_downloader/core/fetchers/aaatxt.py +83 -0
  43. novel_downloader/core/fetchers/{biquge/session.py → b520.py} +10 -10
  44. novel_downloader/core/fetchers/{base/session.py → base.py} +63 -47
  45. novel_downloader/core/fetchers/biquyuedu.py +83 -0
  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} +23 -11
  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} +22 -26
  52. novel_downloader/core/fetchers/ixdzs8.py +113 -0
  53. novel_downloader/core/fetchers/jpxs123.py +101 -0
  54. novel_downloader/core/fetchers/{biquge/browser.py → lewenn.py} +15 -15
  55. novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +16 -12
  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} +9 -9
  59. novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +55 -40
  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 +60 -0
  63. novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +11 -9
  64. novel_downloader/core/fetchers/shencou.py +106 -0
  65. novel_downloader/core/fetchers/{common/browser.py → shuhaige.py} +24 -19
  66. novel_downloader/core/fetchers/tongrenquan.py +84 -0
  67. novel_downloader/core/fetchers/ttkan.py +95 -0
  68. novel_downloader/core/fetchers/{common/session.py → wanbengo.py} +21 -17
  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} +23 -11
  74. novel_downloader/core/fetchers/yibige.py +114 -0
  75. novel_downloader/core/interfaces/__init__.py +8 -14
  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 +4 -17
  79. novel_downloader/core/interfaces/parser.py +5 -6
  80. novel_downloader/core/interfaces/searcher.py +26 -0
  81. novel_downloader/core/parsers/__init__.py +58 -22
  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 +63 -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/main_parser.py → esjzone.py} +67 -67
  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/main_parser.py → linovelib.py} +54 -65
  96. novel_downloader/core/parsers/piaotia.py +189 -0
  97. novel_downloader/core/parsers/qbtr.py +136 -0
  98. novel_downloader/core/parsers/{qianbi/main_parser.py → qianbi.py} +54 -51
  99. novel_downloader/core/parsers/qidian/__init__.py +2 -2
  100. novel_downloader/core/parsers/qidian/book_info_parser.py +58 -59
  101. novel_downloader/core/parsers/qidian/chapter_encrypted.py +290 -346
  102. novel_downloader/core/parsers/qidian/chapter_normal.py +25 -56
  103. novel_downloader/core/parsers/qidian/main_parser.py +19 -57
  104. novel_downloader/core/parsers/qidian/utils/__init__.py +12 -11
  105. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +6 -7
  106. novel_downloader/core/parsers/qidian/utils/fontmap_recover.py +143 -0
  107. novel_downloader/core/parsers/qidian/utils/helpers.py +0 -4
  108. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +2 -2
  109. novel_downloader/core/parsers/quanben5.py +103 -0
  110. novel_downloader/core/parsers/registry.py +57 -0
  111. novel_downloader/core/parsers/{sfacg/main_parser.py → sfacg.py} +46 -48
  112. novel_downloader/core/parsers/shencou.py +215 -0
  113. novel_downloader/core/parsers/shuhaige.py +111 -0
  114. novel_downloader/core/parsers/tongrenquan.py +116 -0
  115. novel_downloader/core/parsers/ttkan.py +132 -0
  116. novel_downloader/core/parsers/wanbengo.py +191 -0
  117. novel_downloader/core/parsers/xiaoshuowu.py +173 -0
  118. novel_downloader/core/parsers/xiguashuwu.py +435 -0
  119. novel_downloader/core/parsers/xs63b.py +161 -0
  120. novel_downloader/core/parsers/xshbook.py +134 -0
  121. novel_downloader/core/parsers/yamibo.py +155 -0
  122. novel_downloader/core/parsers/yibige.py +166 -0
  123. novel_downloader/core/searchers/__init__.py +51 -0
  124. novel_downloader/core/searchers/aaatxt.py +107 -0
  125. novel_downloader/core/searchers/b520.py +84 -0
  126. novel_downloader/core/searchers/base.py +168 -0
  127. novel_downloader/core/searchers/dxmwx.py +105 -0
  128. novel_downloader/core/searchers/eightnovel.py +84 -0
  129. novel_downloader/core/searchers/esjzone.py +102 -0
  130. novel_downloader/core/searchers/hetushu.py +92 -0
  131. novel_downloader/core/searchers/i25zw.py +93 -0
  132. novel_downloader/core/searchers/ixdzs8.py +107 -0
  133. novel_downloader/core/searchers/jpxs123.py +107 -0
  134. novel_downloader/core/searchers/piaotia.py +100 -0
  135. novel_downloader/core/searchers/qbtr.py +106 -0
  136. novel_downloader/core/searchers/qianbi.py +165 -0
  137. novel_downloader/core/searchers/quanben5.py +144 -0
  138. novel_downloader/core/searchers/registry.py +79 -0
  139. novel_downloader/core/searchers/shuhaige.py +124 -0
  140. novel_downloader/core/searchers/tongrenquan.py +110 -0
  141. novel_downloader/core/searchers/ttkan.py +92 -0
  142. novel_downloader/core/searchers/xiaoshuowu.py +122 -0
  143. novel_downloader/core/searchers/xiguashuwu.py +95 -0
  144. novel_downloader/core/searchers/xs63b.py +104 -0
  145. novel_downloader/locales/en.json +36 -79
  146. novel_downloader/locales/zh.json +37 -80
  147. novel_downloader/models/__init__.py +23 -50
  148. novel_downloader/models/book.py +44 -0
  149. novel_downloader/models/config.py +16 -43
  150. novel_downloader/models/login.py +1 -1
  151. novel_downloader/models/search.py +21 -0
  152. novel_downloader/resources/config/settings.toml +39 -74
  153. novel_downloader/resources/css_styles/intro.css +83 -0
  154. novel_downloader/resources/css_styles/main.css +30 -89
  155. novel_downloader/resources/json/xiguashuwu.json +718 -0
  156. novel_downloader/utils/__init__.py +43 -0
  157. novel_downloader/utils/chapter_storage.py +247 -226
  158. novel_downloader/utils/constants.py +5 -50
  159. novel_downloader/utils/cookies.py +6 -18
  160. novel_downloader/utils/crypto_utils/__init__.py +13 -0
  161. novel_downloader/utils/crypto_utils/aes_util.py +90 -0
  162. novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
  163. novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
  164. novel_downloader/utils/{crypto_utils.py → crypto_utils/rc4.py} +3 -10
  165. novel_downloader/utils/epub/__init__.py +34 -0
  166. novel_downloader/utils/epub/builder.py +377 -0
  167. novel_downloader/utils/epub/constants.py +118 -0
  168. novel_downloader/utils/epub/documents.py +297 -0
  169. novel_downloader/utils/epub/models.py +120 -0
  170. novel_downloader/utils/epub/utils.py +179 -0
  171. novel_downloader/utils/file_utils/__init__.py +5 -30
  172. novel_downloader/utils/file_utils/io.py +9 -150
  173. novel_downloader/utils/file_utils/normalize.py +2 -2
  174. novel_downloader/utils/file_utils/sanitize.py +2 -7
  175. novel_downloader/utils/fontocr.py +207 -0
  176. novel_downloader/utils/i18n.py +2 -0
  177. novel_downloader/utils/logger.py +10 -16
  178. novel_downloader/utils/network.py +111 -252
  179. novel_downloader/utils/state.py +5 -90
  180. novel_downloader/utils/text_utils/__init__.py +16 -21
  181. novel_downloader/utils/text_utils/diff_display.py +6 -9
  182. novel_downloader/utils/text_utils/numeric_conversion.py +253 -0
  183. novel_downloader/utils/text_utils/text_cleaner.py +179 -0
  184. novel_downloader/utils/text_utils/truncate_utils.py +62 -0
  185. novel_downloader/utils/time_utils/__init__.py +6 -12
  186. novel_downloader/utils/time_utils/datetime_utils.py +23 -33
  187. novel_downloader/utils/time_utils/sleep_utils.py +5 -10
  188. novel_downloader/web/__init__.py +13 -0
  189. novel_downloader/web/components/__init__.py +11 -0
  190. novel_downloader/web/components/navigation.py +35 -0
  191. novel_downloader/web/main.py +66 -0
  192. novel_downloader/web/pages/__init__.py +17 -0
  193. novel_downloader/web/pages/download.py +78 -0
  194. novel_downloader/web/pages/progress.py +147 -0
  195. novel_downloader/web/pages/search.py +329 -0
  196. novel_downloader/web/services/__init__.py +17 -0
  197. novel_downloader/web/services/client_dialog.py +164 -0
  198. novel_downloader/web/services/cred_broker.py +113 -0
  199. novel_downloader/web/services/cred_models.py +35 -0
  200. novel_downloader/web/services/task_manager.py +264 -0
  201. novel_downloader-2.0.0.dist-info/METADATA +171 -0
  202. novel_downloader-2.0.0.dist-info/RECORD +210 -0
  203. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/entry_points.txt +1 -1
  204. novel_downloader/config/site_rules.py +0 -94
  205. novel_downloader/core/downloaders/biquge.py +0 -25
  206. novel_downloader/core/downloaders/esjzone.py +0 -25
  207. novel_downloader/core/downloaders/linovelib.py +0 -25
  208. novel_downloader/core/downloaders/sfacg.py +0 -25
  209. novel_downloader/core/downloaders/yamibo.py +0 -25
  210. novel_downloader/core/exporters/biquge.py +0 -25
  211. novel_downloader/core/exporters/esjzone.py +0 -25
  212. novel_downloader/core/exporters/qianbi.py +0 -25
  213. novel_downloader/core/exporters/sfacg.py +0 -25
  214. novel_downloader/core/exporters/yamibo.py +0 -25
  215. novel_downloader/core/factory/__init__.py +0 -20
  216. novel_downloader/core/factory/downloader.py +0 -73
  217. novel_downloader/core/factory/exporter.py +0 -58
  218. novel_downloader/core/factory/fetcher.py +0 -96
  219. novel_downloader/core/factory/parser.py +0 -86
  220. novel_downloader/core/fetchers/base/__init__.py +0 -14
  221. novel_downloader/core/fetchers/base/browser.py +0 -403
  222. novel_downloader/core/fetchers/biquge/__init__.py +0 -14
  223. novel_downloader/core/fetchers/common/__init__.py +0 -14
  224. novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
  225. novel_downloader/core/fetchers/esjzone/browser.py +0 -204
  226. novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
  227. novel_downloader/core/fetchers/linovelib/browser.py +0 -193
  228. novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
  229. novel_downloader/core/fetchers/qidian/__init__.py +0 -14
  230. novel_downloader/core/fetchers/qidian/browser.py +0 -318
  231. novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
  232. novel_downloader/core/fetchers/sfacg/browser.py +0 -189
  233. novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
  234. novel_downloader/core/fetchers/yamibo/browser.py +0 -229
  235. novel_downloader/core/parsers/biquge/__init__.py +0 -10
  236. novel_downloader/core/parsers/biquge/main_parser.py +0 -134
  237. novel_downloader/core/parsers/common/__init__.py +0 -13
  238. novel_downloader/core/parsers/common/helper.py +0 -323
  239. novel_downloader/core/parsers/common/main_parser.py +0 -106
  240. novel_downloader/core/parsers/esjzone/__init__.py +0 -10
  241. novel_downloader/core/parsers/linovelib/__init__.py +0 -10
  242. novel_downloader/core/parsers/qianbi/__init__.py +0 -10
  243. novel_downloader/core/parsers/sfacg/__init__.py +0 -10
  244. novel_downloader/core/parsers/yamibo/__init__.py +0 -10
  245. novel_downloader/core/parsers/yamibo/main_parser.py +0 -194
  246. novel_downloader/models/browser.py +0 -21
  247. novel_downloader/models/chapter.py +0 -25
  248. novel_downloader/models/site_rules.py +0 -99
  249. novel_downloader/models/tasks.py +0 -33
  250. novel_downloader/models/types.py +0 -15
  251. novel_downloader/resources/css_styles/volume-intro.css +0 -56
  252. novel_downloader/resources/json/replace_word_map.json +0 -4
  253. novel_downloader/resources/text/blacklist.txt +0 -22
  254. novel_downloader/tui/__init__.py +0 -7
  255. novel_downloader/tui/app.py +0 -32
  256. novel_downloader/tui/main.py +0 -17
  257. novel_downloader/tui/screens/__init__.py +0 -14
  258. novel_downloader/tui/screens/home.py +0 -198
  259. novel_downloader/tui/screens/login.py +0 -74
  260. novel_downloader/tui/styles/home_layout.tcss +0 -79
  261. novel_downloader/tui/widgets/richlog_handler.py +0 -24
  262. novel_downloader/utils/cache.py +0 -24
  263. novel_downloader/utils/fontocr/__init__.py +0 -22
  264. novel_downloader/utils/fontocr/model_loader.py +0 -69
  265. novel_downloader/utils/fontocr/ocr_v1.py +0 -303
  266. novel_downloader/utils/fontocr/ocr_v2.py +0 -752
  267. novel_downloader/utils/hash_store.py +0 -279
  268. novel_downloader/utils/hash_utils.py +0 -103
  269. novel_downloader/utils/text_utils/chapter_formatting.py +0 -46
  270. novel_downloader/utils/text_utils/font_mapping.py +0 -28
  271. novel_downloader/utils/text_utils/text_cleaning.py +0 -107
  272. novel_downloader-1.4.5.dist-info/METADATA +0 -196
  273. novel_downloader-1.4.5.dist-info/RECORD +0 -165
  274. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/WHEEL +0 -0
  275. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/licenses/LICENSE +0 -0
  276. {novel_downloader-1.4.5.dist-info → novel_downloader-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.downloaders.signals
4
+ -----------------------------------------
5
+
6
+ Utilities for signaling task termination and reporting async progress.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Awaitable, Callable
12
+ from typing import Final, final
13
+
14
+
15
+ @final
16
+ class StopToken:
17
+ """Typed sentinel used to end queues."""
18
+
19
+ __slots__ = ()
20
+
21
+ def __repr__(self) -> str:
22
+ return "STOP"
23
+
24
+
25
+ STOP: Final[StopToken] = StopToken()
26
+
27
+ # from typing_extensions import TypeIs
28
+ # def is_stop(x: object) -> TypeIs[StopToken]:
29
+ # """Type guard so `if is_stop(item)` narrows type to StopToken."""
30
+ # return isinstance(x, StopToken)
31
+
32
+
33
+ class Progress:
34
+ """Lightweight progress reporter."""
35
+
36
+ __slots__ = ("done", "total", "hook")
37
+
38
+ def __init__(self, total: int, hook: Callable[[int, int], Awaitable[None]] | None):
39
+ self.done = 0
40
+ self.total = total
41
+ self.hook = hook
42
+
43
+ async def bump(self, n: int = 1) -> None:
44
+ self.done += n
45
+ if self.hook:
46
+ await self.hook(self.done, self.total)
@@ -3,35 +3,17 @@
3
3
  novel_downloader.core.exporters
4
4
  -------------------------------
5
5
 
6
- This module defines exporter classes for different novel platforms.
7
-
8
- Currently supported platforms:
9
- - biquge (笔趣阁)
10
- - esjzone (ESJ Zone)
11
- - linovelib (哔哩轻小说)
12
- - qianbi (铅笔小说)
13
- - qidian (起点中文网)
14
- - sfacg (SF轻小说)
15
- - yamibo (百合会)
16
- - common (通用架构)
6
+ Exporter implementations for saving books in various formats across different sources
17
7
  """
18
8
 
19
- from .biquge import BiqugeExporter
20
- from .common import CommonExporter
21
- from .esjzone import EsjzoneExporter
22
- from .linovelib import LinovelibExporter
23
- from .qianbi import QianbiExporter
24
- from .qidian import QidianExporter
25
- from .sfacg import SfacgExporter
26
- from .yamibo import YamiboExporter
27
-
28
9
  __all__ = [
29
- "BiqugeExporter",
30
- "EsjzoneExporter",
10
+ "get_exporter",
11
+ "CommonExporter",
31
12
  "LinovelibExporter",
32
- "QianbiExporter",
33
13
  "QidianExporter",
34
- "SfacgExporter",
35
- "YamiboExporter",
36
- "CommonExporter",
37
14
  ]
15
+
16
+ from .common import CommonExporter
17
+ from .linovelib import LinovelibExporter
18
+ from .qidian import QidianExporter
19
+ from .registry import get_exporter
@@ -3,20 +3,20 @@
3
3
  novel_downloader.core.exporters.base
4
4
  ------------------------------------
5
5
 
6
- This module provides an abstract base class `BaseExporter` that defines
7
- the common interface and reusable logic for saving assembled novel
8
- content into various output formats.
6
+ Abstract base class providing common structure and utilities for book exporters
9
7
  """
10
8
 
11
9
  import abc
10
+ import json
12
11
  import logging
13
12
  import types
14
13
  from datetime import datetime
15
14
  from pathlib import Path
16
- from typing import Any, Self
15
+ from typing import Any, Self, cast
17
16
 
18
17
  from novel_downloader.core.interfaces import ExporterProtocol
19
- from novel_downloader.models import ExporterConfig
18
+ from novel_downloader.models import BookInfoDict, ChapterDict, ExporterConfig
19
+ from novel_downloader.utils import ChapterStorage
20
20
 
21
21
 
22
22
  class SafeDict(dict[str, Any]):
@@ -31,6 +31,11 @@ class BaseExporter(ExporterProtocol, abc.ABC):
31
31
  such as TXT, EPUB, Markdown, or PDF.
32
32
  """
33
33
 
34
+ DEFAULT_SOURCE_ID = 0
35
+ PRIORITIES_MAP = {
36
+ DEFAULT_SOURCE_ID: 0,
37
+ }
38
+
34
39
  def __init__(
35
40
  self,
36
41
  config: ExporterConfig,
@@ -39,64 +44,66 @@ class BaseExporter(ExporterProtocol, abc.ABC):
39
44
  """
40
45
  Initialize the exporter with given configuration.
41
46
 
42
- :param config: A ExporterConfig object that defines
43
- save paths, formats, and options.
47
+ :param config: Exporter configuration settings.
48
+ :param site: Identifier for the target website or source.
44
49
  """
45
50
  self._config = config
46
51
  self._site = site
52
+ self._storage_cache: dict[str, ChapterStorage] = {}
47
53
 
48
- self._cache_dir = Path(config.cache_dir) / site
49
54
  self._raw_data_dir = Path(config.raw_data_dir) / site
50
55
  self._output_dir = Path(config.output_dir)
51
- self._cache_dir.mkdir(parents=True, exist_ok=True)
52
56
  self._output_dir.mkdir(parents=True, exist_ok=True)
53
57
 
54
58
  self.logger = logging.getLogger(f"{self.__class__.__name__}")
55
59
 
56
- def export(
57
- self,
58
- book_id: str,
59
- ) -> None:
60
+ def export(self, book_id: str) -> dict[str, Path]:
60
61
  """
61
62
  Export the book in the formats specified in config.
62
- If a method is not implemented or fails, log the error and continue.
63
63
 
64
64
  :param book_id: The book identifier (used for filename, lookup, etc.)
65
65
  """
66
66
  TAG = "[Exporter]"
67
+ results: dict[str, Path] = {}
68
+
67
69
  actions = [
68
- ("make_txt", self.export_as_txt),
69
- ("make_epub", self.export_as_epub),
70
- ("make_md", self.export_as_md),
71
- ("make_pdf", self.export_as_pdf),
70
+ ("make_txt", "txt", self.export_as_txt),
71
+ ("make_epub", "epub", self.export_as_epub),
72
+ ("make_md", "md", self.export_as_md),
73
+ ("make_pdf", "pdf", self.export_as_pdf),
72
74
  ]
73
75
 
74
- for flag_name, export_method in actions:
76
+ for flag_name, fmt_key, export_method in actions:
75
77
  if getattr(self._config, flag_name, False):
76
78
  try:
77
79
  self.logger.info(
78
80
  "%s Attempting to export book_id '%s' as %s...",
79
81
  TAG,
80
82
  book_id,
81
- flag_name,
83
+ fmt_key,
82
84
  )
83
- export_method(book_id)
84
- self.logger.info("%s Successfully saved as %s.", TAG, flag_name)
85
+ path = export_method(book_id)
86
+
87
+ if isinstance(path, Path):
88
+ results[fmt_key] = path
89
+ self.logger.info("%s Successfully saved as %s.", TAG, fmt_key)
90
+
85
91
  except NotImplementedError as e:
86
92
  self.logger.warning(
87
93
  "%s Export method for %s not implemented: %s",
88
94
  TAG,
89
- flag_name,
95
+ fmt_key,
90
96
  str(e),
91
97
  )
92
98
  except Exception as e:
93
99
  self.logger.error(
94
- "%s Error while saving as %s: %s", TAG, flag_name, str(e)
100
+ "%s Error while saving as %s: %s", TAG, fmt_key, str(e)
95
101
  )
96
- return
102
+
103
+ return results
97
104
 
98
105
  @abc.abstractmethod
99
- def export_as_txt(self, book_id: str) -> None:
106
+ def export_as_txt(self, book_id: str) -> Path | None:
100
107
  """
101
108
  Persist the assembled book as a .txt file.
102
109
 
@@ -106,7 +113,7 @@ class BaseExporter(ExporterProtocol, abc.ABC):
106
113
  """
107
114
  ...
108
115
 
109
- def export_as_epub(self, book_id: str) -> None:
116
+ def export_as_epub(self, book_id: str) -> Path | None:
110
117
  """
111
118
  Optional: Persist the assembled book as a EPUB (.epub) file.
112
119
 
@@ -115,7 +122,7 @@ class BaseExporter(ExporterProtocol, abc.ABC):
115
122
  """
116
123
  raise NotImplementedError("EPUB export not supported by this Exporter.")
117
124
 
118
- def export_as_md(self, book_id: str) -> None:
125
+ def export_as_md(self, book_id: str) -> Path | None:
119
126
  """
120
127
  Optional: Persist the assembled book as a Markdown file.
121
128
 
@@ -124,7 +131,7 @@ class BaseExporter(ExporterProtocol, abc.ABC):
124
131
  """
125
132
  raise NotImplementedError("Markdown export not supported by this Exporter.")
126
133
 
127
- def export_as_pdf(self, book_id: str) -> None:
134
+ def export_as_pdf(self, book_id: str) -> Path | None:
128
135
  """
129
136
  Optional: Persist the assembled book as a PDF file.
130
137
 
@@ -160,16 +167,84 @@ class BaseExporter(ExporterProtocol, abc.ABC):
160
167
 
161
168
  return f"{name}.{ext}"
162
169
 
170
+ @property
171
+ def site(self) -> str:
172
+ """
173
+ Get the site identifier.
174
+
175
+ :return: The site string.
176
+ """
177
+ return self._site
178
+
163
179
  @property
164
180
  def output_dir(self) -> Path:
165
- """Access the output directory for saving files."""
181
+ """
182
+ Access the output directory for saving files.
183
+ """
166
184
  return self._output_dir
167
185
 
168
186
  @property
169
187
  def filename_template(self) -> str:
170
- """Access the filename template."""
188
+ """
189
+ Access the filename template.
190
+ """
171
191
  return self._config.filename_template
172
192
 
193
+ def _get_chapter(
194
+ self,
195
+ book_id: str,
196
+ chap_id: str,
197
+ ) -> ChapterDict | None:
198
+ if book_id not in self._storage_cache:
199
+ return None
200
+ return self._storage_cache[book_id].get_best_chapter(chap_id)
201
+
202
+ def _get_chapters(
203
+ self,
204
+ book_id: str,
205
+ chap_ids: list[str],
206
+ ) -> dict[str, ChapterDict | None]:
207
+ if book_id not in self._storage_cache:
208
+ return {}
209
+ return self._storage_cache[book_id].get_best_chapters(chap_ids)
210
+
211
+ def _load_book_info(self, book_id: str) -> BookInfoDict | None:
212
+ info_path = self._raw_data_dir / book_id / "book_info.json"
213
+ if not info_path.is_file():
214
+ self.logger.error("Missing metadata file: %s", info_path)
215
+ return None
216
+
217
+ try:
218
+ text = info_path.read_text(encoding="utf-8")
219
+ data: Any = json.loads(text)
220
+ if not isinstance(data, dict):
221
+ self.logger.error(
222
+ "Invalid JSON structure in %s: expected an object at the top",
223
+ info_path,
224
+ )
225
+ return None
226
+ return cast(BookInfoDict, data)
227
+ except json.JSONDecodeError as e:
228
+ self.logger.error("Corrupt JSON in %s: %s", info_path, e)
229
+ return None
230
+
231
+ def _init_chapter_storages(self, book_id: str) -> None:
232
+ if book_id in self._storage_cache:
233
+ return
234
+ self._storage_cache[book_id] = ChapterStorage(
235
+ raw_base=self._raw_data_dir / book_id,
236
+ priorities=self.PRIORITIES_MAP,
237
+ )
238
+ self._storage_cache[book_id].connect()
239
+
240
+ def _close_chapter_storages(self) -> None:
241
+ for storage in self._storage_cache.values():
242
+ try:
243
+ storage.close()
244
+ except Exception as e:
245
+ self.logger.warning("Failed to close storage %s: %s", storage, e)
246
+ self._storage_cache.clear()
247
+
173
248
  def _on_close(self) -> None:
174
249
  """
175
250
  Hook method called at the beginning of close().
@@ -182,6 +257,7 @@ class BaseExporter(ExporterProtocol, abc.ABC):
182
257
  Shutdown and clean up the exporter.
183
258
  """
184
259
  self._on_close()
260
+ self._close_chapter_storages()
185
261
 
186
262
  def __enter__(self) -> Self:
187
263
  return self
@@ -3,10 +3,9 @@
3
3
  novel_downloader.core.exporters.common
4
4
  --------------------------------------
5
5
 
6
- This module provides the `CommonExporter` class for
7
- handling the saving process of novels.
6
+ Shared exporter implementation for producing standard TXT and EPUB outputs.
8
7
  """
9
8
 
10
- from .main_exporter import CommonExporter
11
-
12
9
  __all__ = ["CommonExporter"]
10
+
11
+ from .main_exporter import CommonExporter