novel-downloader 1.3.2__py3-none-any.whl → 1.4.0__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/clean.py +97 -78
- novel_downloader/cli/config.py +177 -0
- novel_downloader/cli/download.py +132 -87
- novel_downloader/cli/export.py +77 -0
- novel_downloader/cli/main.py +21 -28
- novel_downloader/config/__init__.py +1 -25
- novel_downloader/config/adapter.py +32 -31
- novel_downloader/config/loader.py +3 -3
- novel_downloader/config/site_rules.py +1 -2
- novel_downloader/core/__init__.py +3 -6
- novel_downloader/core/downloaders/__init__.py +10 -13
- novel_downloader/core/downloaders/base.py +233 -0
- novel_downloader/core/downloaders/biquge.py +27 -0
- novel_downloader/core/downloaders/common.py +414 -0
- novel_downloader/core/downloaders/esjzone.py +27 -0
- novel_downloader/core/downloaders/linovelib.py +27 -0
- novel_downloader/core/downloaders/qianbi.py +27 -0
- novel_downloader/core/downloaders/qidian.py +352 -0
- novel_downloader/core/downloaders/sfacg.py +27 -0
- novel_downloader/core/downloaders/yamibo.py +27 -0
- novel_downloader/core/exporters/__init__.py +37 -0
- novel_downloader/core/{savers → exporters}/base.py +73 -44
- novel_downloader/core/exporters/biquge.py +25 -0
- novel_downloader/core/exporters/common/__init__.py +12 -0
- novel_downloader/core/{savers → exporters}/common/epub.py +40 -52
- novel_downloader/core/{savers/common/main_saver.py → exporters/common/main_exporter.py} +36 -39
- novel_downloader/core/{savers → exporters}/common/txt.py +20 -24
- novel_downloader/core/exporters/epub_utils/__init__.py +40 -0
- novel_downloader/core/{savers → exporters}/epub_utils/css_builder.py +2 -1
- novel_downloader/core/exporters/epub_utils/image_loader.py +131 -0
- novel_downloader/core/{savers → exporters}/epub_utils/initializer.py +6 -3
- novel_downloader/core/{savers → exporters}/epub_utils/text_to_html.py +49 -2
- novel_downloader/core/{savers → exporters}/epub_utils/volume_intro.py +2 -1
- novel_downloader/core/exporters/esjzone.py +25 -0
- novel_downloader/core/exporters/linovelib/__init__.py +10 -0
- novel_downloader/core/exporters/linovelib/epub.py +449 -0
- novel_downloader/core/exporters/linovelib/main_exporter.py +127 -0
- novel_downloader/core/exporters/linovelib/txt.py +129 -0
- novel_downloader/core/exporters/qianbi.py +25 -0
- novel_downloader/core/{savers → exporters}/qidian.py +8 -8
- novel_downloader/core/exporters/sfacg.py +25 -0
- novel_downloader/core/exporters/yamibo.py +25 -0
- novel_downloader/core/factory/__init__.py +5 -17
- novel_downloader/core/factory/downloader.py +24 -126
- novel_downloader/core/factory/exporter.py +58 -0
- novel_downloader/core/factory/fetcher.py +96 -0
- novel_downloader/core/factory/parser.py +17 -12
- novel_downloader/core/{requesters → fetchers}/__init__.py +22 -15
- novel_downloader/core/{requesters → fetchers}/base/__init__.py +2 -4
- novel_downloader/core/fetchers/base/browser.py +383 -0
- novel_downloader/core/fetchers/base/rate_limiter.py +86 -0
- novel_downloader/core/fetchers/base/session.py +419 -0
- novel_downloader/core/fetchers/biquge/__init__.py +14 -0
- novel_downloader/core/{requesters/biquge/async_session.py → fetchers/biquge/browser.py} +18 -6
- novel_downloader/core/{requesters → fetchers}/biquge/session.py +23 -30
- novel_downloader/core/fetchers/common/__init__.py +14 -0
- novel_downloader/core/fetchers/common/browser.py +79 -0
- novel_downloader/core/{requesters/common/async_session.py → fetchers/common/session.py} +8 -25
- novel_downloader/core/fetchers/esjzone/__init__.py +14 -0
- novel_downloader/core/fetchers/esjzone/browser.py +202 -0
- novel_downloader/core/{requesters/esjzone/async_session.py → fetchers/esjzone/session.py} +62 -42
- novel_downloader/core/fetchers/linovelib/__init__.py +14 -0
- novel_downloader/core/fetchers/linovelib/browser.py +178 -0
- novel_downloader/core/fetchers/linovelib/session.py +178 -0
- novel_downloader/core/fetchers/qianbi/__init__.py +14 -0
- novel_downloader/core/{requesters/qianbi/session.py → fetchers/qianbi/browser.py} +30 -48
- novel_downloader/core/{requesters/qianbi/async_session.py → fetchers/qianbi/session.py} +18 -6
- novel_downloader/core/fetchers/qidian/__init__.py +14 -0
- novel_downloader/core/fetchers/qidian/browser.py +266 -0
- novel_downloader/core/fetchers/qidian/session.py +326 -0
- novel_downloader/core/fetchers/sfacg/__init__.py +14 -0
- novel_downloader/core/fetchers/sfacg/browser.py +189 -0
- novel_downloader/core/{requesters/sfacg/async_session.py → fetchers/sfacg/session.py} +43 -73
- novel_downloader/core/fetchers/yamibo/__init__.py +14 -0
- novel_downloader/core/fetchers/yamibo/browser.py +229 -0
- novel_downloader/core/{requesters/yamibo/async_session.py → fetchers/yamibo/session.py} +62 -44
- novel_downloader/core/interfaces/__init__.py +8 -12
- novel_downloader/core/interfaces/downloader.py +54 -0
- novel_downloader/core/interfaces/{saver.py → exporter.py} +12 -12
- novel_downloader/core/interfaces/fetcher.py +162 -0
- novel_downloader/core/interfaces/parser.py +6 -7
- novel_downloader/core/parsers/__init__.py +5 -6
- novel_downloader/core/parsers/base.py +9 -13
- novel_downloader/core/parsers/biquge/main_parser.py +12 -13
- novel_downloader/core/parsers/common/helper.py +3 -3
- novel_downloader/core/parsers/common/main_parser.py +39 -34
- novel_downloader/core/parsers/esjzone/main_parser.py +24 -17
- novel_downloader/core/parsers/linovelib/__init__.py +10 -0
- novel_downloader/core/parsers/linovelib/main_parser.py +210 -0
- novel_downloader/core/parsers/qianbi/main_parser.py +21 -15
- novel_downloader/core/parsers/qidian/__init__.py +2 -11
- novel_downloader/core/parsers/qidian/book_info_parser.py +113 -0
- novel_downloader/core/parsers/qidian/{browser/chapter_encrypted.py → chapter_encrypted.py} +162 -135
- novel_downloader/core/parsers/qidian/chapter_normal.py +150 -0
- novel_downloader/core/parsers/qidian/{session/chapter_router.py → chapter_router.py} +15 -15
- novel_downloader/core/parsers/qidian/{browser/main_parser.py → main_parser.py} +49 -40
- novel_downloader/core/parsers/qidian/utils/__init__.py +27 -0
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +145 -0
- novel_downloader/core/parsers/qidian/{shared → utils}/helpers.py +41 -68
- novel_downloader/core/parsers/qidian/{session → utils}/node_decryptor.py +64 -50
- novel_downloader/core/parsers/sfacg/main_parser.py +12 -12
- novel_downloader/core/parsers/yamibo/main_parser.py +10 -10
- novel_downloader/locales/en.json +18 -2
- novel_downloader/locales/zh.json +18 -2
- novel_downloader/models/__init__.py +64 -0
- novel_downloader/models/browser.py +21 -0
- novel_downloader/models/chapter.py +25 -0
- novel_downloader/models/config.py +100 -0
- novel_downloader/models/login.py +20 -0
- novel_downloader/models/site_rules.py +99 -0
- novel_downloader/models/tasks.py +33 -0
- novel_downloader/models/types.py +15 -0
- novel_downloader/resources/config/settings.toml +31 -25
- novel_downloader/resources/json/linovelib_font_map.json +3573 -0
- novel_downloader/tui/__init__.py +7 -0
- novel_downloader/tui/app.py +32 -0
- novel_downloader/tui/main.py +17 -0
- novel_downloader/tui/screens/__init__.py +14 -0
- novel_downloader/tui/screens/home.py +191 -0
- novel_downloader/tui/screens/login.py +74 -0
- novel_downloader/tui/styles/home_layout.tcss +79 -0
- novel_downloader/tui/widgets/richlog_handler.py +24 -0
- novel_downloader/utils/__init__.py +6 -0
- novel_downloader/utils/chapter_storage.py +25 -38
- novel_downloader/utils/constants.py +15 -5
- novel_downloader/utils/cookies.py +66 -0
- novel_downloader/utils/crypto_utils.py +1 -74
- novel_downloader/utils/file_utils/io.py +1 -1
- novel_downloader/utils/fontocr/ocr_v1.py +2 -1
- novel_downloader/utils/fontocr/ocr_v2.py +2 -2
- novel_downloader/utils/hash_store.py +10 -18
- novel_downloader/utils/hash_utils.py +3 -2
- novel_downloader/utils/logger.py +2 -3
- novel_downloader/utils/network.py +53 -39
- novel_downloader/utils/text_utils/chapter_formatting.py +6 -1
- novel_downloader/utils/text_utils/font_mapping.py +1 -1
- novel_downloader/utils/text_utils/text_cleaning.py +1 -1
- novel_downloader/utils/time_utils/datetime_utils.py +3 -3
- novel_downloader/utils/time_utils/sleep_utils.py +3 -3
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/METADATA +72 -38
- novel_downloader-1.4.0.dist-info/RECORD +170 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/WHEEL +1 -1
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/entry_points.txt +1 -0
- novel_downloader/cli/interactive.py +0 -66
- novel_downloader/cli/settings.py +0 -177
- novel_downloader/config/models.py +0 -187
- novel_downloader/core/downloaders/base/__init__.py +0 -14
- novel_downloader/core/downloaders/base/base_async.py +0 -153
- novel_downloader/core/downloaders/base/base_sync.py +0 -208
- novel_downloader/core/downloaders/biquge/__init__.py +0 -14
- novel_downloader/core/downloaders/biquge/biquge_async.py +0 -27
- novel_downloader/core/downloaders/biquge/biquge_sync.py +0 -27
- novel_downloader/core/downloaders/common/__init__.py +0 -14
- novel_downloader/core/downloaders/common/common_async.py +0 -218
- novel_downloader/core/downloaders/common/common_sync.py +0 -210
- novel_downloader/core/downloaders/esjzone/__init__.py +0 -14
- novel_downloader/core/downloaders/esjzone/esjzone_async.py +0 -27
- novel_downloader/core/downloaders/esjzone/esjzone_sync.py +0 -27
- novel_downloader/core/downloaders/qianbi/__init__.py +0 -14
- novel_downloader/core/downloaders/qianbi/qianbi_async.py +0 -27
- novel_downloader/core/downloaders/qianbi/qianbi_sync.py +0 -27
- novel_downloader/core/downloaders/qidian/__init__.py +0 -10
- novel_downloader/core/downloaders/qidian/qidian_sync.py +0 -227
- novel_downloader/core/downloaders/sfacg/__init__.py +0 -14
- novel_downloader/core/downloaders/sfacg/sfacg_async.py +0 -27
- novel_downloader/core/downloaders/sfacg/sfacg_sync.py +0 -27
- novel_downloader/core/downloaders/yamibo/__init__.py +0 -14
- novel_downloader/core/downloaders/yamibo/yamibo_async.py +0 -27
- novel_downloader/core/downloaders/yamibo/yamibo_sync.py +0 -27
- novel_downloader/core/factory/requester.py +0 -144
- novel_downloader/core/factory/saver.py +0 -56
- novel_downloader/core/interfaces/async_downloader.py +0 -36
- novel_downloader/core/interfaces/async_requester.py +0 -84
- novel_downloader/core/interfaces/sync_downloader.py +0 -36
- novel_downloader/core/interfaces/sync_requester.py +0 -82
- novel_downloader/core/parsers/qidian/browser/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/browser/chapter_normal.py +0 -93
- novel_downloader/core/parsers/qidian/browser/chapter_router.py +0 -71
- novel_downloader/core/parsers/qidian/session/__init__.py +0 -12
- novel_downloader/core/parsers/qidian/session/chapter_encrypted.py +0 -443
- novel_downloader/core/parsers/qidian/session/chapter_normal.py +0 -115
- novel_downloader/core/parsers/qidian/session/main_parser.py +0 -128
- novel_downloader/core/parsers/qidian/shared/__init__.py +0 -37
- novel_downloader/core/parsers/qidian/shared/book_info_parser.py +0 -150
- novel_downloader/core/requesters/base/async_session.py +0 -410
- novel_downloader/core/requesters/base/browser.py +0 -337
- novel_downloader/core/requesters/base/session.py +0 -378
- novel_downloader/core/requesters/biquge/__init__.py +0 -14
- novel_downloader/core/requesters/common/__init__.py +0 -17
- novel_downloader/core/requesters/common/session.py +0 -113
- novel_downloader/core/requesters/esjzone/__init__.py +0 -13
- novel_downloader/core/requesters/esjzone/session.py +0 -235
- novel_downloader/core/requesters/qianbi/__init__.py +0 -13
- novel_downloader/core/requesters/qidian/__init__.py +0 -21
- novel_downloader/core/requesters/qidian/broswer.py +0 -307
- novel_downloader/core/requesters/qidian/session.py +0 -290
- novel_downloader/core/requesters/sfacg/__init__.py +0 -13
- novel_downloader/core/requesters/sfacg/session.py +0 -242
- novel_downloader/core/requesters/yamibo/__init__.py +0 -13
- novel_downloader/core/requesters/yamibo/session.py +0 -237
- novel_downloader/core/savers/__init__.py +0 -34
- novel_downloader/core/savers/biquge.py +0 -25
- novel_downloader/core/savers/common/__init__.py +0 -12
- novel_downloader/core/savers/epub_utils/__init__.py +0 -26
- novel_downloader/core/savers/esjzone.py +0 -25
- novel_downloader/core/savers/qianbi.py +0 -25
- novel_downloader/core/savers/sfacg.py +0 -25
- novel_downloader/core/savers/yamibo.py +0 -25
- novel_downloader/resources/config/rules.toml +0 -196
- novel_downloader-1.3.2.dist-info/RECORD +0 -165
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.3.2.dist-info → novel_downloader-1.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.linovelib.txt
|
4
|
+
---------------------------------------------
|
5
|
+
|
6
|
+
Contains the logic for exporting novel content as a single `.txt` file.
|
7
|
+
|
8
|
+
This module defines `linovelib_export_as_txt` function, which assembles and formats
|
9
|
+
a novel based on metadata and chapter files found in the raw data directory.
|
10
|
+
It is intended to be used by `LinovelibExporter` as part of the save/export process.
|
11
|
+
"""
|
12
|
+
|
13
|
+
from __future__ import annotations
|
14
|
+
|
15
|
+
import json
|
16
|
+
from typing import TYPE_CHECKING
|
17
|
+
|
18
|
+
from novel_downloader.utils.file_utils import save_as_txt
|
19
|
+
from novel_downloader.utils.text_utils import (
|
20
|
+
format_chapter,
|
21
|
+
)
|
22
|
+
|
23
|
+
if TYPE_CHECKING:
|
24
|
+
from .main_exporter import LinovelibExporter
|
25
|
+
|
26
|
+
|
27
|
+
def linovelib_export_as_txt(
|
28
|
+
exporter: LinovelibExporter,
|
29
|
+
book_id: str,
|
30
|
+
) -> None:
|
31
|
+
"""
|
32
|
+
将 save_path 文件夹中该小说的所有章节 json 文件合并保存为一个完整的 txt 文件,
|
33
|
+
并保存到 out_path 下
|
34
|
+
|
35
|
+
处理流程:
|
36
|
+
1. 从 book_info.json 中加载书籍信息 (包含书名、作者、简介及卷章节列表)
|
37
|
+
2. 遍历各卷, 每个卷先追加卷标题, 然后依次追加该卷下各章节的标题和内容
|
38
|
+
3. 将书籍元信息 (书名、作者、原文截至、内容简介) 与所有章节内容拼接
|
39
|
+
4. 将最终结果保存到 out_path 下 (例如:`{book_name}.txt`)
|
40
|
+
|
41
|
+
:param book_id: Identifier of the novel (used as subdirectory name).
|
42
|
+
"""
|
43
|
+
TAG = "[exporter]"
|
44
|
+
# --- Paths & options ---
|
45
|
+
raw_base = exporter._raw_data_dir / book_id
|
46
|
+
out_dir = exporter.output_dir
|
47
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
48
|
+
|
49
|
+
# --- Load book_info.json ---
|
50
|
+
info_path = raw_base / "book_info.json"
|
51
|
+
try:
|
52
|
+
info_text = info_path.read_text(encoding="utf-8")
|
53
|
+
book_info = json.loads(info_text)
|
54
|
+
except Exception as e:
|
55
|
+
exporter.logger.error("%s Failed to load %s: %s", TAG, info_path, e)
|
56
|
+
return
|
57
|
+
|
58
|
+
# --- Compile chapters ---
|
59
|
+
parts: list[str] = []
|
60
|
+
volumes = book_info.get("volumes", [])
|
61
|
+
|
62
|
+
for vol in volumes:
|
63
|
+
vol_name = vol.get("volume_name", "").strip()
|
64
|
+
vol_intro = vol.get("volume_intro", "").strip()
|
65
|
+
if vol_name:
|
66
|
+
volume_header = f"\n\n{'=' * 6} {vol_name} {'=' * 6}\n\n"
|
67
|
+
parts.append(volume_header)
|
68
|
+
exporter.logger.info("%s Processing volume: %s", TAG, vol_name)
|
69
|
+
if vol_intro:
|
70
|
+
parts.append(f"{vol_intro}\n\n")
|
71
|
+
for chap in vol.get("chapters", []):
|
72
|
+
chap_id = chap.get("chapterId")
|
73
|
+
chap_title = chap.get("title", "")
|
74
|
+
if not chap_id:
|
75
|
+
exporter.logger.warning("%s Missing chapterId, skipping: %s", TAG, chap)
|
76
|
+
continue
|
77
|
+
|
78
|
+
chapter_data = exporter._get_chapter(book_id, chap_id)
|
79
|
+
if not chapter_data:
|
80
|
+
exporter.logger.info(
|
81
|
+
"%s Missing chapter file in: %s (%s), skipping.",
|
82
|
+
TAG,
|
83
|
+
chap_title,
|
84
|
+
chap_id,
|
85
|
+
)
|
86
|
+
continue
|
87
|
+
|
88
|
+
# Extract structured fields
|
89
|
+
title = chapter_data.get("title", chap_title).strip()
|
90
|
+
content = chapter_data.get("content", "").strip()
|
91
|
+
|
92
|
+
parts.append(format_chapter(title, content, ""))
|
93
|
+
|
94
|
+
# --- Build header ---
|
95
|
+
name = book_info.get("book_name")
|
96
|
+
author = book_info.get("author")
|
97
|
+
words = book_info.get("word_count")
|
98
|
+
updated = book_info.get("update_time")
|
99
|
+
summary = book_info.get("summary")
|
100
|
+
|
101
|
+
fields = [
|
102
|
+
("书名", name),
|
103
|
+
("作者", author),
|
104
|
+
("总字数", words),
|
105
|
+
("更新日期", updated),
|
106
|
+
]
|
107
|
+
header_lines = [f"{label}: {value}" for label, value in fields if value]
|
108
|
+
|
109
|
+
if summary:
|
110
|
+
header_lines.append("内容简介:")
|
111
|
+
header_lines.append(summary)
|
112
|
+
|
113
|
+
header_lines += ["", "-" * 10, ""]
|
114
|
+
|
115
|
+
header = "\n".join(header_lines)
|
116
|
+
|
117
|
+
final_text = header + "\n\n" + "\n\n".join(parts).strip()
|
118
|
+
|
119
|
+
# --- Determine output file path ---
|
120
|
+
out_name = exporter.get_filename(title=name, author=author, ext="txt")
|
121
|
+
out_path = out_dir / out_name
|
122
|
+
|
123
|
+
# --- Save final text ---
|
124
|
+
try:
|
125
|
+
save_as_txt(content=final_text, filepath=out_path)
|
126
|
+
exporter.logger.info("%s Novel saved to: %s", TAG, out_path)
|
127
|
+
except Exception as e:
|
128
|
+
exporter.logger.error("%s Failed to save file: %s", TAG, e)
|
129
|
+
return
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.qianbi
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.models import ExporterConfig
|
9
|
+
|
10
|
+
from .common import CommonExporter
|
11
|
+
|
12
|
+
|
13
|
+
class QianbiExporter(CommonExporter):
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
config: ExporterConfig,
|
17
|
+
):
|
18
|
+
super().__init__(
|
19
|
+
config,
|
20
|
+
site="qianbi",
|
21
|
+
chap_folders=["chapters"],
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
__all__ = ["QianbiExporter"]
|
@@ -1,22 +1,22 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.
|
4
|
-
|
3
|
+
novel_downloader.core.exporters.qidian
|
4
|
+
--------------------------------------
|
5
5
|
|
6
|
-
This module provides the `
|
6
|
+
This module provides the `QidianExporter` class for handling the saving process
|
7
7
|
of novels sourced from Qidian (起点中文网). It implements the platform-specific
|
8
8
|
logic required to structure and export novel content into desired formats.
|
9
9
|
"""
|
10
10
|
|
11
|
-
from novel_downloader.
|
11
|
+
from novel_downloader.models import ExporterConfig
|
12
12
|
|
13
|
-
from .common import
|
13
|
+
from .common import CommonExporter
|
14
14
|
|
15
15
|
|
16
|
-
class
|
16
|
+
class QidianExporter(CommonExporter):
|
17
17
|
def __init__(
|
18
18
|
self,
|
19
|
-
config:
|
19
|
+
config: ExporterConfig,
|
20
20
|
):
|
21
21
|
super().__init__(
|
22
22
|
config,
|
@@ -25,4 +25,4 @@ class QidianSaver(CommonSaver):
|
|
25
25
|
)
|
26
26
|
|
27
27
|
|
28
|
-
__all__ = ["
|
28
|
+
__all__ = ["QidianExporter"]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.sfacg
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.models import ExporterConfig
|
9
|
+
|
10
|
+
from .common import CommonExporter
|
11
|
+
|
12
|
+
|
13
|
+
class SfacgExporter(CommonExporter):
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
config: ExporterConfig,
|
17
|
+
):
|
18
|
+
super().__init__(
|
19
|
+
config,
|
20
|
+
site="sfacg",
|
21
|
+
chap_folders=["chapters"],
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
__all__ = ["SfacgExporter"]
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.exporters.yamibo
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
"""
|
7
|
+
|
8
|
+
from novel_downloader.models import ExporterConfig
|
9
|
+
|
10
|
+
from .common import CommonExporter
|
11
|
+
|
12
|
+
|
13
|
+
class YamiboExporter(CommonExporter):
|
14
|
+
def __init__(
|
15
|
+
self,
|
16
|
+
config: ExporterConfig,
|
17
|
+
):
|
18
|
+
super().__init__(
|
19
|
+
config,
|
20
|
+
site="yamibo",
|
21
|
+
chap_folders=["chapters"],
|
22
|
+
)
|
23
|
+
|
24
|
+
|
25
|
+
__all__ = ["YamiboExporter"]
|
@@ -7,26 +7,14 @@ This package provides factory methods for dynamically retrieving components
|
|
7
7
|
based on runtime parameters such as site name or content type.
|
8
8
|
"""
|
9
9
|
|
10
|
-
from .downloader import
|
11
|
-
|
12
|
-
|
13
|
-
get_sync_downloader,
|
14
|
-
)
|
10
|
+
from .downloader import get_downloader
|
11
|
+
from .exporter import get_exporter
|
12
|
+
from .fetcher import get_fetcher
|
15
13
|
from .parser import get_parser
|
16
|
-
from .requester import (
|
17
|
-
get_async_requester,
|
18
|
-
get_requester,
|
19
|
-
get_sync_requester,
|
20
|
-
)
|
21
|
-
from .saver import get_saver
|
22
14
|
|
23
15
|
__all__ = [
|
24
|
-
"get_async_downloader",
|
25
16
|
"get_downloader",
|
26
|
-
"
|
17
|
+
"get_exporter",
|
18
|
+
"get_fetcher",
|
27
19
|
"get_parser",
|
28
|
-
"get_async_requester",
|
29
|
-
"get_requester",
|
30
|
-
"get_sync_requester",
|
31
|
-
"get_saver",
|
32
20
|
]
|
@@ -1,60 +1,42 @@
|
|
1
1
|
#!/usr/bin/env python3
|
2
2
|
"""
|
3
|
-
novel_downloader.core.factory.
|
4
|
-
|
3
|
+
novel_downloader.core.factory.downloader
|
4
|
+
----------------------------------------
|
5
5
|
|
6
6
|
This module implements a factory function for creating downloader instances
|
7
7
|
based on the site name and parser mode specified in the configuration.
|
8
8
|
"""
|
9
9
|
|
10
10
|
from collections.abc import Callable
|
11
|
-
from typing import cast
|
12
11
|
|
13
|
-
from novel_downloader.config import
|
12
|
+
from novel_downloader.config import load_site_rules
|
14
13
|
from novel_downloader.core.downloaders import (
|
15
|
-
BiqugeAsyncDownloader,
|
16
14
|
BiqugeDownloader,
|
17
|
-
CommonAsyncDownloader,
|
18
15
|
CommonDownloader,
|
19
|
-
EsjzoneAsyncDownloader,
|
20
16
|
EsjzoneDownloader,
|
21
|
-
|
17
|
+
LinovelibDownloader,
|
22
18
|
QianbiDownloader,
|
23
19
|
QidianDownloader,
|
24
|
-
SfacgAsyncDownloader,
|
25
20
|
SfacgDownloader,
|
26
|
-
YamiboAsyncDownloader,
|
27
21
|
YamiboDownloader,
|
28
22
|
)
|
29
23
|
from novel_downloader.core.interfaces import (
|
30
|
-
|
31
|
-
|
24
|
+
DownloaderProtocol,
|
25
|
+
ExporterProtocol,
|
26
|
+
FetcherProtocol,
|
32
27
|
ParserProtocol,
|
33
|
-
SaverProtocol,
|
34
|
-
SyncDownloaderProtocol,
|
35
|
-
SyncRequesterProtocol,
|
36
28
|
)
|
29
|
+
from novel_downloader.models import DownloaderConfig
|
37
30
|
|
38
|
-
|
39
|
-
[
|
40
|
-
|
31
|
+
DownloaderBuilder = Callable[
|
32
|
+
[FetcherProtocol, ParserProtocol, ExporterProtocol, DownloaderConfig],
|
33
|
+
DownloaderProtocol,
|
41
34
|
]
|
42
35
|
|
43
|
-
|
44
|
-
[SyncRequesterProtocol, ParserProtocol, SaverProtocol, DownloaderConfig],
|
45
|
-
SyncDownloaderProtocol,
|
46
|
-
]
|
47
|
-
|
48
|
-
_async_site_map: dict[str, AsyncDownloaderBuilder] = {
|
49
|
-
"biquge": BiqugeAsyncDownloader,
|
50
|
-
"esjzone": EsjzoneAsyncDownloader,
|
51
|
-
"qianbi": QianbiAsyncDownloader,
|
52
|
-
"sfacg": SfacgAsyncDownloader,
|
53
|
-
"yamibo": YamiboAsyncDownloader,
|
54
|
-
}
|
55
|
-
_sync_site_map: dict[str, SyncDownloaderBuilder] = {
|
36
|
+
_site_map: dict[str, DownloaderBuilder] = {
|
56
37
|
"biquge": BiqugeDownloader,
|
57
38
|
"esjzone": EsjzoneDownloader,
|
39
|
+
"linovelib": LinovelibDownloader,
|
58
40
|
"qianbi": QianbiDownloader,
|
59
41
|
"qidian": QidianDownloader,
|
60
42
|
"sfacg": SfacgDownloader,
|
@@ -62,117 +44,33 @@ _sync_site_map: dict[str, SyncDownloaderBuilder] = {
|
|
62
44
|
}
|
63
45
|
|
64
46
|
|
65
|
-
def
|
66
|
-
|
67
|
-
parser: ParserProtocol,
|
68
|
-
saver: SaverProtocol,
|
69
|
-
site: str,
|
70
|
-
config: DownloaderConfig,
|
71
|
-
) -> AsyncDownloaderProtocol:
|
72
|
-
"""
|
73
|
-
Returns an AsyncDownloaderProtocol for the given site.
|
74
|
-
|
75
|
-
:param requester: Requester implementation
|
76
|
-
:param parser: Parser implementation
|
77
|
-
:param saver: Saver implementation
|
78
|
-
:param site: Site name (e.g., 'qidian')
|
79
|
-
:param config: Downloader configuration
|
80
|
-
|
81
|
-
:return: An instance of a downloader class
|
82
|
-
|
83
|
-
:raises ValueError: If a site-specific downloader does not support async mode.
|
84
|
-
:raises TypeError: If the provided requester does not match the required protocol.
|
85
|
-
"""
|
86
|
-
site_key = site.lower()
|
87
|
-
|
88
|
-
if not isinstance(requester, AsyncRequesterProtocol):
|
89
|
-
raise TypeError("Async mode requires an AsyncRequesterProtocol")
|
90
|
-
|
91
|
-
# site-specific
|
92
|
-
if site_key in _async_site_map:
|
93
|
-
return _async_site_map[site_key](requester, parser, saver, config)
|
94
|
-
|
95
|
-
# fallback
|
96
|
-
site_rules = load_site_rules()
|
97
|
-
site_rule = site_rules.get(site_key)
|
98
|
-
if site_rule is None:
|
99
|
-
raise ValueError(f"Unsupported site: {site}")
|
100
|
-
|
101
|
-
return CommonAsyncDownloader(requester, parser, saver, config, site_key)
|
102
|
-
|
103
|
-
|
104
|
-
def get_sync_downloader(
|
105
|
-
requester: SyncRequesterProtocol,
|
47
|
+
def get_downloader(
|
48
|
+
fetcher: FetcherProtocol,
|
106
49
|
parser: ParserProtocol,
|
107
|
-
|
50
|
+
exporter: ExporterProtocol,
|
108
51
|
site: str,
|
109
52
|
config: DownloaderConfig,
|
110
|
-
) ->
|
53
|
+
) -> DownloaderProtocol:
|
111
54
|
"""
|
112
|
-
Returns
|
113
|
-
First tries a site-specific downloader (e.g. QidianDownloader),
|
114
|
-
otherwise falls back to CommonDownloader.
|
55
|
+
Returns an DownloaderProtocol for the given site.
|
115
56
|
|
116
|
-
:param
|
57
|
+
:param fetcher: Fetcher implementation
|
117
58
|
:param parser: Parser implementation
|
118
|
-
:param
|
59
|
+
:param exporter: Exporter implementation
|
119
60
|
:param site: Site name (e.g., 'qidian')
|
120
61
|
:param config: Downloader configuration
|
121
62
|
|
122
63
|
:return: An instance of a downloader class
|
123
|
-
|
124
|
-
:raises ValueError: If a site-specific downloader does not support async mode.
|
125
|
-
:raises TypeError: If the provided requester does not match the required protocol.
|
126
64
|
"""
|
127
65
|
site_key = site.lower()
|
128
66
|
|
129
|
-
if not isinstance(requester, SyncRequesterProtocol):
|
130
|
-
raise TypeError("Sync mode requires a RequesterProtocol")
|
131
|
-
|
132
67
|
# site-specific
|
133
|
-
if site_key in
|
134
|
-
return
|
68
|
+
if site_key in _site_map:
|
69
|
+
return _site_map[site_key](fetcher, parser, exporter, config)
|
135
70
|
|
136
71
|
# fallback
|
137
72
|
site_rules = load_site_rules()
|
138
|
-
|
139
|
-
if site_rule is None:
|
73
|
+
if site_key not in site_rules:
|
140
74
|
raise ValueError(f"Unsupported site: {site}")
|
141
75
|
|
142
|
-
return CommonDownloader(
|
143
|
-
|
144
|
-
|
145
|
-
def get_downloader(
|
146
|
-
requester: AsyncRequesterProtocol | SyncRequesterProtocol,
|
147
|
-
parser: ParserProtocol,
|
148
|
-
saver: SaverProtocol,
|
149
|
-
site: str,
|
150
|
-
config: DownloaderConfig,
|
151
|
-
) -> AsyncDownloaderProtocol | SyncDownloaderProtocol:
|
152
|
-
"""
|
153
|
-
Dispatches to get_async_downloader if config.mode == 'async',
|
154
|
-
otherwise to get_sync_downloader.
|
155
|
-
|
156
|
-
:param requester: Requester implementation
|
157
|
-
:param parser: Parser implementation
|
158
|
-
:param saver: Saver implementation
|
159
|
-
:param site: Site name (e.g., 'qidian')
|
160
|
-
:param config: Downloader configuration
|
161
|
-
|
162
|
-
:return: An instance of a downloader class
|
163
|
-
|
164
|
-
:raises ValueError: If a site-specific downloader does not support async mode.
|
165
|
-
:raises TypeError: If the provided requester does not match the required protocol.
|
166
|
-
"""
|
167
|
-
if requester.is_async():
|
168
|
-
if config.mode.lower() != "async":
|
169
|
-
raise TypeError("Requester is async, but config.mode is not 'async'")
|
170
|
-
async_requester = cast(AsyncRequesterProtocol, requester)
|
171
|
-
return get_async_downloader(async_requester, parser, saver, site, config)
|
172
|
-
else:
|
173
|
-
if config.mode.lower() not in ("browser", "session"):
|
174
|
-
raise TypeError(
|
175
|
-
"Requester is sync, but config.mode is not 'browser' or 'session'"
|
176
|
-
)
|
177
|
-
sync_requester = cast(SyncRequesterProtocol, requester)
|
178
|
-
return get_sync_downloader(sync_requester, parser, saver, site, config)
|
76
|
+
return CommonDownloader(fetcher, parser, exporter, config, site_key)
|
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.factory.exporter
|
4
|
+
--------------------------------------
|
5
|
+
|
6
|
+
This module implements a factory function for creating exporter instances
|
7
|
+
based on the site name.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from collections.abc import Callable
|
11
|
+
|
12
|
+
from novel_downloader.config import load_site_rules
|
13
|
+
from novel_downloader.core.exporters import (
|
14
|
+
BiqugeExporter,
|
15
|
+
CommonExporter,
|
16
|
+
EsjzoneExporter,
|
17
|
+
LinovelibExporter,
|
18
|
+
QianbiExporter,
|
19
|
+
QidianExporter,
|
20
|
+
SfacgExporter,
|
21
|
+
YamiboExporter,
|
22
|
+
)
|
23
|
+
from novel_downloader.core.interfaces import ExporterProtocol
|
24
|
+
from novel_downloader.models import ExporterConfig
|
25
|
+
|
26
|
+
ExporterBuilder = Callable[[ExporterConfig], ExporterProtocol]
|
27
|
+
|
28
|
+
_site_map: dict[str, ExporterBuilder] = {
|
29
|
+
"biquge": BiqugeExporter,
|
30
|
+
"esjzone": EsjzoneExporter,
|
31
|
+
"linovelib": LinovelibExporter,
|
32
|
+
"qianbi": QianbiExporter,
|
33
|
+
"qidian": QidianExporter,
|
34
|
+
"sfacg": SfacgExporter,
|
35
|
+
"yamibo": YamiboExporter,
|
36
|
+
}
|
37
|
+
|
38
|
+
|
39
|
+
def get_exporter(site: str, config: ExporterConfig) -> ExporterProtocol:
|
40
|
+
"""
|
41
|
+
Returns a site-specific exporter instance.
|
42
|
+
|
43
|
+
:param site: Site name (e.g., 'qidian')
|
44
|
+
:param config: Configuration for the exporter
|
45
|
+
:return: An instance of a exporter class
|
46
|
+
"""
|
47
|
+
site_key = site.lower()
|
48
|
+
|
49
|
+
# site-specific
|
50
|
+
if site_key in _site_map:
|
51
|
+
return _site_map[site_key](config)
|
52
|
+
|
53
|
+
# Fallback
|
54
|
+
site_rules = load_site_rules()
|
55
|
+
if site_key not in site_rules:
|
56
|
+
raise ValueError(f"Unsupported site: {site}")
|
57
|
+
|
58
|
+
return CommonExporter(config, site_key)
|
@@ -0,0 +1,96 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
novel_downloader.core.factory.fetcher
|
4
|
+
-------------------------------------
|
5
|
+
|
6
|
+
This module implements a factory function for retrieving fetcher instances
|
7
|
+
based on the target novel platform (site).
|
8
|
+
"""
|
9
|
+
|
10
|
+
from collections.abc import Callable
|
11
|
+
|
12
|
+
from novel_downloader.config import load_site_rules
|
13
|
+
from novel_downloader.core.fetchers import (
|
14
|
+
BiqugeBrowser,
|
15
|
+
BiqugeSession,
|
16
|
+
CommonBrowser,
|
17
|
+
CommonSession,
|
18
|
+
EsjzoneBrowser,
|
19
|
+
EsjzoneSession,
|
20
|
+
LinovelibBrowser,
|
21
|
+
LinovelibSession,
|
22
|
+
QianbiBrowser,
|
23
|
+
QianbiSession,
|
24
|
+
QidianBrowser,
|
25
|
+
QidianSession,
|
26
|
+
SfacgBrowser,
|
27
|
+
SfacgSession,
|
28
|
+
YamiboBrowser,
|
29
|
+
YamiboSession,
|
30
|
+
)
|
31
|
+
from novel_downloader.core.interfaces import FetcherProtocol
|
32
|
+
from novel_downloader.models import FetcherConfig
|
33
|
+
|
34
|
+
FetcherBuilder = Callable[[FetcherConfig], FetcherProtocol]
|
35
|
+
|
36
|
+
_site_map: dict[str, dict[str, FetcherBuilder]] = {
|
37
|
+
"biquge": {
|
38
|
+
"browser": BiqugeBrowser,
|
39
|
+
"session": BiqugeSession,
|
40
|
+
},
|
41
|
+
"esjzone": {
|
42
|
+
"browser": EsjzoneBrowser,
|
43
|
+
"session": EsjzoneSession,
|
44
|
+
},
|
45
|
+
"linovelib": {
|
46
|
+
"browser": LinovelibBrowser,
|
47
|
+
"session": LinovelibSession,
|
48
|
+
},
|
49
|
+
"qianbi": {
|
50
|
+
"browser": QianbiBrowser,
|
51
|
+
"session": QianbiSession,
|
52
|
+
},
|
53
|
+
"qidian": {
|
54
|
+
"browser": QidianBrowser,
|
55
|
+
"session": QidianSession,
|
56
|
+
},
|
57
|
+
"sfacg": {
|
58
|
+
"browser": SfacgBrowser,
|
59
|
+
"session": SfacgSession,
|
60
|
+
},
|
61
|
+
"yamibo": {
|
62
|
+
"browser": YamiboBrowser,
|
63
|
+
"session": YamiboSession,
|
64
|
+
},
|
65
|
+
}
|
66
|
+
|
67
|
+
|
68
|
+
def get_fetcher(
|
69
|
+
site: str,
|
70
|
+
config: FetcherConfig,
|
71
|
+
) -> FetcherProtocol:
|
72
|
+
"""
|
73
|
+
Returns an FetcherProtocol for the given site.
|
74
|
+
|
75
|
+
:param site: Site name (e.g., 'qidian')
|
76
|
+
:param config: Configuration for the requester
|
77
|
+
:return: An instance of a requester class
|
78
|
+
"""
|
79
|
+
site_key = site.lower()
|
80
|
+
mode = config.mode
|
81
|
+
|
82
|
+
# site-specific
|
83
|
+
fetcher_cls = _site_map.get(site_key, {}).get(mode)
|
84
|
+
if fetcher_cls is not None:
|
85
|
+
return fetcher_cls(config)
|
86
|
+
|
87
|
+
# fallback: use Common based on mode
|
88
|
+
site_rules = load_site_rules()
|
89
|
+
site_rule = site_rules.get(site_key)
|
90
|
+
if site_rule is None:
|
91
|
+
raise ValueError(f"Unsupported site: {site}")
|
92
|
+
profile = site_rule["profile"]
|
93
|
+
|
94
|
+
if mode == "browser":
|
95
|
+
return CommonBrowser(site_key, profile, config)
|
96
|
+
return CommonSession(site_key, profile, config)
|