novel-downloader 1.3.3__py3-none-any.whl → 1.4.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 (211) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/clean.py +97 -78
  3. novel_downloader/cli/config.py +177 -0
  4. novel_downloader/cli/download.py +132 -87
  5. novel_downloader/cli/export.py +77 -0
  6. novel_downloader/cli/main.py +21 -28
  7. novel_downloader/config/__init__.py +1 -25
  8. novel_downloader/config/adapter.py +32 -31
  9. novel_downloader/config/loader.py +3 -3
  10. novel_downloader/config/site_rules.py +1 -2
  11. novel_downloader/core/__init__.py +3 -6
  12. novel_downloader/core/downloaders/__init__.py +10 -13
  13. novel_downloader/core/downloaders/base.py +233 -0
  14. novel_downloader/core/downloaders/biquge.py +27 -0
  15. novel_downloader/core/downloaders/common.py +414 -0
  16. novel_downloader/core/downloaders/esjzone.py +27 -0
  17. novel_downloader/core/downloaders/linovelib.py +27 -0
  18. novel_downloader/core/downloaders/qianbi.py +27 -0
  19. novel_downloader/core/downloaders/qidian.py +352 -0
  20. novel_downloader/core/downloaders/sfacg.py +27 -0
  21. novel_downloader/core/downloaders/yamibo.py +27 -0
  22. novel_downloader/core/exporters/__init__.py +37 -0
  23. novel_downloader/core/{savers → exporters}/base.py +73 -39
  24. novel_downloader/core/exporters/biquge.py +25 -0
  25. novel_downloader/core/exporters/common/__init__.py +12 -0
  26. novel_downloader/core/{savers → exporters}/common/epub.py +22 -22
  27. novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +35 -40
  28. novel_downloader/core/{savers → exporters}/common/txt.py +20 -23
  29. novel_downloader/core/{savers → exporters}/epub_utils/__init__.py +8 -3
  30. novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -2
  31. novel_downloader/core/{savers → exporters}/epub_utils/image_loader.py +46 -4
  32. novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -4
  33. novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +3 -3
  34. novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -2
  35. novel_downloader/core/exporters/esjzone.py +25 -0
  36. novel_downloader/core/exporters/linovelib/__init__.py +10 -0
  37. novel_downloader/core/exporters/linovelib/epub.py +449 -0
  38. novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
  39. novel_downloader/core/exporters/linovelib/txt.py +129 -0
  40. novel_downloader/core/exporters/qianbi.py +25 -0
  41. novel_downloader/core/{savers → exporters}/qidian.py +8 -8
  42. novel_downloader/core/exporters/sfacg.py +25 -0
  43. novel_downloader/core/exporters/yamibo.py +25 -0
  44. novel_downloader/core/factory/__init__.py +5 -17
  45. novel_downloader/core/factory/downloader.py +24 -126
  46. novel_downloader/core/factory/exporter.py +58 -0
  47. novel_downloader/core/factory/fetcher.py +96 -0
  48. novel_downloader/core/factory/parser.py +17 -12
  49. novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
  50. novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
  51. novel_downloader/core/fetchers/base/browser.py +383 -0
  52. novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
  53. novel_downloader/core/fetchers/base/session.py +419 -0
  54. novel_downloader/core/fetchers/biquge/__init__.py +14 -0
  55. novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
  56. novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
  57. novel_downloader/core/fetchers/common/__init__.py +14 -0
  58. novel_downloader/core/fetchers/common/browser.py +79 -0
  59. novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
  60. novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
  61. novel_downloader/core/fetchers/esjzone/browser.py +202 -0
  62. novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
  63. novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
  64. novel_downloader/core/fetchers/linovelib/browser.py +193 -0
  65. novel_downloader/core/fetchers/linovelib/session.py +193 -0
  66. novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
  67. novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
  68. novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
  69. novel_downloader/core/fetchers/qidian/__init__.py +14 -0
  70. novel_downloader/core/fetchers/qidian/browser.py +266 -0
  71. novel_downloader/core/fetchers/qidian/session.py +326 -0
  72. novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
  73. novel_downloader/core/fetchers/sfacg/browser.py +189 -0
  74. novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
  75. novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
  76. novel_downloader/core/fetchers/yamibo/browser.py +229 -0
  77. novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
  78. novel_downloader/core/interfaces/__init__.py +8 -12
  79. novel_downloader/core/interfaces/downloader.py +54 -0
  80. novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
  81. novel_downloader/core/interfaces/fetcher.py +162 -0
  82. novel_downloader/core/interfaces/parser.py +6 -7
  83. novel_downloader/core/parsers/__init__.py +5 -6
  84. novel_downloader/core/parsers/base.py +9 -13
  85. novel_downloader/core/parsers/biquge/main_parser.py +12 -13
  86. novel_downloader/core/parsers/common/helper.py +3 -3
  87. novel_downloader/core/parsers/common/main_parser.py +39 -34
  88. novel_downloader/core/parsers/esjzone/main_parser.py +20 -14
  89. novel_downloader/core/parsers/linovelib/__init__.py +10 -0
  90. novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
  91. novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
  92. novel_downloader/core/parsers/qidian/__init__.py +2 -11
  93. novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
  94. novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
  95. novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
  96. novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
  97. novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
  98. novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
  99. novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
  100. novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
  101. novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
  102. novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
  103. novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
  104. novel_downloader/locales/en.json +18 -2
  105. novel_downloader/locales/zh.json +18 -2
  106. novel_downloader/models/__init__.py +64 -0
  107. novel_downloader/models/browser.py +21 -0
  108. novel_downloader/models/chapter.py +25 -0
  109. novel_downloader/models/config.py +100 -0
  110. novel_downloader/models/login.py +20 -0
  111. novel_downloader/models/site_rules.py +99 -0
  112. novel_downloader/models/tasks.py +33 -0
  113. novel_downloader/models/types.py +15 -0
  114. novel_downloader/resources/config/settings.toml +31 -25
  115. novel_downloader/resources/json/linovelib_font_map.json +3573 -0
  116. novel_downloader/tui/__init__.py +7 -0
  117. novel_downloader/tui/app.py +32 -0
  118. novel_downloader/tui/main.py +17 -0
  119. novel_downloader/tui/screens/__init__.py +14 -0
  120. novel_downloader/tui/screens/home.py +191 -0
  121. novel_downloader/tui/screens/login.py +74 -0
  122. novel_downloader/tui/styles/home_layout.tcss +79 -0
  123. novel_downloader/tui/widgets/richlog_handler.py +24 -0
  124. novel_downloader/utils/__init__.py +6 -0
  125. novel_downloader/utils/chapter_storage.py +25 -38
  126. novel_downloader/utils/constants.py +11 -5
  127. novel_downloader/utils/cookies.py +66 -0
  128. novel_downloader/utils/crypto_utils.py +1 -74
  129. novel_downloader/utils/fontocr/ocr_v1.py +2 -1
  130. novel_downloader/utils/fontocr/ocr_v2.py +2 -2
  131. novel_downloader/utils/hash_store.py +10 -18
  132. novel_downloader/utils/hash_utils.py +3 -2
  133. novel_downloader/utils/logger.py +2 -3
  134. novel_downloader/utils/network.py +2 -1
  135. novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
  136. novel_downloader/utils/text_utils/font_mapping.py +1 -1
  137. novel_downloader/utils/text_utils/text_cleaning.py +1 -1
  138. novel_downloader/utils/time_utils/datetime_utils.py +3 -3
  139. novel_downloader/utils/time_utils/sleep_utils.py +1 -1
  140. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/METADATA +69 -35
  141. novel_downloader-1.4.1.dist-info/RECORD +170 -0
  142. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/WHEEL +1 -1
  143. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/entry_points.txt +1 -0
  144. novel_downloader/cli/interactive.py +0 -66
  145. novel_downloader/cli/settings.py +0 -177
  146. novel_downloader/config/models.py +0 -187
  147. novel_downloader/core/downloaders/base/__init__.py +0 -14
  148. novel_downloader/core/downloaders/base/base_async.py +0 -153
  149. novel_downloader/core/downloaders/base/base_sync.py +0 -208
  150. novel_downloader/core/downloaders/biquge/__init__.py +0 -14
  151. novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
  152. novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
  153. novel_downloader/core/downloaders/common/__init__.py +0 -14
  154. novel_downloader/core/downloaders/common/common_async.py +0 -210
  155. novel_downloader/core/downloaders/common/common_sync.py +0 -202
  156. novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
  157. novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
  158. novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
  159. novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
  160. novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
  161. novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
  162. novel_downloader/core/downloaders/qidian/__init__.py +0 -10
  163. novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -219
  164. novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
  165. novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
  166. novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
  167. novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
  168. novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
  169. novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
  170. novel_downloader/core/factory/requester.py +0 -144
  171. novel_downloader/core/factory/saver.py +0 -56
  172. novel_downloader/core/interfaces/async_downloader.py +0 -36
  173. novel_downloader/core/interfaces/async_requester.py +0 -84
  174. novel_downloader/core/interfaces/sync_downloader.py +0 -36
  175. novel_downloader/core/interfaces/sync_requester.py +0 -82
  176. novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
  177. novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
  178. novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
  179. novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
  180. novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
  181. novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
  182. novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
  183. novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
  184. novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
  185. novel_downloader/core/requesters/base/async_session.py +0 -410
  186. novel_downloader/core/requesters/base/browser.py +0 -337
  187. novel_downloader/core/requesters/base/session.py +0 -378
  188. novel_downloader/core/requesters/biquge/__init__.py +0 -14
  189. novel_downloader/core/requesters/common/__init__.py +0 -17
  190. novel_downloader/core/requesters/common/session.py +0 -113
  191. novel_downloader/core/requesters/esjzone/__init__.py +0 -13
  192. novel_downloader/core/requesters/esjzone/session.py +0 -235
  193. novel_downloader/core/requesters/qianbi/__init__.py +0 -13
  194. novel_downloader/core/requesters/qidian/__init__.py +0 -21
  195. novel_downloader/core/requesters/qidian/broswer.py +0 -307
  196. novel_downloader/core/requesters/qidian/session.py +0 -290
  197. novel_downloader/core/requesters/sfacg/__init__.py +0 -13
  198. novel_downloader/core/requesters/sfacg/session.py +0 -242
  199. novel_downloader/core/requesters/yamibo/__init__.py +0 -13
  200. novel_downloader/core/requesters/yamibo/session.py +0 -237
  201. novel_downloader/core/savers/__init__.py +0 -34
  202. novel_downloader/core/savers/biquge.py +0 -25
  203. novel_downloader/core/savers/common/__init__.py +0 -12
  204. novel_downloader/core/savers/esjzone.py +0 -25
  205. novel_downloader/core/savers/qianbi.py +0 -25
  206. novel_downloader/core/savers/sfacg.py +0 -25
  207. novel_downloader/core/savers/yamibo.py +0 -25
  208. novel_downloader/resources/config/rules.toml +0 -196
  209. novel_downloader-1.3.3.dist-info/RECORD +0 -166
  210. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/licenses/LICENSE +0 -0
  211. {novel_downloader-1.3.3.dist-info → novel_downloader-1.4.1.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.common.epub
4
- ----------------------------------------
3
+ novel_downloader.core.exporters.common.epub
4
+ -------------------------------------------
5
5
 
6
6
  Contains the logic for exporting novel content as a single `.epub` file.
7
7
  """
@@ -14,7 +14,7 @@ from typing import TYPE_CHECKING
14
14
 
15
15
  from ebooklib import epub
16
16
 
17
- from novel_downloader.core.savers.epub_utils import (
17
+ from novel_downloader.core.exporters.epub_utils import (
18
18
  add_images_from_dir,
19
19
  chapter_txt_to_html,
20
20
  create_css_items,
@@ -32,11 +32,11 @@ from novel_downloader.utils.network import download_image
32
32
  from novel_downloader.utils.text_utils import clean_chapter_title
33
33
 
34
34
  if TYPE_CHECKING:
35
- from .main_saver import CommonSaver
35
+ from .main_exporter import CommonExporter
36
36
 
37
37
 
38
- def common_save_as_epub(
39
- saver: CommonSaver,
38
+ def common_export_as_epub(
39
+ exporter: CommonExporter,
40
40
  book_id: str,
41
41
  ) -> None:
42
42
  """
@@ -52,12 +52,12 @@ def common_save_as_epub(
52
52
  :param saver: The saver instance, carrying config and path info.
53
53
  :param book_id: Identifier of the novel (used as subdirectory name).
54
54
  """
55
- TAG = "[saver]"
56
- config = saver._config
55
+ TAG = "[exporter]"
56
+ config = exporter._config
57
57
  # --- Paths & options ---
58
- raw_base = saver._raw_data_dir / book_id
59
- img_dir = saver._cache_dir / book_id / "images"
60
- out_dir = saver.output_dir
58
+ raw_base = exporter._raw_data_dir / book_id
59
+ img_dir = exporter._cache_dir / book_id / "images"
60
+ out_dir = exporter.output_dir
61
61
  img_dir.mkdir(parents=True, exist_ok=True)
62
62
  out_dir.mkdir(parents=True, exist_ok=True)
63
63
 
@@ -67,11 +67,11 @@ def common_save_as_epub(
67
67
  info_text = info_path.read_text(encoding="utf-8")
68
68
  book_info = json.loads(info_text)
69
69
  except Exception as e:
70
- saver.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
70
+ exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
71
71
  return
72
72
 
73
73
  book_name = book_info.get("book_name", book_id)
74
- saver.logger.info(
74
+ exporter.logger.info(
75
75
  "%s Starting EPUB generation: %s (ID: %s)", TAG, book_name, book_id
76
76
  )
77
77
 
@@ -87,7 +87,7 @@ def common_save_as_epub(
87
87
  on_exist="overwrite",
88
88
  )
89
89
  if not cover_path:
90
- saver.logger.warning("Failed to download cover from %s", cover_url)
90
+ exporter.logger.warning("Failed to download cover from %s", cover_url)
91
91
 
92
92
  # --- Initialize EPUB ---
93
93
  book, spine, toc_list = init_epub(
@@ -108,7 +108,7 @@ def common_save_as_epub(
108
108
  for vol_index, vol in enumerate(volumes, start=1):
109
109
  raw_vol_name = vol.get("volume_name", "").strip()
110
110
  vol_name = clean_chapter_title(raw_vol_name) or f"Unknown Volume {vol_index}"
111
- saver.logger.info("Processing volume %d: %s", vol_index, vol_name)
111
+ exporter.logger.info("Processing volume %d: %s", vol_index, vol_name)
112
112
 
113
113
  # Volume intro
114
114
  vol_intro = epub.EpubHtml(
@@ -132,12 +132,12 @@ def common_save_as_epub(
132
132
  chap_id = chap.get("chapterId")
133
133
  chap_title = chap.get("title", "")
134
134
  if not chap_id:
135
- saver.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
135
+ exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
136
136
  continue
137
137
 
138
- chapter_data = saver._get_chapter(book_id, chap_id)
138
+ chapter_data = exporter._get_chapter(book_id, chap_id)
139
139
  if not chapter_data:
140
- saver.logger.info(
140
+ exporter.logger.info(
141
141
  "%s Missing chapter file: %s (%s), skipping.",
142
142
  TAG,
143
143
  chap_title,
@@ -171,13 +171,13 @@ def common_save_as_epub(
171
171
  book = add_images_from_dir(book, img_dir)
172
172
 
173
173
  # --- 5. Finalize EPUB ---
174
- saver.logger.info("%s Building TOC and spine...", TAG)
174
+ exporter.logger.info("%s Building TOC and spine...", TAG)
175
175
  book.toc = toc_list
176
176
  book.spine = spine
177
177
  book.add_item(epub.EpubNcx())
178
178
  book.add_item(epub.EpubNav())
179
179
 
180
- out_name = saver.get_filename(
180
+ out_name = exporter.get_filename(
181
181
  title=book_name,
182
182
  author=book_info.get("author"),
183
183
  ext="epub",
@@ -186,7 +186,7 @@ def common_save_as_epub(
186
186
 
187
187
  try:
188
188
  epub.write_epub(out_path, book, EPUB_OPTIONS)
189
- saver.logger.info("%s EPUB successfully written to %s", TAG, out_path)
189
+ exporter.logger.info("%s EPUB successfully written to %s", TAG, out_path)
190
190
  except Exception as e:
191
- saver.logger.error("%s Failed to write EPUB to %s: %s", TAG, out_path, e)
191
+ exporter.logger.error("%s Failed to write EPUB to %s: %s", TAG, out_path, e)
192
192
  return
@@ -1,54 +1,44 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.common.main_saver
4
- ----------------------------------------------
3
+ novel_downloader.core.exporters.common.main_exporter
4
+ ----------------------------------------------------
5
5
 
6
- This module implements the `QidianSaver` class, a concrete saver for handling
7
- novel data from Qidian (起点中文网). It defines the logic to compile, structure,
8
- and export novel content in plain text format based on the platform's metadata
9
- and chapter files.
6
+ This module implements the `CommonExporter` class, a concrete exporter for handling
7
+ novel data. It defines the logic to compile, structure, and export novel content
8
+ in plain text format based on the platform's metadata and chapter files.
10
9
  """
11
10
 
12
11
  from collections.abc import Mapping
13
12
  from typing import Any
14
13
 
15
- from novel_downloader.config.models import SaverConfig
14
+ from novel_downloader.core.exporters.base import BaseExporter
15
+ from novel_downloader.models import ExporterConfig
16
16
  from novel_downloader.utils.chapter_storage import ChapterStorage
17
17
 
18
- from ..base import BaseSaver
19
- from .txt import common_save_as_txt
18
+ from .txt import common_export_as_txt
20
19
 
21
20
 
22
- class CommonSaver(BaseSaver):
21
+ class CommonExporter(BaseExporter):
23
22
  """
24
- CommonSaver is a saver that processes and exports novels.
25
- It extends the BaseSaver interface and provides
26
- logic for exporting full novels as plain text (.txt) files.
23
+ CommonExporter is a exporter that processes and exports novels.
24
+ It extends the BaseExporter interface and provides
25
+ logic for exporting full novels as plain text (.txt) files
26
+ and EPUB (.epub) files.
27
27
  """
28
28
 
29
29
  def __init__(
30
30
  self,
31
- config: SaverConfig,
31
+ config: ExporterConfig,
32
32
  site: str,
33
33
  chap_folders: list[str] | None = None,
34
34
  ):
35
- """
36
- Initialize the common saver with site information.
37
-
38
- :param config: A SaverConfig object that defines
39
- save paths, formats, and options.
40
- :param site: Identifier for the site the saver is handling.
41
- """
42
- super().__init__(config)
43
- self._site = site
44
- self._raw_data_dir = self._base_raw_data_dir / site
45
- self._cache_dir = self._base_cache_dir / site
35
+ super().__init__(config, site)
46
36
  self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
47
37
  self._chap_folders: list[str] = chap_folders or ["chapters"]
48
38
 
49
- def save_as_txt(self, book_id: str) -> None:
39
+ def export_as_txt(self, book_id: str) -> None:
50
40
  """
51
- Compile and save a complete novel as a single .txt file.
41
+ Compile and export a complete novel as a single .txt file.
52
42
 
53
43
  Processing steps:
54
44
  1. Load book metadata from `book_info.json`, including title,
@@ -62,9 +52,9 @@ class CommonSaver(BaseSaver):
62
52
  :param book_id: The book identifier (used to locate raw data)
63
53
  """
64
54
  self._init_chapter_storages(book_id)
65
- return common_save_as_txt(self, book_id)
55
+ return common_export_as_txt(self, book_id)
66
56
 
67
- def save_as_epub(self, book_id: str) -> None:
57
+ def export_as_epub(self, book_id: str) -> None:
68
58
  """
69
59
  Persist the assembled book as a EPUB (.epub) file.
70
60
 
@@ -72,14 +62,14 @@ class CommonSaver(BaseSaver):
72
62
  :raises NotImplementedError: If the method is not overridden.
73
63
  """
74
64
  try:
75
- from .epub import common_save_as_epub
65
+ from .epub import common_export_as_epub
76
66
  except ImportError as err:
77
67
  raise NotImplementedError(
78
68
  "EPUB export not supported. Please install 'ebooklib'"
79
69
  ) from err
80
70
 
81
71
  self._init_chapter_storages(book_id)
82
- return common_save_as_epub(self, book_id)
72
+ return common_export_as_epub(self, book_id)
83
73
 
84
74
  @property
85
75
  def site(self) -> str:
@@ -90,15 +80,6 @@ class CommonSaver(BaseSaver):
90
80
  """
91
81
  return self._site
92
82
 
93
- @site.setter
94
- def site(self, value: str) -> None:
95
- """
96
- Set the site identifier.
97
-
98
- :param value: New site string to set.
99
- """
100
- self._site = value
101
-
102
83
  def _get_chapter(
103
84
  self,
104
85
  book_id: str,
@@ -111,6 +92,8 @@ class CommonSaver(BaseSaver):
111
92
  return {}
112
93
 
113
94
  def _init_chapter_storages(self, book_id: str) -> None:
95
+ if book_id in self._chapter_storage_cache:
96
+ return
114
97
  raw_base = self._raw_data_dir / book_id
115
98
  self._chapter_storage_cache[book_id] = [
116
99
  ChapterStorage(
@@ -120,3 +103,15 @@ class CommonSaver(BaseSaver):
120
103
  )
121
104
  for ns in self._chap_folders
122
105
  ]
106
+
107
+ def _on_close(self) -> None:
108
+ """
109
+ Close all ChapterStorage connections in the cache.
110
+ """
111
+ for storages in self._chapter_storage_cache.values():
112
+ for storage in storages:
113
+ try:
114
+ storage.close()
115
+ except Exception as e:
116
+ self.logger.warning("Failed to close storage %s: %s", storage, e)
117
+ self._chapter_storage_cache.clear()
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.common.txt
4
- ---------------------------------------
3
+ novel_downloader.core.exporters.common.txt
4
+ ------------------------------------------
5
5
 
6
6
  Contains the logic for exporting novel content as a single `.txt` file.
7
7
 
8
8
  This module defines `common_save_as_txt` function, which assembles and formats
9
9
  a novel based on metadata and chapter files found in the raw data directory.
10
- It is intended to be used by `CommonSaver` as part of the save/export process.
10
+ It is intended to be used by `CommonExporter` as part of the save/export process.
11
11
  """
12
12
 
13
13
  from __future__ import annotations
@@ -22,32 +22,29 @@ from novel_downloader.utils.text_utils import (
22
22
  )
23
23
 
24
24
  if TYPE_CHECKING:
25
- from .main_saver import CommonSaver
25
+ from .main_exporter import CommonExporter
26
26
 
27
27
 
28
- def common_save_as_txt(
29
- saver: CommonSaver,
28
+ def common_export_as_txt(
29
+ exporter: CommonExporter,
30
30
  book_id: str,
31
31
  ) -> None:
32
32
  """
33
33
  将 save_path 文件夹中该小说的所有章节 json 文件合并保存为一个完整的 txt 文件,
34
34
  并保存到 out_path 下
35
- 假设章节文件名格式为 `{chapterId}.json`
36
35
 
37
- 处理流程:
36
+ 处理流程:
38
37
  1. 从 book_info.json 中加载书籍信息 (包含书名、作者、简介及卷章节列表)
39
- 2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容,
40
- 同时记录最后一个章节标题作为“原文截至”
41
- 3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接,
42
- 构成最终完整文本
38
+ 2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容
39
+ 3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接
43
40
  4. 将最终结果保存到 out_path 下 (例如:`{book_name}.txt`)
44
41
 
45
42
  :param book_id: Identifier of the novel (used as subdirectory name).
46
43
  """
47
- TAG = "[saver]"
44
+ TAG = "[Exporter]"
48
45
  # --- Paths & options ---
49
- raw_base = saver._raw_data_dir / book_id
50
- out_dir = saver.output_dir
46
+ raw_base = exporter._raw_data_dir / book_id
47
+ out_dir = exporter.output_dir
51
48
  out_dir.mkdir(parents=True, exist_ok=True)
52
49
 
53
50
  # --- Load book_info.json ---
@@ -56,7 +53,7 @@ def common_save_as_txt(
56
53
  info_text = info_path.read_text(encoding="utf-8")
57
54
  book_info = json.loads(info_text)
58
55
  except Exception as e:
59
- saver.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
56
+ exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
60
57
  return
61
58
 
62
59
  # --- Compile chapters ---
@@ -70,17 +67,17 @@ def common_save_as_txt(
70
67
  if vol_name:
71
68
  volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
72
69
  parts.append(volume_header)
73
- saver.logger.info("%s Processing volume: %s", TAG, vol_name)
70
+ exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
74
71
  for chap in vol.get("chapters", []):
75
72
  chap_id = chap.get("chapterId")
76
73
  chap_title = chap.get("title", "")
77
74
  if not chap_id:
78
- saver.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
75
+ exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
79
76
  continue
80
77
 
81
- chapter_data = saver._get_chapter(book_id, chap_id)
78
+ chapter_data = exporter._get_chapter(book_id, chap_id)
82
79
  if not chapter_data:
83
- saver.logger.info(
80
+ exporter.logger.info(
84
81
  "%s Missing chapter file in: %s (%s), skipping.",
85
82
  TAG,
86
83
  chap_title,
@@ -133,13 +130,13 @@ def common_save_as_txt(
133
130
  final_text = header + "\n\n" + "\n\n".join(parts).strip()
134
131
 
135
132
  # --- Determine output file path ---
136
- out_name = saver.get_filename(title=name, author=author, ext="txt")
133
+ out_name = exporter.get_filename(title=name, author=author, ext="txt")
137
134
  out_path = out_dir / out_name
138
135
 
139
136
  # --- Save final text ---
140
137
  try:
141
138
  save_as_txt(content=final_text, filepath=out_path)
142
- saver.logger.info("%s Novel saved to: %s", TAG, out_path)
139
+ exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
143
140
  except Exception as e:
144
- saver.logger.error("%s Failed to save file: %s", TAG, e)
141
+ exporter.logger.error("%s Failed to save file: %s", TAG, e)
145
142
  return
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils
4
- ---------------------------------------
3
+ novel_downloader.core.exporters.epub_utils
4
+ ------------------------------------------
5
5
 
6
6
  This package provides utility functions for constructing EPUB files,
7
7
  including:
@@ -14,7 +14,11 @@ including:
14
14
  """
15
15
 
16
16
  from .css_builder import create_css_items
17
- from .image_loader import add_images_from_dir, add_images_from_dirs
17
+ from .image_loader import (
18
+ add_images_from_dir,
19
+ add_images_from_dirs,
20
+ add_images_from_list,
21
+ )
18
22
  from .initializer import init_epub
19
23
  from .text_to_html import (
20
24
  chapter_txt_to_html,
@@ -27,6 +31,7 @@ __all__ = [
27
31
  "create_css_items",
28
32
  "add_images_from_dir",
29
33
  "add_images_from_dirs",
34
+ "add_images_from_list",
30
35
  "init_epub",
31
36
  "chapter_txt_to_html",
32
37
  "create_volume_intro",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils.css_builder
4
- ---------------------------------------------------
3
+ novel_downloader.core.exporters.epub_utils.css_builder
4
+ ------------------------------------------------------
5
5
 
6
6
  Reads local CSS files and wraps them into epub.EpubItem objects,
7
7
  returning a list ready to be added to the EPUB.
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils.image_loader
4
- ----------------------------------------------------
3
+ novel_downloader.core.exporters.epub_utils.image_loader
4
+ -------------------------------------------------------
5
5
 
6
6
  Utilities for embedding image files into an EpubBook.
7
7
  """
8
8
 
9
9
  import logging
10
- from collections.abc import Iterable
10
+ from collections.abc import Iterable, Sequence
11
11
  from pathlib import Path
12
12
 
13
13
  from ebooklib import epub
@@ -27,6 +27,48 @@ _SUPPORTED_IMAGE_MEDIA_TYPES: dict[str, str] = {
27
27
  _DEFAULT_IMAGE_MEDIA_TYPE = "image/jpeg"
28
28
 
29
29
 
30
+ def add_images_from_list(
31
+ book: epub.EpubBook,
32
+ image_list: Sequence[str | Path],
33
+ ) -> epub.EpubBook:
34
+ """
35
+ Add a list of image files to the EPUB's image folder.
36
+
37
+ :param book: The EpubBook object to modify.
38
+ :param image_list: List of paths to image files.
39
+ :return: The same EpubBook instance, with images added.
40
+ """
41
+ for img_path in image_list:
42
+ img_path = Path(img_path)
43
+ if not img_path.is_file():
44
+ continue
45
+
46
+ suffix = img_path.suffix.lower().lstrip(".")
47
+ media_type = _SUPPORTED_IMAGE_MEDIA_TYPES.get(suffix)
48
+ if media_type is None:
49
+ media_type = _DEFAULT_IMAGE_MEDIA_TYPE
50
+ logger.warning(
51
+ "Unknown image suffix '%s' - defaulting media_type to %s",
52
+ suffix,
53
+ media_type,
54
+ )
55
+
56
+ try:
57
+ content = img_path.read_bytes()
58
+ item = epub.EpubItem(
59
+ uid=f"img_{img_path.stem}",
60
+ file_name=f"{EPUB_IMAGE_FOLDER}/{img_path.name}",
61
+ media_type=media_type,
62
+ content=content,
63
+ )
64
+ book.add_item(item)
65
+ logger.debug("Embedded image: %s", img_path.name)
66
+ except Exception:
67
+ logger.exception("Failed to embed image %s", img_path)
68
+
69
+ return book
70
+
71
+
30
72
  def add_images_from_dir(
31
73
  book: epub.EpubBook,
32
74
  image_dir: str | Path,
@@ -66,7 +108,7 @@ def add_images_from_dir(
66
108
  content=content,
67
109
  )
68
110
  book.add_item(item)
69
- logger.info("Embedded image: %s", img_path.name)
111
+ logger.debug("Embedded image: %s", img_path.name)
70
112
  except Exception:
71
113
  logger.exception("Failed to embed image %s", img_path)
72
114
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils.initializer
4
- ---------------------------------------------------
3
+ novel_downloader.core.exporters.epub_utils.initializer
4
+ ------------------------------------------------------
5
5
 
6
6
  Initializes an epub.EpubBook object, sets metadata
7
7
  (identifier, title, author, language, description),
@@ -42,10 +42,12 @@ def init_epub(
42
42
  """
43
43
  book = epub.EpubBook()
44
44
  book.set_identifier(str(book_id))
45
- book.set_title(book_info.get("book_name", "未找到书名"))
45
+ book_name = book_info.get("book_name") or book_info.get("volume_name", "未找到书名")
46
+ book.set_title(book_name)
46
47
  book.set_language("zh-CN")
47
48
  book.add_author(book_info.get("author", "未找到作者"))
48
- book.add_metadata("DC", "description", book_info.get("summary", "未找到作品简介"))
49
+ desc = book_info.get("summary") or book_info.get("volume_intro", "未找到作品简介")
50
+ book.add_metadata("DC", "description", desc)
49
51
 
50
52
  spine = []
51
53
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils.text_to_html
4
- ----------------------------------------------------
3
+ novel_downloader.core.exporters.epub_utils.text_to_html
4
+ -------------------------------------------------------
5
5
 
6
6
  Module for converting raw chapter text to formatted HTML,
7
7
  with automatic word correction and optional image/tag support.
@@ -103,7 +103,7 @@ def inline_remote_images(
103
103
  image_dir: str | Path,
104
104
  ) -> str:
105
105
  """
106
- Download every remote <img src=""> in `content` into `image_dir`,
106
+ Download every remote `<img src="...">` in `content` into `image_dir`,
107
107
  and replace the original tag with EPUB_IMAGE_WRAPPER
108
108
  pointing to the local filename.
109
109
 
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.epub_utils.volume_intro
4
- ----------------------------------------------------
3
+ novel_downloader.core.exporters.epub_utils.volume_intro
4
+ -------------------------------------------------------
5
5
 
6
6
  Responsible for generating HTML code for volume introduction pages,
7
7
  including two style variants and a unified entry point.
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.esjzone
4
+ ---------------------------------------
5
+
6
+ """
7
+
8
+ from novel_downloader.models import ExporterConfig
9
+
10
+ from .common import CommonExporter
11
+
12
+
13
+ class EsjzoneExporter(CommonExporter):
14
+ def __init__(
15
+ self,
16
+ config: ExporterConfig,
17
+ ):
18
+ super().__init__(
19
+ config,
20
+ site="esjzone",
21
+ chap_folders=["chapters"],
22
+ )
23
+
24
+
25
+ __all__ = ["EsjzoneExporter"]
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.linovelib
4
+ -----------------------------------------
5
+
6
+ """
7
+
8
+ from .main_exporter import LinovelibExporter
9
+
10
+ __all__ = ["LinovelibExporter"]