mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
优化项目交互界面
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
from typing import Type
|
||||
|
||||
from textual.app import App
|
||||
|
||||
from source.application import XHS
|
||||
@@ -5,21 +7,27 @@ from source.module import (
|
||||
ROOT,
|
||||
)
|
||||
from source.module import Settings
|
||||
from source.translator import Chinese
|
||||
from source.translator import LANGUAGE
|
||||
from source.translator import (
|
||||
LANGUAGE,
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
from .index import Index
|
||||
from .loading import Loading
|
||||
from .setting import Setting
|
||||
|
||||
__all__ = ["XHSDownloader"]
|
||||
|
||||
|
||||
class XHSDownloader(App):
|
||||
settings = Settings(ROOT)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = Settings(ROOT)
|
||||
self.parameter = self.settings.run()
|
||||
self.prompt = LANGUAGE.get(self.parameter["language"], Chinese)
|
||||
self.APP = XHS(**self.parameter, language_object=self.prompt)
|
||||
self.parameter: dict
|
||||
self.prompt: Type[Chinese | English]
|
||||
self.APP: XHS
|
||||
self.__initialization()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.APP.__aenter__()
|
||||
@@ -28,13 +36,30 @@ class XHSDownloader(App):
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.APP.__aexit__(exc_type, exc_value, traceback)
|
||||
|
||||
def __initialization(self) -> None:
|
||||
self.parameter = self.settings.run()
|
||||
self.prompt = LANGUAGE.get(self.parameter["language"], Chinese)
|
||||
self.APP = XHS(**self.parameter, language_object=self.prompt)
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
self.install_screen(Setting(), name="setting")
|
||||
self.install_screen(Setting(self.parameter), name="setting")
|
||||
self.install_screen(Index(self.APP, self.prompt), name="index")
|
||||
self.install_screen(Loading(), name="loading")
|
||||
await self.push_screen("index")
|
||||
|
||||
async def action_settings(self):
|
||||
await self.push_screen("setting")
|
||||
async def save_settings(data: dict) -> None:
|
||||
self.settings.update(data)
|
||||
await self.refresh_screen()
|
||||
|
||||
await self.push_screen("setting", save_settings)
|
||||
|
||||
async def action_index(self):
|
||||
await self.push_screen("index")
|
||||
|
||||
async def refresh_screen(self):
|
||||
await self.push_screen("loading")
|
||||
self.uninstall_screen("setting")
|
||||
self.__initialization()
|
||||
self.install_screen(Setting(self.parameter), name="setting")
|
||||
await self.push_screen("index")
|
||||
|
||||
@@ -53,11 +53,9 @@ def show_state(function):
|
||||
|
||||
|
||||
class Index(Screen):
|
||||
CSS_PATH = ROOT.joinpath(
|
||||
"static/css/index.tcss")
|
||||
CSS_PATH = ROOT.joinpath("static/XHS-Downloader.tcss")
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="退出程序"),
|
||||
# ("d", "toggle_dark", "切换主题"),
|
||||
Binding(key="u", action="check_update", description="检查更新"),
|
||||
Binding(key="m", action="user_script", description="获取脚本"),
|
||||
Binding(key="s", action="settings", description="程序设置"),
|
||||
@@ -74,18 +72,29 @@ class Index(Screen):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(Label(Text(f"{self.prompt.open_source_protocol}{LICENCE}", style=MASTER)),
|
||||
Label(
|
||||
Text(
|
||||
f"{self.prompt.project_address}{REPOSITORY}",
|
||||
style=MASTER)),
|
||||
Label(Text(self.prompt.input_box_title,
|
||||
style=PROMPT), id="prompt"),
|
||||
Input(placeholder=self.prompt.input_prompt),
|
||||
HorizontalScroll(Button(self.prompt.download_button, id="deal"),
|
||||
Button(self.prompt.paste_button, id="paste"),
|
||||
Button(self.prompt.reset_button, id="reset"), ),
|
||||
)
|
||||
yield ScrollableContainer(
|
||||
Label(
|
||||
Text(
|
||||
f"{self.prompt.open_source_protocol}{LICENCE}",
|
||||
style=MASTER)
|
||||
),
|
||||
Label(
|
||||
Text(
|
||||
f"{self.prompt.project_address}{REPOSITORY}",
|
||||
style=MASTER)
|
||||
),
|
||||
Label(
|
||||
Text(
|
||||
self.prompt.input_box_title,
|
||||
style=PROMPT), id="prompt",
|
||||
),
|
||||
Input(placeholder=self.prompt.input_prompt),
|
||||
HorizontalScroll(
|
||||
Button(self.prompt.download_button, id="deal"),
|
||||
Button(self.prompt.paste_button, id="paste"),
|
||||
Button(self.prompt.reset_button, id="reset"),
|
||||
),
|
||||
)
|
||||
with Center():
|
||||
yield ProgressBar(total=None, show_percentage=False, show_eta=False)
|
||||
yield RichLog(markup=True)
|
||||
|
||||
10
source/TUI/loading.py
Normal file
10
source/TUI/loading.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import LoadingIndicator
|
||||
|
||||
__all__ = ["Loading"]
|
||||
|
||||
|
||||
class Loading(Screen):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield LoadingIndicator()
|
||||
@@ -1,9 +1,16 @@
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container
|
||||
from textual.containers import ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button
|
||||
from textual.widgets import Checkbox
|
||||
from textual.widgets import Footer
|
||||
from textual.widgets import Header
|
||||
from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import Select
|
||||
|
||||
from source.module import ROOT
|
||||
|
||||
@@ -11,17 +18,83 @@ __all__ = ["Setting"]
|
||||
|
||||
|
||||
class Setting(Screen):
|
||||
CSS_PATH = ROOT.joinpath(
|
||||
"static/css/setting.tcss")
|
||||
CSS_PATH = ROOT.joinpath("static/XHS-Downloader.tcss")
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="退出程序"),
|
||||
Binding(key="b", action="index", description="返回首页"),
|
||||
]
|
||||
|
||||
def __init__(self, data: dict):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label("我是设置页,敬请期待!")
|
||||
yield ScrollableContainer(
|
||||
Label("工作路径:", classes="params", ),
|
||||
Input(self.data["work_path"], placeholder="程序根路径", valid_empty=True, id="work_path", ),
|
||||
Label("文件夹名称:", classes="params", ),
|
||||
Input(self.data["folder_name"], placeholder="Download", id="folder_name", ),
|
||||
Label("User-Agent:", classes="params", ),
|
||||
Input(self.data["user_agent"], placeholder="默认 UA", valid_empty=True, id="user_agent", ),
|
||||
Label("Cookie:", classes="params", ),
|
||||
Input(self.data["cookie"], placeholder="内置 Cookie,建议自行设置", valid_empty=True, id="cookie", ),
|
||||
Label("网络代理:", classes="params", ),
|
||||
Input(self.data["proxy"], placeholder="无代理", valid_empty=True, id="proxy", ),
|
||||
Label("请求超时限制:", classes="params", ),
|
||||
Input(str(self.data["timeout"]), placeholder="10", type="integer", id="timeout", ),
|
||||
Label("数据块大小:", classes="params", ),
|
||||
Input(str(self.data["chunk"]), placeholder="1048576", type="integer", id="chunk", ),
|
||||
Label("最大重试次数:", classes="params", ),
|
||||
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
|
||||
Container(
|
||||
Label("", classes="params", ),
|
||||
Label("", classes="params", ),
|
||||
Label("图片下载格式", classes="params", ),
|
||||
Label("程序语言", classes="params", ),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Container(
|
||||
Checkbox("记录作品数据", id="record_data", value=self.data["record_data"], ),
|
||||
Checkbox("文件夹归档模式", id="folder_mode", value=self.data["folder_mode"], ),
|
||||
Select.from_values(
|
||||
("PNG", "WEBP"),
|
||||
value=self.data["image_format"],
|
||||
allow_blank=False,
|
||||
id="image_format"),
|
||||
Select.from_values(("zh-CN", "en-US"),
|
||||
value=self.data["language"],
|
||||
allow_blank=False,
|
||||
id="language",
|
||||
disabled=True, ),
|
||||
classes="horizontal-layout"),
|
||||
Container(
|
||||
Button("保存设置", id="save", ),
|
||||
Button("放弃更改", id="abandon", ),
|
||||
classes="settings_button", ),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = "程序设置"
|
||||
|
||||
@on(Button.Pressed, "#save")
|
||||
def save_settings(self):
|
||||
self.dismiss({
|
||||
"work_path": self.query_one("#work_path").value,
|
||||
"folder_name": self.query_one("#folder_name").value,
|
||||
"user_agent": self.query_one("#user_agent").value,
|
||||
"cookie": self.query_one("#cookie").value,
|
||||
"proxy": self.query_one("#proxy").value or None,
|
||||
"timeout": int(self.query_one("#timeout").value),
|
||||
"chunk": int(self.query_one("#chunk").value),
|
||||
"max_retry": int(self.query_one("#max_retry").value),
|
||||
"record_data": self.query_one("#record_data").value,
|
||||
"image_format": self.query_one("#image_format").value,
|
||||
"folder_mode": self.query_one("#folder_mode").value,
|
||||
"language": self.query_one("#language").value,
|
||||
})
|
||||
|
||||
@on(Button.Pressed, "#abandon")
|
||||
def reset(self):
|
||||
self.dismiss(self.data)
|
||||
|
||||
@@ -9,16 +9,17 @@ from source.module import (
|
||||
WARNING,
|
||||
)
|
||||
from source.module import logging
|
||||
from source.module import wait
|
||||
from source.translator import (
|
||||
LANGUAGE,
|
||||
Chinese,
|
||||
English,
|
||||
)
|
||||
from .Downloader import Download
|
||||
from .Explore import Explore
|
||||
from .Html import Html
|
||||
from .Image import Image
|
||||
from .Video import Video
|
||||
from .download import Download
|
||||
from .explore import Explore
|
||||
from .image import Image
|
||||
from .request import Html
|
||||
from .video import Video
|
||||
|
||||
__all__ = ["XHS"]
|
||||
|
||||
@@ -89,7 +90,7 @@ class XHS:
|
||||
logging(log, self.prompt.download_link_error, ERROR)
|
||||
self.manager.save_data(path, name, container)
|
||||
|
||||
async def extract(self, url: str, download=False, log=None, bar=None) -> list[dict]:
|
||||
async def extract(self, url: str, download=False, efficient=False, log=None, bar=None) -> list[dict]:
|
||||
# return # 调试代码
|
||||
urls = await self.__extract_links(url, log)
|
||||
if not urls:
|
||||
@@ -97,7 +98,7 @@ class XHS:
|
||||
else:
|
||||
logging(log, self.prompt.pending_processing(len(urls)))
|
||||
# return urls # 调试代码
|
||||
return [await self.__deal_extract(i, download, log, bar) for i in urls]
|
||||
return [await self.__deal_extract(i, download, efficient, log, bar) for i in urls]
|
||||
|
||||
async def __extract_links(self, url: str, log) -> list:
|
||||
urls = []
|
||||
@@ -111,13 +112,14 @@ class XHS:
|
||||
urls.append(u.group())
|
||||
return urls
|
||||
|
||||
async def __deal_extract(self, url: str, download: bool, log, bar):
|
||||
async def __deal_extract(self, url: str, download: bool, efficient: bool, log, bar):
|
||||
logging(log, self.prompt.start_processing(url))
|
||||
html = await self.html.request_url(url, log=log)
|
||||
namespace = self.__generate_data_object(html)
|
||||
if not namespace:
|
||||
logging(log, self.prompt.get_data_failure(url), ERROR)
|
||||
return {}
|
||||
await self.__suspend(efficient)
|
||||
data = self.explore.run(namespace)
|
||||
# logging(log, data) # 调试代码
|
||||
if not data:
|
||||
@@ -140,7 +142,15 @@ class XHS:
|
||||
|
||||
def __naming_rules(self, data: dict) -> str:
|
||||
"""下载文件默认使用 作品标题 或 作品 ID 作为文件名称,可修改此方法自定义文件名称格式"""
|
||||
return self.manager.filter_name(data["作品标题"]) or data["作品ID"]
|
||||
author = self.manager.filter_name(data["作者昵称"]) or data["作者ID"]
|
||||
title = self.manager.filter_name(data["作品标题"]) or data["作品ID"]
|
||||
return f"{author}-{title}"
|
||||
|
||||
@staticmethod
|
||||
async def __suspend(efficient: bool) -> None:
|
||||
if efficient:
|
||||
return
|
||||
await wait()
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
@@ -53,6 +53,8 @@ class Download:
|
||||
async def __download(self, url: str, path: Path, name: str, format_: str, log, bar):
|
||||
try:
|
||||
async with self.session.get(url, proxy=self.proxy) as response:
|
||||
if response.status != 200:
|
||||
return False
|
||||
suffix = self.__extract_type(
|
||||
response.headers.get("Content-Type")) or format_
|
||||
temp = self.temp.joinpath(name)
|
||||
@@ -1,5 +1,5 @@
|
||||
from source.expansion import Namespace
|
||||
from .Html import Html
|
||||
from .request import Html
|
||||
|
||||
__all__ = ['Image']
|
||||
|
||||
@@ -27,6 +27,8 @@ class Html:
|
||||
url,
|
||||
proxy=self.proxy,
|
||||
) as response:
|
||||
if response.status != 200:
|
||||
return ""
|
||||
return await response.text() if content else str(response.url)
|
||||
except ClientError as error:
|
||||
logging(log, str(error), ERROR)
|
||||
@@ -1,5 +1,5 @@
|
||||
from source.expansion import Namespace
|
||||
from .Html import Html
|
||||
from .request import Html
|
||||
|
||||
__all__ = ['Video']
|
||||
|
||||
@@ -29,4 +29,4 @@ def logging(log, text, style=INFO):
|
||||
|
||||
|
||||
async def wait():
|
||||
await sleep(randint(15, 35) * 0.1)
|
||||
await sleep(randint(15, 45) * 0.1)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from .template import Language
|
||||
|
||||
__all__ = ["Chinese"]
|
||||
|
||||
|
||||
class Chinese(Language):
|
||||
class Chinese:
|
||||
code: str = "zh-CN"
|
||||
disclaimer: tuple[str] = (
|
||||
"关于 XHS-Downloader 的 免责声明:",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from .template import Language
|
||||
from .chinese import Chinese
|
||||
|
||||
__all__ = ["English"]
|
||||
|
||||
|
||||
class English(Language):
|
||||
class English(Chinese):
|
||||
pass
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
__all__ = ["Language"]
|
||||
|
||||
|
||||
class Language:
|
||||
code: str = None
|
||||
disclaimer: tuple[str] = None
|
||||
|
||||
download_link_error: str = None
|
||||
extract_link_failure: str = None
|
||||
invalid_link: str = None
|
||||
download_failure: str = None
|
||||
check_update_notification: str = None
|
||||
development_version_update: str = None
|
||||
latest_development_version: str = None
|
||||
latest_official_version: str = None
|
||||
check_update_failure: str = None
|
||||
|
||||
open_source_protocol: str = None
|
||||
project_address: str = None
|
||||
input_box_title: str = None
|
||||
input_prompt: str = None
|
||||
download_button: str = None
|
||||
paste_button: str = None
|
||||
reset_button: str = None
|
||||
|
||||
exit_program: str = None
|
||||
check_updates: str = None
|
||||
get_script: str = None
|
||||
choose_language: str = None
|
||||
|
||||
@staticmethod
|
||||
def request_error(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def skip_download(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def download_success(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def download_error(name: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def pending_processing(num: int) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def start_processing(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_data_failure(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def extract_data_failure(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def processing_completed(url: str) -> str:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def official_version_update(major: int, minor: int) -> str:
|
||||
pass
|
||||
@@ -3,10 +3,21 @@ Button {
|
||||
margin: 1 1;
|
||||
text-style: bold;
|
||||
}
|
||||
Button#deal, Button#paste {
|
||||
.vertical-layout {
|
||||
layout: vertical;
|
||||
height: auto;
|
||||
}
|
||||
.horizontal-layout, .settings_button {
|
||||
layout: horizontal;
|
||||
height: auto;
|
||||
}
|
||||
.horizontal-layout > * {
|
||||
width: 25vw;
|
||||
}
|
||||
Button#deal, Button#paste, Button#save {
|
||||
tint: #27ae60 60%;
|
||||
}
|
||||
Button#reset {
|
||||
Button#reset, Button#abandon {
|
||||
tint: #c0392b 60%;
|
||||
}
|
||||
Label {
|
||||
@@ -15,6 +26,9 @@ Label {
|
||||
content-align-vertical: middle;
|
||||
text-style: bold;
|
||||
}
|
||||
Label.params {
|
||||
margin: 1 0 0 0;
|
||||
}
|
||||
Label#prompt {
|
||||
padding: 1;
|
||||
}
|
||||
Reference in New Issue
Block a user