novel-downloader 2.0.1__py3-none-any.whl → 2.0.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 (104) hide show
  1. novel_downloader/__init__.py +1 -1
  2. novel_downloader/cli/download.py +11 -8
  3. novel_downloader/cli/export.py +17 -17
  4. novel_downloader/cli/ui.py +28 -1
  5. novel_downloader/config/adapter.py +27 -1
  6. novel_downloader/core/archived/deqixs/fetcher.py +1 -28
  7. novel_downloader/core/downloaders/__init__.py +2 -0
  8. novel_downloader/core/downloaders/base.py +34 -85
  9. novel_downloader/core/downloaders/common.py +147 -171
  10. novel_downloader/core/downloaders/qianbi.py +30 -64
  11. novel_downloader/core/downloaders/qidian.py +157 -184
  12. novel_downloader/core/downloaders/qqbook.py +292 -0
  13. novel_downloader/core/downloaders/registry.py +2 -2
  14. novel_downloader/core/exporters/__init__.py +2 -0
  15. novel_downloader/core/exporters/base.py +37 -59
  16. novel_downloader/core/exporters/common.py +620 -0
  17. novel_downloader/core/exporters/linovelib.py +47 -0
  18. novel_downloader/core/exporters/qidian.py +41 -12
  19. novel_downloader/core/exporters/qqbook.py +28 -0
  20. novel_downloader/core/exporters/registry.py +2 -2
  21. novel_downloader/core/fetchers/__init__.py +4 -2
  22. novel_downloader/core/fetchers/aaatxt.py +2 -22
  23. novel_downloader/core/fetchers/b520.py +3 -23
  24. novel_downloader/core/fetchers/base.py +80 -105
  25. novel_downloader/core/fetchers/biquyuedu.py +2 -22
  26. novel_downloader/core/fetchers/dxmwx.py +10 -22
  27. novel_downloader/core/fetchers/esjzone.py +6 -29
  28. novel_downloader/core/fetchers/guidaye.py +2 -22
  29. novel_downloader/core/fetchers/hetushu.py +9 -29
  30. novel_downloader/core/fetchers/i25zw.py +2 -16
  31. novel_downloader/core/fetchers/ixdzs8.py +2 -16
  32. novel_downloader/core/fetchers/jpxs123.py +2 -16
  33. novel_downloader/core/fetchers/lewenn.py +2 -22
  34. novel_downloader/core/fetchers/linovelib.py +4 -20
  35. novel_downloader/core/fetchers/{eightnovel.py → n8novel.py} +12 -40
  36. novel_downloader/core/fetchers/piaotia.py +2 -16
  37. novel_downloader/core/fetchers/qbtr.py +2 -16
  38. novel_downloader/core/fetchers/qianbi.py +1 -20
  39. novel_downloader/core/fetchers/qidian.py +7 -33
  40. novel_downloader/core/fetchers/qqbook.py +177 -0
  41. novel_downloader/core/fetchers/quanben5.py +9 -29
  42. novel_downloader/core/fetchers/rate_limiter.py +22 -53
  43. novel_downloader/core/fetchers/sfacg.py +3 -16
  44. novel_downloader/core/fetchers/shencou.py +2 -16
  45. novel_downloader/core/fetchers/shuhaige.py +2 -22
  46. novel_downloader/core/fetchers/tongrenquan.py +2 -22
  47. novel_downloader/core/fetchers/ttkan.py +3 -14
  48. novel_downloader/core/fetchers/wanbengo.py +2 -22
  49. novel_downloader/core/fetchers/xiaoshuowu.py +2 -16
  50. novel_downloader/core/fetchers/xiguashuwu.py +4 -20
  51. novel_downloader/core/fetchers/xs63b.py +3 -15
  52. novel_downloader/core/fetchers/xshbook.py +2 -22
  53. novel_downloader/core/fetchers/yamibo.py +4 -28
  54. novel_downloader/core/fetchers/yibige.py +13 -26
  55. novel_downloader/core/interfaces/exporter.py +19 -7
  56. novel_downloader/core/interfaces/fetcher.py +21 -47
  57. novel_downloader/core/parsers/__init__.py +4 -2
  58. novel_downloader/core/parsers/b520.py +2 -2
  59. novel_downloader/core/parsers/base.py +4 -39
  60. novel_downloader/core/parsers/{eightnovel.py → n8novel.py} +5 -5
  61. novel_downloader/core/parsers/{qidian/main_parser.py → qidian.py} +147 -266
  62. novel_downloader/core/parsers/qqbook.py +709 -0
  63. novel_downloader/core/parsers/xiguashuwu.py +3 -4
  64. novel_downloader/core/searchers/__init__.py +2 -2
  65. novel_downloader/core/searchers/b520.py +1 -1
  66. novel_downloader/core/searchers/base.py +2 -2
  67. novel_downloader/core/searchers/{eightnovel.py → n8novel.py} +5 -5
  68. novel_downloader/models/__init__.py +2 -0
  69. novel_downloader/models/book.py +1 -0
  70. novel_downloader/models/config.py +12 -0
  71. novel_downloader/resources/config/settings.toml +23 -5
  72. novel_downloader/resources/js_scripts/expr_to_json.js +14 -0
  73. novel_downloader/resources/js_scripts/qidian_decrypt_node.js +21 -16
  74. novel_downloader/resources/js_scripts/qq_decrypt_node.js +92 -0
  75. novel_downloader/utils/constants.py +6 -0
  76. novel_downloader/utils/crypto_utils/aes_util.py +1 -1
  77. novel_downloader/utils/epub/constants.py +1 -6
  78. novel_downloader/utils/fontocr/core.py +2 -0
  79. novel_downloader/utils/fontocr/loader.py +10 -8
  80. novel_downloader/utils/node_decryptor/__init__.py +13 -0
  81. novel_downloader/utils/node_decryptor/decryptor.py +342 -0
  82. novel_downloader/{core/parsers/qidian/utils → utils/node_decryptor}/decryptor_fetcher.py +5 -6
  83. novel_downloader/web/pages/download.py +1 -1
  84. novel_downloader/web/pages/search.py +1 -1
  85. novel_downloader/web/services/task_manager.py +2 -0
  86. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/METADATA +4 -1
  87. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/RECORD +91 -94
  88. novel_downloader/core/exporters/common/__init__.py +0 -11
  89. novel_downloader/core/exporters/common/epub.py +0 -198
  90. novel_downloader/core/exporters/common/main_exporter.py +0 -64
  91. novel_downloader/core/exporters/common/txt.py +0 -146
  92. novel_downloader/core/exporters/epub_util.py +0 -215
  93. novel_downloader/core/exporters/linovelib/__init__.py +0 -11
  94. novel_downloader/core/exporters/linovelib/epub.py +0 -349
  95. novel_downloader/core/exporters/linovelib/main_exporter.py +0 -66
  96. novel_downloader/core/exporters/linovelib/txt.py +0 -139
  97. novel_downloader/core/exporters/txt_util.py +0 -67
  98. novel_downloader/core/parsers/qidian/__init__.py +0 -10
  99. novel_downloader/core/parsers/qidian/utils/__init__.py +0 -11
  100. novel_downloader/core/parsers/qidian/utils/node_decryptor.py +0 -175
  101. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/WHEEL +0 -0
  102. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/entry_points.txt +0 -0
  103. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/licenses/LICENSE +0 -0
  104. {novel_downloader-2.0.1.dist-info → novel_downloader-2.0.2.dist-info}/top_level.txt +0 -0
