novel-downloader 1.4.0__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.
Files changed (31) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +69 -10
  3. novel_downloader/config/adapter.py +42 -9
  4. novel_downloader/core/downloaders/base.py +26 -22
  5. novel_downloader/core/downloaders/common.py +41 -5
  6. novel_downloader/core/downloaders/qidian.py +60 -32
  7. novel_downloader/core/exporters/common/epub.py +153 -68
  8. novel_downloader/core/exporters/epub_util.py +1358 -0
  9. novel_downloader/core/exporters/linovelib/epub.py +147 -190
  10. novel_downloader/core/fetchers/linovelib/browser.py +15 -0
  11. novel_downloader/core/fetchers/linovelib/session.py +15 -0
  12. novel_downloader/core/fetchers/qidian/browser.py +62 -10
  13. novel_downloader/core/interfaces/downloader.py +13 -12
  14. novel_downloader/locales/en.json +2 -0
  15. novel_downloader/locales/zh.json +2 -0
  16. novel_downloader/models/__init__.py +2 -0
  17. novel_downloader/models/config.py +8 -0
  18. novel_downloader/tui/screens/home.py +5 -4
  19. novel_downloader/utils/constants.py +0 -29
  20. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/METADATA +4 -2
  21. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/RECORD +25 -30
  22. novel_downloader/core/exporters/epub_utils/__init__.py +0 -40
  23. novel_downloader/core/exporters/epub_utils/css_builder.py +0 -75
  24. novel_downloader/core/exporters/epub_utils/image_loader.py +0 -131
  25. novel_downloader/core/exporters/epub_utils/initializer.py +0 -100
  26. novel_downloader/core/exporters/epub_utils/text_to_html.py +0 -178
  27. novel_downloader/core/exporters/epub_utils/volume_intro.py +0 -60
  28. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/WHEEL +0 -0
  29. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/entry_points.txt +0 -0
  30. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/licenses/LICENSE +0 -0
  31. {novel_downloader-1.4.0.dist-info → novel_downloader-1.4.2.dist-info}/top_level.txt +0 -0
@@ -1,100 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.epub_utils.initializer
4
- ------------------------------------------------------
5
-
6
- Initializes an epub.EpubBook object, sets metadata
7
- (identifier, title, author, language, description),
8
- adds a cover, and prepares the initial spine and TOC entries.
9
- """
10
-
11
- import logging
12
- from pathlib import Path
13
- from typing import Any
14
-
15
- from ebooklib import epub
16
-
17
- from novel_downloader.utils.constants import (
18
- EPUB_IMAGE_FOLDER,
19
- EPUB_TEXT_FOLDER,
20
- VOLUME_BORDER_IMAGE_PATH,
21
- )
22
-
23
- logger = logging.getLogger(__name__)
24
-
25
-
26
- def init_epub(
27
- book_info: dict[str, Any],
28
- book_id: str,
29
- intro_html: str,
30
- book_cover_path: Path | None = None,
31
- include_toc: bool = False,
32
- ) -> tuple[epub.EpubBook, list[Any], list[Any]]:
33
- """
34
- Initialize an EPUB book with metadata, optional cover, and intro page.
35
-
36
- :param book_info: Dict with keys 'book_name', 'author', 'summary'.
37
- :param book_id: Book identifier (numeric or string).
38
- :param intro_html: Intro content in XHTML format.
39
- :param book_cover_path: Optional Path to the cover image file.
40
- :param include_toc: Whether to include the <nav> item in the spine.
41
- :return: (book, spine, toc_list)
42
- """
43
- book = epub.EpubBook()
44
- book.set_identifier(str(book_id))
45
- book_name = book_info.get("book_name") or book_info.get("volume_name", "未找到书名")
46
- book.set_title(book_name)
47
- book.set_language("zh-CN")
48
- book.add_author(book_info.get("author", "未找到作者"))
49
- desc = book_info.get("summary") or book_info.get("volume_intro", "未找到作品简介")
50
- book.add_metadata("DC", "description", desc)
51
-
52
- spine = []
53
-
54
- # cover
55
- if book_cover_path:
56
- try:
57
- cover_bytes = book_cover_path.read_bytes()
58
- ext = book_cover_path.suffix.lower()
59
- ext = ext if ext in [".jpg", ".jpeg", ".png"] else ".jpeg"
60
- filename = f"{EPUB_IMAGE_FOLDER}/cover{ext}"
61
- book.set_cover(filename, cover_bytes)
62
- spine.append("cover")
63
- except FileNotFoundError:
64
- logger.info(f"[epub] 封面图片不存在: {book_cover_path}")
65
- except Exception as e:
66
- logger.info(f"[epub] 读取封面失败: {book_cover_path},错误:{e}")
67
-
68
- # 导航菜单
69
- if include_toc:
70
- spine.append("nav")
71
-
72
- # 简介页面
73
- intro = epub.EpubHtml(
74
- title="书籍简介",
75
- file_name=f"{EPUB_TEXT_FOLDER}/intro.xhtml",
76
- lang="zh-CN",
77
- uid="intro",
78
- )
79
- intro.content = intro_html
80
- intro.add_link(href="../Styles/main.css", rel="stylesheet", type="text/css")
81
- book.add_item(intro)
82
- spine.append(intro)
83
-
84
- # 添加卷边框图像 (volume_border.png)
85
- try:
86
- border_bytes = VOLUME_BORDER_IMAGE_PATH.read_bytes()
87
- border_item = epub.EpubItem(
88
- uid="volume-border",
89
- file_name=f"{EPUB_IMAGE_FOLDER}/volume_border.png",
90
- media_type="image/png",
91
- content=border_bytes,
92
- )
93
- book.add_item(border_item)
94
- except FileNotFoundError:
95
- logger.info(f"[epub] 卷边框图片不存在: {VOLUME_BORDER_IMAGE_PATH}")
96
- except Exception as e:
97
- logger.info(f"[epub] 读取卷边框失败: {VOLUME_BORDER_IMAGE_PATH}: {e}")
98
-
99
- toc_list = [epub.Link(intro.file_name, "书籍简介", intro.id)]
100
- return book, spine, toc_list
@@ -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)