diff --git a/README.md b/README.md index 61599eb..974c9f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@

XHS-Downloader

+

简体中文 | English

GitHub GitHub forks GitHub Repo stars @@ -9,7 +10,9 @@ GitHub code size in bytes GitHub release (with filter) GitHub all releases +

+

🔥 小红书作品采集工具:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!

❤️ 作者仅在 GitHub 发布 XHS-Downloader,未与任何个人或网站合作,且没有任何收费计划!

@@ -172,6 +175,12 @@ async with XHS(work_path=work_path, 是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致 false + +language +str +设置程序语言,目前支持:zh-CN +zh-CN +

🌐 Cookie

diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..e69de29 diff --git a/source/Static.py b/source/Static.py deleted file mode 100644 index e2525cd..0000000 --- a/source/Static.py +++ /dev/null @@ -1,70 +0,0 @@ -from pathlib import Path - -__all__ = [ - "VERSION_MAJOR", - "VERSION_MINOR", - "VERSION_BETA", - "ROOT", - "REPOSITORY", - "LICENCE", - "RELEASES", - "MASTER", - "PROMPT", - "GENERAL", - "PROGRESS", - "ERROR", - "WARNING", - "INFO", - "DISCLAIMER_TEXT", - "USERSCRIPT", - "USERAGENT", - "COOKIE", -] - -VERSION_MAJOR = 1 -VERSION_MINOR = 8 -VERSION_BETA = True -ROOT = Path(__file__).resolve().parent.parent - -REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader" -LICENCE = "GNU General Public License v3.0" -RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest" -DISCLAIMER_TEXT = ( - "关于 XHS-Downloader 的 免责声明:", - "", - "1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。", - "2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。", - "3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。", - "4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。", - "5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。", - "6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。", - "7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因" - "二次开发可能带来的各种情况负全部责任。", - "", - "在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果" - "您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。", - "", - ">" * 50, -) - -USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js" - -USERAGENT = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 " - "Safari/537.36") -COOKIE = ( - "abRequestId=54c534bb-a2c6-558f-8e03-5b4c5c45635c; xsecappid=xhs-pc-web; a1=18c286a400" - "4jy56qvzejvp631col0hd3032h4zjez50000106381; webId=779c977da3a15b5623015be94bdcc9e9; g" - "id=yYSJYK0qDW8KyYSJYK048quV84Vv2KAhudVhJduUKqySlx2818xfq4888y8KqYy8y2y2f8Jy; web_sess" - "ion=030037a259ce5f15c8d560dc12224a9fdc2ed1; webBuild=3.19.4; websectiga=984412fef754c" - "018e472127b8effd174be8a5d51061c991aadd200c69a2801d6; sec_poison_id=3dd48845-d604-4535" - "-bcc2-a859e97518bf; unread={%22ub%22:%22655eb3d60000000032033955%22%2C%22ue%22:%22656" - "e9ef2000000003801ff3d%22%2C%22uc%22:29}; cache_feeds=[]") - -MASTER = "b #fff200" -PROMPT = "b turquoise2" -GENERAL = "b bright_white" -PROGRESS = "b bright_magenta" -ERROR = "b bright_red" -WARNING = "b bright_yellow" -INFO = "b bright_green" diff --git a/source/TUI/__init__.py b/source/TUI/__init__.py new file mode 100644 index 0000000..e91bd0f --- /dev/null +++ b/source/TUI/__init__.py @@ -0,0 +1,3 @@ +from .index import XHSDownloader + +__all__ = ['XHSDownloader'] diff --git a/source/TUI.py b/source/TUI/index.py similarity index 60% rename from source/TUI.py rename to source/TUI/index.py index 8853ef0..3a76075 100644 --- a/source/TUI.py +++ b/source/TUI/index.py @@ -16,9 +16,9 @@ from textual.widgets import Label from textual.widgets import ProgressBar from textual.widgets import RichLog -from .App import XHS -from .Settings import Settings -from .Static import ( +from source.application import XHS +from source.module import Settings +from source.module import ( VERSION_MAJOR, VERSION_MINOR, VERSION_BETA, @@ -32,9 +32,11 @@ from .Static import ( REPOSITORY, RELEASES, GENERAL, - DISCLAIMER_TEXT, USERSCRIPT, ) +from source.translator import Chinese +from source.translator import LANGUAGE +from .setting import Setting __all__ = ["XHSDownloader"] @@ -53,20 +55,25 @@ def show_state(function): class XHSDownloader(App): CSS_PATH = ROOT.joinpath( - "static/XHS-Downloader.tcss") + "static/css/index.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="l", action="choose_language", description="切换语言"), + # Binding(key="s", action="settings", description="程序设置"), ] def __init__(self): super().__init__() - self.APP = XHS(**Settings(ROOT).run()) + settings = Settings(ROOT).run() + self.prompt = LANGUAGE.get(settings["language"], Chinese) + self.APP = XHS(**settings, language_object=self.prompt) self.url = None self.tip = None self.bar = None + self.setting = None self.disclaimer = True async def __aenter__(self): @@ -78,18 +85,18 @@ class XHSDownloader(App): def compose(self) -> ComposeResult: yield Header() - yield ScrollableContainer(Label(Text(f"开源协议:{LICENCE}", style=MASTER)), + yield ScrollableContainer(Label(Text(f"{self.prompt.open_source_protocol}{LICENCE}", style=MASTER)), Label( Text( - f"项目地址:{REPOSITORY}", + f"{self.prompt.project_address}{REPOSITORY}", style=MASTER)), - Label(Text("请输入小红书图文/视频作品链接:", + Label(Text(self.prompt.input_box_title, style=PROMPT), id="prompt"), - Input(placeholder="多个链接之间使用空格分隔"), - HorizontalScroll(Button("下载无水印图片/视频", id="deal"), - Button("读取剪贴板", id="paste"), - Button("清空输入框", id="reset"), ), - # Label(Text("准备就绪", style=INFO), id="state"), + 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"), ), + id="index", ) with Center(): yield ProgressBar(total=None, show_percentage=False, show_eta=False) @@ -104,7 +111,7 @@ class XHSDownloader(App): self.url = self.query_one(Input) self.tip = self.query_one(RichLog) self.bar = self.query_one(ProgressBar) - self.tip.write(Text("\n".join(DISCLAIMER_TEXT), style=MASTER)) + self.tip.write(Text("\n".join(self.prompt.disclaimer), style=MASTER)) def close_disclaimer(self): if self.disclaimer: @@ -122,16 +129,19 @@ class XHSDownloader(App): @show_state async def deal(self): if not self.url.value: - self.tip.write(Text("未输入任何小红书作品链接!", style=WARNING)) + self.tip.write(Text(self.prompt.invalid_link, style=WARNING)) return if any(await self.APP.extract(self.url.value, True, log=self.tip)): self.url.value = "" else: - self.tip.write(Text("下载小红书作品文件失败!", style=ERROR)) + self.tip.write(Text(self.prompt.download_failure, style=ERROR)) @show_state async def action_check_update(self): - self.tip.write(Text("正在检查新版本,请稍等...", style=WARNING)) + self.tip.write( + Text( + self.prompt.check_update_notification, + style=WARNING)) try: url = await self.APP.html.request_url(RELEASES, False, self.tip) latest_major, latest_minor = map( @@ -139,20 +149,41 @@ class XHSDownloader(App): if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR: self.tip.write( Text( - f"检测到新版本:{latest_major}.{latest_minor}", + self.prompt.official_version_update( + latest_major, + latest_minor), style=WARNING)) self.tip.write(RELEASES) elif latest_minor == VERSION_MINOR and VERSION_BETA: self.tip.write( - Text("当前版本为开发版, 可更新至正式版!", style=WARNING)) + Text( + self.prompt.development_version_update, + style=WARNING)) self.tip.write(RELEASES) elif VERSION_BETA: - self.tip.write(Text("当前已是最新开发版!", style=WARNING)) + self.tip.write( + Text( + self.prompt.latest_development_version, + style=WARNING)) else: - self.tip.write(Text("当前已是最新正式版!", style=INFO)) + self.tip.write( + Text( + self.prompt.latest_official_version, + style=INFO)) except ValueError: - self.tip.write(Text("检测新版本失败!", style=ERROR)) + self.tip.write(Text(self.prompt.check_update_failure, style=ERROR)) @staticmethod def action_user_script(): open(USERSCRIPT) + + def action_choose_language(self): + pass + + def action_settings(self): + if self.setting: + self.setting.remove() + self.setting = None + else: + self.setting = Setting() + self.query_one("#index").mount(self.setting) diff --git a/source/TUI/setting.py b/source/TUI/setting.py new file mode 100644 index 0000000..83e8b7d --- /dev/null +++ b/source/TUI/setting.py @@ -0,0 +1,16 @@ +from textual.app import ComposeResult +from textual.widgets import Label +from textual.widgets import Static + +from source.module import ROOT + +__all__ = ["Setting"] + + +class Setting(Static): + CSS_PATH = ROOT.joinpath( + "static/css/setting.tcss") + + def compose(self) -> ComposeResult: + """Create child widgets for the app.""" + yield Label("我是设置页") diff --git a/source/__init__.py b/source/__init__.py index 3cf8dd0..ef9a157 100644 --- a/source/__init__.py +++ b/source/__init__.py @@ -1,4 +1,4 @@ -from .App import XHS from .TUI import XHSDownloader +from .application import XHS __all__ = ['XHS', 'XHSDownloader'] diff --git a/source/Downloader.py b/source/application/Downloader.py similarity index 88% rename from source/Downloader.py rename to source/application/Downloader.py index ab5e5b7..dfadfbb 100644 --- a/source/Downloader.py +++ b/source/application/Downloader.py @@ -2,10 +2,10 @@ from pathlib import Path from aiohttp import ClientError -from .Manager import Manager -from .Static import ERROR -from .Tools import logging -from .Tools import retry as re_download +from source.module import ERROR +from source.module import Manager +from source.module import logging +from source.module import retry as re_download __all__ = ['Download'] @@ -20,6 +20,7 @@ class Download: self.chunk = manager.chunk self.session = manager.download_session self.retry = manager.retry + self.prompt = manager.prompt self.folder_mode = manager.folder_mode self.video_format = "mp4" self.image_format = manager.image_format @@ -50,7 +51,7 @@ class Download: temp = self.temp.joinpath(name) file = path.joinpath(name).with_suffix(f".{suffix}") if self.manager.is_exists(file): - logging(log, f"{name} 已存在,跳过下载!") + logging(log, self.prompt.skip_download(name)) return True # self.__create_progress( # bar, int( @@ -62,13 +63,13 @@ class Download: # self.__update_progress(bar, len(chunk)) self.manager.move(temp, file) # self.__create_progress(bar, None) - logging(log, f"{name} 下载成功!") + logging(log, self.prompt.download_success(name)) return True except ClientError as error: self.manager.delete(temp) # self.__create_progress(bar, None) logging(log, error, ERROR) - logging(log, f"网络异常,{name} 下载失败!", ERROR) + logging(log, self.prompt.download_error(name), ERROR) return False @staticmethod diff --git a/source/Explore.py b/source/application/Explore.py similarity index 98% rename from source/Explore.py rename to source/application/Explore.py index baab9d3..8b97ade 100644 --- a/source/Explore.py +++ b/source/application/Explore.py @@ -1,6 +1,6 @@ from datetime import datetime -from .Converter import Namespace +from source.expansion import Namespace __all__ = ['Explore'] diff --git a/source/Html.py b/source/application/Html.py similarity index 77% rename from source/Html.py rename to source/application/Html.py index 991b4a2..2f77e70 100644 --- a/source/Html.py +++ b/source/application/Html.py @@ -1,9 +1,9 @@ from aiohttp import ClientError -from .Manager import Manager -from .Static import ERROR -from .Tools import logging -from .Tools import retry +from source.module import ERROR +from source.module import Manager +from source.module import logging +from source.module import retry __all__ = ["Html"] @@ -12,6 +12,7 @@ class Html: def __init__(self, manager: Manager, ): self.proxy = manager.proxy self.retry = manager.retry + self.prompt = manager.prompt self.session = manager.request_session @retry @@ -29,7 +30,7 @@ class Html: return await response.text() if content else str(response.url) except ClientError as error: logging(log, error, ERROR) - logging(log, f"网络异常,请求 {url} 失败!", ERROR) + logging(log, self.prompt.request_error(url), ERROR) return "" @staticmethod diff --git a/source/Image.py b/source/application/Image.py similarity index 97% rename from source/Image.py rename to source/application/Image.py index 39664e7..c699fff 100644 --- a/source/Image.py +++ b/source/application/Image.py @@ -1,4 +1,4 @@ -from .Converter import Namespace +from source.expansion import Namespace from .Html import Html __all__ = ['Image'] diff --git a/source/Video.py b/source/application/Video.py similarity index 90% rename from source/Video.py rename to source/application/Video.py index 9e926c0..2105b7a 100644 --- a/source/Video.py +++ b/source/application/Video.py @@ -1,4 +1,4 @@ -from .Converter import Namespace +from source.expansion import Namespace from .Html import Html __all__ = ['Video'] diff --git a/source/application/__init__.py b/source/application/__init__.py new file mode 100644 index 0000000..f58ddb2 --- /dev/null +++ b/source/application/__init__.py @@ -0,0 +1,3 @@ +from .app import XHS + +__all__ = ["XHS"] diff --git a/source/App.py b/source/application/app.py similarity index 82% rename from source/App.py rename to source/application/app.py index 45018a6..b5c474d 100644 --- a/source/App.py +++ b/source/application/app.py @@ -1,18 +1,23 @@ from re import compile -from .Converter import Converter -from .Converter import Namespace -from .Downloader import Download -from .Explore import Explore -from .Html import Html -from .Image import Image -from .Manager import Manager -from .Static import ( +from source.expansion import Converter +from source.expansion import Namespace +from source.module import Manager +from source.module import ( ROOT, ERROR, WARNING, ) -from .Tools import logging +from source.module import logging +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 __all__ = ["XHS"] @@ -42,7 +47,10 @@ class XHS: record_data=False, image_format="PNG", folder_mode=False, + language="zh-CN", + language_object: Chinese | English = None, ): + self.prompt = language_object or LANGUAGE.get(language, Chinese) self.manager = Manager( ROOT, work_path, @@ -56,6 +64,7 @@ class XHS: record_data, image_format, folder_mode, + self.prompt, ) self.html = Html(self.manager) self.image = Image() @@ -77,16 +86,16 @@ class XHS: if (u := container["下载地址"]) and download: path = await self.download.run(u, name, container["作品类型"], log, bar) elif not u: - logging(log, "提取作品文件下载地址失败!", ERROR) + 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]: # return # 调试代码 urls = await self.__extract_links(url, log) if not urls: - logging(log, "提取小红书作品链接失败!", WARNING) + logging(log, self.prompt.extract_link_failure, WARNING) else: - logging(log, f"共 {len(urls)} 个小红书作品待处理...") + logging(log, self.prompt.pending_processing(len(urls))) # return urls # 调试代码 return [await self.__deal_extract(i, download, log, bar) for i in urls] @@ -103,17 +112,17 @@ class XHS: return urls async def __deal_extract(self, url: str, download: bool, log, bar): - logging(log, f"开始处理作品:{url}") + logging(log, self.prompt.start_processing(url)) html = await self.html.request_url(url, log=log) # logging(log, html) # 调试代码 if not html: - logging(log, f"{url} 获取数据失败!", ERROR) + logging(log, self.prompt.get_data_failure(url), ERROR) return {} namespace = self.__generate_data_object(html) data = self.explore.run(namespace) # logging(log, data) # 调试代码 if not data: - logging(log, f"{url} 提取数据失败!", ERROR) + logging(log, self.prompt.extract_data_failure(url), ERROR) return {} match data["作品类型"]: case "视频": @@ -123,7 +132,7 @@ class XHS: case _: data["下载地址"] = [] await self.__download_files(data, download, log, bar) - logging(log, f"作品处理完成:{url}") + logging(log, self.prompt.processing_completed(url)) return data def __generate_data_object(self, html: str) -> Namespace: diff --git a/source/expansion/__init__.py b/source/expansion/__init__.py new file mode 100644 index 0000000..8dfb9b4 --- /dev/null +++ b/source/expansion/__init__.py @@ -0,0 +1,4 @@ +from .converter import Converter +from .namespace import Namespace + +__all__ = ["Converter", "Namespace", ] diff --git a/source/expansion/converter.py b/source/expansion/converter.py new file mode 100644 index 0000000..c55516c --- /dev/null +++ b/source/expansion/converter.py @@ -0,0 +1,51 @@ +from lxml.etree import HTML +from yaml import safe_load + +__all__ = ["Converter"] + + +class Converter: + INITIAL_STATE = "(//script)[last()]/text()" + KEYS_LINK = ( + "note", + "noteDetailMap", + "[-1]", + "note", + ) + + def run(self, content: str) -> dict: + return self.__filter_object( + self.__convert_object( + self.__extract_object(content))) + + def __extract_object(self, html: str) -> str: + html_tree = HTML(html) + return d[0] if (d := html_tree.xpath(self.INITIAL_STATE)) else "" + + @staticmethod + def __convert_object(text: str) -> dict: + return safe_load(text.lstrip("window.__INITIAL_STATE__=")) + + @classmethod + def __filter_object(cls, data: dict) -> dict: + return cls.deep_get(data, cls.KEYS_LINK) or {} + + @classmethod + def deep_get(cls, data: dict, keys: list | tuple, default=None): + try: + for key in keys: + if key.startswith("[") and key.endswith("]"): + data = cls.safe_get(data, int(key[1:-1])) + else: + data = data[key] + return data + except (KeyError, IndexError, ValueError): + return default + + @staticmethod + def safe_get(data: dict | list | tuple | set, index: int): + if isinstance(data, dict): + return list(data.values())[index] + elif isinstance(data, list | tuple | set): + return data[index] + raise TypeError diff --git a/source/Converter.py b/source/expansion/namespace.py similarity index 58% rename from source/Converter.py rename to source/expansion/namespace.py index a706d2e..1fbf534 100644 --- a/source/Converter.py +++ b/source/expansion/namespace.py @@ -1,57 +1,7 @@ from copy import deepcopy from types import SimpleNamespace -from lxml.etree import HTML -from yaml import safe_load - -__all__ = ["Converter", "Namespace"] - - -class Converter: - INITIAL_STATE = "(//script)[last()]/text()" - KEYS_LINK = ( - "note", - "noteDetailMap", - "[-1]", - "note", - ) - - def run(self, content: str) -> dict: - return self.__filter_object( - self.__convert_object( - self.__extract_object(content))) - - def __extract_object(self, html: str) -> str: - html_tree = HTML(html) - return d[0] if (d := html_tree.xpath(self.INITIAL_STATE)) else "" - - @staticmethod - def __convert_object(text: str) -> dict: - return safe_load(text.lstrip("window.__INITIAL_STATE__=")) - - @classmethod - def __filter_object(cls, data: dict) -> dict: - return cls.deep_get(data, cls.KEYS_LINK) or {} - - @classmethod - def deep_get(cls, data: dict, keys: list | tuple, default=None): - try: - for key in keys: - if key.startswith("[") and key.endswith("]"): - data = cls.safe_get(data, int(key[1:-1])) - else: - data = data[key] - return data - except (KeyError, IndexError, ValueError): - return default - - @staticmethod - def safe_get(data: dict | list | tuple | set, index: int): - if isinstance(data, dict): - return list(data.values())[index] - elif isinstance(data, list | tuple | set): - return data[index] - raise TypeError +__all__ = ["Namespace"] class Namespace: diff --git a/source/module/__init__.py b/source/module/__init__.py new file mode 100644 index 0000000..071ba6d --- /dev/null +++ b/source/module/__init__.py @@ -0,0 +1,53 @@ +from .extend import Account +from .manager import Manager +from .recorder import Recorder +from .settings import Settings +from .static import ( + VERSION_MAJOR, + VERSION_MINOR, + VERSION_BETA, + ROOT, + REPOSITORY, + LICENCE, + RELEASES, + MASTER, + PROMPT, + GENERAL, + PROGRESS, + ERROR, + WARNING, + INFO, + USERSCRIPT, + USERAGENT, + COOKIE, +) +from .tools import ( + retry, + logging, +) + +__all__ = [ + "Account", + "Settings", + "Recorder", + "Manager", + "VERSION_MAJOR", + "VERSION_MINOR", + "VERSION_BETA", + "ROOT", + "REPOSITORY", + "LICENCE", + "RELEASES", + "MASTER", + "PROMPT", + "GENERAL", + "PROGRESS", + "ERROR", + "WARNING", + "INFO", + "USERSCRIPT", + "USERAGENT", + "COOKIE", + "retry", + "logging", +] diff --git a/source/Extend.py b/source/module/extend.py similarity index 100% rename from source/Extend.py rename to source/module/extend.py diff --git a/source/Manager.py b/source/module/manager.py similarity index 94% rename from source/Manager.py rename to source/module/manager.py index 99943f3..a120bfa 100644 --- a/source/Manager.py +++ b/source/module/manager.py @@ -9,8 +9,10 @@ from shutil import rmtree from aiohttp import ClientSession from aiohttp import ClientTimeout -from .Static import COOKIE -from .Static import USERAGENT +from source.translator import Chinese +from source.translator import English +from .static import COOKIE +from .static import USERAGENT __all__ = ["Manager"] @@ -32,6 +34,7 @@ class Manager: record_data: bool, image_format: str, folder_mode: bool, + language: Chinese | English, ): self.root = root self.temp = root.joinpath("./temp") @@ -54,6 +57,7 @@ class Manager: self.download_session = ClientSession( headers={"User-Agent": self.headers["User-Agent"]}, timeout=ClientTimeout(connect=timeout)) + self.prompt = language def __check_path(self, path: str) -> Path: if not path: diff --git a/source/Recorder.py b/source/module/recorder.py similarity index 100% rename from source/Recorder.py rename to source/module/recorder.py diff --git a/source/Settings.py b/source/module/settings.py similarity index 95% rename from source/Settings.py rename to source/module/settings.py index d37da13..f5f2356 100644 --- a/source/Settings.py +++ b/source/module/settings.py @@ -19,6 +19,8 @@ class Settings: "record_data": False, "image_format": "PNG", "folder_mode": False, + "language": "zh-CN", + # "server": False, } encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8" diff --git a/source/module/static.py b/source/module/static.py new file mode 100644 index 0000000..d9d8b3c --- /dev/null +++ b/source/module/static.py @@ -0,0 +1,52 @@ +from pathlib import Path + +__all__ = [ + "VERSION_MAJOR", + "VERSION_MINOR", + "VERSION_BETA", + "ROOT", + "REPOSITORY", + "LICENCE", + "RELEASES", + "MASTER", + "PROMPT", + "GENERAL", + "PROGRESS", + "ERROR", + "WARNING", + "INFO", + "USERSCRIPT", + "USERAGENT", + "COOKIE", +] + +VERSION_MAJOR = 1 +VERSION_MINOR = 8 +VERSION_BETA = True +ROOT = Path(__file__).resolve().parent.parent.parent + +REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader" +LICENCE = "GNU General Public License v3.0" +RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest" + +USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js" + +USERAGENT = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 " + "Safari/537.36") +COOKIE = ( + "abRequestId=54c534bb-a2c6-558f-8e03-5b4c5c45635c; xsecappid=xhs-pc-web; a1=18c286a400" + "4jy56qvzejvp631col0hd3032h4zjez50000106381; webId=779c977da3a15b5623015be94bdcc9e9; g" + "id=yYSJYK0qDW8KyYSJYK048quV84Vv2KAhudVhJduUKqySlx2818xfq4888y8KqYy8y2y2f8Jy; web_sess" + "ion=030037a259ce5f15c8d560dc12224a9fdc2ed1; webBuild=3.19.4; websectiga=984412fef754c" + "018e472127b8effd174be8a5d51061c991aadd200c69a2801d6; sec_poison_id=3dd48845-d604-4535" + "-bcc2-a859e97518bf; unread={%22ub%22:%22655eb3d60000000032033955%22%2C%22ue%22:%22656" + "e9ef2000000003801ff3d%22%2C%22uc%22:29}; cache_feeds=[]") + +MASTER = "b #fff200" +PROMPT = "b turquoise2" +GENERAL = "b bright_white" +PROGRESS = "b bright_magenta" +ERROR = "b bright_red" +WARNING = "b bright_yellow" +INFO = "b bright_green" diff --git a/source/Tools.py b/source/module/tools.py similarity index 95% rename from source/Tools.py rename to source/module/tools.py index e9a4040..a6658ad 100644 --- a/source/Tools.py +++ b/source/module/tools.py @@ -1,6 +1,6 @@ from rich.text import Text -from .Static import INFO +from .static import INFO __all__ = ["retry", "logging"] diff --git a/source/translator/__init__.py b/source/translator/__init__.py new file mode 100644 index 0000000..f43334e --- /dev/null +++ b/source/translator/__init__.py @@ -0,0 +1,13 @@ +from .chinese import Chinese +from .english import English + +__all__ = [ + "LANGUAGE", + "Chinese", + "English", +] + +LANGUAGE = { + Chinese.code: Chinese, + English.code: English, +} diff --git a/source/translator/chinese.py b/source/translator/chinese.py new file mode 100644 index 0000000..f36eedf --- /dev/null +++ b/source/translator/chinese.py @@ -0,0 +1,87 @@ +from .template import Language + +__all__ = ["Chinese"] + + +class Chinese(Language): + code: str = "zh-CN" + disclaimer: tuple[str] = ( + "关于 XHS-Downloader 的 免责声明:", + "", + "1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。", + "2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。", + "3. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。", + "4. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。", + "5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。", + "6. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。", + "7. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因" + "二次开发可能带来的各种情况负全部责任。", + "", + "在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果" + "您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。", + "", + ">" * 50, + ) + + download_link_error: str = "提取作品文件下载地址失败!" + extract_link_failure: str = "提取小红书作品链接失败!" + invalid_link: str = "未输入任何小红书作品链接!" + download_failure: str = "下载小红书作品文件失败!" + check_update_notification: str = "正在检查新版本,请稍等..." + development_version_update: str = "当前版本为开发版, 可更新至正式版!" + latest_development_version: str = "当前已是最新开发版!" + latest_official_version: str = "当前已是最新正式版!" + check_update_failure: str = "检测新版本失败!" + + open_source_protocol: str = "开源协议:" + project_address: str = "项目地址:" + input_box_title: str = "请输入小红书图文/视频作品链接:" + input_prompt: str = "多个链接之间使用空格分隔" + download_button: str = "下载无水印图片/视频" + paste_button: str = "读取剪贴板" + reset_button: str = "清空输入框" + + exit_program: str = "退出程序" + check_updates: str = "检查更新" + get_script: str = "获取脚本" + choose_language: str = "选择语言" + + @staticmethod + def request_error(url: str) -> str: + return f"网络异常,请求 {url} 失败!" + + @staticmethod + def skip_download(name: str) -> str: + return f"{name} 已存在,跳过下载!" + + @staticmethod + def download_success(name: str) -> str: + return f"{name} 下载成功!" + + @staticmethod + def download_error(name: str) -> str: + return f"网络异常,{name} 下载失败!" + + @staticmethod + def pending_processing(num: int) -> str: + return f"共 {num} 个小红书作品待处理..." + + @staticmethod + def start_processing(url: str) -> str: + return f"开始处理作品:{url}" + + @staticmethod + def get_data_failure(url: str) -> str: + return f"{url} 获取数据失败!" + + @staticmethod + def extract_data_failure(url: str) -> str: + return f"{url} 提取数据失败!" + + @staticmethod + def processing_completed(url: str) -> str: + return f"作品处理完成:{url}" + + @staticmethod + def official_version_update(major: int, minor: int) -> str: + return f"检测到新版本:{major}.{minor}" diff --git a/source/translator/english.py b/source/translator/english.py new file mode 100644 index 0000000..e7f11dc --- /dev/null +++ b/source/translator/english.py @@ -0,0 +1,7 @@ +from .template import Language + +__all__ = ["English"] + + +class English(Language): + pass diff --git a/source/translator/template.py b/source/translator/template.py new file mode 100644 index 0000000..a2c0a86 --- /dev/null +++ b/source/translator/template.py @@ -0,0 +1,69 @@ +__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/XHS-Downloader.tcss b/static/css/index.tcss similarity index 100% rename from static/XHS-Downloader.tcss rename to static/css/index.tcss diff --git a/static/css/setting.tcss b/static/css/setting.tcss new file mode 100644 index 0000000..e69de29