novel-downloader 1.3.1__py3-none-any.whl → 1.3.3__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 (98) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +1 -1
  3. novel_downloader/config/adapter.py +3 -0
  4. novel_downloader/config/models.py +3 -0
  5. novel_downloader/core/downloaders/__init__.py +23 -1
  6. novel_downloader/core/downloaders/biquge/__init__.py +2 -0
  7. novel_downloader/core/downloaders/biquge/biquge_async.py +27 -0
  8. novel_downloader/core/downloaders/biquge/biquge_sync.py +5 -3
  9. novel_downloader/core/downloaders/common/common_async.py +5 -11
  10. novel_downloader/core/downloaders/common/common_sync.py +18 -18
  11. novel_downloader/core/downloaders/esjzone/__init__.py +14 -0
  12. novel_downloader/core/downloaders/esjzone/esjzone_async.py +27 -0
  13. novel_downloader/core/downloaders/esjzone/esjzone_sync.py +27 -0
  14. novel_downloader/core/downloaders/qianbi/__init__.py +14 -0
  15. novel_downloader/core/downloaders/qianbi/qianbi_async.py +27 -0
  16. novel_downloader/core/downloaders/qianbi/qianbi_sync.py +27 -0
  17. novel_downloader/core/downloaders/qidian/qidian_sync.py +9 -14
  18. novel_downloader/core/downloaders/sfacg/__init__.py +14 -0
  19. novel_downloader/core/downloaders/sfacg/sfacg_async.py +27 -0
  20. novel_downloader/core/downloaders/sfacg/sfacg_sync.py +27 -0
  21. novel_downloader/core/downloaders/yamibo/__init__.py +14 -0
  22. novel_downloader/core/downloaders/yamibo/yamibo_async.py +27 -0
  23. novel_downloader/core/downloaders/yamibo/yamibo_sync.py +27 -0
  24. novel_downloader/core/factory/downloader.py +35 -7
  25. novel_downloader/core/factory/parser.py +23 -2
  26. novel_downloader/core/factory/requester.py +32 -7
  27. novel_downloader/core/factory/saver.py +14 -2
  28. novel_downloader/core/interfaces/async_requester.py +3 -3
  29. novel_downloader/core/interfaces/parser.py +7 -2
  30. novel_downloader/core/interfaces/sync_requester.py +3 -3
  31. novel_downloader/core/parsers/__init__.py +15 -5
  32. novel_downloader/core/parsers/base.py +7 -2
  33. novel_downloader/core/parsers/biquge/main_parser.py +13 -4
  34. novel_downloader/core/parsers/common/main_parser.py +13 -4
  35. novel_downloader/core/parsers/esjzone/__init__.py +10 -0
  36. novel_downloader/core/parsers/esjzone/main_parser.py +220 -0
  37. novel_downloader/core/parsers/qianbi/__init__.py +10 -0
  38. novel_downloader/core/parsers/qianbi/main_parser.py +142 -0
  39. novel_downloader/core/parsers/qidian/browser/main_parser.py +13 -4
  40. novel_downloader/core/parsers/qidian/session/main_parser.py +13 -4
  41. novel_downloader/core/parsers/sfacg/__init__.py +10 -0
  42. novel_downloader/core/parsers/sfacg/main_parser.py +166 -0
  43. novel_downloader/core/parsers/yamibo/__init__.py +10 -0
  44. novel_downloader/core/parsers/yamibo/main_parser.py +194 -0
  45. novel_downloader/core/requesters/__init__.py +33 -3
  46. novel_downloader/core/requesters/base/async_session.py +14 -10
  47. novel_downloader/core/requesters/base/browser.py +4 -7
  48. novel_downloader/core/requesters/base/session.py +25 -11
  49. novel_downloader/core/requesters/biquge/__init__.py +2 -0
  50. novel_downloader/core/requesters/biquge/async_session.py +71 -0
  51. novel_downloader/core/requesters/biquge/session.py +6 -6
  52. novel_downloader/core/requesters/common/async_session.py +4 -4
  53. novel_downloader/core/requesters/common/session.py +6 -6
  54. novel_downloader/core/requesters/esjzone/__init__.py +13 -0
  55. novel_downloader/core/requesters/esjzone/async_session.py +211 -0
  56. novel_downloader/core/requesters/esjzone/session.py +235 -0
  57. novel_downloader/core/requesters/qianbi/__init__.py +13 -0
  58. novel_downloader/core/requesters/qianbi/async_session.py +96 -0
  59. novel_downloader/core/requesters/qianbi/session.py +125 -0
  60. novel_downloader/core/requesters/qidian/broswer.py +9 -9
  61. novel_downloader/core/requesters/qidian/session.py +14 -11
  62. novel_downloader/core/requesters/sfacg/__init__.py +13 -0
  63. novel_downloader/core/requesters/sfacg/async_session.py +204 -0
  64. novel_downloader/core/requesters/sfacg/session.py +242 -0
  65. novel_downloader/core/requesters/yamibo/__init__.py +13 -0
  66. novel_downloader/core/requesters/yamibo/async_session.py +211 -0
  67. novel_downloader/core/requesters/yamibo/session.py +237 -0
  68. novel_downloader/core/savers/__init__.py +15 -3
  69. novel_downloader/core/savers/base.py +3 -7
  70. novel_downloader/core/savers/common/epub.py +21 -33
  71. novel_downloader/core/savers/common/main_saver.py +3 -1
  72. novel_downloader/core/savers/common/txt.py +1 -2
  73. novel_downloader/core/savers/epub_utils/__init__.py +14 -5
  74. novel_downloader/core/savers/epub_utils/css_builder.py +1 -0
  75. novel_downloader/core/savers/epub_utils/image_loader.py +89 -0
  76. novel_downloader/core/savers/epub_utils/initializer.py +1 -0
  77. novel_downloader/core/savers/epub_utils/text_to_html.py +48 -1
  78. novel_downloader/core/savers/epub_utils/volume_intro.py +1 -0
  79. novel_downloader/core/savers/esjzone.py +25 -0
  80. novel_downloader/core/savers/qianbi.py +25 -0
  81. novel_downloader/core/savers/sfacg.py +25 -0
  82. novel_downloader/core/savers/yamibo.py +25 -0
  83. novel_downloader/locales/en.json +1 -0
  84. novel_downloader/locales/zh.json +1 -0
  85. novel_downloader/resources/config/settings.toml +40 -4
  86. novel_downloader/utils/constants.py +4 -0
  87. novel_downloader/utils/file_utils/io.py +1 -1
  88. novel_downloader/utils/network.py +51 -38
  89. novel_downloader/utils/time_utils/__init__.py +2 -1
  90. novel_downloader/utils/time_utils/datetime_utils.py +3 -1
  91. novel_downloader/utils/time_utils/sleep_utils.py +44 -2
  92. {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/METADATA +29 -24
  93. novel_downloader-1.3.3.dist-info/RECORD +166 -0
  94. novel_downloader-1.3.1.dist-info/RECORD +0 -127
  95. {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/WHEEL +0 -0
  96. {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/entry_points.txt +0 -0
  97. {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/licenses/LICENSE +0 -0
  98. {novel_downloader-1.3.1.dist-info → novel_downloader-1.3.3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ novel_downloader
6
6
  Core package for the Novel Downloader project.
7
7
  """
8
8
 
9
- __version__ = "1.3.1"
9
+ __version__ = "1.3.3"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -119,7 +119,7 @@ def download_cli(ctx: Context, book_ids: list[str], site: str) -> None:
119
119
  config=downloader_cfg,
120
120
  )
121
121
 
122
- for book_id in book_ids:
122
+ for book_id in valid_book_ids:
123
123
  click.echo(t("download_downloading", book_id=book_id, site=site))
124
124
  sync_downloader.download_one(book_id)
125
125
 
@@ -90,6 +90,8 @@ class ConfigAdapter:
90
90
  disable_images=req.get("disable_images", True),
91
91
  mute_audio=req.get("mute_audio", True),
92
92
  mode=site_cfg.get("mode", "session"),
93
+ username=site_cfg.get("username", ""),
94
+ password=site_cfg.get("password", ""),
93
95
  )
94
96
 
95
97
  def get_downloader_config(self) -> DownloaderConfig:
@@ -150,6 +152,7 @@ class ConfigAdapter:
150
152
  naming = out.get("naming", {})
151
153
  epub_opts = out.get("epub", {})
152
154
  return SaverConfig(
155
+ cache_dir=gen.get("cache_dir", "./novel_cache"),
153
156
  raw_data_dir=gen.get("raw_data_dir", "./raw_data"),
154
157
  output_dir=gen.get("output_dir", "./downloads"),
155
158
  storage_backend=gen.get("storage_backend", "json"),
@@ -38,6 +38,8 @@ class RequesterConfig:
38
38
  mode: ModeType = "session"
39
39
  max_connections: int = 10
40
40
  max_rps: float | None = None # Maximum requests per second
41
+ username: str = ""
42
+ password: str = ""
41
43
 
42
44
 
43
45
  # === Downloaders ===
@@ -78,6 +80,7 @@ class ParserConfig:
78
80
  # === Savers ===
79
81
  @dataclass
80
82
  class SaverConfig:
83
+ cache_dir: str = "./novel_cache"
81
84
  raw_data_dir: str = "./raw_data"
82
85
  output_dir: str = "./downloads"
83
86
  storage_backend: StorageBackend = "json"
@@ -8,15 +8,37 @@ specific novel platforms.
8
8
 
9
9
  Each downloader is responsible for orchestrating the full lifecycle
10
10
  of retrieving, parsing, and saving novel content for a given source.
11
+
12
+ Currently supported platforms:
13
+ - biquge (笔趣阁)
14
+ - esjzone (ESJ Zone)
15
+ - qianbi (铅笔小说)
16
+ - qidian (起点中文网)
17
+ - sfacg (SF轻小说)
18
+ - yamibo (百合会)
19
+ - common (通用架构)
11
20
  """
12
21
 
13
- from .biquge import BiqugeDownloader
22
+ from .biquge import BiqugeAsyncDownloader, BiqugeDownloader
14
23
  from .common import CommonAsyncDownloader, CommonDownloader
24
+ from .esjzone import EsjzoneAsyncDownloader, EsjzoneDownloader
25
+ from .qianbi import QianbiAsyncDownloader, QianbiDownloader
15
26
  from .qidian import QidianDownloader
27
+ from .sfacg import SfacgAsyncDownloader, SfacgDownloader
28
+ from .yamibo import YamiboAsyncDownloader, YamiboDownloader
16
29
 
17
30
  __all__ = [
31
+ "BiqugeAsyncDownloader",
18
32
  "BiqugeDownloader",
19
33
  "CommonAsyncDownloader",
20
34
  "CommonDownloader",
35
+ "EsjzoneAsyncDownloader",
36
+ "EsjzoneDownloader",
37
+ "QianbiAsyncDownloader",
38
+ "QianbiDownloader",
21
39
  "QidianDownloader",
40
+ "SfacgAsyncDownloader",
41
+ "SfacgDownloader",
42
+ "YamiboAsyncDownloader",
43
+ "YamiboDownloader",
22
44
  ]
@@ -5,8 +5,10 @@ novel_downloader.core.downloaders.biquge
5
5
 
6
6
  """
7
7
 
8
+ from .biquge_async import BiqugeAsyncDownloader
8
9
  from .biquge_sync import BiqugeDownloader
9
10
 
10
11
  __all__ = [
12
+ "BiqugeAsyncDownloader",
11
13
  "BiqugeDownloader",
12
14
  ]
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.downloaders.biquge.biquge_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 BiqugeAsyncDownloader(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, "biquge")
@@ -7,9 +7,11 @@ novel_downloader.core.downloaders.biquge.biquge_sync
7
7
 
8
8
  from novel_downloader.config.models import DownloaderConfig
9
9
  from novel_downloader.core.downloaders.common import CommonDownloader
10
- from novel_downloader.core.interfaces.parser import ParserProtocol
11
- from novel_downloader.core.interfaces.saver import SaverProtocol
12
- from novel_downloader.core.interfaces.sync_requester import SyncRequesterProtocol
10
+ from novel_downloader.core.interfaces import (
11
+ ParserProtocol,
12
+ SaverProtocol,
13
+ SyncRequesterProtocol,
14
+ )
13
15
 
14
16
 
15
17
  class BiqugeDownloader(CommonDownloader):
@@ -20,7 +20,6 @@ from novel_downloader.core.interfaces import (
20
20
  )
21
21
  from novel_downloader.utils.chapter_storage import ChapterDict, ChapterStorage
22
22
  from novel_downloader.utils.file_utils import save_as_json, save_as_txt
23
- from novel_downloader.utils.network import download_image_as_bytes
24
23
  from novel_downloader.utils.time_utils import calculate_time_difference
25
24
 
26
25
  logger = logging.getLogger(__name__)
@@ -48,7 +47,7 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
48
47
  Perform login
49
48
  """
50
49
  if self.login_required and not self._is_logged_in:
51
- success = await self.requester.login(max_retries=3)
50
+ success = await self.requester.login()
52
51
  if not success:
53
52
  raise RuntimeError("Login failed")
54
53
  self._is_logged_in = True
@@ -60,6 +59,7 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
60
59
  :param book_id: The identifier of the book to download.
61
60
  """
62
61
  assert isinstance(self.requester, AsyncRequesterProtocol)
62
+ await self.prepare()
63
63
 
64
64
  TAG = "[AsyncDownloader]"
65
65
  wait_time = self.config.request_interval
@@ -95,7 +95,8 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
95
95
  if re_fetch:
96
96
  info_html = await self.requester.get_book_info(book_id)
97
97
  if self.save_html:
98
- save_as_txt(info_html, chapters_html_dir / "info.html")
98
+ for i, html in enumerate(info_html):
99
+ save_as_txt(html, chapters_html_dir / f"info_{i}.html")
99
100
  book_info = self.parser.parse_book_info(info_html)
100
101
  if book_info.get("book_name") != "未找到书名":
101
102
  save_as_json(book_info, info_path)
@@ -105,16 +106,9 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
105
106
  else:
106
107
  book_info = json.loads(info_path.read_text("utf-8"))
107
108
 
108
- # download cover
109
- cover_url = book_info.get("cover_url", "")
110
- if cover_url:
111
- await asyncio.get_running_loop().run_in_executor(
112
- None, download_image_as_bytes, cover_url, raw_base
113
- )
114
-
115
109
  # setup queue, semaphore, executor
116
110
  semaphore = asyncio.Semaphore(self.download_workers)
117
- queue: asyncio.Queue[tuple[str, str]] = asyncio.Queue()
111
+ queue: asyncio.Queue[tuple[str, list[str]]] = asyncio.Queue()
118
112
  save_queue: asyncio.Queue[ChapterDict] = asyncio.Queue()
119
113
  loop = asyncio.get_running_loop()
120
114
  executor = (
@@ -19,7 +19,6 @@ from novel_downloader.core.interfaces import (
19
19
  )
20
20
  from novel_downloader.utils.chapter_storage import ChapterStorage
21
21
  from novel_downloader.utils.file_utils import save_as_json, save_as_txt
22
- from novel_downloader.utils.network import download_image_as_bytes
23
22
  from novel_downloader.utils.time_utils import (
24
23
  calculate_time_difference,
25
24
  sleep_with_random_delay,
@@ -52,6 +51,17 @@ class CommonDownloader(BaseDownloader):
52
51
  """
53
52
  super().__init__(requester, parser, saver, config, site)
54
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
55
65
 
56
66
  def download_one(self, book_id: str) -> None:
57
67
  """
@@ -59,6 +69,8 @@ class CommonDownloader(BaseDownloader):
59
69
 
60
70
  :param book_id: The identifier of the book to download.
61
71
  """
72
+ self.prepare()
73
+
62
74
  TAG = "[Downloader]"
63
75
  save_html = self.config.save_html
64
76
  skip_existing = self.config.skip_existing
@@ -96,8 +108,8 @@ class CommonDownloader(BaseDownloader):
96
108
  except Exception:
97
109
  info_html = self.requester.get_book_info(book_id)
98
110
  if save_html:
99
- info_html_path = chapters_html_dir / "info.html"
100
- save_as_txt(info_html, info_html_path)
111
+ for i, html in enumerate(info_html):
112
+ save_as_txt(html, chapters_html_dir / f"info_{i}.html")
101
113
  book_info = self.parser.parse_book_info(info_html)
102
114
  if (
103
115
  book_info.get("book_name", "") != "未找到书名"
@@ -106,13 +118,6 @@ class CommonDownloader(BaseDownloader):
106
118
  save_as_json(book_info, info_path)
107
119
  sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
108
120
 
109
- # download cover
110
- cover_url = book_info.get("cover_url", "")
111
- if cover_url:
112
- cover_bytes = download_image_as_bytes(cover_url, raw_base)
113
- if not cover_bytes:
114
- logger.warning("%s Failed to download cover: %s", TAG, cover_url)
115
-
116
121
  # enqueue chapters
117
122
  for vol in book_info.get("volumes", []):
118
123
  vol_name = vol.get("volume_name", "")
@@ -138,14 +143,9 @@ class CommonDownloader(BaseDownloader):
138
143
  chap_html = self.requester.get_book_chapter(book_id, cid)
139
144
 
140
145
  if save_html:
141
- html_path = chapters_html_dir / f"{cid}.html"
142
- save_as_txt(chap_html, html_path, on_exist="skip")
143
- logger.debug(
144
- "%s Saved raw HTML for chapter %s to %s",
145
- TAG,
146
- cid,
147
- html_path,
148
- )
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
149
 
150
150
  chap_json = self.parser.parse_chapter(chap_html, cid)
151
151
 
@@ -0,0 +1,14 @@
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
+ ]
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,14 @@
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
+ ]
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,27 @@
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")
@@ -19,7 +19,6 @@ from novel_downloader.core.interfaces import (
19
19
  )
20
20
  from novel_downloader.utils.chapter_storage import ChapterStorage
21
21
  from novel_downloader.utils.file_utils import save_as_json, save_as_txt
22
- from novel_downloader.utils.network import download_image_as_bytes
23
22
  from novel_downloader.utils.state import state_mgr
24
23
  from novel_downloader.utils.time_utils import (
25
24
  calculate_time_difference,
@@ -100,9 +99,9 @@ class QidianDownloader(BaseDownloader):
100
99
  raise FileNotFoundError # trigger re-fetch
101
100
  except Exception:
102
101
  info_html = self.requester.get_book_info(book_id)
103
- if save_html:
102
+ if save_html and info_html:
104
103
  info_html_path = chapters_html_dir / "info.html"
105
- save_as_txt(info_html, info_html_path)
104
+ save_as_txt(info_html[0], info_html_path)
106
105
  book_info = self.parser.parse_book_info(info_html)
107
106
  if (
108
107
  book_info.get("book_name", "") != "未找到书名"
@@ -111,13 +110,6 @@ class QidianDownloader(BaseDownloader):
111
110
  save_as_json(book_info, info_path)
112
111
  sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
113
112
 
114
- # download cover
115
- cover_url = book_info.get("cover_url", "")
116
- if cover_url:
117
- cover_bytes = download_image_as_bytes(cover_url, raw_base)
118
- if not cover_bytes:
119
- self.logger.warning("%s Failed to download cover: %s", TAG, cover_url)
120
-
121
113
  # enqueue chapters
122
114
  for vol in book_info.get("volumes", []):
123
115
  vol_name = vol.get("volume_name", "")
@@ -140,6 +132,9 @@ class QidianDownloader(BaseDownloader):
140
132
  chap_title = chap.get("title", "")
141
133
  self.logger.info("%s Fetching chapter: %s (%s)", TAG, chap_title, cid)
142
134
  chap_html = self.requester.get_book_chapter(book_id, cid)
135
+ if not chap_html:
136
+ continue
137
+
143
138
  if scroll:
144
139
  self.requester.scroll_page(wait_time * 2) # type: ignore[attr-defined]
145
140
  else:
@@ -147,7 +142,7 @@ class QidianDownloader(BaseDownloader):
147
142
  wait_time, mul_spread=1.1, max_sleep=wait_time + 2
148
143
  )
149
144
 
150
- is_encrypted = self.parser.is_encrypted(chap_html) # type: ignore[attr-defined]
145
+ is_encrypted = self.parser.is_encrypted(chap_html[0]) # type: ignore[attr-defined]
151
146
 
152
147
  if is_encrypted and encrypted_cs.exists(cid) and skip_existing:
153
148
  self.logger.debug(
@@ -157,18 +152,18 @@ class QidianDownloader(BaseDownloader):
157
152
  )
158
153
  continue
159
154
 
160
- if save_html and not is_vip(chap_html):
155
+ if save_html and chap_html and not is_vip(chap_html[0]):
161
156
  folder = chapters_html_dir / (
162
157
  "html_encrypted" if is_encrypted else "html_plain"
163
158
  )
164
159
  html_path = folder / f"{cid}.html"
165
- save_as_txt(chap_html, html_path, on_exist="skip")
160
+ save_as_txt(chap_html[0], html_path, on_exist="skip")
166
161
  self.logger.debug(
167
162
  "%s Saved raw HTML for chapter %s to %s", TAG, cid, html_path
168
163
  )
169
164
 
170
165
  chap_json = self.parser.parse_chapter(chap_html, cid)
171
- if not chap_json:
166
+ if not chap_json or not chap_json.get("content"):
172
167
  self.logger.warning(
173
168
  "%s Parsed chapter json is empty, skipping: %s (%s)",
174
169
  TAG,
@@ -0,0 +1,14 @@
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
+ ]
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,14 @@
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
+ ]
@@ -0,0 +1,27 @@
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")
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.downloaders.yamibo.yamibo_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 YamiboDownloader(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, "yamibo")