mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
更新项目代码
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="static/XHS-Downloader.png" alt="" height="256" width="256"><br>
|
<img src="static/XHS-Downloader.png" alt="" height="256" width="256"><br>
|
||||||
<h1>XHS-Downloader</h1>
|
<h1>XHS-Downloader</h1>
|
||||||
|
<p>简体中文 | <a href="README_EN.md">English</a></p>
|
||||||
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7a45">
|
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7a45">
|
||||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=for-the-badge&color=9254de">
|
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=for-the-badge&color=9254de">
|
||||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7875">
|
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=for-the-badge&color=ff7875">
|
||||||
@@ -9,7 +10,9 @@
|
|||||||
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JoeanAmier/XHS-Downloader?style=for-the-badge&color=73d13d">
|
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JoeanAmier/XHS-Downloader?style=for-the-badge&color=73d13d">
|
||||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=for-the-badge&color=40a9ff">
|
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=for-the-badge&color=40a9ff">
|
||||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=for-the-badge&color=f759ab">
|
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=for-the-badge&color=f759ab">
|
||||||
|
</div>
|
||||||
<br>
|
<br>
|
||||||
|
<div align="center">
|
||||||
<p>🔥 <b>小红书作品采集工具</b>:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
|
<p>🔥 <b>小红书作品采集工具</b>:采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
|
||||||
<p>❤️ 作者仅在 GitHub 发布 XHS-Downloader,未与任何个人或网站合作,且没有任何收费计划!</p>
|
<p>❤️ 作者仅在 GitHub 发布 XHS-Downloader,未与任何个人或网站合作,且没有任何收费计划!</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -172,6 +175,12 @@ async with XHS(work_path=work_path,
|
|||||||
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
|
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
|
||||||
<td align="center">false</td>
|
<td align="center">false</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center">language</td>
|
||||||
|
<td align="center">str</td>
|
||||||
|
<td align="center">设置程序语言,目前支持:<code>zh-CN</code></td>
|
||||||
|
<td align="center">zh-CN</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<h1>🌐 Cookie</h1>
|
<h1>🌐 Cookie</h1>
|
||||||
|
|||||||
0
README_EN.md
Normal file
0
README_EN.md
Normal file
@@ -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"
|
|
||||||
3
source/TUI/__init__.py
Normal file
3
source/TUI/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .index import XHSDownloader
|
||||||
|
|
||||||
|
__all__ = ['XHSDownloader']
|
||||||
@@ -16,9 +16,9 @@ from textual.widgets import Label
|
|||||||
from textual.widgets import ProgressBar
|
from textual.widgets import ProgressBar
|
||||||
from textual.widgets import RichLog
|
from textual.widgets import RichLog
|
||||||
|
|
||||||
from .App import XHS
|
from source.application import XHS
|
||||||
from .Settings import Settings
|
from source.module import Settings
|
||||||
from .Static import (
|
from source.module import (
|
||||||
VERSION_MAJOR,
|
VERSION_MAJOR,
|
||||||
VERSION_MINOR,
|
VERSION_MINOR,
|
||||||
VERSION_BETA,
|
VERSION_BETA,
|
||||||
@@ -32,9 +32,11 @@ from .Static import (
|
|||||||
REPOSITORY,
|
REPOSITORY,
|
||||||
RELEASES,
|
RELEASES,
|
||||||
GENERAL,
|
GENERAL,
|
||||||
DISCLAIMER_TEXT,
|
|
||||||
USERSCRIPT,
|
USERSCRIPT,
|
||||||
)
|
)
|
||||||
|
from source.translator import Chinese
|
||||||
|
from source.translator import LANGUAGE
|
||||||
|
from .setting import Setting
|
||||||
|
|
||||||
__all__ = ["XHSDownloader"]
|
__all__ = ["XHSDownloader"]
|
||||||
|
|
||||||
@@ -53,20 +55,25 @@ def show_state(function):
|
|||||||
|
|
||||||
class XHSDownloader(App):
|
class XHSDownloader(App):
|
||||||
CSS_PATH = ROOT.joinpath(
|
CSS_PATH = ROOT.joinpath(
|
||||||
"static/XHS-Downloader.tcss")
|
"static/css/index.tcss")
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding(key="q", action="quit", description="退出程序"),
|
Binding(key="q", action="quit", description="退出程序"),
|
||||||
# ("d", "toggle_dark", "切换主题"),
|
# ("d", "toggle_dark", "切换主题"),
|
||||||
Binding(key="u", action="check_update", description="检查更新"),
|
Binding(key="u", action="check_update", description="检查更新"),
|
||||||
Binding(key="m", action="user_script", description="获取脚本"),
|
Binding(key="m", action="user_script", description="获取脚本"),
|
||||||
|
# Binding(key="l", action="choose_language", description="切换语言"),
|
||||||
|
# Binding(key="s", action="settings", description="程序设置"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
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.url = None
|
||||||
self.tip = None
|
self.tip = None
|
||||||
self.bar = None
|
self.bar = None
|
||||||
|
self.setting = None
|
||||||
self.disclaimer = True
|
self.disclaimer = True
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
@@ -78,18 +85,18 @@ class XHSDownloader(App):
|
|||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
yield Header()
|
yield Header()
|
||||||
yield ScrollableContainer(Label(Text(f"开源协议:{LICENCE}", style=MASTER)),
|
yield ScrollableContainer(Label(Text(f"{self.prompt.open_source_protocol}{LICENCE}", style=MASTER)),
|
||||||
Label(
|
Label(
|
||||||
Text(
|
Text(
|
||||||
f"项目地址:{REPOSITORY}",
|
f"{self.prompt.project_address}{REPOSITORY}",
|
||||||
style=MASTER)),
|
style=MASTER)),
|
||||||
Label(Text("请输入小红书图文/视频作品链接:",
|
Label(Text(self.prompt.input_box_title,
|
||||||
style=PROMPT), id="prompt"),
|
style=PROMPT), id="prompt"),
|
||||||
Input(placeholder="多个链接之间使用空格分隔"),
|
Input(placeholder=self.prompt.input_prompt),
|
||||||
HorizontalScroll(Button("下载无水印图片/视频", id="deal"),
|
HorizontalScroll(Button(self.prompt.download_button, id="deal"),
|
||||||
Button("读取剪贴板", id="paste"),
|
Button(self.prompt.paste_button, id="paste"),
|
||||||
Button("清空输入框", id="reset"), ),
|
Button(self.prompt.reset_button, id="reset"), ),
|
||||||
# Label(Text("准备就绪", style=INFO), id="state"),
|
id="index",
|
||||||
)
|
)
|
||||||
with Center():
|
with Center():
|
||||||
yield ProgressBar(total=None, show_percentage=False, show_eta=False)
|
yield ProgressBar(total=None, show_percentage=False, show_eta=False)
|
||||||
@@ -104,7 +111,7 @@ class XHSDownloader(App):
|
|||||||
self.url = self.query_one(Input)
|
self.url = self.query_one(Input)
|
||||||
self.tip = self.query_one(RichLog)
|
self.tip = self.query_one(RichLog)
|
||||||
self.bar = self.query_one(ProgressBar)
|
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):
|
def close_disclaimer(self):
|
||||||
if self.disclaimer:
|
if self.disclaimer:
|
||||||
@@ -122,16 +129,19 @@ class XHSDownloader(App):
|
|||||||
@show_state
|
@show_state
|
||||||
async def deal(self):
|
async def deal(self):
|
||||||
if not self.url.value:
|
if not self.url.value:
|
||||||
self.tip.write(Text("未输入任何小红书作品链接!", style=WARNING))
|
self.tip.write(Text(self.prompt.invalid_link, style=WARNING))
|
||||||
return
|
return
|
||||||
if any(await self.APP.extract(self.url.value, True, log=self.tip)):
|
if any(await self.APP.extract(self.url.value, True, log=self.tip)):
|
||||||
self.url.value = ""
|
self.url.value = ""
|
||||||
else:
|
else:
|
||||||
self.tip.write(Text("下载小红书作品文件失败!", style=ERROR))
|
self.tip.write(Text(self.prompt.download_failure, style=ERROR))
|
||||||
|
|
||||||
@show_state
|
@show_state
|
||||||
async def action_check_update(self):
|
async def action_check_update(self):
|
||||||
self.tip.write(Text("正在检查新版本,请稍等...", style=WARNING))
|
self.tip.write(
|
||||||
|
Text(
|
||||||
|
self.prompt.check_update_notification,
|
||||||
|
style=WARNING))
|
||||||
try:
|
try:
|
||||||
url = await self.APP.html.request_url(RELEASES, False, self.tip)
|
url = await self.APP.html.request_url(RELEASES, False, self.tip)
|
||||||
latest_major, latest_minor = map(
|
latest_major, latest_minor = map(
|
||||||
@@ -139,20 +149,41 @@ class XHSDownloader(App):
|
|||||||
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
|
if latest_major > VERSION_MAJOR or latest_minor > VERSION_MINOR:
|
||||||
self.tip.write(
|
self.tip.write(
|
||||||
Text(
|
Text(
|
||||||
f"检测到新版本:{latest_major}.{latest_minor}",
|
self.prompt.official_version_update(
|
||||||
|
latest_major,
|
||||||
|
latest_minor),
|
||||||
style=WARNING))
|
style=WARNING))
|
||||||
self.tip.write(RELEASES)
|
self.tip.write(RELEASES)
|
||||||
elif latest_minor == VERSION_MINOR and VERSION_BETA:
|
elif latest_minor == VERSION_MINOR and VERSION_BETA:
|
||||||
self.tip.write(
|
self.tip.write(
|
||||||
Text("当前版本为开发版, 可更新至正式版!", style=WARNING))
|
Text(
|
||||||
|
self.prompt.development_version_update,
|
||||||
|
style=WARNING))
|
||||||
self.tip.write(RELEASES)
|
self.tip.write(RELEASES)
|
||||||
elif VERSION_BETA:
|
elif VERSION_BETA:
|
||||||
self.tip.write(Text("当前已是最新开发版!", style=WARNING))
|
self.tip.write(
|
||||||
|
Text(
|
||||||
|
self.prompt.latest_development_version,
|
||||||
|
style=WARNING))
|
||||||
else:
|
else:
|
||||||
self.tip.write(Text("当前已是最新正式版!", style=INFO))
|
self.tip.write(
|
||||||
|
Text(
|
||||||
|
self.prompt.latest_official_version,
|
||||||
|
style=INFO))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.tip.write(Text("检测新版本失败!", style=ERROR))
|
self.tip.write(Text(self.prompt.check_update_failure, style=ERROR))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def action_user_script():
|
def action_user_script():
|
||||||
open(USERSCRIPT)
|
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)
|
||||||
16
source/TUI/setting.py
Normal file
16
source/TUI/setting.py
Normal file
@@ -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("我是设置页")
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from .App import XHS
|
|
||||||
from .TUI import XHSDownloader
|
from .TUI import XHSDownloader
|
||||||
|
from .application import XHS
|
||||||
|
|
||||||
__all__ = ['XHS', 'XHSDownloader']
|
__all__ = ['XHS', 'XHSDownloader']
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from .Manager import Manager
|
from source.module import ERROR
|
||||||
from .Static import ERROR
|
from source.module import Manager
|
||||||
from .Tools import logging
|
from source.module import logging
|
||||||
from .Tools import retry as re_download
|
from source.module import retry as re_download
|
||||||
|
|
||||||
__all__ = ['Download']
|
__all__ = ['Download']
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ class Download:
|
|||||||
self.chunk = manager.chunk
|
self.chunk = manager.chunk
|
||||||
self.session = manager.download_session
|
self.session = manager.download_session
|
||||||
self.retry = manager.retry
|
self.retry = manager.retry
|
||||||
|
self.prompt = manager.prompt
|
||||||
self.folder_mode = manager.folder_mode
|
self.folder_mode = manager.folder_mode
|
||||||
self.video_format = "mp4"
|
self.video_format = "mp4"
|
||||||
self.image_format = manager.image_format
|
self.image_format = manager.image_format
|
||||||
@@ -50,7 +51,7 @@ class Download:
|
|||||||
temp = self.temp.joinpath(name)
|
temp = self.temp.joinpath(name)
|
||||||
file = path.joinpath(name).with_suffix(f".{suffix}")
|
file = path.joinpath(name).with_suffix(f".{suffix}")
|
||||||
if self.manager.is_exists(file):
|
if self.manager.is_exists(file):
|
||||||
logging(log, f"{name} 已存在,跳过下载!")
|
logging(log, self.prompt.skip_download(name))
|
||||||
return True
|
return True
|
||||||
# self.__create_progress(
|
# self.__create_progress(
|
||||||
# bar, int(
|
# bar, int(
|
||||||
@@ -62,13 +63,13 @@ class Download:
|
|||||||
# self.__update_progress(bar, len(chunk))
|
# self.__update_progress(bar, len(chunk))
|
||||||
self.manager.move(temp, file)
|
self.manager.move(temp, file)
|
||||||
# self.__create_progress(bar, None)
|
# self.__create_progress(bar, None)
|
||||||
logging(log, f"{name} 下载成功!")
|
logging(log, self.prompt.download_success(name))
|
||||||
return True
|
return True
|
||||||
except ClientError as error:
|
except ClientError as error:
|
||||||
self.manager.delete(temp)
|
self.manager.delete(temp)
|
||||||
# self.__create_progress(bar, None)
|
# self.__create_progress(bar, None)
|
||||||
logging(log, error, ERROR)
|
logging(log, error, ERROR)
|
||||||
logging(log, f"网络异常,{name} 下载失败!", ERROR)
|
logging(log, self.prompt.download_error(name), ERROR)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from .Converter import Namespace
|
from source.expansion import Namespace
|
||||||
|
|
||||||
__all__ = ['Explore']
|
__all__ = ['Explore']
|
||||||
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
|
|
||||||
from .Manager import Manager
|
from source.module import ERROR
|
||||||
from .Static import ERROR
|
from source.module import Manager
|
||||||
from .Tools import logging
|
from source.module import logging
|
||||||
from .Tools import retry
|
from source.module import retry
|
||||||
|
|
||||||
__all__ = ["Html"]
|
__all__ = ["Html"]
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ class Html:
|
|||||||
def __init__(self, manager: Manager, ):
|
def __init__(self, manager: Manager, ):
|
||||||
self.proxy = manager.proxy
|
self.proxy = manager.proxy
|
||||||
self.retry = manager.retry
|
self.retry = manager.retry
|
||||||
|
self.prompt = manager.prompt
|
||||||
self.session = manager.request_session
|
self.session = manager.request_session
|
||||||
|
|
||||||
@retry
|
@retry
|
||||||
@@ -29,7 +30,7 @@ class Html:
|
|||||||
return await response.text() if content else str(response.url)
|
return await response.text() if content else str(response.url)
|
||||||
except ClientError as error:
|
except ClientError as error:
|
||||||
logging(log, error, ERROR)
|
logging(log, error, ERROR)
|
||||||
logging(log, f"网络异常,请求 {url} 失败!", ERROR)
|
logging(log, self.prompt.request_error(url), ERROR)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from .Converter import Namespace
|
from source.expansion import Namespace
|
||||||
from .Html import Html
|
from .Html import Html
|
||||||
|
|
||||||
__all__ = ['Image']
|
__all__ = ['Image']
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from .Converter import Namespace
|
from source.expansion import Namespace
|
||||||
from .Html import Html
|
from .Html import Html
|
||||||
|
|
||||||
__all__ = ['Video']
|
__all__ = ['Video']
|
||||||
3
source/application/__init__.py
Normal file
3
source/application/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from .app import XHS
|
||||||
|
|
||||||
|
__all__ = ["XHS"]
|
||||||
@@ -1,18 +1,23 @@
|
|||||||
from re import compile
|
from re import compile
|
||||||
|
|
||||||
from .Converter import Converter
|
from source.expansion import Converter
|
||||||
from .Converter import Namespace
|
from source.expansion import Namespace
|
||||||
from .Downloader import Download
|
from source.module import Manager
|
||||||
from .Explore import Explore
|
from source.module import (
|
||||||
from .Html import Html
|
|
||||||
from .Image import Image
|
|
||||||
from .Manager import Manager
|
|
||||||
from .Static import (
|
|
||||||
ROOT,
|
ROOT,
|
||||||
ERROR,
|
ERROR,
|
||||||
WARNING,
|
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
|
from .Video import Video
|
||||||
|
|
||||||
__all__ = ["XHS"]
|
__all__ = ["XHS"]
|
||||||
@@ -42,7 +47,10 @@ class XHS:
|
|||||||
record_data=False,
|
record_data=False,
|
||||||
image_format="PNG",
|
image_format="PNG",
|
||||||
folder_mode=False,
|
folder_mode=False,
|
||||||
|
language="zh-CN",
|
||||||
|
language_object: Chinese | English = None,
|
||||||
):
|
):
|
||||||
|
self.prompt = language_object or LANGUAGE.get(language, Chinese)
|
||||||
self.manager = Manager(
|
self.manager = Manager(
|
||||||
ROOT,
|
ROOT,
|
||||||
work_path,
|
work_path,
|
||||||
@@ -56,6 +64,7 @@ class XHS:
|
|||||||
record_data,
|
record_data,
|
||||||
image_format,
|
image_format,
|
||||||
folder_mode,
|
folder_mode,
|
||||||
|
self.prompt,
|
||||||
)
|
)
|
||||||
self.html = Html(self.manager)
|
self.html = Html(self.manager)
|
||||||
self.image = Image()
|
self.image = Image()
|
||||||
@@ -77,16 +86,16 @@ class XHS:
|
|||||||
if (u := container["下载地址"]) and download:
|
if (u := container["下载地址"]) and download:
|
||||||
path = await self.download.run(u, name, container["作品类型"], log, bar)
|
path = await self.download.run(u, name, container["作品类型"], log, bar)
|
||||||
elif not u:
|
elif not u:
|
||||||
logging(log, "提取作品文件下载地址失败!", ERROR)
|
logging(log, self.prompt.download_link_error, ERROR)
|
||||||
self.manager.save_data(path, name, container)
|
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, log=None, bar=None) -> list[dict]:
|
||||||
# return # 调试代码
|
# return # 调试代码
|
||||||
urls = await self.__extract_links(url, log)
|
urls = await self.__extract_links(url, log)
|
||||||
if not urls:
|
if not urls:
|
||||||
logging(log, "提取小红书作品链接失败!", WARNING)
|
logging(log, self.prompt.extract_link_failure, WARNING)
|
||||||
else:
|
else:
|
||||||
logging(log, f"共 {len(urls)} 个小红书作品待处理...")
|
logging(log, self.prompt.pending_processing(len(urls)))
|
||||||
# return urls # 调试代码
|
# return urls # 调试代码
|
||||||
return [await self.__deal_extract(i, download, log, bar) for i in urls]
|
return [await self.__deal_extract(i, download, log, bar) for i in urls]
|
||||||
|
|
||||||
@@ -103,17 +112,17 @@ class XHS:
|
|||||||
return urls
|
return urls
|
||||||
|
|
||||||
async def __deal_extract(self, url: str, download: bool, log, bar):
|
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)
|
html = await self.html.request_url(url, log=log)
|
||||||
# logging(log, html) # 调试代码
|
# logging(log, html) # 调试代码
|
||||||
if not html:
|
if not html:
|
||||||
logging(log, f"{url} 获取数据失败!", ERROR)
|
logging(log, self.prompt.get_data_failure(url), ERROR)
|
||||||
return {}
|
return {}
|
||||||
namespace = self.__generate_data_object(html)
|
namespace = self.__generate_data_object(html)
|
||||||
data = self.explore.run(namespace)
|
data = self.explore.run(namespace)
|
||||||
# logging(log, data) # 调试代码
|
# logging(log, data) # 调试代码
|
||||||
if not data:
|
if not data:
|
||||||
logging(log, f"{url} 提取数据失败!", ERROR)
|
logging(log, self.prompt.extract_data_failure(url), ERROR)
|
||||||
return {}
|
return {}
|
||||||
match data["作品类型"]:
|
match data["作品类型"]:
|
||||||
case "视频":
|
case "视频":
|
||||||
@@ -123,7 +132,7 @@ class XHS:
|
|||||||
case _:
|
case _:
|
||||||
data["下载地址"] = []
|
data["下载地址"] = []
|
||||||
await self.__download_files(data, download, log, bar)
|
await self.__download_files(data, download, log, bar)
|
||||||
logging(log, f"作品处理完成:{url}")
|
logging(log, self.prompt.processing_completed(url))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def __generate_data_object(self, html: str) -> Namespace:
|
def __generate_data_object(self, html: str) -> Namespace:
|
||||||
4
source/expansion/__init__.py
Normal file
4
source/expansion/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from .converter import Converter
|
||||||
|
from .namespace import Namespace
|
||||||
|
|
||||||
|
__all__ = ["Converter", "Namespace", ]
|
||||||
51
source/expansion/converter.py
Normal file
51
source/expansion/converter.py
Normal file
@@ -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
|
||||||
@@ -1,57 +1,7 @@
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
from lxml.etree import HTML
|
__all__ = ["Namespace"]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Namespace:
|
class Namespace:
|
||||||
53
source/module/__init__.py
Normal file
53
source/module/__init__.py
Normal file
@@ -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",
|
||||||
|
]
|
||||||
@@ -9,8 +9,10 @@ from shutil import rmtree
|
|||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
from aiohttp import ClientTimeout
|
from aiohttp import ClientTimeout
|
||||||
|
|
||||||
from .Static import COOKIE
|
from source.translator import Chinese
|
||||||
from .Static import USERAGENT
|
from source.translator import English
|
||||||
|
from .static import COOKIE
|
||||||
|
from .static import USERAGENT
|
||||||
|
|
||||||
__all__ = ["Manager"]
|
__all__ = ["Manager"]
|
||||||
|
|
||||||
@@ -32,6 +34,7 @@ class Manager:
|
|||||||
record_data: bool,
|
record_data: bool,
|
||||||
image_format: str,
|
image_format: str,
|
||||||
folder_mode: bool,
|
folder_mode: bool,
|
||||||
|
language: Chinese | English,
|
||||||
):
|
):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.temp = root.joinpath("./temp")
|
self.temp = root.joinpath("./temp")
|
||||||
@@ -54,6 +57,7 @@ class Manager:
|
|||||||
self.download_session = ClientSession(
|
self.download_session = ClientSession(
|
||||||
headers={"User-Agent": self.headers["User-Agent"]},
|
headers={"User-Agent": self.headers["User-Agent"]},
|
||||||
timeout=ClientTimeout(connect=timeout))
|
timeout=ClientTimeout(connect=timeout))
|
||||||
|
self.prompt = language
|
||||||
|
|
||||||
def __check_path(self, path: str) -> Path:
|
def __check_path(self, path: str) -> Path:
|
||||||
if not path:
|
if not path:
|
||||||
@@ -19,6 +19,8 @@ class Settings:
|
|||||||
"record_data": False,
|
"record_data": False,
|
||||||
"image_format": "PNG",
|
"image_format": "PNG",
|
||||||
"folder_mode": False,
|
"folder_mode": False,
|
||||||
|
"language": "zh-CN",
|
||||||
|
# "server": False,
|
||||||
}
|
}
|
||||||
encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8"
|
encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8"
|
||||||
|
|
||||||
52
source/module/static.py
Normal file
52
source/module/static.py
Normal file
@@ -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"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from .Static import INFO
|
from .static import INFO
|
||||||
|
|
||||||
__all__ = ["retry", "logging"]
|
__all__ = ["retry", "logging"]
|
||||||
|
|
||||||
13
source/translator/__init__.py
Normal file
13
source/translator/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from .chinese import Chinese
|
||||||
|
from .english import English
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"LANGUAGE",
|
||||||
|
"Chinese",
|
||||||
|
"English",
|
||||||
|
]
|
||||||
|
|
||||||
|
LANGUAGE = {
|
||||||
|
Chinese.code: Chinese,
|
||||||
|
English.code: English,
|
||||||
|
}
|
||||||
87
source/translator/chinese.py
Normal file
87
source/translator/chinese.py
Normal file
@@ -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}"
|
||||||
7
source/translator/english.py
Normal file
7
source/translator/english.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from .template import Language
|
||||||
|
|
||||||
|
__all__ = ["English"]
|
||||||
|
|
||||||
|
|
||||||
|
class English(Language):
|
||||||
|
pass
|
||||||
69
source/translator/template.py
Normal file
69
source/translator/template.py
Normal file
@@ -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
|
||||||
0
static/css/setting.tcss
Normal file
0
static/css/setting.tcss
Normal file
Reference in New Issue
Block a user