novel-downloader 1.3.2__py3-none-any.whl → 1.4.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 (213) 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 -44
  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 +40 -52
  27. novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
  28. novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
  29. novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
  30. novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
  31. novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
  32. novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
  33. novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
  34. novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
  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 +178 -0
  65. novel_downloader/core/fetchers/linovelib/session.py +178 -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 +24 -17
  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 +15 -5
  127. novel_downloader/utils/cookies.py +66 -0
  128. novel_downloader/utils/crypto_utils.py +1 -74
  129. novel_downloader/utils/file_utils/io.py +1 -1
  130. novel_downloader/utils/fontocr/ocr_v1.py +2 -1
  131. novel_downloader/utils/fontocr/ocr_v2.py +2 -2
  132. novel_downloader/utils/hash_store.py +10 -18
  133. novel_downloader/utils/hash_utils.py +3 -2
  134. novel_downloader/utils/logger.py +2 -3
  135. novel_downloader/utils/network.py +53 -39
  136. novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
  137. novel_downloader/utils/text_utils/font_mapping.py +1 -1
  138. novel_downloader/utils/text_utils/text_cleaning.py +1 -1
  139. novel_downloader/utils/time_utils/datetime_utils.py +3 -3
  140. novel_downloader/utils/time_utils/sleep_utils.py +3 -3
  141. {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
  142. novel_downloader-1.4.0.dist-info/RECORD +170 -0
  143. {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
  144. {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
  145. novel_downloader/cli/interactive.py +0 -66
  146. novel_downloader/cli/settings.py +0 -177
  147. novel_downloader/config/models.py +0 -187
  148. novel_downloader/core/downloaders/base/__init__.py +0 -14
  149. novel_downloader/core/downloaders/base/base_async.py +0 -153
  150. novel_downloader/core/downloaders/base/base_sync.py +0 -208
  151. novel_downloader/core/downloaders/biquge/__init__.py +0 -14
  152. novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
  153. novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
  154. novel_downloader/core/downloaders/common/__init__.py +0 -14
  155. novel_downloader/core/downloaders/common/common_async.py +0 -218
  156. novel_downloader/core/downloaders/common/common_sync.py +0 -210
  157. novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
  158. novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
  159. novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
  160. novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
  161. novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
  162. novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
  163. novel_downloader/core/downloaders/qidian/__init__.py +0 -10
  164. novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -227
  165. novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
  166. novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
  167. novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
  168. novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
  169. novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
  170. novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
  171. novel_downloader/core/factory/requester.py +0 -144
  172. novel_downloader/core/factory/saver.py +0 -56
  173. novel_downloader/core/interfaces/async_downloader.py +0 -36
  174. novel_downloader/core/interfaces/async_requester.py +0 -84
  175. novel_downloader/core/interfaces/sync_downloader.py +0 -36
  176. novel_downloader/core/interfaces/sync_requester.py +0 -82
  177. novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
  178. novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
  179. novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
  180. novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
  181. novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
  182. novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
  183. novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
  184. novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
  185. novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
  186. novel_downloader/core/requesters/base/async_session.py +0 -410
  187. novel_downloader/core/requesters/base/browser.py +0 -337
  188. novel_downloader/core/requesters/base/session.py +0 -378
  189. novel_downloader/core/requesters/biquge/__init__.py +0 -14
  190. novel_downloader/core/requesters/common/__init__.py +0 -17
  191. novel_downloader/core/requesters/common/session.py +0 -113
  192. novel_downloader/core/requesters/esjzone/__init__.py +0 -13
  193. novel_downloader/core/requesters/esjzone/session.py +0 -235
  194. novel_downloader/core/requesters/qianbi/__init__.py +0 -13
  195. novel_downloader/core/requesters/qidian/__init__.py +0 -21
  196. novel_downloader/core/requesters/qidian/broswer.py +0 -307
  197. novel_downloader/core/requesters/qidian/session.py +0 -290
  198. novel_downloader/core/requesters/sfacg/__init__.py +0 -13
  199. novel_downloader/core/requesters/sfacg/session.py +0 -242
  200. novel_downloader/core/requesters/yamibo/__init__.py +0 -13
  201. novel_downloader/core/requesters/yamibo/session.py +0 -237
  202. novel_downloader/core/savers/__init__.py +0 -34
  203. novel_downloader/core/savers/biquge.py +0 -25
  204. novel_downloader/core/savers/common/__init__.py +0 -12
  205. novel_downloader/core/savers/epub_utils/__init__.py +0 -26
  206. novel_downloader/core/savers/esjzone.py +0 -25
  207. novel_downloader/core/savers/qianbi.py +0 -25
  208. novel_downloader/core/savers/sfacg.py +0 -25
  209. novel_downloader/core/savers/yamibo.py +0 -25
  210. novel_downloader/resources/config/rules.toml +0 -196
  211. novel_downloader-1.3.2.dist-info/RECORD +0 -165
  212. {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
  213. {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.linovelib.txt
4
+ ---------------------------------------------
5
+
6
+ Contains the logic for exporting novel content as a single `.txt` file.
7
+
8
+ This module defines `linovelib_export_as_txt` function, which assembles and formats
9
+ a novel based on metadata and chapter files found in the raw data directory.
10
+ It is intended to be used by `LinovelibExporter` as part of the save/export process.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import TYPE_CHECKING
17
+
18
+ from novel_downloader.utils.file_utils import save_as_txt
19
+ from novel_downloader.utils.text_utils import (
20
+ format_chapter,
21
+ )
22
+
23
+ if TYPE_CHECKING:
24
+ from .main_exporter import LinovelibExporter
25
+
26
+
27
+ def linovelib_export_as_txt(
28
+ exporter: LinovelibExporter,
29
+ book_id: str,
30
+ ) -> None:
31
+ """
32
+ 将 save_path 文件夹中该小说的所有章节 json 文件合并保存为一个完整的 txt 文件,
33
+ 并保存到 out_path 下
34
+
35
+ 处理流程:
36
+ 1. 从 book_info.json 中加载书籍信息 (包含书名、作者、简介及卷章节列表)
37
+ 2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容
38
+ 3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接
39
+ 4. 将最终结果保存到 out_path 下 (例如:`{book_name}.txt`)
40
+
41
+ :param book_id: Identifier of the novel (used as subdirectory name).
42
+ """
43
+ TAG = "[exporter]"
44
+ # --- Paths & options ---
45
+ raw_base = exporter._raw_data_dir / book_id
46
+ out_dir = exporter.output_dir
47
+ out_dir.mkdir(parents=True, exist_ok=True)
48
+
49
+ # --- Load book_info.json ---
50
+ info_path = raw_base / "book_info.json"
51
+ try:
52
+ info_text = info_path.read_text(encoding="utf-8")
53
+ book_info = json.loads(info_text)
54
+ except Exception as e:
55
+ exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
56
+ return
57
+
58
+ # --- Compile chapters ---
59
+ parts: list[str] = []
60
+ volumes = book_info.get("volumes", [])
61
+
62
+ for vol in volumes:
63
+ vol_name = vol.get("volume_name", "").strip()
64
+ vol_intro = vol.get("volume_intro", "").strip()
65
+ if vol_name:
66
+ volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
67
+ parts.append(volume_header)
68
+ exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
69
+ if vol_intro:
70
+ parts.append(f"{vol_intro}\n\n")
71
+ for chap in vol.get("chapters", []):
72
+ chap_id = chap.get("chapterId")
73
+ chap_title = chap.get("title", "")
74
+ if not chap_id:
75
+ exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
76
+ continue
77
+
78
+ chapter_data = exporter._get_chapter(book_id, chap_id)
79
+ if not chapter_data:
80
+ exporter.logger.info(
81
+ "%s Missing chapter file in: %s (%s), skipping.",
82
+ TAG,
83
+ chap_title,
84
+ chap_id,
85
+ )
86
+ continue
87
+
88
+ # Extract structured fields
89
+ title = chapter_data.get("title", chap_title).strip()
90
+ content = chapter_data.get("content", "").strip()
91
+
92
+ parts.append(format_chapter(title, content, ""))
93
+
94
+ # --- Build header ---
95
+ name = book_info.get("book_name")
96
+ author = book_info.get("author")
97
+ words = book_info.get("word_count")
98
+ updated = book_info.get("update_time")
99
+ summary = book_info.get("summary")
100
+
101
+ fields = [
102
+ ("书名", name),
103
+ ("作者", author),
104
+ ("总字数", words),
105
+ ("更新日期", updated),
106
+ ]
107
+ header_lines = [f"{label}: {value}" for label, value in fields if value]
108
+
109
+ if summary:
110
+ header_lines.append("内容简介:")
111
+ header_lines.append(summary)
112
+
113
+ header_lines += ["", "-" * 10, ""]
114
+
115
+ header = "\n".join(header_lines)
116
+
117
+ final_text = header + "\n\n" + "\n\n".join(parts).strip()
118
+
119
+ # --- Determine output file path ---
120
+ out_name = exporter.get_filename(title=name, author=author, ext="txt")
121
+ out_path = out_dir / out_name
122
+
123
+ # --- Save final text ---
124
+ try:
125
+ save_as_txt(content=final_text, filepath=out_path)
126
+ exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
127
+ except Exception as e:
128
+ exporter.logger.error("%s Failed to save file: %s", TAG, e)
129
+ return
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.qianbi
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ from novel_downloader.models import ExporterConfig
9
+
10
+ from .common import CommonExporter
11
+
12
+
13
+ class QianbiExporter(CommonExporter):
14
+ def __init__(
15
+ self,
16
+ config: ExporterConfig,
17
+ ):
18
+ super().__init__(
19
+ config,
20
+ site="qianbi",
21
+ chap_folders=["chapters"],
22
+ )
23
+
24
+
25
+ __all__ = ["QianbiExporter"]
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.savers.qidian
4
- -----------------------------------
3
+ novel_downloader.core.exporters.qidian
4
+ --------------------------------------
5
5
 
6
- This module provides the `QidianSaver` class for handling the saving process
6
+ This module provides the `QidianExporter` class for handling the saving process
7
7
  of novels sourced from Qidian (起点中文网). It implements the platform-specific
8
8
  logic required to structure and export novel content into desired formats.
9
9
  """
10
10
 
11
- from novel_downloader.config.models import SaverConfig
11
+ from novel_downloader.models import ExporterConfig
12
12
 
13
- from .common import CommonSaver
13
+ from .common import CommonExporter
14
14
 
15
15
 
16
- class QidianSaver(CommonSaver):
16
+ class QidianExporter(CommonExporter):
17
17
  def __init__(
18
18
  self,
19
- config: SaverConfig,
19
+ config: ExporterConfig,
20
20
  ):
21
21
  super().__init__(
22
22
  config,
@@ -25,4 +25,4 @@ class QidianSaver(CommonSaver):
25
25
  )
26
26
 
27
27
 
28
- __all__ = ["QidianSaver"]
28
+ __all__ = ["QidianExporter"]
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.sfacg
4
+ -------------------------------------
5
+
6
+ """
7
+
8
+ from novel_downloader.models import ExporterConfig
9
+
10
+ from .common import CommonExporter
11
+
12
+
13
+ class SfacgExporter(CommonExporter):
14
+ def __init__(
15
+ self,
16
+ config: ExporterConfig,
17
+ ):
18
+ super().__init__(
19
+ config,
20
+ site="sfacg",
21
+ chap_folders=["chapters"],
22
+ )
23
+
24
+
25
+ __all__ = ["SfacgExporter"]
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.exporters.yamibo
4
+ --------------------------------------
5
+
6
+ """
7
+
8
+ from novel_downloader.models import ExporterConfig
9
+
10
+ from .common import CommonExporter
11
+
12
+
13
+ class YamiboExporter(CommonExporter):
14
+ def __init__(
15
+ self,
16
+ config: ExporterConfig,
17
+ ):
18
+ super().__init__(
19
+ config,
20
+ site="yamibo",
21
+ chap_folders=["chapters"],
22
+ )
23
+
24
+
25
+ __all__ = ["YamiboExporter"]
@@ -7,26 +7,14 @@ This package provides factory methods for dynamically retrieving components
7
7
  based on runtime parameters such as site name or content type.
8
8
  """
9
9
 
10
- from .downloader import (
11
- get_async_downloader,
12
- get_downloader,
13
- get_sync_downloader,
14
- )
10
+ from .downloader import get_downloader
11
+ from .exporter import get_exporter
12
+ from .fetcher import get_fetcher
15
13
  from .parser import get_parser
16
- from .requester import (
17
- get_async_requester,
18
- get_requester,
19
- get_sync_requester,
20
- )
21
- from .saver import get_saver
22
14
 
23
15
  __all__ = [
24
- "get_async_downloader",
25
16
  "get_downloader",
26
- "get_sync_downloader",
17
+ "get_exporter",
18
+ "get_fetcher",
27
19
  "get_parser",
28
- "get_async_requester",
29
- "get_requester",
30
- "get_sync_requester",
31
- "get_saver",
32
20
  ]
@@ -1,60 +1,42 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- novel_downloader.core.factory.downloader_factory
4
- ------------------------------------------------
3
+ novel_downloader.core.factory.downloader
4
+ ----------------------------------------
5
5
 
6
6
  This module implements a factory function for creating downloader instances
7
7
  based on the site name and parser mode specified in the configuration.
8
8
  """
9
9
 
10
10
  from collections.abc import Callable
11
- from typing import cast
12
11
 
13
- from novel_downloader.config import DownloaderConfig, load_site_rules
12
+ from novel_downloader.config import load_site_rules
14
13
  from novel_downloader.core.downloaders import (
15
- BiqugeAsyncDownloader,
16
14
  BiqugeDownloader,
17
- CommonAsyncDownloader,
18
15
  CommonDownloader,
19
- EsjzoneAsyncDownloader,
20
16
  EsjzoneDownloader,
21
- QianbiAsyncDownloader,
17
+ LinovelibDownloader,
22
18
  QianbiDownloader,
23
19
  QidianDownloader,
24
- SfacgAsyncDownloader,
25
20
  SfacgDownloader,
26
- YamiboAsyncDownloader,
27
21
  YamiboDownloader,
28
22
  )
29
23
  from novel_downloader.core.interfaces import (
30
- AsyncDownloaderProtocol,
31
- AsyncRequesterProtocol,
24
+ DownloaderProtocol,
25
+ ExporterProtocol,
26
+ FetcherProtocol,
32
27
  ParserProtocol,
33
- SaverProtocol,
34
- SyncDownloaderProtocol,
35
- SyncRequesterProtocol,
36
28
  )
29
+ from novel_downloader.models import DownloaderConfig
37
30
 
38
- AsyncDownloaderBuilder = Callable[
39
- [AsyncRequesterProtocol, ParserProtocol, SaverProtocol, DownloaderConfig],
40
- AsyncDownloaderProtocol,
31
+ DownloaderBuilder = Callable[
32
+ [FetcherProtocol, ParserProtocol, ExporterProtocol, DownloaderConfig],
33
+ DownloaderProtocol,
41
34
  ]
42
35
 
43
- SyncDownloaderBuilder = Callable[
44
- [SyncRequesterProtocol, ParserProtocol, SaverProtocol, DownloaderConfig],
45
- SyncDownloaderProtocol,
46
- ]
47
-
48
- _async_site_map: dict[str, AsyncDownloaderBuilder] = {
49
- "biquge": BiqugeAsyncDownloader,
50
- "esjzone": EsjzoneAsyncDownloader,
51
- "qianbi": QianbiAsyncDownloader,
52
- "sfacg": SfacgAsyncDownloader,
53
- "yamibo": YamiboAsyncDownloader,
54
- }
55
- _sync_site_map: dict[str, SyncDownloaderBuilder] = {
36
+ _site_map: dict[str, DownloaderBuilder] = {
56
37
  "biquge": BiqugeDownloader,
57
38
  "esjzone": EsjzoneDownloader,
39
+ "linovelib": LinovelibDownloader,
58
40
  "qianbi": QianbiDownloader,
59
41
  "qidian": QidianDownloader,
60
42
  "sfacg": SfacgDownloader,
@@ -62,117 +44,33 @@ _sync_site_map: dict[str, SyncDownloaderBuilder] = {
62
44
  }
63
45
 
64
46
 
65
- def get_async_downloader(
66
- requester: AsyncRequesterProtocol,
67
- parser: ParserProtocol,
68
- saver: SaverProtocol,
69
- site: str,
70
- config: DownloaderConfig,
71
- ) -> AsyncDownloaderProtocol:
72
- """
73
- Returns an AsyncDownloaderProtocol for the given site.
74
-
75
- :param requester: Requester implementation
76
- :param parser: Parser implementation
77
- :param saver: Saver implementation
78
- :param site: Site name (e.g., 'qidian')
79
- :param config: Downloader configuration
80
-
81
- :return: An instance of a downloader class
82
-
83
- :raises ValueError: If a site-specific downloader does not support async mode.
84
- :raises TypeError: If the provided requester does not match the required protocol.
85
- """
86
- site_key = site.lower()
87
-
88
- if not isinstance(requester, AsyncRequesterProtocol):
89
- raise TypeError("Async mode requires an AsyncRequesterProtocol")
90
-
91
- # site-specific
92
- if site_key in _async_site_map:
93
- return _async_site_map[site_key](requester, parser, saver, config)
94
-
95
- # fallback
96
- site_rules = load_site_rules()
97
- site_rule = site_rules.get(site_key)
98
- if site_rule is None:
99
- raise ValueError(f"Unsupported site: {site}")
100
-
101
- return CommonAsyncDownloader(requester, parser, saver, config, site_key)
102
-
103
-
104
- def get_sync_downloader(
105
- requester: SyncRequesterProtocol,
47
+ def get_downloader(
48
+ fetcher: FetcherProtocol,
106
49
  parser: ParserProtocol,
107
- saver: SaverProtocol,
50
+ exporter: ExporterProtocol,
108
51
  site: str,
109
52
  config: DownloaderConfig,
110
- ) -> SyncDownloaderProtocol:
53
+ ) -> DownloaderProtocol:
111
54
  """
112
- Returns a DownloaderProtocol for the given site.
113
- First tries a site-specific downloader (e.g. QidianDownloader),
114
- otherwise falls back to CommonDownloader.
55
+ Returns an DownloaderProtocol for the given site.
115
56
 
116
- :param requester: Requester implementation
57
+ :param fetcher: Fetcher implementation
117
58
  :param parser: Parser implementation
118
- :param saver: Saver implementation
59
+ :param exporter: Exporter implementation
119
60
  :param site: Site name (e.g., 'qidian')
120
61
  :param config: Downloader configuration
121
62
 
122
63
  :return: An instance of a downloader class
123
-
124
- :raises ValueError: If a site-specific downloader does not support async mode.
125
- :raises TypeError: If the provided requester does not match the required protocol.
126
64
  """
127
65
  site_key = site.lower()
128
66
 
129
- if not isinstance(requester, SyncRequesterProtocol):
130
- raise TypeError("Sync mode requires a RequesterProtocol")
131
-
132
67
  # site-specific
133
- if site_key in _sync_site_map:
134
- return _sync_site_map[site_key](requester, parser, saver, config)
68
+ if site_key in _site_map:
69
+ return _site_map[site_key](fetcher, parser, exporter, config)
135
70
 
136
71
  # fallback
137
72
  site_rules = load_site_rules()
138
- site_rule = site_rules.get(site_key)
139
- if site_rule is None:
73
+ if site_key not in site_rules:
140
74
  raise ValueError(f"Unsupported site: {site}")
141
75
 
142
- return CommonDownloader(requester, parser, saver, config, site_key)
143
-
144
-
145
- def get_downloader(
146
- requester: AsyncRequesterProtocol | SyncRequesterProtocol,
147
- parser: ParserProtocol,
148
- saver: SaverProtocol,
149
- site: str,
150
- config: DownloaderConfig,
151
- ) -> AsyncDownloaderProtocol | SyncDownloaderProtocol:
152
- """
153
- Dispatches to get_async_downloader if config.mode == 'async',
154
- otherwise to get_sync_downloader.
155
-
156
- :param requester: Requester implementation
157
- :param parser: Parser implementation
158
- :param saver: Saver implementation
159
- :param site: Site name (e.g., 'qidian')
160
- :param config: Downloader configuration
161
-
162
- :return: An instance of a downloader class
163
-
164
- :raises ValueError: If a site-specific downloader does not support async mode.
165
- :raises TypeError: If the provided requester does not match the required protocol.
166
- """
167
- if requester.is_async():
168
- if config.mode.lower() != "async":
169
- raise TypeError("Requester is async, but config.mode is not 'async'")
170
- async_requester = cast(AsyncRequesterProtocol, requester)
171
- return get_async_downloader(async_requester, parser, saver, site, config)
172
- else:
173
- if config.mode.lower() not in ("browser", "session"):
174
- raise TypeError(
175
- "Requester is sync, but config.mode is not 'browser' or 'session'"
176
- )
177
- sync_requester = cast(SyncRequesterProtocol, requester)
178
- return get_sync_downloader(sync_requester, parser, saver, site, config)
76
+ return CommonDownloader(fetcher, parser, exporter, config, site_key)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.factory.exporter
4
+ --------------------------------------
5
+
6
+ This module implements a factory function for creating exporter instances
7
+ based on the site name.
8
+ """
9
+
10
+ from collections.abc import Callable
11
+
12
+ from novel_downloader.config import load_site_rules
13
+ from novel_downloader.core.exporters import (
14
+ BiqugeExporter,
15
+ CommonExporter,
16
+ EsjzoneExporter,
17
+ LinovelibExporter,
18
+ QianbiExporter,
19
+ QidianExporter,
20
+ SfacgExporter,
21
+ YamiboExporter,
22
+ )
23
+ from novel_downloader.core.interfaces import ExporterProtocol
24
+ from novel_downloader.models import ExporterConfig
25
+
26
+ ExporterBuilder = Callable[[ExporterConfig], ExporterProtocol]
27
+
28
+ _site_map: dict[str, ExporterBuilder] = {
29
+ "biquge": BiqugeExporter,
30
+ "esjzone": EsjzoneExporter,
31
+ "linovelib": LinovelibExporter,
32
+ "qianbi": QianbiExporter,
33
+ "qidian": QidianExporter,
34
+ "sfacg": SfacgExporter,
35
+ "yamibo": YamiboExporter,
36
+ }
37
+
38
+
39
+ def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
40
+ """
41
+ Returns a site-specific exporter instance.
42
+
43
+ :param site: Site name (e.g., 'qidian')
44
+ :param config: Configuration for the exporter
45
+ :return: An instance of a exporter class
46
+ """
47
+ site_key = site.lower()
48
+
49
+ # site-specific
50
+ if site_key in _site_map:
51
+ return _site_map[site_key](config)
52
+
53
+ # Fallback
54
+ site_rules = load_site_rules()
55
+ if site_key not in site_rules:
56
+ raise ValueError(f"Unsupported site: {site}")
57
+
58
+ return CommonExporter(config, site_key)
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.factory.fetcher
4
+ -------------------------------------
5
+
6
+ This module implements a factory function for retrieving fetcher instances
7
+ based on the target novel platform (site).
8
+ """
9
+
10
+ from collections.abc import Callable
11
+
12
+ from novel_downloader.config import load_site_rules
13
+ from novel_downloader.core.fetchers import (
14
+ BiqugeBrowser,
15
+ BiqugeSession,
16
+ CommonBrowser,
17
+ CommonSession,
18
+ EsjzoneBrowser,
19
+ EsjzoneSession,
20
+ LinovelibBrowser,
21
+ LinovelibSession,
22
+ QianbiBrowser,
23
+ QianbiSession,
24
+ QidianBrowser,
25
+ QidianSession,
26
+ SfacgBrowser,
27
+ SfacgSession,
28
+ YamiboBrowser,
29
+ YamiboSession,
30
+ )
31
+ from novel_downloader.core.interfaces import FetcherProtocol
32
+ from novel_downloader.models import FetcherConfig
33
+
34
+ FetcherBuilder = Callable[[FetcherConfig], FetcherProtocol]
35
+
36
+ _site_map: dict[str, dict[str, FetcherBuilder]] = {
37
+ "biquge": {
38
+ "browser": BiqugeBrowser,
39
+ "session": BiqugeSession,
40
+ },
41
+ "esjzone": {
42
+ "browser": EsjzoneBrowser,
43
+ "session": EsjzoneSession,
44
+ },
45
+ "linovelib": {
46
+ "browser": LinovelibBrowser,
47
+ "session": LinovelibSession,
48
+ },
49
+ "qianbi": {
50
+ "browser": QianbiBrowser,
51
+ "session": QianbiSession,
52
+ },
53
+ "qidian": {
54
+ "browser": QidianBrowser,
55
+ "session": QidianSession,
56
+ },
57
+ "sfacg": {
58
+ "browser": SfacgBrowser,
59
+ "session": SfacgSession,
60
+ },
61
+ "yamibo": {
62
+ "browser": YamiboBrowser,
63
+ "session": YamiboSession,
64
+ },
65
+ }
66
+
67
+
68
+ def get_fetcher(
69
+ site: str,
70
+ config: FetcherConfig,
71
+ ) -> FetcherProtocol:
72
+ """
73
+ Returns an FetcherProtocol for the given site.
74
+
75
+ :param site: Site name (e.g., 'qidian')
76
+ :param config: Configuration for the requester
77
+ :return: An instance of a requester class
78
+ """
79
+ site_key = site.lower()
80
+ mode = config.mode
81
+
82
+ # site-specific
83
+ fetcher_cls = _site_map.get(site_key, {}).get(mode)
84
+ if fetcher_cls is not None:
85
+ return fetcher_cls(config)
86
+
87
+ # fallback: use Common based on mode
88
+ site_rules = load_site_rules()
89
+ site_rule = site_rules.get(site_key)
90
+ if site_rule is None:
91
+ raise ValueError(f"Unsupported site: {site}")
92
+ profile = site_rule["profile"]
93
+
94
+ if mode == "browser":
95
+ return CommonBrowser(site_key, profile, config)
96
+ return CommonSession(site_key, profile, config)