novel-downloader 1.4.1__py3-none-any.whl → 1.4.2__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/cli/download.py +69 -10
- novel_downloader/config/adapter.py +42 -9
- novel_downloader/core/downloaders/base.py +26 -22
- novel_downloader/core/downloaders/common.py +41 -5
- novel_downloader/core/downloaders/qidian.py +60 -32
- novel_downloader/core/exporters/common/epub.py +153 -68
- novel_downloader/core/exporters/epub_util.py +1358 -0
- novel_downloader/core/exporters/linovelib/epub.py +147 -190
- novel_downloader/core/fetchers/qidian/browser.py +62 -10
- novel_downloader/core/interfaces/downloader.py +13 -12
- novel_downloader/locales/en.json +2 -0
- novel_downloader/locales/zh.json +2 -0
- novel_downloader/models/__init__.py +2 -0
- novel_downloader/models/config.py +8 -0
- novel_downloader/tui/screens/home.py +5 -4
- novel_downloader/utils/constants.py +0 -29
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/METADATA +4 -2
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/RECORD +23 -28
- novel_downloader/core/exporters/epub_utils/__init__.py +0 -40
- novel_downloader/core/exporters/epub_utils/css_builder.py +0 -75
- novel_downloader/core/exporters/epub_utils/image_loader.py +0 -131
- novel_downloader/core/exporters/epub_utils/initializer.py +0 -100
- novel_downloader/core/exporters/epub_utils/text_to_html.py +0 -178
- novel_downloader/core/exporters/epub_utils/volume_intro.py +0 -60
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/WHEEL +0 -0
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/entry_points.txt +0 -0
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.4.1.dist-info → novel_downloader-1.4.2.dist-info}/top_level.txt +0 -0
@@ -1,178 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.exporters.epub_utils.text_to_html
|
4
|
-
-------------------------------------------------------
|
5
|
-
|
6
|
-
Module for converting raw chapter text to formatted HTML,
|
7
|
-
with automatic word correction and optional image/tag support.
|
8
|
-
"""
|
9
|
-
|
10
|
-
import json
|
11
|
-
import logging
|
12
|
-
import re
|
13
|
-
from pathlib import Path
|
14
|
-
from typing import Any
|
15
|
-
|
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
|
21
|
-
from novel_downloader.utils.text_utils import diff_inline_display
|
22
|
-
|
23
|
-
logger = logging.getLogger(__name__)
|
24
|
-
|
25
|
-
_IMG_TAG_PATTERN = re.compile(
|
26
|
-
r'<img\s+[^>]*src=[\'"]([^\'"]+)[\'"][^>]*>', re.IGNORECASE
|
27
|
-
)
|
28
|
-
|
29
|
-
|
30
|
-
# Load and sort replacement map from JSON
|
31
|
-
try:
|
32
|
-
replace_map_raw = REPLACE_WORD_MAP_PATH.read_text(encoding="utf-8")
|
33
|
-
REPLACE_WORDS_MAP = json.loads(replace_map_raw)
|
34
|
-
REPLACE_WORDS_MAP = dict(
|
35
|
-
sorted(REPLACE_WORDS_MAP.items(), key=lambda x: len(x[0]), reverse=True)
|
36
|
-
)
|
37
|
-
except Exception as e:
|
38
|
-
REPLACE_WORDS_MAP = {}
|
39
|
-
logger.info(
|
40
|
-
f"[epub] Failed to load REPLACE_WORDS_MAP from {REPLACE_WORD_MAP_PATH}: {e}"
|
41
|
-
)
|
42
|
-
|
43
|
-
|
44
|
-
def _check_and_correct_words(txt_str: str) -> str:
|
45
|
-
"""
|
46
|
-
Perform word replacement using REPLACE_WORDS_MAP.
|
47
|
-
|
48
|
-
:param txt_str: Raw string of text.
|
49
|
-
:return: String with corrected words.
|
50
|
-
"""
|
51
|
-
for k, v in REPLACE_WORDS_MAP.items():
|
52
|
-
txt_str = txt_str.replace(k, v)
|
53
|
-
return txt_str
|
54
|
-
|
55
|
-
|
56
|
-
def chapter_txt_to_html(
|
57
|
-
chapter_title: str,
|
58
|
-
chapter_text: str,
|
59
|
-
author_say: str,
|
60
|
-
) -> str:
|
61
|
-
"""
|
62
|
-
Convert chapter text and author note to styled HTML.
|
63
|
-
|
64
|
-
:param chapter_title: Title of the chapter.
|
65
|
-
:param chapter_text: Main content of the chapter.
|
66
|
-
:param author_say: Optional author note content.
|
67
|
-
:return: Rendered HTML as a string.
|
68
|
-
"""
|
69
|
-
|
70
|
-
def _render_lines(text: str) -> str:
|
71
|
-
parts = []
|
72
|
-
for line in text.strip().splitlines():
|
73
|
-
line = line.strip()
|
74
|
-
if not line:
|
75
|
-
continue
|
76
|
-
|
77
|
-
if (
|
78
|
-
line.startswith("<img")
|
79
|
-
and line.endswith("/>")
|
80
|
-
or line.startswith('<div class="duokan-image-single illus">')
|
81
|
-
and line.endswith("</div>")
|
82
|
-
):
|
83
|
-
parts.append(line)
|
84
|
-
else:
|
85
|
-
corrected = _check_and_correct_words(line)
|
86
|
-
if corrected != line:
|
87
|
-
diff = diff_inline_display(line, corrected)
|
88
|
-
logger.info("[epub] Correction diff:\n%s", diff)
|
89
|
-
parts.append(f"<p>{corrected}</p>")
|
90
|
-
return "\n".join(parts)
|
91
|
-
|
92
|
-
html_parts = [f"<h2>{chapter_title}</h2>"]
|
93
|
-
html_parts.append(_render_lines(chapter_text))
|
94
|
-
|
95
|
-
if author_say.strip():
|
96
|
-
html_parts.extend(["<hr />", "<p>作者说:</p>", _render_lines(author_say)])
|
97
|
-
|
98
|
-
return "\n".join(html_parts)
|
99
|
-
|
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
|
-
|
137
|
-
def generate_book_intro_html(book_info: dict[str, Any]) -> str:
|
138
|
-
"""
|
139
|
-
Generate HTML string for a book's information and summary.
|
140
|
-
|
141
|
-
This function takes a dictionary containing book details and formats
|
142
|
-
it into a styled HTML block, skipping any missing fields gracefully.
|
143
|
-
|
144
|
-
:param book_info: A dictionary containing keys like 'book_name'...
|
145
|
-
|
146
|
-
:return: An HTML-formatted string presenting the book's information.
|
147
|
-
"""
|
148
|
-
book_name = book_info.get("book_name")
|
149
|
-
author = book_info.get("author")
|
150
|
-
serial_status = book_info.get("serial_status")
|
151
|
-
word_count = book_info.get("word_count")
|
152
|
-
summary = book_info.get("summary", "").strip()
|
153
|
-
|
154
|
-
# Start composing the HTML output
|
155
|
-
html_parts = ["<h1>书籍简介</h1>", '<div class="list">', "<ul>"]
|
156
|
-
|
157
|
-
if book_name:
|
158
|
-
html_parts.append(f"<li>书名: 《{book_name}》</li>")
|
159
|
-
if author:
|
160
|
-
html_parts.append(f"<li>作者: {author}</li>")
|
161
|
-
|
162
|
-
if word_count:
|
163
|
-
html_parts.append(f"<li>字数: {word_count}</li>")
|
164
|
-
if serial_status:
|
165
|
-
html_parts.append(f"<li>状态: {serial_status}</li>")
|
166
|
-
|
167
|
-
html_parts.append("</ul>")
|
168
|
-
html_parts.append("</div>")
|
169
|
-
html_parts.append('<p class="new-page-after"><br/></p>')
|
170
|
-
|
171
|
-
if summary:
|
172
|
-
html_parts.append("<h2>简介</h2>")
|
173
|
-
for paragraph in summary.split("\n"):
|
174
|
-
paragraph = paragraph.strip()
|
175
|
-
if paragraph:
|
176
|
-
html_parts.append(f"<p>{paragraph}</p>")
|
177
|
-
|
178
|
-
return "\n".join(html_parts)
|
@@ -1,60 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.exporters.epub_utils.volume_intro
|
4
|
-
-------------------------------------------------------
|
5
|
-
|
6
|
-
Responsible for generating HTML code for volume introduction pages,
|
7
|
-
including two style variants and a unified entry point.
|
8
|
-
"""
|
9
|
-
|
10
|
-
|
11
|
-
from novel_downloader.utils.constants import EPUB_IMAGE_FOLDER
|
12
|
-
|
13
|
-
|
14
|
-
def split_volume_title(volume_title: str) -> tuple[str, str]:
|
15
|
-
"""
|
16
|
-
Split volume title into two parts for better display.
|
17
|
-
|
18
|
-
:param volume_title: Original volume title string.
|
19
|
-
:return: Tuple of (line1, line2)
|
20
|
-
"""
|
21
|
-
if " " in volume_title:
|
22
|
-
parts = volume_title.split(" ")
|
23
|
-
elif "-" in volume_title:
|
24
|
-
parts = volume_title.split("-")
|
25
|
-
else:
|
26
|
-
return "", volume_title
|
27
|
-
|
28
|
-
return parts[0], "".join(parts[1:])
|
29
|
-
|
30
|
-
|
31
|
-
def create_volume_intro(volume_title: str, volume_intro_text: str = "") -> str:
|
32
|
-
"""
|
33
|
-
Generate the HTML snippet for a volume's introduction section.
|
34
|
-
|
35
|
-
:param volume_title: Title of the volume.
|
36
|
-
:param volume_intro_text: Optional introduction text for the volume.
|
37
|
-
:return: HTML string representing the volume's intro section.
|
38
|
-
"""
|
39
|
-
line1, line2 = split_volume_title(volume_title)
|
40
|
-
|
41
|
-
def make_border_img(class_name: str) -> str:
|
42
|
-
return (
|
43
|
-
f'<div class="{class_name}">'
|
44
|
-
f'<img alt="" class="{class_name}" '
|
45
|
-
f'src="../{EPUB_IMAGE_FOLDER}/volume_border.png" />'
|
46
|
-
f"</div>"
|
47
|
-
)
|
48
|
-
|
49
|
-
html_parts = [make_border_img("border1")]
|
50
|
-
|
51
|
-
if line1:
|
52
|
-
html_parts.append(f'<h1 class="volume-title-line1">{line1}</h1>')
|
53
|
-
|
54
|
-
html_parts.append(f'<p class="volume-title-line2">{line2}</p>')
|
55
|
-
html_parts.append(make_border_img("border2"))
|
56
|
-
|
57
|
-
if volume_intro_text:
|
58
|
-
html_parts.append(f'<p class="intro">{volume_intro_text}</p>')
|
59
|
-
|
60
|
-
return "\n".join(html_parts)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|