@@ -1,146 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.common.txt
4
- ------------------------------------------
5
-
6
- Defines `common_export_as_txt` to assemble and export a novel
7
- into a single `.txt` file. Intended for use by `CommonExporter`.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- from pathlib import Path
13
- from typing import TYPE_CHECKING
14
-
15
- from novel_downloader.core.exporters.txt_util import (
16
- build_txt_chapter,
17
- build_txt_header,
18
- )
19
- from novel_downloader.utils import get_cleaner, write_file
20
-
21
- if TYPE_CHECKING:
22
- from .main_exporter import CommonExporter
23
-
24
-
25
- def common_export_as_txt(
26
- exporter: CommonExporter,
27
- book_id: str,
28
- ) -> Path | None:
29
- """
30
- Export a novel as a single text file by merging all chapter data.
31
-
32
- Steps:
33
- 1. Load book metadata.
34
- 2. For each volume:
35
- a. Append the volume title.
36
- b. Batch-fetch all chapters in that volume to minimize SQLite calls.
37
- c. Append each chapter's title, content, and optional author note.
38
- 3. Build a header with book metadata and the latest chapter title.
39
- 4. Concatenate header and all chapter contents.
40
- 5. Save the resulting .txt file to the output directory
41
- (e.g., '{book_name}.txt').
42
-
43
- :param exporter: The CommonExporter instance managing paths and config.
44
- :param book_id: Identifier of the novel (subdirectory under raw data).
45
- """
46
- TAG = "[Exporter]"
47
- # --- Paths & options ---
48
- out_dir = exporter.output_dir
49
- out_dir.mkdir(parents=True, exist_ok=True)
50
- cleaner = get_cleaner(
51
- enabled=exporter._config.clean_text,
52
- config=exporter._config.cleaner_cfg,
53
- )
54
-
55
- # --- Load book_info.json ---
56
- book_info = exporter._load_book_info(book_id)
57
- if not book_info:
58
- return None
59
-
60
- # --- Compile chapters ---
61
- parts: list[str] = []
62
- latest_chapter_title: str = ""
63
-
64
- for vol in book_info.get("volumes", []):
65
- vol_name = cleaner.clean_title(vol.get("volume_name", ""))
66
- if vol_name:
67
- # e.g. "\n\n====== 第三卷 ======\n\n"
68
- parts.append(f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n")
69
- exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
70
-
71
- # Batch-fetch chapters for this volume
72
- chap_ids = [
73
- chap["chapterId"]
74
- for chap in vol.get("chapters", [])
75
- if chap.get("chapterId")
76
- ]
77
- chap_map = exporter._get_chapters(book_id, chap_ids)
78
-
79
- for chap_meta in vol.get("chapters", []):
80
- chap_id = chap_meta.get("chapterId")
81
- if not chap_id:
82
- exporter.logger.warning(
83
- "%s Missing chapterId, skipping: %s", TAG, chap_meta
84
- )
85
- continue
86
-
87
- chap_title = chap_meta.get("title", "")
88
- data = chap_map.get(chap_id)
89
- if not data:
90
- exporter.logger.info(
91
- "%s Missing chapter: %s (%s), skipping.",
92
- TAG,
93
- chap_title,
94
- chap_id,
95
- )
96
- continue
97
-
98
- # Extract and clean fields
99
- title = cleaner.clean_title(data.get("title", chap_title))
100
- content = cleaner.clean_content(data.get("content", ""))
101
- extra = data.get("extra", {})
102
- author_note = cleaner.clean_content(extra.get("author_say", ""))
103
-
104
- extras = {"作者说": author_note} if author_note else {}
105
- parts.append(
106
- build_txt_chapter(title=title, paragraphs=content, extras=extras)
107
- )
108
-
109
- latest_chapter_title = title
110
-
111
- # --- Build header ---
112
- name = book_info.get("book_name") or ""
113
- author = book_info.get("author") or ""
114
- words = book_info.get("word_count") or ""
115
- updated = book_info.get("update_time") or ""
116
- summary = book_info.get("summary") or ""
117
-
118
- header_fields = [
119
- ("书名", name),
120
- ("作者", author),
121
- ("总字数", words),
122
- ("更新日期", updated),
123
- ("原文截至", latest_chapter_title),
124
- ("内容简介", summary),
125
- ]
126
-
127
- header = build_txt_header(header_fields)
128
-
129
- final_text = header + "\n\n" + "\n\n".join(parts).strip()
130
-
131
- # --- Determine output file path ---
132
- out_name = exporter.get_filename(title=name, author=author, ext="txt")
133
- out_path = out_dir / out_name
134
-
135
- # --- Save final text ---
136
- try:
137
- result = write_file(
138
- content=final_text,
139
- filepath=out_path,
140
- on_exist="overwrite",
141
- )
142
- exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
143
- except Exception as e:
144
- exporter.logger.error("%s Failed to write novel to %s: %s", TAG, out_path, e)
145
- return None
146
- return result
@@ -1,215 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.epub_util
4
- -----------------------------------------
5
-
6
- Utilities for preparing and formatting chapter HTML for EPUB exports.
7
- """
8
-
9
- __all__ = [
10
- "download_cover",
11
- "prepare_builder",
12
- "finalize_export",
13
- "inline_remote_images",
14
- "remove_all_images",
15
- "build_epub_chapter",
16
- ]
17
-
18
- import logging
19
- import re
20
- from html import escape
21
- from pathlib import Path
22
-
23
- from novel_downloader.utils import download, sanitize_filename
24
- from novel_downloader.utils.constants import (
25
- CSS_MAIN_PATH,
26
- DEFAULT_HEADERS,
27
- DEFAULT_IMAGE_SUFFIX,
28
- )
29
- from novel_downloader.utils.epub import EpubBuilder, StyleSheet
30
-
31
- _IMAGE_WRAPPER = '<div class="duokan-image-single illus">{img}</div>'
32
- _IMG_TAG_RE = re.compile(r"<img[^>]*>", re.IGNORECASE)
33
- _IMG_SRC_RE = re.compile(
34
- r'<img[^>]*\bsrc=["\'](https?://[^"\']+)["\'][^>]*>',
35
- re.IGNORECASE,
36
- )
37
-
38
-
39
- def download_cover(
40
- cover_url: str,
41
- raw_base: Path,
42
- include_cover: bool,
43
- logger: logging.Logger,
44
- tag: str,
45
- headers: dict[str, str] | None = None,
46
- ) -> Path | None:
47
- if include_cover and cover_url:
48
- path = download(
49
- cover_url,
50
- raw_base,
51
- filename="cover",
52
- headers=headers or DEFAULT_HEADERS,
53
- on_exist="overwrite",
54
- default_suffix=DEFAULT_IMAGE_SUFFIX,
55
- )
56
- if not path:
57
- logger.warning("%s Failed to download cover from %s", tag, cover_url)
58
- return path
59
- return None
60
-
61
-
62
- def prepare_builder(
63
- site_name: str,
64
- book_id: str,
65
- title: str,
66
- author: str,
67
- description: str,
68
- subject: list[str],
69
- serial_status: str,
70
- word_count: str,
71
- cover_path: Path | None,
72
- ) -> tuple[EpubBuilder, StyleSheet]:
73
- book = EpubBuilder(
74
- title=title,
75
- author=author,
76
- description=description,
77
- cover_path=cover_path,
78
- subject=subject,
79
- serial_status=serial_status,
80
- word_count=word_count,
81
- uid=f"{site_name}_{book_id}",
82
- )
83
- css_text = CSS_MAIN_PATH.read_text(encoding="utf-8")
84
- main_css = StyleSheet(id="main_style", content=css_text, filename="main.css")
85
- book.add_stylesheet(main_css)
86
- return book, main_css
87
-
88
-
89
- def finalize_export(
90
- book: EpubBuilder,
91
- out_dir: Path,
92
- filename: str,
93
- logger: logging.Logger,
94
- tag: str,
95
- ) -> Path | None:
96
- out_path = out_dir / sanitize_filename(filename)
97
- try:
98
- book.export(out_path)
99
- logger.info("%s EPUB successfully written to %s", tag, out_path)
100
- return out_path
101
- except OSError as e:
102
- logger.error("%s Failed to write EPUB to %s: %s", tag, out_path, e)
103
- return None
104
-
105
-
106
- def inline_remote_images(
107
- book: EpubBuilder,
108
- content: str,
109
- image_dir: Path,
110
- headers: dict[str, str] | None = None,
111
- ) -> str:
112
- """
113
- Download every remote `<img src="...">` in `content` into `image_dir`,
114
- and replace the original url with local path.
115
-
116
- :param content: HTML/text of the chapter containing <img> tags.
117
- :param image_dir: Directory to save downloaded images into.
118
- :return: modified_content.
119
- """
120
-
121
- def _replace(m: re.Match[str]) -> str:
122
- url = m.group(1)
123
- try:
124
- local_path = download(
125
- url,
126
- image_dir,
127
- headers=headers or DEFAULT_HEADERS,
128
- on_exist="skip",
129
- default_suffix=DEFAULT_IMAGE_SUFFIX,
130
- )
131
- if not local_path:
132
- return m.group(0)
133
- filename = book.add_image(local_path)
134
- return f'<img src="../Images/{filename}" />'
135
- except Exception:
136
- return m.group(0)
137
-
138
- return _IMG_SRC_RE.sub(_replace, content)
139
-
140
-
141
- def remove_all_images(content: str) -> str:
142
- """
143
- Remove all <img> tags from the given content.
144
-
145
- :param content: HTML/text of the chapter containing <img> tags.
146
- """
147
- return _IMG_TAG_RE.sub("", content)
148
-
149
-
150
- def build_epub_chapter(
151
- title: str,
152
- paragraphs: str,
153
- extras: dict[str, str] | None = None,
154
- ) -> str:
155
- """
156
- Build a formatted chapter epub HTML including title, body paragraphs,
157
- and optional extra sections.
158
-
159
- :param title: Chapter title.
160
- :param paragraphs: Raw multi-line string. Blank lines are ignored.
161
- :param extras: Optional dict mapping section titles to multi-line strings.
162
- :return: A HTML include title, paragraphs, and extras.
163
- """
164
-
165
- def _render_block(text: str) -> str:
166
- out: list[str] = []
167
- for raw in text.splitlines():
168
- line = raw.strip()
169
- if not line:
170
- continue
171
-
172
- # case 1: already wrapped in a <div>...</div>
173
- if line.startswith("<div") and line.endswith("</div>"):
174
- out.append(line)
175
- continue
176
-
177
- # case 2: single <img> line
178
- if _IMG_TAG_RE.fullmatch(line):
179
- out.append(_IMAGE_WRAPPER.format(img=line))
180
- continue
181
-
182
- # case 3: inline <img> in text -> escape other text, preserve <img>
183
- if "<img " in line:
184
- pieces = []
185
- last = 0
186
- for m in _IMG_TAG_RE.finditer(line):
187
- pieces.append(escape(line[last : m.start()]))
188
- pieces.append(m.group(0))
189
- last = m.end()
190
- pieces.append(escape(line[last:]))
191
- out.append("<p>" + "".join(pieces) + "</p>")
192
- else:
193
- # plain text line
194
- out.append(f"<p>{escape(line)}</p>")
195
-
196
- return "\n".join(out)
197
-
198
- parts = []
199
- parts.append(f"<h2>{escape(title)}</h2>")
200
- parts.append(_render_block(paragraphs))
201
-
202
- if extras:
203
- for title, note in extras.items():
204
- note = note.strip()
205
- if not note:
206
- continue
207
- parts.extend(
208
- [
209
- "<hr />",
210
- f"<h3>{escape(title)}</h3>",
211
- _render_block(note),
212
- ]
213
- )
214
-
215
- return "\n".join(parts)
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- novel_downloader.core.exporters.linovelib
4
- -----------------------------------------
5
-
6
- Exporter implementation for handling Linovelib novels.
7
- """
8
-
9
- __all__ = ["LinovelibExporter"]
10
-
11
- from .main_exporter import LinovelibExporter