diff --git a/static/css/setting.tcss b/source/CLI/__init__.py similarity index 100% rename from static/css/setting.tcss rename to source/CLI/__init__.py diff --git a/source/TUI/app.py b/source/TUI/app.py index ddd9bbb..06d2a9b 100644 --- a/source/TUI/app.py +++ b/source/TUI/app.py @@ -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") diff --git a/source/TUI/index.py b/source/TUI/index.py index 0981b75..c54999c 100644 --- a/source/TUI/index.py +++ b/source/TUI/index.py @@ -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) diff --git a/source/TUI/loading.py b/source/TUI/loading.py new file mode 100644 index 0000000..69b6e25 --- /dev/null +++ b/source/TUI/loading.py @@ -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() diff --git a/source/TUI/setting.py b/source/TUI/setting.py index 637f0b2..3271461 100644 --- a/source/TUI/setting.py +++ b/source/TUI/setting.py @@ -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) diff --git a/source/application/app.py b/source/application/app.py index a33286c..c8822e7 100644 --- a/source/application/app.py +++ b/source/application/app.py @@ -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 diff --git a/source/application/Downloader.py b/source/application/download.py similarity index 97% rename from source/application/Downloader.py rename to source/application/download.py index b4f2424..f213263 100644 --- a/source/application/Downloader.py +++ b/source/application/download.py @@ -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) diff --git a/source/application/Explore.py b/source/application/explore.py similarity index 100% rename from source/application/Explore.py rename to source/application/explore.py diff --git a/source/application/Image.py b/source/application/image.py similarity index 98% rename from source/application/Image.py rename to source/application/image.py index c699fff..fd29f69 100644 --- a/source/application/Image.py +++ b/source/application/image.py @@ -1,5 +1,5 @@ from source.expansion import Namespace -from .Html import Html +from .request import Html __all__ = ['Image'] diff --git a/source/application/Html.py b/source/application/request.py similarity index 93% rename from source/application/Html.py rename to source/application/request.py index e99829b..1ea7bbf 100644 --- a/source/application/Html.py +++ b/source/application/request.py @@ -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) diff --git a/source/application/Video.py b/source/application/video.py similarity index 93% rename from source/application/Video.py rename to source/application/video.py index 2105b7a..89cf042 100644 --- a/source/application/Video.py +++ b/source/application/video.py @@ -1,5 +1,5 @@ from source.expansion import Namespace -from .Html import Html +from .request import Html __all__ = ['Video'] diff --git a/source/module/tools.py b/source/module/tools.py index 48ea6d3..e5c23cd 100644 --- a/source/module/tools.py +++ b/source/module/tools.py @@ -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) diff --git a/source/translator/chinese.py b/source/translator/chinese.py index f36eedf..8b9b0ed 100644 --- a/source/translator/chinese.py +++ b/source/translator/chinese.py @@ -1,9 +1,7 @@ -from .template import Language - __all__ = ["Chinese"] -class Chinese(Language): +class Chinese: code: str = "zh-CN" disclaimer: tuple[str] = ( "关于 XHS-Downloader 的 免责声明:", diff --git a/source/translator/english.py b/source/translator/english.py index e7f11dc..a2e9137 100644 --- a/source/translator/english.py +++ b/source/translator/english.py @@ -1,7 +1,7 @@ -from .template import Language +from .chinese import Chinese __all__ = ["English"] -class English(Language): +class English(Chinese): pass diff --git a/source/translator/template.py b/source/translator/template.py deleted file mode 100644 index a2c0a86..0000000 --- a/source/translator/template.py +++ /dev/null @@ -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 diff --git a/static/css/index.tcss b/static/XHS-Downloader.tcss similarity index 56% rename from static/css/index.tcss rename to static/XHS-Downloader.tcss index f466f13..3808f7c 100644 --- a/static/css/index.tcss +++ b/static/XHS-Downloader.tcss @@ -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; }