novel-downloader 1.3.2__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 (25) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/core/downloaders/common/common_async.py +0 -8
  3. novel_downloader/core/downloaders/common/common_sync.py +0 -8
  4. novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -8
  5. novel_downloader/core/parsers/esjzone/main_parser.py +4 -3
  6. novel_downloader/core/savers/base.py +2 -7
  7. novel_downloader/core/savers/common/epub.py +21 -33
  8. novel_downloader/core/savers/common/main_saver.py +3 -1
  9. novel_downloader/core/savers/common/txt.py +1 -2
  10. novel_downloader/core/savers/epub_utils/__init__.py +14 -5
  11. novel_downloader/core/savers/epub_utils/css_builder.py +1 -0
  12. novel_downloader/core/savers/epub_utils/image_loader.py +89 -0
  13. novel_downloader/core/savers/epub_utils/initializer.py +1 -0
  14. novel_downloader/core/savers/epub_utils/text_to_html.py +48 -1
  15. novel_downloader/core/savers/epub_utils/volume_intro.py +1 -0
  16. novel_downloader/utils/constants.py +4 -0
  17. novel_downloader/utils/file_utils/io.py +1 -1
  18. novel_downloader/utils/network.py +51 -38
  19. novel_downloader/utils/time_utils/sleep_utils.py +2 -2
  20. {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/METADATA +14 -14
  21. {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/RECORD +25 -24
  22. {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/WHEEL +0 -0
  23. {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/entry_points.txt +0 -0
  24. {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/licenses/LICENSE +0 -0
  25. {novel_downloader-1.3.2.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.2"
9
+ __version__ = "1.3.3"
10
10
 
11
11
  __author__ = "Saudade Z"
12
12
  __email__ = "saudadez217@gmail.com"
@@ -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__)
@@ -107,13 +106,6 @@ class CommonAsyncDownloader(BaseAsyncDownloader):
107
106
  else:
108
107
  book_info = json.loads(info_path.read_text("utf-8"))
109
108
 
110
- # download cover
111
- cover_url = book_info.get("cover_url", "")
112
- if cover_url:
113
- await asyncio.get_running_loop().run_in_executor(
114
- None, download_image_as_bytes, cover_url, raw_base
115
- )
116
-
117
109
  # setup queue, semaphore, executor
118
110
  semaphore = asyncio.Semaphore(self.download_workers)
119
111
  queue: asyncio.Queue[tuple[str, list[str]]] = asyncio.Queue()
@@ -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,
@@ -119,13 +118,6 @@ class CommonDownloader(BaseDownloader):
119
118
  save_as_json(book_info, info_path)
120
119
  sleep_with_random_delay(wait_time, mul_spread=1.1, max_sleep=wait_time + 2)
121
120
 
122
- # download cover
123
- cover_url = book_info.get("cover_url", "")
124
- if cover_url:
125
- cover_bytes = download_image_as_bytes(cover_url, raw_base)
126
- if not cover_bytes:
127
- logger.warning("%s Failed to download cover: %s", TAG, cover_url)
128
-
129
121
  # enqueue chapters
130
122
  for vol in book_info.get("volumes", []):
131
123
  vol_name = vol.get("volume_name", "")
@@ -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,
@@ -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", "")
@@ -85,9 +85,10 @@ class EsjzoneParser(BaseParser):
85
85
 
86
86
  _start_volume("單卷")
87
87
 
88
- nodes = tree.xpath('//div[@id="chapterList"]/details') + tree.xpath(
89
- '//div[@id="chapterList"]/*[not(self::details)]'
90
- )
88
+ # nodes = tree.xpath('//div[@id="chapterList"]/details') + tree.xpath(
89
+ # '//div[@id="chapterList"]/*[not(self::details)]'
90
+ # )
91
+ nodes = tree.xpath('//div[@id="chapterList"]/*')
91
92
 
92
93
  for node in nodes:
93
94
  tag = node.tag.lower()
@@ -40,9 +40,9 @@ class BaseSaver(SaverProtocol, abc.ABC):
40
40
  self._config = config
41
41
 
42
42
  self._base_cache_dir = Path(config.cache_dir)
43
- self._raw_data_dir = Path(config.raw_data_dir)
43
+ self._base_raw_data_dir = Path(config.raw_data_dir)
44
44
  self._output_dir = Path(config.output_dir)
45
- self._raw_data_dir.mkdir(parents=True, exist_ok=True)
45
+ self._base_cache_dir.mkdir(parents=True, exist_ok=True)
46
46
  self._output_dir.mkdir(parents=True, exist_ok=True)
47
47
 
48
48
  self._filename_template = config.filename_template
@@ -158,11 +158,6 @@ class BaseSaver(SaverProtocol, abc.ABC):
158
158
  """Access the output directory for saving files."""
159
159
  return self._output_dir
160
160
 
161
- @property
162
- def raw_data_dir(self) -> Path:
163
- """Access the raw data directory."""
164
- return self._raw_data_dir
165
-
166
161
  @property
167
162
  def filename_template(self) -> str:
168
163
  """Access the filename template."""
@@ -11,53 +11,30 @@ from __future__ import annotations
11
11
  import json
12
12
  from pathlib import Path
13
13
  from typing import TYPE_CHECKING
14
- from urllib.parse import unquote, urlparse
15
14
 
16
15
  from ebooklib import epub
17
16
 
18
17
  from novel_downloader.core.savers.epub_utils import (
18
+ add_images_from_dir,
19
19
  chapter_txt_to_html,
20
20
  create_css_items,
21
21
  create_volume_intro,
22
22
  generate_book_intro_html,
23
23
  init_epub,
24
+ inline_remote_images,
24
25
  )
25
26
  from novel_downloader.utils.constants import (
26
- DEFAULT_IMAGE_SUFFIX,
27
27
  EPUB_OPTIONS,
28
28
  EPUB_TEXT_FOLDER,
29
29
  )
30
30
  from novel_downloader.utils.file_utils import sanitize_filename
31
+ from novel_downloader.utils.network import download_image
31
32
  from novel_downloader.utils.text_utils import clean_chapter_title
32
33
 
33
34
  if TYPE_CHECKING:
34
35
  from .main_saver import CommonSaver
35
36
 
36
37
 
37
- def _image_url_to_filename(url: str) -> str:
38
- """
39
- Parse and sanitize a image filename from a URL.
40
- If no filename or suffix exists, fallback to default name and extension.
41
-
42
- :param url: URL string
43
- :return: Safe filename string
44
- """
45
- if not url:
46
- return ""
47
-
48
- parsed_url = urlparse(url)
49
- path = unquote(parsed_url.path)
50
- filename = Path(path).name
51
-
52
- if not filename:
53
- filename = "image"
54
-
55
- if not Path(filename).suffix:
56
- filename += DEFAULT_IMAGE_SUFFIX
57
-
58
- return filename
59
-
60
-
61
38
  def common_save_as_epub(
62
39
  saver: CommonSaver,
63
40
  book_id: str,
@@ -76,11 +53,12 @@ def common_save_as_epub(
76
53
  :param book_id: Identifier of the novel (used as subdirectory name).
77
54
  """
78
55
  TAG = "[saver]"
79
- site = saver.site
80
56
  config = saver._config
81
57
  # --- Paths & options ---
82
- raw_base = saver.raw_data_dir / site / book_id
58
+ raw_base = saver._raw_data_dir / book_id
59
+ img_dir = saver._cache_dir / book_id / "images"
83
60
  out_dir = saver.output_dir
61
+ img_dir.mkdir(parents=True, exist_ok=True)
84
62
  out_dir.mkdir(parents=True, exist_ok=True)
85
63
 
86
64
  # --- Load book_info.json ---
@@ -100,10 +78,16 @@ def common_save_as_epub(
100
78
  # --- Generate intro + cover ---
101
79
  intro_html = generate_book_intro_html(book_info)
102
80
  cover_path: Path | None = None
103
- if config.include_cover:
104
- cover_filename = _image_url_to_filename(book_info.get("cover_url", ""))
105
- if cover_filename:
106
- cover_path = raw_base / cover_filename
81
+ cover_url = book_info.get("cover_url", "")
82
+ if config.include_cover and cover_url:
83
+ cover_path = download_image(
84
+ cover_url,
85
+ raw_base,
86
+ target_name="cover",
87
+ on_exist="overwrite",
88
+ )
89
+ if not cover_path:
90
+ saver.logger.warning("Failed to download cover from %s", cover_url)
107
91
 
108
92
  # --- Initialize EPUB ---
109
93
  book, spine, toc_list = init_epub(
@@ -162,9 +146,11 @@ def common_save_as_epub(
162
146
  continue
163
147
 
164
148
  title = clean_chapter_title(chapter_data.get("title", "")) or chap_id
149
+ content: str = chapter_data.get("content", "")
150
+ content = inline_remote_images(content, img_dir)
165
151
  chap_html = chapter_txt_to_html(
166
152
  chapter_title=title,
167
- chapter_text=chapter_data.get("content", ""),
153
+ chapter_text=content,
168
154
  author_say=chapter_data.get("author_say", ""),
169
155
  )
170
156
 
@@ -182,6 +168,8 @@ def common_save_as_epub(
182
168
 
183
169
  toc_list.append((section, chapter_items))
184
170
 
171
+ book = add_images_from_dir(book, img_dir)
172
+
185
173
  # --- 5. Finalize EPUB ---
186
174
  saver.logger.info("%s Building TOC and spine...", TAG)
187
175
  book.toc = toc_list
@@ -41,6 +41,8 @@ class CommonSaver(BaseSaver):
41
41
  """
42
42
  super().__init__(config)
43
43
  self._site = site
44
+ self._raw_data_dir = self._base_raw_data_dir / site
45
+ self._cache_dir = self._base_cache_dir / site
44
46
  self._chapter_storage_cache: dict[str, list[ChapterStorage]] = {}
45
47
  self._chap_folders: list[str] = chap_folders or ["chapters"]
46
48
 
@@ -109,7 +111,7 @@ class CommonSaver(BaseSaver):
109
111
  return {}
110
112
 
111
113
  def _init_chapter_storages(self, book_id: str) -> None:
112
- raw_base = self.raw_data_dir / self._site / book_id
114
+ raw_base = self._raw_data_dir / book_id
113
115
  self._chapter_storage_cache[book_id] = [
114
116
  ChapterStorage(
115
117
  raw_base=raw_base,
@@ -45,9 +45,8 @@ def common_save_as_txt(
45
45
  :param book_id: Identifier of the novel (used as subdirectory name).
46
46
  """
47
47
  TAG = "[saver]"
48
- site = saver.site
49
48
  # --- Paths & options ---
50
- raw_base = saver.raw_data_dir / site / book_id
49
+ raw_base = saver._raw_data_dir / book_id
51
50
  out_dir = saver.output_dir
52
51
  out_dir.mkdir(parents=True, exist_ok=True)
53
52
 
@@ -6,21 +6,30 @@ novel_downloader.core.savers.epub_utils
6
6
  This package provides utility functions for constructing EPUB files,
7
7
  including:
8
8
 
9
- - CSS inclusion (create_css_items)
10
- - EPUB book initialization (init_epub)
11
- - Chapter text-to-HTML conversion (chapter_txt_to_html)
12
- - Volume intro HTML generation (create_volume_intro)
9
+ - CSS inclusion (css_builder)
10
+ - Image embedding (image_loader)
11
+ - EPUB book initialization (initializer)
12
+ - Chapter text-to-HTML conversion (text_to_html)
13
+ - Volume intro HTML generation (volume_intro)
13
14
  """
14
15
 
15
16
  from .css_builder import create_css_items
17
+ from .image_loader import add_images_from_dir, add_images_from_dirs
16
18
  from .initializer import init_epub
17
- from .text_to_html import chapter_txt_to_html, generate_book_intro_html
19
+ from .text_to_html import (
20
+ chapter_txt_to_html,
21
+ generate_book_intro_html,
22
+ inline_remote_images,
23
+ )
18
24
  from .volume_intro import create_volume_intro
19
25
 
20
26
  __all__ = [
21
27
  "create_css_items",
28
+ "add_images_from_dir",
29
+ "add_images_from_dirs",
22
30
  "init_epub",
23
31
  "chapter_txt_to_html",
24
32
  "create_volume_intro",
25
33
  "generate_book_intro_html",
34
+ "inline_remote_images",
26
35
  ]
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.core.savers.epub_utils.css_builder
4
+ ---------------------------------------------------
4
5
 
5
6
  Reads local CSS files and wraps them into epub.EpubItem objects,
6
7
  returning a list ready to be added to the EPUB.
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ novel_downloader.core.savers.epub_utils.image_loader
4
+ ----------------------------------------------------
5
+
6
+ Utilities for embedding image files into an EpubBook.
7
+ """
8
+
9
+ import logging
10
+ from collections.abc import Iterable
11
+ from pathlib import Path
12
+
13
+ from ebooklib import epub
14
+
15
+ from novel_downloader.utils.constants import EPUB_IMAGE_FOLDER
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ _SUPPORTED_IMAGE_MEDIA_TYPES: dict[str, str] = {
20
+ "png": "image/png",
21
+ "jpg": "image/jpeg",
22
+ "jpeg": "image/jpeg",
23
+ "gif": "image/gif",
24
+ "svg": "image/svg+xml",
25
+ "webp": "image/webp",
26
+ }
27
+ _DEFAULT_IMAGE_MEDIA_TYPE = "image/jpeg"
28
+
29
+
30
+ def add_images_from_dir(
31
+ book: epub.EpubBook,
32
+ image_dir: str | Path,
33
+ ) -> epub.EpubBook:
34
+ """
35
+ Load every file in `image_dir` into the EPUB's image folder.
36
+
37
+ :param book: The EpubBook object to modify.
38
+ :param image_dir: Path to the directory containing image files.
39
+ :return: The same EpubBook instance, with images added.
40
+ """
41
+ image_dir = Path(image_dir)
42
+ if not image_dir.is_dir():
43
+ logger.warning("Image directory not found or not a directory: %s", image_dir)
44
+ return book
45
+
46
+ for img_path in image_dir.iterdir():
47
+ if not img_path.is_file():
48
+ continue
49
+
50
+ suffix = img_path.suffix.lower().lstrip(".")
51
+ media_type = _SUPPORTED_IMAGE_MEDIA_TYPES.get(suffix)
52
+ if media_type is None:
53
+ media_type = _DEFAULT_IMAGE_MEDIA_TYPE
54
+ logger.warning(
55
+ "Unknown image suffix '%s' - defaulting media_type to %s",
56
+ suffix,
57
+ media_type,
58
+ )
59
+
60
+ try:
61
+ content = img_path.read_bytes()
62
+ item = epub.EpubItem(
63
+ uid=f"img_{img_path.stem}",
64
+ file_name=f"{EPUB_IMAGE_FOLDER}/{img_path.name}",
65
+ media_type=media_type,
66
+ content=content,
67
+ )
68
+ book.add_item(item)
69
+ logger.info("Embedded image: %s", img_path.name)
70
+ except Exception:
71
+ logger.exception("Failed to embed image %s", img_path)
72
+
73
+ return book
74
+
75
+
76
+ def add_images_from_dirs(
77
+ book: epub.EpubBook,
78
+ image_dirs: Iterable[str | Path],
79
+ ) -> epub.EpubBook:
80
+ """
81
+ Add all images from multiple directories into the given EpubBook.
82
+
83
+ :param book: The EpubBook object to modify.
84
+ :param image_dirs: An iterable of directory paths to scan for images.
85
+ :return: The same EpubBook instance, with all images added.
86
+ """
87
+ for img_dir in image_dirs:
88
+ book = add_images_from_dir(book, img_dir)
89
+ return book
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.core.savers.epub_utils.initializer
4
+ ---------------------------------------------------
4
5
 
5
6
  Initializes an epub.EpubBook object, sets metadata
6
7
  (identifier, title, author, language, description),
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.core.savers.epub_utils.text_to_html
4
+ ----------------------------------------------------
4
5
 
5
6
  Module for converting raw chapter text to formatted HTML,
6
7
  with automatic word correction and optional image/tag support.
@@ -8,13 +9,23 @@ with automatic word correction and optional image/tag support.
8
9
 
9
10
  import json
10
11
  import logging
12
+ import re
13
+ from pathlib import Path
11
14
  from typing import Any
12
15
 
13
- from novel_downloader.utils.constants import REPLACE_WORD_MAP_PATH
16
+ from novel_downloader.utils.constants import (
17
+ EPUB_IMAGE_WRAPPER,
18
+ REPLACE_WORD_MAP_PATH,
19
+ )
20
+ from novel_downloader.utils.network import download_image
14
21
  from novel_downloader.utils.text_utils import diff_inline_display
15
22
 
16
23
  logger = logging.getLogger(__name__)
17
24
 
25
+ _IMG_TAG_PATTERN = re.compile(
26
+ r'<img\s+[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>', re.IGNORECASE
27
+ )
28
+
18
29
 
19
30
  # Load and sort replacement map from JSON
20
31
  try:
@@ -87,6 +98,42 @@ def chapter_txt_to_html(
87
98
  return "\n".join(html_parts)
88
99
 
89
100
 
101
+ def inline_remote_images(
102
+ content: str,
103
+ image_dir: str | Path,
104
+ ) -> str:
105
+ """
106
+ Download every remote <img src="…"> in `content` into `image_dir`,
107
+ and replace the original tag with EPUB_IMAGE_WRAPPER
108
+ pointing to the local filename.
109
+
110
+ :param content: HTML/text of the chapter containing <img> tags.
111
+ :param image_dir: Directory to save downloaded images into.
112
+ :return: Modified content with local image references.
113
+ """
114
+
115
+ def _replace(match: re.Match[str]) -> str:
116
+ url = match.group(1)
117
+ try:
118
+ # download_image returns a Path or None
119
+ local_path = download_image(
120
+ url, image_dir, target_name=None, on_exist="skip"
121
+ )
122
+ if not local_path:
123
+ logger.warning(
124
+ "Failed to download image, leaving original tag: %s", url
125
+ )
126
+ return match.group(0)
127
+
128
+ # wrap with the EPUB_IMAGE_WRAPPER, inserting just the filename
129
+ return EPUB_IMAGE_WRAPPER.format(filename=local_path.name)
130
+ except Exception:
131
+ logger.exception("Error processing image URL: %s", url)
132
+ return match.group(0)
133
+
134
+ return _IMG_TAG_PATTERN.sub(_replace, content)
135
+
136
+
90
137
  def generate_book_intro_html(book_info: dict[str, Any]) -> str:
91
138
  """
92
139
  Generate HTML string for a book's information and summary.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
3
  novel_downloader.core.savers.epub_utils.volume_intro
4
+ ----------------------------------------------------
4
5
 
5
6
  Responsible for generating HTML code for volume introduction pages,
6
7
  including two style variants and a unified entry point.
@@ -116,6 +116,10 @@ BLACKLIST_PATH = files("novel_downloader.resources.text").joinpath("blacklist.tx
116
116
  EPUB_IMAGE_FOLDER = "Images"
117
117
  EPUB_TEXT_FOLDER = "Text"
118
118
 
119
+ EPUB_IMAGE_WRAPPER = (
120
+ '<div class="duokan-image-single illus"><img src="../Images/{filename}" /></div>'
121
+ )
122
+
119
123
  EPUB_OPTIONS = {
120
124
  # guide 是 EPUB 2 的一个部分, 包含封面, 目录, 索引等重要导航信息
121
125
  "epub2_guide": True,
@@ -103,7 +103,7 @@ def _write_file(
103
103
  tmp.write(content_to_write)
104
104
  tmp_path = Path(tmp.name)
105
105
  tmp_path.replace(path)
106
- logger.info("[file] '%s' written successfully", path)
106
+ logger.debug("[file] '%s' written successfully", path)
107
107
  return True
108
108
  except Exception as exc:
109
109
  logger.warning("[file] Error writing %r: %s", path, exc)
@@ -16,7 +16,7 @@ from urllib.parse import unquote, urlparse
16
16
  import requests
17
17
 
18
18
  from .constants import DEFAULT_HEADERS, DEFAULT_IMAGE_SUFFIX
19
- from .file_utils.io import _get_non_conflicting_path, _write_file, read_binary_file
19
+ from .file_utils.io import _get_non_conflicting_path, _write_file
20
20
 
21
21
  logger = logging.getLogger(__name__)
22
22
 
@@ -84,28 +84,28 @@ def image_url_to_filename(url: str) -> str:
84
84
  return filename
85
85
 
86
86
 
87
- def download_image_as_bytes(
87
+ def download_image(
88
88
  url: str,
89
89
  target_folder: str | Path | None = None,
90
+ target_name: str | None = None,
90
91
  *,
91
92
  timeout: int = 10,
92
93
  retries: int = 3,
93
94
  backoff: float = 0.5,
94
95
  on_exist: Literal["overwrite", "skip", "rename"] = "overwrite",
95
- ) -> bytes | None:
96
+ ) -> Path | None:
96
97
  """
97
- Download an image from a given URL and return its content as bytes.
98
-
99
- If on_exist='skip' and the file already exists, it will be read from disk
100
- instead of being downloaded again.
98
+ Download an image from `url` and save it to `target_folder`, returning the Path.
99
+ Can override the filename via `target_name`.
101
100
 
102
101
  :param url: Image URL. Can start with 'http', '//', or without protocol.
103
- :param target_folder: Optional folder to save the image (str or Path).
102
+ :param target_folder: Directory to save into (defaults to cwd).
103
+ :param target_name: Optional filename (with or without extension).
104
104
  :param timeout: Request timeout in seconds.
105
105
  :param retries: Number of retry attempts.
106
106
  :param backoff: Base delay between retries (exponential backoff).
107
107
  :param on_exist: What to do if file exists: 'overwrite', 'skip', or 'rename'.
108
- :return: Image content as bytes, or None if failed.
108
+ :return: Path to the saved image, or `None` on any failure.
109
109
  """
110
110
  # Normalize URL
111
111
  if url.startswith("//"):
@@ -113,21 +113,28 @@ def download_image_as_bytes(
113
113
  elif not url.startswith("http"):
114
114
  url = "https://" + url
115
115
 
116
- save_path = None
117
- if target_folder:
118
- target_folder = Path(target_folder)
119
- filename = image_url_to_filename(url)
120
- save_path = target_folder / filename
121
-
122
- if on_exist == "skip" and save_path.exists():
123
- logger.info(
124
- "[image] '%s' exists, skipping download and reading from disk.",
125
- save_path,
126
- )
127
- return read_binary_file(save_path)
116
+ folder = Path(target_folder) if target_folder else Path.cwd()
117
+ folder.mkdir(parents=True, exist_ok=True)
118
+
119
+ if target_name:
120
+ name = target_name
121
+ if not Path(name).suffix:
122
+ # infer ext from URL-derived name
123
+ name += Path(image_url_to_filename(url)).suffix
124
+ else:
125
+ name = image_url_to_filename(url)
126
+ save_path = folder / name
127
+
128
+ # Handle existing file
129
+ if save_path.exists():
130
+ if on_exist == "skip":
131
+ logger.debug("Skipping download; file exists: %s", save_path)
132
+ return save_path
133
+ if on_exist == "rename":
134
+ save_path = _get_non_conflicting_path(save_path)
128
135
 
129
136
  # Proceed with download
130
- response = http_get_with_retry(
137
+ resp = http_get_with_retry(
131
138
  url,
132
139
  retries=retries,
133
140
  timeout=timeout,
@@ -136,19 +143,25 @@ def download_image_as_bytes(
136
143
  stream=False,
137
144
  )
138
145
 
139
- if response and response.ok:
140
- content = response.content
141
-
142
- if save_path:
143
- _write_file(
144
- content=content,
145
- filepath=save_path,
146
- mode="wb",
147
- on_exist=on_exist,
148
- )
149
-
150
- return content
146
+ if not (resp and resp.ok):
147
+ logger.warning(
148
+ "Failed to download %s (status=%s)",
149
+ url,
150
+ getattr(resp, "status_code", None),
151
+ )
152
+ return None
151
153
 
154
+ # Write to disk
155
+ try:
156
+ _write_file(
157
+ content=resp.content,
158
+ filepath=save_path,
159
+ mode="wb",
160
+ on_exist=on_exist,
161
+ )
162
+ return save_path
163
+ except Exception:
164
+ logger.exception("Error saving image to %s", save_path)
152
165
  return None
153
166
 
154
167
 
@@ -191,7 +204,7 @@ def download_font_file(
191
204
 
192
205
  # If skip and file exists -> return immediately
193
206
  if on_exist == "skip" and font_path.exists():
194
- logger.info("[font] File exists, skipping download: %s", font_path)
207
+ logger.debug("[font] File exists, skipping download: %s", font_path)
195
208
  return font_path
196
209
 
197
210
  # Retry download with exponential backoff
@@ -214,7 +227,7 @@ def download_font_file(
214
227
  if chunk:
215
228
  f.write(chunk)
216
229
 
217
- logger.info("[font] Font saved to: %s", font_path)
230
+ logger.debug("[font] Font saved to: %s", font_path)
218
231
  return font_path
219
232
 
220
233
  except Exception as e:
@@ -258,7 +271,7 @@ def download_js_file(
258
271
  save_path = target_folder / filename
259
272
 
260
273
  if on_exist == "skip" and save_path.exists():
261
- logger.info("[js] File exists, skipping download: %s", save_path)
274
+ logger.debug("[js] File exists, skipping download: %s", save_path)
262
275
  return save_path
263
276
 
264
277
  response = http_get_with_retry(
@@ -278,7 +291,7 @@ def download_js_file(
278
291
 
279
292
  try:
280
293
  _write_file(content=content, filepath=save_path, mode="wb")
281
- logger.info("[js] JS file saved to: %s", save_path)
294
+ logger.debug("[js] JS file saved to: %s", save_path)
282
295
  return save_path
283
296
  except Exception as e:
284
297
  logger.error("[js] Error writing JS to disk: %s", e)
@@ -56,7 +56,7 @@ def sleep_with_random_delay(
56
56
  if max_sleep is not None:
57
57
  duration = min(duration, max_sleep)
58
58
 
59
- logger.info("[time] Sleeping for %.2f seconds", duration)
59
+ logger.debug("[time] Sleeping for %.2f seconds", duration)
60
60
  time.sleep(duration)
61
61
  return
62
62
 
@@ -98,7 +98,7 @@ async def async_sleep_with_random_delay(
98
98
  if max_sleep is not None:
99
99
  duration = min(duration, max_sleep)
100
100
 
101
- logger.info("[async time] Sleeping for %.2f seconds", duration)
101
+ logger.debug("[async time] Sleeping for %.2f seconds", duration)
102
102
  await asyncio.sleep(duration)
103
103
 
104
104
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: novel-downloader
3
- Version: 1.3.2
3
+ Version: 1.3.3
4
4
  Summary: A command-line tool for downloading Chinese web novels from Qidian and similar platforms.
5
5
  Author-email: Saudade Z <saudadez217@gmail.com>
6
6
  License: MIT License
@@ -69,33 +69,33 @@ Dynamic: license-file
69
69
 
70
70
  # novel-downloader
71
71
 
72
- 一个基于 [DrissionPage](https://www.drissionpage.cn) 和 [requests](https://github.com/psf/requests) 的小说下载器。
72
+ 一个基于 [DrissionPage](https://www.drissionpage.cn) 和 [requests](https://github.com/psf/requests) 的小说下载工具/库。
73
73
 
74
74
  ---
75
75
 
76
76
  ## 项目简介
77
77
 
78
- **novel-downloader** 是一个通用的小说下载库 / CLI 工具,
79
- - 大多数支持的站点仅依赖 [`requests`](https://github.com/psf/requests) 进行 HTTP 抓取
78
+ **novel-downloader** 支持多种小说网站的章节抓取与合并导出,
79
+ - **轻量化抓取**: 绝大多数站点仅依赖 `requests` 实现 HTTP 请求, 无需额外浏览器驱动
80
80
  - 对于起点中文网 (Qidian), 可在配置中选择:
81
81
  - `mode: session` : 纯 Requests 模式
82
- - `mode: browser` : 基于 DrissionPage 驱动 Chrome 的浏览器模式 (可处理更复杂的 JS/加密)。
83
- - 若配置 `login_required: true`, 程序会在运行时自动检查登录状态, 支持自动重用历史 Cookie, 仅在首次登录或 Cookie 失效时需要人工介入:
84
- - 若使用 `browser` 模式, 请在程序打开的浏览器窗口登录, 登录后回车继续
85
- - 若使用 `session` 模式, 请根据程序提示粘贴浏览器中登录成功后的 Cookie ([复制 Cookies](https://github.com/BowenZ217/novel-downloader/blob/main/docs/copy-cookies.md))
82
+ - `mode: browser` : 基于 `DrissionPage` 驱动 Chrome 的浏览器模式 (可处理更复杂的 JS/加密)。
83
+ - **自动登录** (可选)
84
+ - 配置 `login_required: true` 后自动检测并重用历史 Cookie
85
+ - 首次登录或 Cookie 失效时:
86
+ - **browser** 模式: 在程序打开的浏览器窗口登录, 登录后回车继续
87
+ - **session** 模式: 根据提示粘贴浏览器中已登录的 Cookie (参考 [复制 Cookies](https://github.com/BowenZ217/novel-downloader/blob/main/docs/copy-cookies.md))
86
88
 
87
89
  ## 功能特性
88
90
 
89
- - 爬取起点中文网的小说章节内容 (支持免费与已订阅章节)
90
- - 爬取部分小说网站
91
- - 断点续爬
92
- - 自动整合所有章节并导出为
91
+ - 抓取起点中文网免费及已订阅章节内容
92
+ - 支持断点续爬, 自动续传未完成任务
93
+ - 自动整合所有章节并导出为:
93
94
  - TXT
94
- - EPUB
95
+ - EPUB (可选包含章节插图)
95
96
  - 支持活动广告过滤:
96
97
  - [x] 章节标题
97
98
  - [ ] 章节正文
98
- - [ ] 作者说
99
99
 
100
100
  ---
101
101
 
@@ -1,4 +1,4 @@
1
- novel_downloader/__init__.py,sha256=tgzKUoWTjv0K2gxgUrJQ1651mAwAMSJ53YBUWRd0oIc,218
1
+ novel_downloader/__init__.py,sha256=PZq_WVjwD4TOnmRr2jf4YwV4Hpt3y6WSvHDpEl4jsE0,218
2
2
  novel_downloader/cli/__init__.py,sha256=-2HAut_U1e67MZGdvbpEJ1n5J-bRchzto6L4c-nWeXY,174
3
3
  novel_downloader/cli/clean.py,sha256=yjPDEoJRZnP_Xq-y0vA2ZhyNP-GxCM1UosW4gVR3VBI,3687
4
4
  novel_downloader/cli/download.py,sha256=r-l-HHFdRb6pWkJVcDl_MoC8pf57l2ixoVFnDLtEWcc,4001
@@ -19,8 +19,8 @@ novel_downloader/core/downloaders/biquge/__init__.py,sha256=qlbLU0WWx9AY_hYW_brd
19
19
  novel_downloader/core/downloaders/biquge/biquge_async.py,sha256=8cEUmhCz1YTCYWpZezZIX7DlbqFDXvZF-T9ylXT56mE,705
20
20
  novel_downloader/core/downloaders/biquge/biquge_sync.py,sha256=bxgZRw4sUMhbMBCivoKxHLIpixEHxpwXOjUdr7g-Qks,686
21
21
  novel_downloader/core/downloaders/common/__init__.py,sha256=KZwtl9K7ZhN8Nnnk9BBOVWZ-QJ5GQBsRAoG7x2OlVYM,273
22
- novel_downloader/core/downloaders/common/common_async.py,sha256=wXRCdw17ieeuSwqw34yY3lXIecIydlKs5qeofubV0yE,7535
23
- novel_downloader/core/downloaders/common/common_sync.py,sha256=X37mJukScti-Ic3s1AOVdYP-EPNRtL-MKGkw2RnhH1M,7162
22
+ novel_downloader/core/downloaders/common/common_async.py,sha256=n2rBLzHwF7QawRI5PPticzcM0QTLH_SAzx21uQoFODk,7226
23
+ novel_downloader/core/downloaders/common/common_sync.py,sha256=sekb0g-FRP3JX51I4hAeUnUe6mKRfOa_6LrA3uJ1Blo,6811
24
24
  novel_downloader/core/downloaders/esjzone/__init__.py,sha256=6RV3aodbuR0znTAGHn8G6GPGTO6TbJa4ykrFWyPI4iA,281
25
25
  novel_downloader/core/downloaders/esjzone/esjzone_async.py,sha256=g52LD5iO1c0jGlNPP795a82VoQY1wggh12GATBNklQg,711
26
26
  novel_downloader/core/downloaders/esjzone/esjzone_sync.py,sha256=Spp9Nnu1CqKCQlxp0XSCf_cbGTOEkLQuD2u5GIE0QoY,692
@@ -28,7 +28,7 @@ novel_downloader/core/downloaders/qianbi/__init__.py,sha256=zj8DyyEnK7xRXVsZF3SW
28
28
  novel_downloader/core/downloaders/qianbi/qianbi_async.py,sha256=-bJaRC-QCgZoQTmUg02bcjEpL_f0L7fHtLrMnkiZ-sU,705
29
29
  novel_downloader/core/downloaders/qianbi/qianbi_sync.py,sha256=mLDXNLKKqKlyY_SOWAVOtD88sQ4UrDecnZzaWxCvfqg,686
30
30
  novel_downloader/core/downloaders/qidian/__init__.py,sha256=LCdrqLI2iTCGERmlAwKtr-PKtvDh_rs2vKD891-ixvo,189
31
- novel_downloader/core/downloaders/qidian/qidian_sync.py,sha256=1dvw7b82Hgo4bEPgLNb5gNJhtsWgGkcNEe3iR9jKa4U,8201
31
+ novel_downloader/core/downloaders/qidian/qidian_sync.py,sha256=7W0WKB7hh-k5iaz0zsoFgimU1VCEv2_nA1oqvgnli6Q,7845
32
32
  novel_downloader/core/downloaders/sfacg/__init__.py,sha256=GB9jof4Mlr9av-NWSh-3-fWf-KqZm9jBJXVFS4z-Uxs,265
33
33
  novel_downloader/core/downloaders/sfacg/sfacg_async.py,sha256=4XWW7rSCYO7o4wzhHSz_Oq14zPj3BW-WZn-rasNLpIA,699
34
34
  novel_downloader/core/downloaders/sfacg/sfacg_sync.py,sha256=G0cfA-CLWMtQ_C71ISjNbI9I8EBmvpZyeE5X9b7OXJQ,680
@@ -55,7 +55,7 @@ novel_downloader/core/parsers/common/__init__.py,sha256=MzNUUxvf7jmeO0LvQ_FRW0QL
55
55
  novel_downloader/core/parsers/common/helper.py,sha256=SSNES1AlGu5LqPXN61Rp6SmVgR4oKaI2gvLJfEYBsF4,12054
56
56
  novel_downloader/core/parsers/common/main_parser.py,sha256=c3n6byn_zRg9ROH3DpNl0J-2Dgw7y9OekXeQVDxnO0c,3170
57
57
  novel_downloader/core/parsers/esjzone/__init__.py,sha256=RSsUdOvaiqv-rTaYVc-qO25jytRCj9X2EYErMltA_b8,177
58
- novel_downloader/core/parsers/esjzone/main_parser.py,sha256=GO4DH6LjEG9ptnT5quj1fz3djZOgbzgaTdM41IRueYI,8231
58
+ novel_downloader/core/parsers/esjzone/main_parser.py,sha256=Uw6lhLf9-uEGVtoudLN0-1C3mwTLUiH2D7zoHGRH4Rs,8294
59
59
  novel_downloader/core/parsers/qianbi/__init__.py,sha256=CNmoER8U2u4-ix5S0DDq-pHTtkLR0IZf2SLaTTYXee4,173
60
60
  novel_downloader/core/parsers/qianbi/main_parser.py,sha256=AebvhngCq62S8LZX4vKu40U--V4cvvggHxGKFO5dkVI,4776
61
61
  novel_downloader/core/parsers/qidian/__init__.py,sha256=-KvMBzggUaj5zeZKFpVbbx5GRJdykwaFPfxJc4KzF-Q,484
@@ -104,7 +104,7 @@ novel_downloader/core/requesters/yamibo/__init__.py,sha256=_bhw5RZ7-X8P9X5U3rteM
104
104
  novel_downloader/core/requesters/yamibo/async_session.py,sha256=ezANq7EQaFNUYgvy59g26mqC0TbaaYLziLFQmbcl8pU,6881
105
105
  novel_downloader/core/requesters/yamibo/session.py,sha256=_QUboNnJOipDLm9o7E6ZKt-D68I3r40B6VX8fQmNeOQ,7437
106
106
  novel_downloader/core/savers/__init__.py,sha256=QFsxESFvTx6yaW3dgLwBBkrpydroVOlfgOnhGnVjsmo,720
107
- novel_downloader/core/savers/base.py,sha256=Kx-gPZwnqMGCV2u_tO6-cH0zx640RfF9rF_qeipQxOw,5568
107
+ novel_downloader/core/savers/base.py,sha256=UUTirwqU16IoUamWwvs2VOtwN2elkXBXsyacVtcSDrw,5445
108
108
  novel_downloader/core/savers/biquge.py,sha256=8kZ6hcXuXmaizaKKytjy3LIGX5kMJRwgUJdhYzH5ajE,445
109
109
  novel_downloader/core/savers/esjzone.py,sha256=WNIgIEOfW0_fstTCgtcTAZF3YZmDqSVxIQI5-veTPZ4,450
110
110
  novel_downloader/core/savers/qianbi.py,sha256=wVxR3WJf1dqLCgbaAnXU1q1LOAnjYHNE9bQX5Oql4us,445
@@ -112,14 +112,15 @@ novel_downloader/core/savers/qidian.py,sha256=HtIz2SHLL-BZbVuahu6voKpgWtlli14_YY
112
112
  novel_downloader/core/savers/sfacg.py,sha256=LeiVyIIeoBdHmR3BZlXn4OaQzn9psqXC0FQkBb6VF-c,440
113
113
  novel_downloader/core/savers/yamibo.py,sha256=aMfp1cu6PGEUcexvYR8QwB3PzJDa4p4dQTT1rZ6RzIk,445
114
114
  novel_downloader/core/savers/common/__init__.py,sha256=V5EbaRwmdXlQKi4Ce9SLstl0c9x9yTTRUB23rXFCvXw,256
115
- novel_downloader/core/savers/common/epub.py,sha256=ZmTt3kAODa8lQKpgegbJY_7qcKQmoVQ8h5JPZYBSJbI,6370
116
- novel_downloader/core/savers/common/main_saver.py,sha256=O-XFfDAlOun0kSBBHEIktwUcm47CsXgmFOkyK3fP_1M,3826
117
- novel_downloader/core/savers/common/txt.py,sha256=nsYQp3-xti5uAj3r736ZKDg0gzrme2YoQ8bUTOG2jHk,4853
118
- novel_downloader/core/savers/epub_utils/__init__.py,sha256=ZzOOsuHL9O0CsE9xl0vAcvxxlBz4AkDVOlTnsfNGN7Q,714
119
- novel_downloader/core/savers/epub_utils/css_builder.py,sha256=sa8t72YNKkGeqC6dflXz1iefcpUqZ6t80VVZE91XfNw,2087
120
- novel_downloader/core/savers/epub_utils/initializer.py,sha256=6qId5UTSjXnqSs9S9XrWGELAgq4fNut71CYIq7b6LQE,3199
121
- novel_downloader/core/savers/epub_utils/text_to_html.py,sha256=omqob2yKFYru2dvHTQuGVQH9RH_VQXxMSopzwd_-6_I,4087
122
- novel_downloader/core/savers/epub_utils/volume_intro.py,sha256=aJAdEU2YvYkX2sqnU5VkZ33XRfb5x083GdDmDXcQpUI,1771
115
+ novel_downloader/core/savers/common/epub.py,sha256=_m1_M5Q6wlwRZsTvSzEeucUQXUllC8mg0KLvlByTVug,6260
116
+ novel_downloader/core/savers/common/main_saver.py,sha256=8t8Ql22P-ufB4k2bgxFfNHwHWpNW7T1FzfLeWkkWigU,3928
117
+ novel_downloader/core/savers/common/txt.py,sha256=wn3L12Rxioe3kZzzDcDWKxMKdkgkkY4BVlhhTXNe-as,4825
118
+ novel_downloader/core/savers/epub_utils/__init__.py,sha256=E0FDYMy1JsWoaywY9YYhPhhoa2Ei_Nf1Syu3hRiZ5uc,920
119
+ novel_downloader/core/savers/epub_utils/css_builder.py,sha256=YtLqyvzz3PaQCvdQPaxnxp3kJiT21FUufsJEOeP2Duo,2139
120
+ novel_downloader/core/savers/epub_utils/image_loader.py,sha256=Dg72e5Eje3orclJIBgLaqKKJgZdLB0F2zDB_0QIJTy8,2608
121
+ novel_downloader/core/savers/epub_utils/initializer.py,sha256=kFE7QhVqokh5_gsRqnMZ48gVTIftrZwH9UEs1mRKvvY,3251
122
+ novel_downloader/core/savers/epub_utils/text_to_html.py,sha256=pVQv4r5N6KAfQflnSUz7dapt9DzXZOXPJ3XFZXeNUiE,5609
123
+ novel_downloader/core/savers/epub_utils/volume_intro.py,sha256=y91JCIuvUGn6V7cF5UpTmBgL8IwhoUGBvQs8enaooZs,1824
123
124
  novel_downloader/locales/en.json,sha256=jxo_hdwRPnWFHLWNkqz5QPMVD7l_JHHwBrYDinWPSTg,5991
124
125
  novel_downloader/locales/zh.json,sha256=64Dujc70etkWq7bR4DuQOtZ-VdoynH8HJl8E8hxV1B4,5864
125
126
  novel_downloader/resources/config/rules.toml,sha256=hrED6h3Z3cjSY5hRPQhp4TFAU5QXnN9xHfVABOJQNrM,4979
@@ -133,17 +134,17 @@ novel_downloader/resources/text/blacklist.txt,sha256=sovK9JgARZP3lud5b1EZgvv8LSV
133
134
  novel_downloader/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
135
  novel_downloader/utils/cache.py,sha256=_jVEWAaMGY86kWVnAuuIvJA_hA8dH2ClI7o3p99mEvE,618
135
136
  novel_downloader/utils/chapter_storage.py,sha256=bbQ6mLvIzhWNUypIcgg6-nPo1_fIgGCOyQ3eejuvcK4,10100
136
- novel_downloader/utils/constants.py,sha256=7qr6_w7bM-Xf-Pz6ZFgamsR9JNl7RgFNmHvSWN2fcgg,5507
137
+ novel_downloader/utils/constants.py,sha256=nUZFjbkvUV-kHryzAKhNiKpPud67tlzyH47uqQRbSV0,5619
137
138
  novel_downloader/utils/crypto_utils.py,sha256=9uMNpg0rAWaoD3YsqDucFajPjoQuwEfeZDInUBISuSI,4166
138
139
  novel_downloader/utils/hash_store.py,sha256=pFg2SzPF9sZqezNx-24jSDvYw4YMSBOpiIjyUbZx_r8,9739
139
140
  novel_downloader/utils/hash_utils.py,sha256=oaXICmANsqImXW1qNSVmVQbMfNbx35FHe7EHL2VT2tM,2974
140
141
  novel_downloader/utils/i18n.py,sha256=pdAcSIA5Tp-uPEBwNByHL7C1NayTnpOsl7zFv9p2G1k,1033
141
142
  novel_downloader/utils/logger.py,sha256=n5xggOejtNdZv2X9f4Cq8weOaT1KG0n0-im2LIYycu0,3377
142
143
  novel_downloader/utils/model_loader.py,sha256=JKgRFrr4HlAW9zuDUBAuuo_Kk_T_g9dWiU8E3zYk0vo,1996
143
- novel_downloader/utils/network.py,sha256=2VX33jo11tzHSqvinpd0VoXDkXK_uxdxqaq50ndWXXg,8622
144
+ novel_downloader/utils/network.py,sha256=163lRigwVEgZ9nozxHZVjxMKt-9s5j8s3mBJAnOwR4c,9089
144
145
  novel_downloader/utils/state.py,sha256=FcNJ85GvBu7uEIjy0QHGr4sXMbHPEMkCjwUKNg5EabI,5132
145
146
  novel_downloader/utils/file_utils/__init__.py,sha256=zvOm2qSEmWd_mRGJceGBZb5MYMSDAlWYjS5MkVQNZgI,1159
146
- novel_downloader/utils/file_utils/io.py,sha256=73uRrr41_Obqy9HMFBnvcOcApMZYBNi4KWT4DpskSk8,7472
147
+ novel_downloader/utils/file_utils/io.py,sha256=AZ3NUe6lifGsYt3iYyXyQ2BO41WV8j013LpZy4KSjJQ,7473
147
148
  novel_downloader/utils/file_utils/normalize.py,sha256=MrsCq4FqmskKRkHRV_J0z0dmn69OerMum-9sqx2XOGM,2023
148
149
  novel_downloader/utils/file_utils/sanitize.py,sha256=rE-u4vpDL10zH8FT8d9wqwWsz-7dR6PJ-LE45K8VaeE,2112
149
150
  novel_downloader/utils/fontocr/__init__.py,sha256=fe-04om3xxBvFKt5BBCApXCzv-Z0K_AY7lv9IB1jEHM,543
@@ -156,10 +157,10 @@ novel_downloader/utils/text_utils/font_mapping.py,sha256=ePpNLSrIEwIpqpnPZBMODj-
156
157
  novel_downloader/utils/text_utils/text_cleaning.py,sha256=KQhTGKiSX-eBVmjSpggxtf1hSjQLTu3cIjt65Ir4SWs,1632
157
158
  novel_downloader/utils/time_utils/__init__.py,sha256=725vY2PvqFhjbAz0hCOuIuhSCK8HrEqQ_k3YwvubmXo,624
158
159
  novel_downloader/utils/time_utils/datetime_utils.py,sha256=u8jiC1NHzn2jKpuatMlbBhTEZnZ-8nAC_yY7-hJ-Yws,4936
159
- novel_downloader/utils/time_utils/sleep_utils.py,sha256=PkG1OUs6V8tDGg3xWgh1yRpot8tPC6W5xYTjaqKY37I,3229
160
- novel_downloader-1.3.2.dist-info/licenses/LICENSE,sha256=XgmnH0mBf-qEiizoVAfJQAKzPB9y3rBa-ni7M0Aqv4A,1066
161
- novel_downloader-1.3.2.dist-info/METADATA,sha256=Wiycc-7wEaLskTP70uY5UWMJT-8jSm72KMsbdszeeWU,6739
162
- novel_downloader-1.3.2.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
163
- novel_downloader-1.3.2.dist-info/entry_points.txt,sha256=v23QrJrfrAcYpxUYslCVxubOVRRTaTw7vlG_tfMsFP8,65
164
- novel_downloader-1.3.2.dist-info/top_level.txt,sha256=hP4jYWM2LTm1jxsW4hqEB8N0dsRvldO2QdhggJT917I,17
165
- novel_downloader-1.3.2.dist-info/RECORD,,
160
+ novel_downloader/utils/time_utils/sleep_utils.py,sha256=JQ-GH_jvjojcNAcq5N8TnRvsCwOmd_4bltPtYBSqgRw,3231
161
+ novel_downloader-1.3.3.dist-info/licenses/LICENSE,sha256=XgmnH0mBf-qEiizoVAfJQAKzPB9y3rBa-ni7M0Aqv4A,1066
162
+ novel_downloader-1.3.3.dist-info/METADATA,sha256=i9zmpPImOvOAzQpuduWZQ5MAk3OpZsno688d2CuUQFM,6700
163
+ novel_downloader-1.3.3.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
164
+ novel_downloader-1.3.3.dist-info/entry_points.txt,sha256=v23QrJrfrAcYpxUYslCVxubOVRRTaTw7vlG_tfMsFP8,65
165
+ novel_downloader-1.3.3.dist-info/top_level.txt,sha256=hP4jYWM2LTm1jxsW4hqEB8N0dsRvldO2QdhggJT917I,17
166
+ novel_downloader-1.3.3.dist-info/RECORD,,