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.
- novel_downloader/__init__.py +1 -1
- novel_downloader/core/downloaders/common/common_async.py +0 -8
- novel_downloader/core/downloaders/common/common_sync.py +0 -8
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -8
- novel_downloader/core/parsers/esjzone/main_parser.py +4 -3
- novel_downloader/core/savers/base.py +2 -7
- novel_downloader/core/savers/common/epub.py +21 -33
- novel_downloader/core/savers/common/main_saver.py +3 -1
- novel_downloader/core/savers/common/txt.py +1 -2
- novel_downloader/core/savers/epub_utils/__init__.py +14 -5
- novel_downloader/core/savers/epub_utils/css_builder.py +1 -0
- novel_downloader/core/savers/epub_utils/image_loader.py +89 -0
- novel_downloader/core/savers/epub_utils/initializer.py +1 -0
- novel_downloader/core/savers/epub_utils/text_to_html.py +48 -1
- novel_downloader/core/savers/epub_utils/volume_intro.py +1 -0
- novel_downloader/utils/constants.py +4 -0
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/network.py +51 -38
- novel_downloader/utils/time_utils/sleep_utils.py +2 -2
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/METADATA +14 -14
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/RECORD +25 -24
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/WHEEL +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.3.3.dist-info}/top_level.txt +0 -0
novel_downloader/__init__.py
CHANGED
@@ -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
|
-
|
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.
|
43
|
+
self._base_raw_data_dir = Path(config.raw_data_dir)
|
44
44
|
self._output_dir = Path(config.output_dir)
|
45
|
-
self.
|
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.
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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=
|
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.
|
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.
|
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 (
|
10
|
-
-
|
11
|
-
-
|
12
|
-
-
|
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
|
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
|
]
|
@@ -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.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
|
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.
|
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
|
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
|
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
|
-
) ->
|
96
|
+
) -> Path | None:
|
96
97
|
"""
|
97
|
-
Download an image from
|
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:
|
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:
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
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
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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**
|
79
|
-
-
|
78
|
+
**novel-downloader** 支持多种小说网站的章节抓取与合并导出,
|
79
|
+
- **轻量化抓取**: 绝大多数站点仅依赖 `requests` 实现 HTTP 请求, 无需额外浏览器驱动
|
80
80
|
- 对于起点中文网 (Qidian), 可在配置中选择:
|
81
81
|
- `mode: session` : 纯 Requests 模式
|
82
|
-
- `mode: browser` : 基于 DrissionPage 驱动 Chrome 的浏览器模式 (可处理更复杂的 JS/加密)。
|
83
|
-
-
|
84
|
-
-
|
85
|
-
-
|
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=
|
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=
|
23
|
-
novel_downloader/core/downloaders/common/common_sync.py,sha256=
|
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=
|
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=
|
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=
|
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=
|
116
|
-
novel_downloader/core/savers/common/main_saver.py,sha256=
|
117
|
-
novel_downloader/core/savers/common/txt.py,sha256=
|
118
|
-
novel_downloader/core/savers/epub_utils/__init__.py,sha256=
|
119
|
-
novel_downloader/core/savers/epub_utils/css_builder.py,sha256=
|
120
|
-
novel_downloader/core/savers/epub_utils/
|
121
|
-
novel_downloader/core/savers/epub_utils/
|
122
|
-
novel_downloader/core/savers/epub_utils/
|
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=
|
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=
|
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=
|
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=
|
160
|
-
novel_downloader-1.3.
|
161
|
-
novel_downloader-1.3.
|
162
|
-
novel_downloader-1.3.
|
163
|
-
novel_downloader-1.3.
|
164
|
-
novel_downloader-1.3.
|
165
|
-
novel_downloader-1.3.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|