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,202 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.common.common_sync
4
- ----------------------------------------------------
5
-
6
- This module defines `CommonDownloader`.
7
- """
8
-
9
- import json
10
- import logging
11
- from typing import Any
12
-
13
- from novel_downloader.config import DownloaderConfig
14
- from novel_downloader.core.downloaders.base import BaseDownloader
15
- from novel_downloader.core.interfaces import (
16
- ParserProtocol,
17
- SaverProtocol,
18
- SyncRequesterProtocol,
19
- )
20
- from novel_downloader.utils.chapter_storage import ChapterStorage
21
- from novel_downloader.utils.file_utils import save_as_json, save_as_txt
22
- from novel_downloader.utils.time_utils import (
23
- calculate_time_difference,
24
- sleep_with_random_delay,
25
- )
26
-
27
- logger = logging.getLogger(__name__)
28
-
29
-
30
- class CommonDownloader(BaseDownloader):
31
- """
32
- Specialized downloader for common novels.
33
- """
34
-
35
- def __init__(
36
- self,
37
- requester: SyncRequesterProtocol,
38
- parser: ParserProtocol,
39
- saver: SaverProtocol,
40
- config: DownloaderConfig,
41
- site: str,
42
- ):
43
- """
44
- Initialize the common novel downloader with site information.
45
-
46
- :param requester: Object implementing RequesterProtocol, used to fetch raw data.
47
- :param parser: Object implementing ParserProtocol, used to parse page content.
48
- :param saver: Object implementing SaverProtocol, used to save final output.
49
- :param config: Downloader configuration object.
50
- :param site: Identifier for the site the downloader is targeting.
51
- """
52
- super().__init__(requester, parser, saver, config, site)
53
- self._site = site
54
- self._is_logged_in = False
55
-
56
- def prepare(self) -> None:
57
- """
58
- Perform login
59
- """
60
- if self.login_required and not self._is_logged_in:
61
- success = self.requester.login()
62
- if not success:
63
- raise RuntimeError("Login failed")
64
- self._is_logged_in = True
65
-
66
- def download_one(self, book_id: str) -> None:
67
- """
68
- The full download logic for a single book.
69
-
70
- :param book_id: The identifier of the book to download.
71
- """
72
- self.prepare()
73
-
74
- TAG = "[Downloader]"
75
- save_html = self.config.save_html
76
- skip_existing = self.config.skip_existing
77
- wait_time = self.config.request_interval
78
-
79
- raw_base = self.raw_data_dir / book_id
80
- cache_base = self.cache_dir / book_id
81
- info_path = raw_base / "book_info.json"
82
- chapters_html_dir = cache_base / "html"
83
-
84
- raw_base.mkdir(parents=True, exist_ok=True)
85
- if self.save_html:
86
- chapters_html_dir.mkdir(parents=True, exist_ok=True)
87
- normal_cs = ChapterStorage(
88
- raw_base=raw_base,
89
- namespace="chapters",
90
- backend_type=self._config.storage_backend,
91
- batch_size=self._config.storage_batch_size,
92
- )
93
-
94
- book_info: dict[str, Any]
95
-
96
- try:
97
- if not info_path.exists():
98
- raise FileNotFoundError
99
- book_info = json.loads(info_path.read_text(encoding="utf-8"))
100
- days, hrs, mins, secs = calculate_time_difference(
101
- book_info.get("update_time", ""), "UTC+8"
102
- )
103
- logger.info(
104
- "%s Last updated %dd %dh %dm %ds ago", TAG, days, hrs, mins, secs
105
- )
106
- if days > 1:
107
- raise FileNotFoundError # trigger re-fetch
108
- except Exception:
109
- info_html = self.requester.get_book_info(book_id)
110
- if save_html:
111
- for i, html in enumerate(info_html):
112
- save_as_txt(html, chapters_html_dir / f"info_{i}.html")
113
- book_info = self.parser.parse_book_info(info_html)
114
- if (
115
- book_info.get("book_name", "") != "未找到书名"
116
- and book_info.get("update_time", "") != "未找到更新时间"
117
- ):
118
- save_as_json(book_info, info_path)
119
- sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
120
-
121
- # enqueue chapters
122
- for vol in book_info.get("volumes", []):
123
- vol_name = vol.get("volume_name", "")
124
- logger.info("%s Enqueuing volume: %s", TAG, vol_name)
125
-
126
- for chap in vol.get("chapters", []):
127
- cid = chap.get("chapterId")
128
- if not cid:
129
- logger.warning("%s Skipping chapter without chapterId", TAG)
130
- continue
131
-
132
- if normal_cs.exists(cid) and skip_existing:
133
- logger.debug(
134
- "%s Chapter already exists, skipping: %s",
135
- TAG,
136
- cid,
137
- )
138
- continue
139
-
140
- chap_title = chap.get("title", "")
141
- logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
142
- try:
143
- chap_html = self.requester.get_book_chapter(book_id, cid)
144
-
145
- if save_html:
146
- for i, html in enumerate(chap_html):
147
- html_path = chapters_html_dir / f"{cid}_{i}.html"
148
- save_as_txt(html, html_path, on_exist="skip")
149
-
150
- chap_json = self.parser.parse_chapter(chap_html, cid)
151
-
152
- sleep_with_random_delay(
153
- wait_time, mul_spread=1.1, max_sleep=wait_time + 2
154
- )
155
- if not chap_json:
156
- logger.warning(
157
- "%s Parsed chapter json is empty, skipping: %s (%s)",
158
- TAG,
159
- chap_title,
160
- cid,
161
- )
162
- continue
163
- except Exception as e:
164
- logger.warning(
165
- "%s Error while processing chapter %s (%s): %s",
166
- TAG,
167
- chap_title,
168
- cid,
169
- str(e),
170
- )
171
- continue
172
-
173
- normal_cs.save(chap_json)
174
- logger.info("%s Saved chapter: %s (%s)", TAG, chap_title, cid)
175
-
176
- normal_cs.close()
177
- self.saver.save(book_id)
178
-
179
- logger.info(
180
- "%s Novel '%s' download completed.",
181
- TAG,
182
- book_info.get("book_name", "unknown"),
183
- )
184
- return
185
-
186
- @property
187
- def site(self) -> str:
188
- """
189
- Get the site identifier.
190
-
191
- :return: The site string.
192
- """
193
- return self._site
194
-
195
- @site.setter
196
- def site(self, value: str) -> None:
197
- """
198
- Set the site identifier.
199
-
200
- :param value: New site string to set.
201
- """
202
- self._site = value
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.esjzone
4
- -----------------------------------------
5
-
6
- """
7
-
8
- from .esjzone_async import EsjzoneAsyncDownloader
9
- from .esjzone_sync import EsjzoneDownloader
10
-
11
- __all__ = [
12
- "EsjzoneAsyncDownloader",
13
- "EsjzoneDownloader",
14
- ]
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.esjzone.esjzone_async
4
- -------------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonAsyncDownloader
10
- from novel_downloader.core.interfaces import (
11
- AsyncRequesterProtocol,
12
- ParserProtocol,
13
- SaverProtocol,
14
- )
15
-
16
-
17
- class EsjzoneAsyncDownloader(CommonAsyncDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: AsyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "esjzone")
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.esjzone.esjzone_sync
4
- ------------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonDownloader
10
- from novel_downloader.core.interfaces import (
11
- ParserProtocol,
12
- SaverProtocol,
13
- SyncRequesterProtocol,
14
- )
15
-
16
-
17
- class EsjzoneDownloader(CommonDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: SyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "esjzone")
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.qianbi
4
- ----------------------------------------
5
-
6
- """
7
-
8
- from .qianbi_async import QianbiAsyncDownloader
9
- from .qianbi_sync import QianbiDownloader
10
-
11
- __all__ = [
12
- "QianbiAsyncDownloader",
13
- "QianbiDownloader",
14
- ]
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.qianbi.qianbi_async
4
- -----------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonAsyncDownloader
10
- from novel_downloader.core.interfaces import (
11
- AsyncRequesterProtocol,
12
- ParserProtocol,
13
- SaverProtocol,
14
- )
15
-
16
-
17
- class QianbiAsyncDownloader(CommonAsyncDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: AsyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "qianbi")
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.qianbi.qianbi_sync
4
- ----------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonDownloader
10
- from novel_downloader.core.interfaces import (
11
- ParserProtocol,
12
- SaverProtocol,
13
- SyncRequesterProtocol,
14
- )
15
-
16
-
17
- class QianbiDownloader(CommonDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: SyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "qianbi")
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.qidian
4
- ----------------------------------------
5
-
6
- """
7
-
8
- from .qidian_sync import QidianDownloader
9
-
10
- __all__ = ["QidianDownloader"]
@@ -1,219 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.qidian.qidian_sync
4
- ----------------------------------------------------
5
-
6
- This module defines `QidianDownloader`, a platform-specific downloader
7
- implementation for retrieving novels from Qidian (起点中文网).
8
- """
9
-
10
- import json
11
- from typing import Any
12
-
13
- from novel_downloader.config import DownloaderConfig
14
- from novel_downloader.core.downloaders.base import BaseDownloader
15
- from novel_downloader.core.interfaces import (
16
- ParserProtocol,
17
- SaverProtocol,
18
- SyncRequesterProtocol,
19
- )
20
- from novel_downloader.utils.chapter_storage import ChapterStorage
21
- from novel_downloader.utils.file_utils import save_as_json, save_as_txt
22
- from novel_downloader.utils.state import state_mgr
23
- from novel_downloader.utils.time_utils import (
24
- calculate_time_difference,
25
- sleep_with_random_delay,
26
- )
27
-
28
-
29
- class QidianDownloader(BaseDownloader):
30
- """
31
- Specialized downloader for Qidian novels.
32
- """
33
-
34
- def __init__(
35
- self,
36
- requester: SyncRequesterProtocol,
37
- parser: ParserProtocol,
38
- saver: SaverProtocol,
39
- config: DownloaderConfig,
40
- ):
41
- super().__init__(requester, parser, saver, config, "qidian")
42
-
43
- self._site_key = "qidian"
44
- self._is_logged_in = self._handle_login()
45
- state_mgr.set_manual_login_flag(self._site_key, not self._is_logged_in)
46
-
47
- def download_one(self, book_id: str) -> None:
48
- """
49
- The full download logic for a single book.
50
-
51
- :param book_id: The identifier of the book to download.
52
- """
53
- if not self._is_logged_in:
54
- self.logger.warning(
55
- "[%s] login failed, skipping download of %s",
56
- self._site_key,
57
- book_id,
58
- )
59
- return
60
-
61
- TAG = "[Downloader]"
62
- save_html = self.config.save_html
63
- skip_existing = self.config.skip_existing
64
- wait_time = self.config.request_interval
65
- scroll = self.config.mode == "browser"
66
-
67
- raw_base = self.raw_data_dir / book_id
68
- cache_base = self.cache_dir / book_id
69
- info_path = raw_base / "book_info.json"
70
- chapters_html_dir = cache_base / "html"
71
-
72
- raw_base.mkdir(parents=True, exist_ok=True)
73
- normal_cs = ChapterStorage(
74
- raw_base=raw_base,
75
- namespace="chapters",
76
- backend_type=self._config.storage_backend,
77
- batch_size=self._config.storage_batch_size,
78
- )
79
- encrypted_cs = ChapterStorage(
80
- raw_base=raw_base,
81
- namespace="encrypted_chapters",
82
- backend_type=self._config.storage_backend,
83
- batch_size=self._config.storage_batch_size,
84
- )
85
-
86
- book_info: dict[str, Any]
87
-
88
- try:
89
- if not info_path.exists():
90
- raise FileNotFoundError
91
- book_info = json.loads(info_path.read_text(encoding="utf-8"))
92
- days, hrs, mins, secs = calculate_time_difference(
93
- book_info.get("update_time", ""), "UTC+8"
94
- )
95
- self.logger.info(
96
- "%s Last updated %dd %dh %dm %ds ago", TAG, days, hrs, mins, secs
97
- )
98
- if days > 1:
99
- raise FileNotFoundError # trigger re-fetch
100
- except Exception:
101
- info_html = self.requester.get_book_info(book_id)
102
- if save_html and info_html:
103
- info_html_path = chapters_html_dir / "info.html"
104
- save_as_txt(info_html[0], info_html_path)
105
- book_info = self.parser.parse_book_info(info_html)
106
- if (
107
- book_info.get("book_name", "") != "未找到书名"
108
- and book_info.get("update_time", "") != "未找到更新时间"
109
- ):
110
- save_as_json(book_info, info_path)
111
- sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
112
-
113
- # enqueue chapters
114
- for vol in book_info.get("volumes", []):
115
- vol_name = vol.get("volume_name", "")
116
- self.logger.info("%s Enqueuing volume: %s", TAG, vol_name)
117
-
118
- for chap in vol.get("chapters", []):
119
- cid = chap.get("chapterId")
120
- if not cid:
121
- self.logger.warning("%s Skipping chapter without chapterId", TAG)
122
- continue
123
-
124
- if normal_cs.exists(cid) and skip_existing:
125
- self.logger.debug(
126
- "%s Chapter already exists, skipping: %s",
127
- TAG,
128
- cid,
129
- )
130
- continue
131
-
132
- chap_title = chap.get("title", "")
133
- self.logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
134
- chap_html = self.requester.get_book_chapter(book_id, cid)
135
- if not chap_html:
136
- continue
137
-
138
- if scroll:
139
- self.requester.scroll_page(wait_time * 2) # type: ignore[attr-defined]
140
- else:
141
- sleep_with_random_delay(
142
- wait_time, mul_spread=1.1, max_sleep=wait_time + 2
143
- )
144
-
145
- is_encrypted = self.parser.is_encrypted(chap_html[0]) # type: ignore[attr-defined]
146
-
147
- if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
148
- self.logger.debug(
149
- "%s Chapter already exists, skipping: %s",
150
- TAG,
151
- cid,
152
- )
153
- continue
154
-
155
- if save_html and chap_html and not is_vip(chap_html[0]):
156
- folder = chapters_html_dir / (
157
- "html_encrypted" if is_encrypted else "html_plain"
158
- )
159
- html_path = folder / f"{cid}.html"
160
- save_as_txt(chap_html[0], html_path, on_exist="skip")
161
- self.logger.debug(
162
- "%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
163
- )
164
-
165
- chap_json = self.parser.parse_chapter(chap_html, cid)
166
- if not chap_json or not chap_json.get("content"):
167
- self.logger.warning(
168
- "%s Parsed chapter json is empty, skipping: %s (%s)",
169
- TAG,
170
- chap_title,
171
- cid,
172
- )
173
- continue
174
-
175
- if is_encrypted:
176
- encrypted_cs.save(chap_json)
177
- else:
178
- normal_cs.save(chap_json)
179
- self.logger.info("%s Saved chapter: %s (%s)", TAG, chap_title, cid)
180
-
181
- normal_cs.close()
182
- encrypted_cs.close()
183
-
184
- self.saver.save(book_id)
185
-
186
- self.logger.info(
187
- "%s Novel '%s' download completed.",
188
- TAG,
189
- book_info.get("book_name", "unknown"),
190
- )
191
- return
192
-
193
- def _handle_login(self) -> bool:
194
- """
195
- Perform login with automatic fallback to manual:
196
-
197
- 1. If manual_flag is False, try automatic login:
198
- - On success, return True immediately.
199
- 2. Always attempt manual login if manual_flag is True.
200
- 3. Return True if manual login succeeds, False otherwise.
201
- """
202
- manual_flag = state_mgr.get_manual_login_flag(self._site_key)
203
-
204
- # First try automatic login
205
- if not manual_flag and self._requester.login(manual_login=False):
206
- return True
207
-
208
- # try manual login
209
- return self._requester.login(manual_login=True)
210
-
211
-
212
- def is_vip(html_str: str) -> bool:
213
- """
214
- Return True if page indicates VIP-only content.
215
-
216
- :param html_str: Raw HTML string.
217
- """
218
- markers = ["这是VIP章节", "需要订阅", "订阅后才能阅读"]
219
- return any(m in html_str for m in markers)
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.sfacg
4
- ---------------------------------------
5
-
6
- """
7
-
8
- from .sfacg_async import SfacgAsyncDownloader
9
- from .sfacg_sync import SfacgDownloader
10
-
11
- __all__ = [
12
- "SfacgAsyncDownloader",
13
- "SfacgDownloader",
14
- ]
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.sfacg.sfacg_async
4
- ---------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonAsyncDownloader
10
- from novel_downloader.core.interfaces import (
11
- AsyncRequesterProtocol,
12
- ParserProtocol,
13
- SaverProtocol,
14
- )
15
-
16
-
17
- class SfacgAsyncDownloader(CommonAsyncDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: AsyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "sfacg")
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.sfacg.sfacg_sync
4
- --------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonDownloader
10
- from novel_downloader.core.interfaces import (
11
- ParserProtocol,
12
- SaverProtocol,
13
- SyncRequesterProtocol,
14
- )
15
-
16
-
17
- class SfacgDownloader(CommonDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: SyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "sfacg")
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.yamibo
4
- ----------------------------------------
5
-
6
- """
7
-
8
- from .yamibo_async import YamiboAsyncDownloader
9
- from .yamibo_sync import YamiboDownloader
10
-
11
- __all__ = [
12
- "YamiboAsyncDownloader",
13
- "YamiboDownloader",
14
- ]
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.downloaders.yamibo.yamibo_async
4
- -----------------------------------------------------
5
-
6
- """
7
-
8
- from novel_downloader.config.models import DownloaderConfig
9
- from novel_downloader.core.downloaders.common import CommonAsyncDownloader
10
- from novel_downloader.core.interfaces import (
11
- AsyncRequesterProtocol,
12
- ParserProtocol,
13
- SaverProtocol,
14
- )
15
-
16
-
17
- class YamiboAsyncDownloader(CommonAsyncDownloader):
18
- """"""
19
-
20
- def __init__(
21
- self,
22
- requester: AsyncRequesterProtocol,
23
- parser: ParserProtocol,
24
- saver: SaverProtocol,
25
- config: DownloaderConfig,
26
- ):
27
- super().__init__(requester, parser, saver, config, "yamibo")