novel-downloader 1.5.0__py3-none-any.whl → 2.0.1__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/__init__.py +1 -3
- novel_downloader/cli/clean.py +21 -88
- novel_downloader/cli/config.py +26 -21
- novel_downloader/cli/download.py +79 -66
- novel_downloader/cli/export.py +17 -21
- novel_downloader/cli/main.py +1 -1
- novel_downloader/cli/search.py +62 -65
- novel_downloader/cli/ui.py +156 -0
- novel_downloader/config/__init__.py +8 -5
- novel_downloader/config/adapter.py +206 -209
- novel_downloader/config/{loader.py → file_io.py} +53 -26
- novel_downloader/core/__init__.py +5 -5
- novel_downloader/core/archived/deqixs/fetcher.py +115 -0
- novel_downloader/core/archived/deqixs/parser.py +132 -0
- novel_downloader/core/archived/deqixs/searcher.py +89 -0
- novel_downloader/core/{searchers/qidian.py → archived/qidian/searcher.py} +12 -20
- novel_downloader/core/archived/wanbengo/searcher.py +98 -0
- novel_downloader/core/archived/xshbook/searcher.py +93 -0
- novel_downloader/core/downloaders/__init__.py +3 -24
- novel_downloader/core/downloaders/base.py +49 -23
- novel_downloader/core/downloaders/common.py +191 -137
- novel_downloader/core/downloaders/qianbi.py +187 -146
- novel_downloader/core/downloaders/qidian.py +187 -141
- novel_downloader/core/downloaders/registry.py +4 -2
- novel_downloader/core/downloaders/signals.py +46 -0
- novel_downloader/core/exporters/__init__.py +3 -20
- novel_downloader/core/exporters/base.py +33 -37
- novel_downloader/core/exporters/common/__init__.py +1 -2
- novel_downloader/core/exporters/common/epub.py +15 -10
- novel_downloader/core/exporters/common/main_exporter.py +19 -12
- novel_downloader/core/exporters/common/txt.py +17 -12
- novel_downloader/core/exporters/epub_util.py +59 -29
- novel_downloader/core/exporters/linovelib/__init__.py +1 -0
- novel_downloader/core/exporters/linovelib/epub.py +23 -25
- novel_downloader/core/exporters/linovelib/main_exporter.py +8 -12
- novel_downloader/core/exporters/linovelib/txt.py +20 -14
- novel_downloader/core/exporters/qidian.py +2 -8
- novel_downloader/core/exporters/registry.py +4 -2
- novel_downloader/core/exporters/txt_util.py +7 -7
- novel_downloader/core/fetchers/__init__.py +54 -48
- novel_downloader/core/fetchers/aaatxt.py +83 -0
- novel_downloader/core/fetchers/{biquge/session.py → b520.py} +6 -11
- novel_downloader/core/fetchers/{base/session.py → base.py} +37 -46
- novel_downloader/core/fetchers/{biquge/browser.py → biquyuedu.py} +12 -17
- novel_downloader/core/fetchers/dxmwx.py +110 -0
- novel_downloader/core/fetchers/eightnovel.py +139 -0
- novel_downloader/core/fetchers/{esjzone/session.py → esjzone.py} +19 -12
- novel_downloader/core/fetchers/guidaye.py +85 -0
- novel_downloader/core/fetchers/hetushu.py +92 -0
- novel_downloader/core/fetchers/{qianbi/browser.py → i25zw.py} +19 -28
- novel_downloader/core/fetchers/ixdzs8.py +113 -0
- novel_downloader/core/fetchers/jpxs123.py +101 -0
- novel_downloader/core/fetchers/lewenn.py +83 -0
- novel_downloader/core/fetchers/{linovelib/session.py → linovelib.py} +12 -13
- novel_downloader/core/fetchers/piaotia.py +105 -0
- novel_downloader/core/fetchers/qbtr.py +101 -0
- novel_downloader/core/fetchers/{qianbi/session.py → qianbi.py} +5 -10
- novel_downloader/core/fetchers/{qidian/session.py → qidian.py} +56 -64
- novel_downloader/core/fetchers/quanben5.py +92 -0
- novel_downloader/core/fetchers/{base/rate_limiter.py → rate_limiter.py} +2 -2
- novel_downloader/core/fetchers/registry.py +5 -16
- novel_downloader/core/fetchers/{sfacg/session.py → sfacg.py} +7 -10
- novel_downloader/core/fetchers/shencou.py +106 -0
- novel_downloader/core/fetchers/shuhaige.py +84 -0
- novel_downloader/core/fetchers/tongrenquan.py +84 -0
- novel_downloader/core/fetchers/ttkan.py +95 -0
- novel_downloader/core/fetchers/wanbengo.py +83 -0
- novel_downloader/core/fetchers/xiaoshuowu.py +106 -0
- novel_downloader/core/fetchers/xiguashuwu.py +177 -0
- novel_downloader/core/fetchers/xs63b.py +171 -0
- novel_downloader/core/fetchers/xshbook.py +85 -0
- novel_downloader/core/fetchers/{yamibo/session.py → yamibo.py} +19 -12
- novel_downloader/core/fetchers/yibige.py +114 -0
- novel_downloader/core/interfaces/__init__.py +1 -9
- novel_downloader/core/interfaces/downloader.py +6 -2
- novel_downloader/core/interfaces/exporter.py +7 -7
- novel_downloader/core/interfaces/fetcher.py +6 -19
- novel_downloader/core/interfaces/parser.py +7 -8
- novel_downloader/core/interfaces/searcher.py +9 -1
- novel_downloader/core/parsers/__init__.py +49 -12
- novel_downloader/core/parsers/aaatxt.py +132 -0
- novel_downloader/core/parsers/b520.py +116 -0
- novel_downloader/core/parsers/base.py +64 -12
- novel_downloader/core/parsers/biquyuedu.py +133 -0
- novel_downloader/core/parsers/dxmwx.py +162 -0
- novel_downloader/core/parsers/eightnovel.py +224 -0
- novel_downloader/core/parsers/esjzone.py +64 -69
- novel_downloader/core/parsers/guidaye.py +128 -0
- novel_downloader/core/parsers/hetushu.py +139 -0
- novel_downloader/core/parsers/i25zw.py +137 -0
- novel_downloader/core/parsers/ixdzs8.py +186 -0
- novel_downloader/core/parsers/jpxs123.py +137 -0
- novel_downloader/core/parsers/lewenn.py +142 -0
- novel_downloader/core/parsers/linovelib.py +48 -64
- novel_downloader/core/parsers/piaotia.py +189 -0
- novel_downloader/core/parsers/qbtr.py +136 -0
- novel_downloader/core/parsers/qianbi.py +48 -50
- novel_downloader/core/parsers/qidian/main_parser.py +756 -48
- novel_downloader/core/parsers/qidian/utils/__init__.py +3 -21
- novel_downloader/core/parsers/qidian/utils/decryptor_fetcher.py +1 -1
- novel_downloader/core/parsers/qidian/utils/node_decryptor.py +4 -4
- novel_downloader/core/parsers/quanben5.py +103 -0
- novel_downloader/core/parsers/registry.py +5 -16
- novel_downloader/core/parsers/sfacg.py +38 -45
- novel_downloader/core/parsers/shencou.py +215 -0
- novel_downloader/core/parsers/shuhaige.py +111 -0
- novel_downloader/core/parsers/tongrenquan.py +116 -0
- novel_downloader/core/parsers/ttkan.py +132 -0
- novel_downloader/core/parsers/wanbengo.py +191 -0
- novel_downloader/core/parsers/xiaoshuowu.py +173 -0
- novel_downloader/core/parsers/xiguashuwu.py +429 -0
- novel_downloader/core/parsers/xs63b.py +161 -0
- novel_downloader/core/parsers/xshbook.py +134 -0
- novel_downloader/core/parsers/yamibo.py +87 -131
- novel_downloader/core/parsers/yibige.py +166 -0
- novel_downloader/core/searchers/__init__.py +34 -3
- novel_downloader/core/searchers/aaatxt.py +107 -0
- novel_downloader/core/searchers/{biquge.py → b520.py} +29 -28
- novel_downloader/core/searchers/base.py +112 -36
- novel_downloader/core/searchers/dxmwx.py +105 -0
- novel_downloader/core/searchers/eightnovel.py +84 -0
- novel_downloader/core/searchers/esjzone.py +43 -25
- novel_downloader/core/searchers/hetushu.py +92 -0
- novel_downloader/core/searchers/i25zw.py +93 -0
- novel_downloader/core/searchers/ixdzs8.py +107 -0
- novel_downloader/core/searchers/jpxs123.py +107 -0
- novel_downloader/core/searchers/piaotia.py +100 -0
- novel_downloader/core/searchers/qbtr.py +106 -0
- novel_downloader/core/searchers/qianbi.py +74 -40
- novel_downloader/core/searchers/quanben5.py +144 -0
- novel_downloader/core/searchers/registry.py +24 -8
- novel_downloader/core/searchers/shuhaige.py +124 -0
- novel_downloader/core/searchers/tongrenquan.py +110 -0
- novel_downloader/core/searchers/ttkan.py +92 -0
- novel_downloader/core/searchers/xiaoshuowu.py +122 -0
- novel_downloader/core/searchers/xiguashuwu.py +95 -0
- novel_downloader/core/searchers/xs63b.py +104 -0
- novel_downloader/locales/en.json +34 -85
- novel_downloader/locales/zh.json +35 -86
- novel_downloader/models/__init__.py +21 -22
- novel_downloader/models/book.py +44 -0
- novel_downloader/models/config.py +4 -37
- novel_downloader/models/login.py +1 -1
- novel_downloader/models/search.py +5 -0
- novel_downloader/resources/config/settings.toml +8 -70
- novel_downloader/resources/json/xiguashuwu.json +718 -0
- novel_downloader/utils/__init__.py +13 -24
- novel_downloader/utils/chapter_storage.py +5 -5
- novel_downloader/utils/constants.py +4 -31
- novel_downloader/utils/cookies.py +38 -35
- novel_downloader/utils/crypto_utils/__init__.py +7 -0
- novel_downloader/utils/crypto_utils/aes_util.py +90 -0
- novel_downloader/utils/crypto_utils/aes_v1.py +619 -0
- novel_downloader/utils/crypto_utils/aes_v2.py +1143 -0
- novel_downloader/utils/crypto_utils/rc4.py +54 -0
- novel_downloader/utils/epub/__init__.py +3 -4
- novel_downloader/utils/epub/builder.py +6 -6
- novel_downloader/utils/epub/constants.py +62 -21
- novel_downloader/utils/epub/documents.py +95 -201
- novel_downloader/utils/epub/models.py +8 -22
- novel_downloader/utils/epub/utils.py +73 -106
- novel_downloader/utils/file_utils/__init__.py +2 -23
- novel_downloader/utils/file_utils/io.py +53 -188
- novel_downloader/utils/file_utils/normalize.py +1 -7
- novel_downloader/utils/file_utils/sanitize.py +4 -15
- novel_downloader/utils/fontocr/__init__.py +5 -14
- novel_downloader/utils/fontocr/core.py +216 -0
- novel_downloader/utils/fontocr/loader.py +50 -0
- novel_downloader/utils/logger.py +81 -65
- novel_downloader/utils/network.py +17 -41
- novel_downloader/utils/state.py +4 -90
- novel_downloader/utils/text_utils/__init__.py +1 -7
- novel_downloader/utils/text_utils/diff_display.py +5 -7
- novel_downloader/utils/text_utils/text_cleaner.py +39 -30
- novel_downloader/utils/text_utils/truncate_utils.py +3 -14
- novel_downloader/utils/time_utils/__init__.py +5 -11
- novel_downloader/utils/time_utils/datetime_utils.py +20 -29
- novel_downloader/utils/time_utils/sleep_utils.py +55 -49
- novel_downloader/web/__init__.py +13 -0
- novel_downloader/web/components/__init__.py +11 -0
- novel_downloader/web/components/navigation.py +35 -0
- novel_downloader/web/main.py +66 -0
- novel_downloader/web/pages/__init__.py +17 -0
- novel_downloader/web/pages/download.py +78 -0
- novel_downloader/web/pages/progress.py +147 -0
- novel_downloader/web/pages/search.py +329 -0
- novel_downloader/web/services/__init__.py +17 -0
- novel_downloader/web/services/client_dialog.py +164 -0
- novel_downloader/web/services/cred_broker.py +113 -0
- novel_downloader/web/services/cred_models.py +35 -0
- novel_downloader/web/services/task_manager.py +264 -0
- novel_downloader-2.0.1.dist-info/METADATA +172 -0
- novel_downloader-2.0.1.dist-info/RECORD +206 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/entry_points.txt +1 -1
- novel_downloader/core/downloaders/biquge.py +0 -29
- novel_downloader/core/downloaders/esjzone.py +0 -29
- novel_downloader/core/downloaders/linovelib.py +0 -29
- novel_downloader/core/downloaders/sfacg.py +0 -29
- novel_downloader/core/downloaders/yamibo.py +0 -29
- novel_downloader/core/exporters/biquge.py +0 -22
- novel_downloader/core/exporters/esjzone.py +0 -22
- novel_downloader/core/exporters/qianbi.py +0 -22
- novel_downloader/core/exporters/sfacg.py +0 -22
- novel_downloader/core/exporters/yamibo.py +0 -22
- novel_downloader/core/fetchers/base/__init__.py +0 -14
- novel_downloader/core/fetchers/base/browser.py +0 -422
- novel_downloader/core/fetchers/biquge/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/__init__.py +0 -14
- novel_downloader/core/fetchers/esjzone/browser.py +0 -209
- novel_downloader/core/fetchers/linovelib/__init__.py +0 -14
- novel_downloader/core/fetchers/linovelib/browser.py +0 -198
- novel_downloader/core/fetchers/qianbi/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/__init__.py +0 -14
- novel_downloader/core/fetchers/qidian/browser.py +0 -326
- novel_downloader/core/fetchers/sfacg/__init__.py +0 -14
- novel_downloader/core/fetchers/sfacg/browser.py +0 -194
- novel_downloader/core/fetchers/yamibo/__init__.py +0 -14
- novel_downloader/core/fetchers/yamibo/browser.py +0 -234
- novel_downloader/core/parsers/biquge.py +0 -139
- novel_downloader/core/parsers/qidian/book_info_parser.py +0 -90
- novel_downloader/core/parsers/qidian/chapter_encrypted.py +0 -528
- novel_downloader/core/parsers/qidian/chapter_normal.py +0 -157
- novel_downloader/core/parsers/qidian/chapter_router.py +0 -68
- novel_downloader/core/parsers/qidian/utils/helpers.py +0 -114
- novel_downloader/models/chapter.py +0 -25
- novel_downloader/models/types.py +0 -13
- novel_downloader/tui/__init__.py +0 -7
- novel_downloader/tui/app.py +0 -32
- novel_downloader/tui/main.py +0 -17
- novel_downloader/tui/screens/__init__.py +0 -14
- novel_downloader/tui/screens/home.py +0 -198
- novel_downloader/tui/screens/login.py +0 -74
- novel_downloader/tui/styles/home_layout.tcss +0 -79
- novel_downloader/tui/widgets/richlog_handler.py +0 -24
- novel_downloader/utils/cache.py +0 -24
- novel_downloader/utils/crypto_utils.py +0 -71
- novel_downloader/utils/fontocr/hash_store.py +0 -280
- novel_downloader/utils/fontocr/hash_utils.py +0 -103
- novel_downloader/utils/fontocr/model_loader.py +0 -69
- novel_downloader/utils/fontocr/ocr_v1.py +0 -315
- novel_downloader/utils/fontocr/ocr_v2.py +0 -764
- novel_downloader/utils/fontocr/ocr_v3.py +0 -744
- novel_downloader-1.5.0.dist-info/METADATA +0 -196
- novel_downloader-1.5.0.dist-info/RECORD +0 -164
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/WHEEL +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {novel_downloader-1.5.0.dist-info → novel_downloader-2.0.1.dist-info}/top_level.txt +0 -0
@@ -1,68 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.chapter_router
|
4
|
-
---------------------------------------------------
|
5
|
-
|
6
|
-
Routing logic for selecting the correct chapter parser for Qidian pages.
|
7
|
-
"""
|
8
|
-
|
9
|
-
from __future__ import annotations
|
10
|
-
|
11
|
-
import logging
|
12
|
-
from typing import TYPE_CHECKING
|
13
|
-
|
14
|
-
from novel_downloader.models import ChapterDict
|
15
|
-
|
16
|
-
from .chapter_normal import parse_normal_chapter
|
17
|
-
from .utils import (
|
18
|
-
can_view_chapter,
|
19
|
-
find_ssr_page_context,
|
20
|
-
is_encrypted,
|
21
|
-
)
|
22
|
-
|
23
|
-
if TYPE_CHECKING:
|
24
|
-
from .main_parser import QidianParser
|
25
|
-
|
26
|
-
logger = logging.getLogger(__name__)
|
27
|
-
|
28
|
-
|
29
|
-
def parse_chapter(
|
30
|
-
parser: QidianParser,
|
31
|
-
html_str: str,
|
32
|
-
chapter_id: str,
|
33
|
-
) -> ChapterDict | None:
|
34
|
-
"""
|
35
|
-
Extract and return the formatted textual content of chapter.
|
36
|
-
|
37
|
-
:param parser: Instance of QidianParser.
|
38
|
-
:param html_str: Raw HTML content of the chapter page.
|
39
|
-
:param chapter_id: Identifier of the chapter being parsed.
|
40
|
-
:return: Formatted chapter text or empty string if not parsable.
|
41
|
-
"""
|
42
|
-
try:
|
43
|
-
ssr_data = find_ssr_page_context(html_str)
|
44
|
-
|
45
|
-
if not can_view_chapter(ssr_data):
|
46
|
-
logger.warning(
|
47
|
-
"[Parser] Chapter '%s' is not purchased or inaccessible.", chapter_id
|
48
|
-
)
|
49
|
-
return None
|
50
|
-
|
51
|
-
if is_encrypted(ssr_data):
|
52
|
-
if not parser._decode_font:
|
53
|
-
return None
|
54
|
-
try:
|
55
|
-
from .chapter_encrypted import parse_encrypted_chapter
|
56
|
-
|
57
|
-
return parse_encrypted_chapter(parser, html_str, chapter_id)
|
58
|
-
except ImportError:
|
59
|
-
logger.warning(
|
60
|
-
"[Parser] Encrypted chapter '%s' requires extra dependencies.",
|
61
|
-
chapter_id,
|
62
|
-
)
|
63
|
-
return None
|
64
|
-
|
65
|
-
return parse_normal_chapter(parser, html_str, chapter_id)
|
66
|
-
except Exception as e:
|
67
|
-
logger.warning("[Parser] parse error for chapter '%s': %s", chapter_id, e)
|
68
|
-
return None
|
@@ -1,114 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.core.parsers.qidian.utils.helpers
|
4
|
-
--------------------------------------------------
|
5
|
-
|
6
|
-
Shared utility functions for parsing Qidian pages.
|
7
|
-
|
8
|
-
This module provides reusable helpers to:
|
9
|
-
- Extract SSR-rendered JSON page context and structured chapter metadata.
|
10
|
-
- Identify VIP chapters, encrypted content, and viewability conditions.
|
11
|
-
"""
|
12
|
-
|
13
|
-
import json
|
14
|
-
import logging
|
15
|
-
from typing import Any
|
16
|
-
|
17
|
-
from lxml import html
|
18
|
-
|
19
|
-
logger = logging.getLogger(__name__)
|
20
|
-
|
21
|
-
|
22
|
-
def find_ssr_page_context(html_str: str) -> dict[str, Any]:
|
23
|
-
"""
|
24
|
-
Extract SSR JSON from <script id="vite-plugin-ssr_pageContext">.
|
25
|
-
"""
|
26
|
-
try:
|
27
|
-
tree = html.fromstring(html_str)
|
28
|
-
script = tree.xpath('//script[@id="vite-plugin-ssr_pageContext"]/text()')
|
29
|
-
if script:
|
30
|
-
data: dict[str, Any] = json.loads(script[0].strip())
|
31
|
-
return data
|
32
|
-
except Exception as e:
|
33
|
-
logger.warning("[Parser] SSR JSON parse error: %s", e)
|
34
|
-
return {}
|
35
|
-
|
36
|
-
|
37
|
-
def extract_chapter_info(ssr_data: dict[str, Any]) -> dict[str, Any]:
|
38
|
-
"""
|
39
|
-
Extract the 'chapterInfo' dictionary from the SSR page context.
|
40
|
-
|
41
|
-
This handles nested key access and returns an empty dict if missing.
|
42
|
-
|
43
|
-
:param ssr_data: The full SSR data object from _find_ssr_page_context().
|
44
|
-
:return: A dict with chapter metadata such as chapterName, authorSay, etc.
|
45
|
-
"""
|
46
|
-
try:
|
47
|
-
page_context = ssr_data.get("pageContext", {})
|
48
|
-
page_props = page_context.get("pageProps", {})
|
49
|
-
page_data = page_props.get("pageData", {})
|
50
|
-
chapter_info = page_data.get("chapterInfo", {})
|
51
|
-
|
52
|
-
assert isinstance(chapter_info, dict)
|
53
|
-
return chapter_info
|
54
|
-
except Exception:
|
55
|
-
return {}
|
56
|
-
|
57
|
-
|
58
|
-
def is_restricted_page(html_str: str) -> bool:
|
59
|
-
"""
|
60
|
-
Return True if page content indicates access restriction
|
61
|
-
(e.g. not subscribed/purchased).
|
62
|
-
|
63
|
-
:param html_str: Raw HTML string.
|
64
|
-
"""
|
65
|
-
markers = ["这是VIP章节", "需要订阅", "订阅后才能阅读"]
|
66
|
-
return any(m in html_str for m in markers)
|
67
|
-
|
68
|
-
|
69
|
-
def vip_status(ssr_data: dict[str, Any]) -> bool:
|
70
|
-
"""
|
71
|
-
:return: True if VIP, False otherwise.
|
72
|
-
"""
|
73
|
-
chapter_info = extract_chapter_info(ssr_data)
|
74
|
-
vip_flag = chapter_info.get("vipStatus", 0)
|
75
|
-
fens_flag = chapter_info.get("fEnS", 0)
|
76
|
-
return bool(vip_flag == 1 and fens_flag != 0)
|
77
|
-
|
78
|
-
|
79
|
-
def can_view_chapter(ssr_data: dict[str, Any]) -> bool:
|
80
|
-
"""
|
81
|
-
A chapter is not viewable if it is marked as VIP
|
82
|
-
and has not been purchased.
|
83
|
-
|
84
|
-
:return: True if viewable, False otherwise.
|
85
|
-
"""
|
86
|
-
chapter_info = extract_chapter_info(ssr_data)
|
87
|
-
is_buy = chapter_info.get("isBuy", 0)
|
88
|
-
vip_status = chapter_info.get("vipStatus", 0)
|
89
|
-
return not (vip_status == 1 and is_buy == 0)
|
90
|
-
|
91
|
-
|
92
|
-
def is_duplicated(ssr_data: dict[str, Any]) -> bool:
|
93
|
-
"""
|
94
|
-
Check if chapter is marked as duplicated (eFW = 1).
|
95
|
-
"""
|
96
|
-
chapter_info = extract_chapter_info(ssr_data)
|
97
|
-
efw_flag = chapter_info.get("eFW", 0)
|
98
|
-
return bool(efw_flag == 1)
|
99
|
-
|
100
|
-
|
101
|
-
def is_encrypted(content: str | dict[str, Any]) -> bool:
|
102
|
-
"""
|
103
|
-
Return True if content is encrypted.
|
104
|
-
|
105
|
-
Chapter Encryption Status (cES):
|
106
|
-
- 0: 内容是'明文'
|
107
|
-
- 2: 字体加密
|
108
|
-
|
109
|
-
:param content: HTML content, either as a raw string or a BeautifulSoup object.
|
110
|
-
:return: True if encrypted marker is found, else False.
|
111
|
-
"""
|
112
|
-
ssr_data = find_ssr_page_context(content) if isinstance(content, str) else content
|
113
|
-
chapter_info = extract_chapter_info(ssr_data)
|
114
|
-
return int(chapter_info.get("cES", 0)) == 2
|
@@ -1,25 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.models.chapter
|
4
|
-
-------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any, TypedDict
|
9
|
-
|
10
|
-
|
11
|
-
class ChapterDict(TypedDict, total=True):
|
12
|
-
"""
|
13
|
-
TypedDict for a novel chapter.
|
14
|
-
|
15
|
-
Fields:
|
16
|
-
id -- Unique chapter identifier
|
17
|
-
title -- Chapter title
|
18
|
-
content -- Chapter text
|
19
|
-
extra -- Arbitrary metadata (e.g. author remarks, timestamps)
|
20
|
-
"""
|
21
|
-
|
22
|
-
id: str
|
23
|
-
title: str
|
24
|
-
content: str
|
25
|
-
extra: dict[str, Any]
|
novel_downloader/models/types.py
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.models.types
|
4
|
-
-----------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Literal
|
9
|
-
|
10
|
-
ModeType = Literal["browser", "session"]
|
11
|
-
SplitMode = Literal["book", "volume"]
|
12
|
-
LogLevel = Literal["DEBUG", "INFO", "WARNING", "ERROR"]
|
13
|
-
BrowserType = Literal["chromium", "firefox", "webkit"]
|
novel_downloader/tui/__init__.py
DELETED
novel_downloader/tui/app.py
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.app
|
4
|
-
------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from textual.app import App, ComposeResult
|
11
|
-
from textual.containers import Container
|
12
|
-
from textual.widgets import Footer, Header
|
13
|
-
|
14
|
-
from novel_downloader.config import load_config
|
15
|
-
from novel_downloader.tui.screens import HomeScreen
|
16
|
-
|
17
|
-
|
18
|
-
class NovelDownloaderTUI(App): # type: ignore[misc]
|
19
|
-
TITLE = "Novel Downloader TUI"
|
20
|
-
SCREENS = {
|
21
|
-
"home": HomeScreen,
|
22
|
-
}
|
23
|
-
config: dict[str, Any]
|
24
|
-
|
25
|
-
def compose(self) -> ComposeResult:
|
26
|
-
yield Header()
|
27
|
-
yield Container(id="main_area")
|
28
|
-
yield Footer()
|
29
|
-
|
30
|
-
def on_mount(self) -> None:
|
31
|
-
self.config = load_config()
|
32
|
-
self.push_screen("home")
|
novel_downloader/tui/main.py
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.main
|
4
|
-
-------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from novel_downloader.tui.app import NovelDownloaderTUI
|
9
|
-
|
10
|
-
|
11
|
-
def tui_main() -> None:
|
12
|
-
app = NovelDownloaderTUI()
|
13
|
-
app.run()
|
14
|
-
|
15
|
-
|
16
|
-
if __name__ == "__main__":
|
17
|
-
tui_main()
|
@@ -1,198 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.screens.home
|
4
|
-
---------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
import asyncio
|
9
|
-
import logging
|
10
|
-
from typing import Any
|
11
|
-
|
12
|
-
from textual.app import ComposeResult
|
13
|
-
from textual.containers import Horizontal, Vertical
|
14
|
-
from textual.screen import Screen
|
15
|
-
from textual.widgets import Button, Input, ProgressBar, RichLog, Select, Static
|
16
|
-
|
17
|
-
from novel_downloader.config import ConfigAdapter
|
18
|
-
from novel_downloader.core.factory import (
|
19
|
-
get_downloader,
|
20
|
-
get_exporter,
|
21
|
-
get_fetcher,
|
22
|
-
get_parser,
|
23
|
-
)
|
24
|
-
from novel_downloader.core.interfaces import FetcherProtocol
|
25
|
-
from novel_downloader.models import LoginField
|
26
|
-
from novel_downloader.tui.widgets.richlog_handler import RichLogHandler
|
27
|
-
from novel_downloader.utils.i18n import t
|
28
|
-
|
29
|
-
|
30
|
-
class HomeScreen(Screen): # type: ignore[misc]
|
31
|
-
CSS_PATH = "../styles/home_layout.tcss"
|
32
|
-
|
33
|
-
def compose(self) -> ComposeResult:
|
34
|
-
yield Vertical(
|
35
|
-
self._make_title_bar(),
|
36
|
-
self._make_input_row(),
|
37
|
-
ProgressBar(id="prog", name="下载进度"),
|
38
|
-
Static("下载进度: 0/0 章", id="label-progress"),
|
39
|
-
RichLog(id="log", highlight=True, markup=False),
|
40
|
-
id="main-layout",
|
41
|
-
)
|
42
|
-
|
43
|
-
def on_mount(self) -> None:
|
44
|
-
log_widget = self.query_one("#log", RichLog)
|
45
|
-
|
46
|
-
self._log_handler = RichLogHandler(log_widget)
|
47
|
-
self._log_handler.setLevel(logging.INFO)
|
48
|
-
self._log_handler.setFormatter(logging.Formatter("[%(levelname)s] %(message)s"))
|
49
|
-
|
50
|
-
self._setup_logging(self._log_handler)
|
51
|
-
|
52
|
-
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
53
|
-
if event.button.id == "exit":
|
54
|
-
logging.info("退出应用")
|
55
|
-
self.app.exit()
|
56
|
-
|
57
|
-
elif event.button.id == "settings":
|
58
|
-
logging.info("设置功能暂未实现")
|
59
|
-
|
60
|
-
elif event.button.id == "download":
|
61
|
-
site = self.query_one("#site", Select).value
|
62
|
-
ids = self.query_one("#book_ids", Input).value
|
63
|
-
if not site or not ids.strip():
|
64
|
-
logging.warning("请填写完整信息")
|
65
|
-
return
|
66
|
-
id_list = {x.strip() for x in ids.split(",") if x.strip()}
|
67
|
-
adapter = ConfigAdapter(config=self.app.config, site=str(site))
|
68
|
-
# asyncio.create_task(self._download(adapter, str(site), id_list))
|
69
|
-
self.run_worker(
|
70
|
-
self._download(adapter, str(site), id_list),
|
71
|
-
name="download",
|
72
|
-
group="downloads",
|
73
|
-
description="正在下载书籍...",
|
74
|
-
)
|
75
|
-
|
76
|
-
def _make_title_bar(self) -> Horizontal:
|
77
|
-
return Horizontal(
|
78
|
-
Static("小说下载器", id="title"),
|
79
|
-
Button("设置", id="settings"),
|
80
|
-
Button("关闭", id="exit"),
|
81
|
-
id="title-bar",
|
82
|
-
)
|
83
|
-
|
84
|
-
def _make_input_row(self) -> Horizontal:
|
85
|
-
return Horizontal(
|
86
|
-
Vertical(self._make_site_select(), classes="left"),
|
87
|
-
Vertical(
|
88
|
-
Input(placeholder="输入书籍ID (支持逗号分隔)", id="book_ids"),
|
89
|
-
classes="middle",
|
90
|
-
),
|
91
|
-
Vertical(Button("下载", id="download"), classes="right"),
|
92
|
-
id="input-row",
|
93
|
-
)
|
94
|
-
|
95
|
-
def _make_site_select(self) -> Select:
|
96
|
-
return Select(
|
97
|
-
options=[
|
98
|
-
("起点中文网", "qidian"),
|
99
|
-
("笔趣阁", "biquge"),
|
100
|
-
("铅笔小说", "qianbi"),
|
101
|
-
("SF轻小说", "sfacg"),
|
102
|
-
("ESJ Zone", "esjzone"),
|
103
|
-
("百合会", "yamibo"),
|
104
|
-
("哔哩轻小说", "linovelib"),
|
105
|
-
],
|
106
|
-
prompt="选择站点",
|
107
|
-
value="qidian",
|
108
|
-
id="site",
|
109
|
-
)
|
110
|
-
|
111
|
-
async def _download(
|
112
|
-
self,
|
113
|
-
adapter: ConfigAdapter,
|
114
|
-
site: str,
|
115
|
-
book_ids: set[str],
|
116
|
-
) -> None:
|
117
|
-
btn = self.query_one("#download", Button)
|
118
|
-
btn.disabled = True
|
119
|
-
try:
|
120
|
-
logging.info(f"下载请求: {site} | {book_ids}")
|
121
|
-
downloader_cfg = adapter.get_downloader_config()
|
122
|
-
fetcher_cfg = adapter.get_fetcher_config()
|
123
|
-
parser_cfg = adapter.get_parser_config()
|
124
|
-
exporter_cfg = adapter.get_exporter_config()
|
125
|
-
|
126
|
-
parser = get_parser(site, parser_cfg)
|
127
|
-
exporter = get_exporter(site, exporter_cfg)
|
128
|
-
self._setup_logging(self._log_handler)
|
129
|
-
|
130
|
-
async with get_fetcher(site, fetcher_cfg) as fetcher:
|
131
|
-
if downloader_cfg.login_required and not await fetcher.load_state():
|
132
|
-
login_data = await self._prompt_login_fields(
|
133
|
-
fetcher, fetcher.login_fields, downloader_cfg
|
134
|
-
)
|
135
|
-
if not await fetcher.login(**login_data):
|
136
|
-
logging.info(t("download_login_failed"))
|
137
|
-
return
|
138
|
-
await fetcher.save_state()
|
139
|
-
|
140
|
-
downloader = get_downloader(
|
141
|
-
fetcher=fetcher,
|
142
|
-
parser=parser,
|
143
|
-
site=site,
|
144
|
-
config=downloader_cfg,
|
145
|
-
)
|
146
|
-
|
147
|
-
for book_id in book_ids:
|
148
|
-
logging.info(t("download_downloading", book_id=book_id, site=site))
|
149
|
-
await downloader.download(
|
150
|
-
{"book_id": book_id},
|
151
|
-
progress_hook=self._update_progress,
|
152
|
-
)
|
153
|
-
await asyncio.to_thread(exporter.export, book_id)
|
154
|
-
|
155
|
-
if downloader_cfg.login_required and fetcher.is_logged_in:
|
156
|
-
await fetcher.save_state()
|
157
|
-
finally:
|
158
|
-
btn.disabled = False
|
159
|
-
|
160
|
-
async def _prompt_login_fields(
|
161
|
-
self,
|
162
|
-
fetcher: FetcherProtocol,
|
163
|
-
fields: list[LoginField],
|
164
|
-
cfg: Any = None,
|
165
|
-
) -> dict[str, Any]:
|
166
|
-
"""
|
167
|
-
Push a LoginScreen to collect all required fields,
|
168
|
-
then return the dict of values when the user submits.
|
169
|
-
"""
|
170
|
-
# cfg_dict = asdict(cfg) if cfg else {}
|
171
|
-
# login_screen = LoginScreen(fields, cfg_dict)
|
172
|
-
# await self.app.push_screen(login_screen)
|
173
|
-
# await self.app.pop_screen()
|
174
|
-
return {}
|
175
|
-
|
176
|
-
def _setup_logging(self, handler: logging.Handler) -> None:
|
177
|
-
"""
|
178
|
-
Attach the given handler to the root logger.
|
179
|
-
"""
|
180
|
-
ft_logger = logging.getLogger("fontTools.ttLib.tables._p_o_s_t")
|
181
|
-
ft_logger.setLevel(logging.ERROR)
|
182
|
-
ft_logger.propagate = False
|
183
|
-
|
184
|
-
logger = logging.getLogger()
|
185
|
-
logger.setLevel(logging.INFO)
|
186
|
-
|
187
|
-
logger.handlers = [
|
188
|
-
h for h in logger.handlers if not isinstance(h, RichLogHandler)
|
189
|
-
]
|
190
|
-
logger.addHandler(handler)
|
191
|
-
|
192
|
-
async def _update_progress(self, done: int, total: int) -> None:
|
193
|
-
prog = self.query_one("#prog", ProgressBar)
|
194
|
-
label = self.query_one("#label-progress", Static)
|
195
|
-
|
196
|
-
prog.update(total=total, progress=min(done, total))
|
197
|
-
|
198
|
-
label.update(f"下载进度: {done}/{total} 章")
|
@@ -1,74 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.screens.login
|
4
|
-
----------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
from typing import Any
|
9
|
-
|
10
|
-
from textual.app import ComposeResult
|
11
|
-
from textual.containers import Vertical
|
12
|
-
from textual.screen import Screen
|
13
|
-
from textual.widgets import Button, Input, Static
|
14
|
-
|
15
|
-
from novel_downloader.models import LoginField
|
16
|
-
|
17
|
-
|
18
|
-
class LoginScreen(Screen): # type: ignore[misc]
|
19
|
-
"""
|
20
|
-
A modal screen that gathers login fields, then fires LoginScreen.Submitted.
|
21
|
-
"""
|
22
|
-
|
23
|
-
BINDINGS = [("escape", "app.pop_screen", "取消")]
|
24
|
-
|
25
|
-
def __init__(
|
26
|
-
self,
|
27
|
-
fields: list[LoginField],
|
28
|
-
cfg: dict[str, Any] | None = None,
|
29
|
-
) -> None:
|
30
|
-
super().__init__()
|
31
|
-
self.fields = fields
|
32
|
-
self.cfg = cfg or {}
|
33
|
-
|
34
|
-
def compose(self) -> ComposeResult:
|
35
|
-
widgets = []
|
36
|
-
for field in self.fields:
|
37
|
-
# show label and optional description
|
38
|
-
widgets.append(Static(field.label))
|
39
|
-
if field.description:
|
40
|
-
widgets.append(Static(f"[i]{field.description}[/]"))
|
41
|
-
|
42
|
-
# pick input type
|
43
|
-
if field.type == "password":
|
44
|
-
inp = Input(
|
45
|
-
placeholder=field.placeholder or "",
|
46
|
-
password=True,
|
47
|
-
id=field.name,
|
48
|
-
)
|
49
|
-
else:
|
50
|
-
inp = Input(
|
51
|
-
placeholder=field.placeholder or "",
|
52
|
-
id=field.name,
|
53
|
-
)
|
54
|
-
|
55
|
-
# pre-fill from config if present
|
56
|
-
existing = self.cfg.get(field.name, "").strip()
|
57
|
-
if existing:
|
58
|
-
inp.value = existing
|
59
|
-
|
60
|
-
widgets.append(inp)
|
61
|
-
|
62
|
-
# submit button at the end
|
63
|
-
widgets.append(Button("提交", id="submit"))
|
64
|
-
yield Vertical(*widgets, id="login-form")
|
65
|
-
|
66
|
-
async def on_button_pressed(self, event: Button.Pressed) -> None:
|
67
|
-
if event.button.id == "submit":
|
68
|
-
data: dict[str, Any] = {}
|
69
|
-
for field in self.fields:
|
70
|
-
inp = self.query_one(f"#{field.name}", Input)
|
71
|
-
value = inp.value
|
72
|
-
if not value and self.cfg.get(field.name):
|
73
|
-
value = self.cfg[field.name]
|
74
|
-
data[field.name] = value
|
@@ -1,79 +0,0 @@
|
|
1
|
-
#main-layout {
|
2
|
-
grid-rows: 3 auto 1 auto 1fr;
|
3
|
-
grid-columns: 1fr;
|
4
|
-
grid-gutter: 1;
|
5
|
-
padding: 1;
|
6
|
-
height: 100%;
|
7
|
-
}
|
8
|
-
|
9
|
-
#title-bar {
|
10
|
-
height: 3;
|
11
|
-
layout: horizontal;
|
12
|
-
align: left middle;
|
13
|
-
padding: 0 1;
|
14
|
-
background: $boost;
|
15
|
-
}
|
16
|
-
|
17
|
-
#title {
|
18
|
-
width: 1fr;
|
19
|
-
content-align: left middle;
|
20
|
-
}
|
21
|
-
|
22
|
-
#settings,
|
23
|
-
#exit {
|
24
|
-
width: 8;
|
25
|
-
padding: 0 1;
|
26
|
-
}
|
27
|
-
|
28
|
-
#input-row {
|
29
|
-
layout: horizontal;
|
30
|
-
padding: 1 0;
|
31
|
-
overflow-x: auto;
|
32
|
-
}
|
33
|
-
|
34
|
-
#site {
|
35
|
-
width: 20;
|
36
|
-
margin-right: 1;
|
37
|
-
}
|
38
|
-
|
39
|
-
#book_ids {
|
40
|
-
width: 1fr;
|
41
|
-
min-width: 0;
|
42
|
-
margin-right: 1;
|
43
|
-
}
|
44
|
-
|
45
|
-
#download {
|
46
|
-
width: 15;
|
47
|
-
}
|
48
|
-
|
49
|
-
#site,
|
50
|
-
#book_ids,
|
51
|
-
#download {
|
52
|
-
width: 100%;
|
53
|
-
}
|
54
|
-
|
55
|
-
Button#download {
|
56
|
-
border: round $accent;
|
57
|
-
padding: 0 1;
|
58
|
-
}
|
59
|
-
Button#download:hover {
|
60
|
-
background: $accent-lighten-3;
|
61
|
-
color: $text;
|
62
|
-
}
|
63
|
-
|
64
|
-
|
65
|
-
#prog {
|
66
|
-
height: 1;
|
67
|
-
color: $success;
|
68
|
-
}
|
69
|
-
|
70
|
-
#label {
|
71
|
-
content-align: left middle;
|
72
|
-
padding-left: 1;
|
73
|
-
}
|
74
|
-
|
75
|
-
#log {
|
76
|
-
border: round $primary;
|
77
|
-
padding: 1;
|
78
|
-
overflow-y: auto;
|
79
|
-
}
|
@@ -1,24 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.tui.widgets.richlog_handler
|
4
|
-
--------------------------------------------
|
5
|
-
|
6
|
-
"""
|
7
|
-
|
8
|
-
import logging
|
9
|
-
from logging import LogRecord
|
10
|
-
|
11
|
-
from textual.widgets import RichLog
|
12
|
-
|
13
|
-
|
14
|
-
class RichLogHandler(logging.Handler):
|
15
|
-
def __init__(self, rich_log_widget: RichLog):
|
16
|
-
super().__init__()
|
17
|
-
self.rich_log_widget = rich_log_widget
|
18
|
-
|
19
|
-
def emit(self, record: LogRecord) -> None:
|
20
|
-
msg = self.format(record)
|
21
|
-
try:
|
22
|
-
self.rich_log_widget.write(msg)
|
23
|
-
except Exception:
|
24
|
-
self.handleError(record)
|
novel_downloader/utils/cache.py
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
novel_downloader.utils.cache
|
4
|
-
----------------------------
|
5
|
-
|
6
|
-
Provides decorators for caching function results,
|
7
|
-
specifically optimized for configuration loading functions.
|
8
|
-
"""
|
9
|
-
|
10
|
-
from collections.abc import Callable
|
11
|
-
from functools import lru_cache, wraps
|
12
|
-
from typing import Any, TypeVar, cast
|
13
|
-
|
14
|
-
T = TypeVar("T", bound=Callable[..., Any])
|
15
|
-
|
16
|
-
|
17
|
-
def cached_load_config(func: T) -> T:
|
18
|
-
"""
|
19
|
-
A decorator to cache the result of a config-loading function.
|
20
|
-
Uses LRU cache with maxsize=1.
|
21
|
-
"""
|
22
|
-
cached = lru_cache(maxsize=1)(func)
|
23
|
-
wrapped = wraps(func)(cached)
|
24
|
-
return cast(T, wrapped)
|