refactor: 重构项目翻译模块

This commit is contained in:
JoeanAmier 2024-12-22 11:13:28 +08:00
parent cdd574a00b
commit 80a3997ee7
26 changed files with 286 additions and 267 deletions

View File

@ -1,13 +1,30 @@
# 命令参考
**运行命令前,确保已经安装了 `gettext` 软件包,并配置好环境变量。**
**Before running the command, ensure that the `gettext` package is installed and the environment variables are properly
configured.**
* `xgettext --files-from=py_files.txt -d xhs -o xhs.pot`
* `mkdir zh_CN\LC_MESSAGES`
* `msginit -l zh_CN -o zh_CN/LC_MESSAGES/xhs.po -i xhs.pot`
* `mkdir en_US\LC_MESSAGES`
* `msginit -l en_US -o en_US/LC_MESSAGES/xhs.po -i xhs.pot`
* `msgmerge -U zh_CN/LC_MESSAGES/xhs.po xhs.pot`
* `msgmerge -U en_US/LC_MESSAGES/xhs.po xhs.pot`
# 翻译贡献指南
* 如果想要贡献支持更多语言,请参考 `zh_CN``en_US` 的文件夹层级结构,复制 `locale/zh_CN/LC_MESSAGES/xhs.po` 文件并编辑翻译。
* 如果想要贡献支持更多语言,请在终端切换至 `locale` 文件夹,运行命令
`msginit -l 语言代码 -o 语言代码/LC_MESSAGES/xhs.po -i xhs.pot`
生成 po 文件并编辑翻译。
* 如果想要贡献改进翻译结果,请直接编辑 `xhs.po` 文件内容。
* 不需要提交 `xhs.mo` 文件,提交 `xhs.po` 文件后,作者会转换格式并合并。
* 仅需提交 `xhs.po` 文件,作者会转换格式并合并。
# Translation Contribution Guide
* If you want to contribute by supporting more languages, please refer to the folder structure of `zh_CN` and `en_US`,
copy the `locale/zh_CN/LC_MESSAGES/xhs.po` file, and edit the translation.
* If you want to contribute by improving the translation results, please edit the content of the `xhs.po` file directly.
* There is no need to submit the `xhs.mo` file; after submitting the `xhs.po` file, the author will convert the format
and merge it.
* If you want to contribute support for more languages, please switch to the `locale` folder in the terminal and run the
command `msginit -l language_code -o language_code/LC_MESSAGES/xhs.po -i xhs.pot` to generate the po file and edit the
translation.
* If you want to contribute to improving the translation, please directly edit the content of the `xhs.po` file.
* Only the `xhs.po` file needs to be submitted, and the author will convert the format and merge it.

View File

@ -1,7 +1,6 @@
from pathlib import Path
from subprocess import run
__all__ = []
ROOT = Path(__file__).resolve().parent

View File

@ -23,7 +23,7 @@ from source.module import (
PROJECT,
)
from source.module import Settings
from source.module import Translate
from source.translation import switch_language, _
__all__ = ["cli"]
@ -106,7 +106,6 @@ class CLI:
@staticmethod
@check_value
def help_(ctx: Context, param, value) -> None:
_ = Translate("").message()
table = Table(highlight=True, box=None, show_header=True)
# 添加表格的列名
@ -115,7 +114,6 @@ class CLI:
table.add_column("type", no_wrap=True, style="bold")
table.add_column("description", no_wrap=True, )
# TODO: 语言设置未生效
options = (
("--url", "-u", "str", _("小红书作品链接")),
("--index", "-i", "str", _("下载指定序号的图片文件,仅对图文作品生效;多个序号输入示例:\"1 3 5 7\"")),
@ -158,7 +156,6 @@ class CLI:
border_style="bold",
title="XHS-Downloader CLI Parameters",
title_align="left"))
ctx.exit()
@command(name="XHS-Downloader", help=PROJECT)
@ -170,8 +167,6 @@ class CLI:
)
@option("--folder_name", "-fn", )
@option("--name_format", "-nf", )
# @option("--sec_ch_ua", "-su", )
# @option("--sec_ch_ua_platform", "-sp", )
@option("--user_agent", "-ua", )
@option("--cookie", "-ck", )
@option("--proxy", "-p", )
@ -192,19 +187,35 @@ class CLI:
@option("--update_settings", "-us", type=bool,
is_flag=True, )
@option("-h",
is_flag=True,
is_eager=True,
expose_value=False,
callback=CLI.help_, )
"--help",
is_flag=True, )
@option("--version", "-v",
is_flag=True,
is_eager=True,
expose_value=False,
callback=CLI.version, )
@pass_context
def cli(ctx, **kwargs):
def cli(ctx, help, language, **kwargs):
# Step 1: 切换语言
if language:
switch_language(language)
# Step 2: 如果请求了帮助信息,则显示帮助并退出
if help:
ctx.obj = kwargs # 保留当前上下文的参数
CLI.help_(ctx, None, help)
return
# Step 3: 主逻辑
async def main():
async with CLI(ctx, **kwargs) as xhs:
await xhs.run()
run(main())
if __name__ == "__main__":
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(cli, ['-l', 'en_US', '-h'])

View File

@ -1,5 +1,3 @@
from typing import Callable
from rich.text import Text
from textual.app import ComposeResult
from textual.binding import Binding
@ -14,6 +12,7 @@ from ..module import (
MASTER,
INFO,
)
from ..translation import _
__all__ = ["About"]
@ -34,17 +33,16 @@ class About(Screen):
description="返回首页/Back"),
]
def __init__(self, message: Callable[[str], str]):
def __init__(self, ):
super().__init__()
self.message = message
def compose(self) -> ComposeResult:
yield Header()
yield Label(Text(self.message("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star感谢您的支持"), style=INFO),
yield Label(Text(_("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star感谢您的支持"), style=INFO),
classes="prompt", )
yield Label(Text("Discord 社区", style=PROMPT), classes="prompt", )
yield Label(f"{self.message("邀请链接:")}https://discord.com/invite/ZYtmgKud9Y")
yield Label(Text(self.message("作者的其他开源项目"), style=PROMPT), classes="prompt", )
yield Label(f"{_("邀请链接:")}https://discord.com/invite/ZYtmgKud9Y")
yield Label(Text(_("作者的其他开源项目"), style=PROMPT), classes="prompt", )
yield Label(Text("TikTokDownloader (抖音 / TikTok)", style=MASTER), classes="prompt", )
yield Label("https://github.com/JoeanAmier/TikTokDownloader")
yield Label(Text("KS-Downloader (快手)", style=MASTER), classes="prompt", )

View File

@ -1,5 +1,3 @@
from typing import Callable
from textual.app import App
from textual.widgets import RichLog
@ -15,8 +13,8 @@ from ..module import (
ERROR,
)
from ..module import Settings
from ..module import Translate
from ..module import logging
from ..translation import _
__all__ = ["XHSDownloader"]
@ -28,7 +26,6 @@ class XHSDownloader(App):
def __init__(self):
super().__init__()
self.parameter: dict
self.message: Callable[[str], str]
self.APP: XHS
self.__initialization()
@ -41,10 +38,8 @@ class XHSDownloader(App):
def __initialization(self) -> None:
self.parameter = self.SETTINGS.run()
self.message = Translate(self.parameter["language"]).message()
self.APP = XHS(
**self.parameter,
transition=self.message,
_print=False,
)
@ -52,18 +47,18 @@ class XHSDownloader(App):
self.install_screen(
Setting(
self.parameter,
self.message),
),
name="setting")
self.install_screen(Index(self.APP, self.message), name="index")
self.install_screen(Loading(self.message), name="loading")
self.install_screen(About(self.message), name="about")
self.install_screen(Record(self.APP, self.message), name="record")
self.install_screen(Index(self.APP, ), name="index")
self.install_screen(Loading(), name="loading")
self.install_screen(About(), name="about")
self.install_screen(Record(self.APP, ), name="record")
await self.push_screen("index")
self.SETTINGS.check_keys(
self.parameter,
logging,
self.query_one(RichLog),
self.message("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") +
_("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") +
f"\n{
">" *
50}",
@ -88,15 +83,15 @@ class XHSDownloader(App):
self.uninstall_screen("loading")
self.uninstall_screen("about")
self.uninstall_screen("record")
self.install_screen(Index(self.APP, self.message), name="index")
self.install_screen(Index(self.APP, ), name="index")
self.install_screen(
Setting(
self.parameter,
self.message),
),
name="setting")
self.install_screen(Loading(self.message), name="loading")
self.install_screen(About(self.message), name="about")
self.install_screen(Record(self.APP, self.message), name="record")
self.install_screen(Loading(), name="loading")
self.install_screen(About(), name="about")
self.install_screen(Record(self.APP, ), name="record")
await self.push_screen("index")
def update_result(self, tip: str) -> None:
@ -105,7 +100,7 @@ class XHSDownloader(App):
log.write(">" * 50)
async def action_check_update(self):
await self.push_screen(Update(self.APP, self.message), callback=self.update_result)
await self.push_screen(Update(self.APP, ), callback=self.update_result)
async def action_update_and_return(self):
await self.push_screen("index")

View File

@ -1,5 +1,3 @@
from typing import Callable
from pyperclip import paste
from rich.text import Text
from textual import on
@ -28,6 +26,7 @@ from ..module import (
REPOSITORY,
GENERAL,
)
from ..translation import _
__all__ = ["Index"]
@ -42,10 +41,9 @@ class Index(Screen):
Binding(key="A", action="about", description="关于项目/About"),
]
def __init__(self, app: XHS, message: Callable[[str], str]):
def __init__(self, app: XHS, ):
super().__init__()
self.xhs = app
self.message = message
self.url = None
self.tip = None
@ -54,24 +52,24 @@ class Index(Screen):
yield ScrollableContainer(
Label(
Text(
f"{self.message("开源协议")}: {LICENCE}",
f"{_("开源协议")}: {LICENCE}",
style=MASTER)
),
Label(
Text(
f"{self.message("项目地址")}{REPOSITORY}",
f"{_("项目地址")}{REPOSITORY}",
style=MASTER)
),
Label(
Text(
self.message("请输入小红书图文/视频作品链接"),
_("请输入小红书图文/视频作品链接"),
style=PROMPT), classes="prompt",
),
Input(placeholder=self.message("多个链接之间使用空格分隔")),
Input(placeholder=_("多个链接之间使用空格分隔")),
HorizontalScroll(
Button(self.message("下载无水印作品文件"), id="deal"),
Button(self.message("读取剪贴板"), id="paste"),
Button(self.message("清空输入框"), id="reset"),
Button(_("下载无水印作品文件"), id="deal"),
Button(_("读取剪贴板"), id="paste"),
Button(_("清空输入框"), id="reset"),
),
)
yield RichLog(markup=True, )
@ -83,7 +81,7 @@ class Index(Screen):
self.tip = self.query_one(RichLog)
self.tip.write(
Text(
self.message("免责声明\n") +
_("免责声明\n") +
f"\n{
">" *
50}",
@ -96,7 +94,7 @@ class Index(Screen):
if self.url.value:
self.deal()
else:
self.tip.write(Text(self.message("未输入任何小红书作品链接"), style=WARNING))
self.tip.write(Text(_("未输入任何小红书作品链接"), style=WARNING))
self.tip.write(Text(">" * 50, style=GENERAL))
@on(Button.Pressed, "#reset")
@ -113,7 +111,7 @@ class Index(Screen):
if any(await self.xhs.extract(self.url.value, True, log=self.tip, data=False, )):
self.url.value = ""
else:
self.tip.write(Text(self.message("下载小红书作品文件失败"), style=ERROR))
self.tip.write(Text(_("下载小红书作品文件失败"), style=ERROR))
self.tip.write(Text(">" * 50, style=GENERAL))
self.app.pop_screen()
@ -127,7 +125,7 @@ class Index(Screen):
await self.app.run_action("settings")
async def action_monitor(self):
await self.app.push_screen(Monitor(self.xhs, self.message))
await self.app.push_screen(Monitor(self.xhs, ))
async def action_about(self):
await self.app.push_screen("about")

View File

@ -1,22 +1,21 @@
from typing import Callable
from textual.app import ComposeResult
from textual.containers import Grid
from textual.screen import ModalScreen
from textual.widgets import Label
from textual.widgets import LoadingIndicator
from ..translation import _
__all__ = ["Loading"]
class Loading(ModalScreen):
def __init__(self, message: Callable[[str], str]):
def __init__(self, ):
super().__init__()
self.message = message
def compose(self) -> ComposeResult:
yield Grid(
Label(self.message("程序处理中...")),
Label(_("程序处理中...")),
LoadingIndicator(),
classes="loading",
)

View File

@ -1,5 +1,3 @@
from typing import Callable
from rich.text import Text
from textual import on
from textual import work
@ -18,6 +16,7 @@ from ..module import (
MASTER,
INFO,
)
from ..translation import _
__all__ = ["Monitor"]
@ -28,16 +27,15 @@ class Monitor(Screen):
Binding(key="C", action="close", description="关闭监听/Close"),
]
def __init__(self, app: XHS, message: Callable[[str], str]):
def __init__(self, app: XHS, ):
super().__init__()
self.xhs = app
self.message = message
def compose(self) -> ComposeResult:
yield Header()
yield Label(Text(self.message("已启动监听剪贴板模式"), style=INFO), classes="prompt")
yield Label(Text(_("已启动监听剪贴板模式"), style=INFO), classes="prompt")
yield RichLog(markup=True, wrap=True)
yield Button(self.message("退出监听剪贴板模式"), id="close")
yield Button(_("退出监听剪贴板模式"), id="close")
yield Footer()
@on(Button.Pressed, "#close")
@ -52,7 +50,7 @@ class Monitor(Screen):
def on_mount(self) -> None:
self.title = PROJECT
self.query_one(RichLog).write(
Text(self.message(
Text(_(
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"),
style=MASTER))
self.run_monitor()

View File

@ -1,5 +1,3 @@
from typing import Callable
from textual import on
from textual.app import ComposeResult
from textual.containers import Grid
@ -10,24 +8,24 @@ from textual.widgets import Input
from textual.widgets import Label
from ..application import XHS
from ..translation import _
__all__ = ["Record"]
class Record(ModalScreen):
def __init__(self, app: XHS, message: Callable[[str], str]):
def __init__(self, app: XHS, ):
super().__init__()
self.xhs = app
self.message = message
def compose(self) -> ComposeResult:
yield Grid(
Label(self.message("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"),
Input(placeholder=self.message("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"),
Label(_("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"),
Input(placeholder=_("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"),
id="id", ),
HorizontalScroll(
Button(self.message("删除指定作品 ID"), id="enter", ),
Button(self.message("返回首页"), id="close"), ),
Button(_("删除指定作品 ID"), id="enter", ),
Button(_("返回首页"), id="close"), ),
id="record",
)

View File

@ -1,5 +1,3 @@
from typing import Callable
from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
@ -14,6 +12,8 @@ from textual.widgets import Input
from textual.widgets import Label
from textual.widgets import Select
from ..translation import _
__all__ = ["Setting"]
@ -23,50 +23,49 @@ class Setting(Screen):
Binding(key="B", action="index", description="返回首页/Back"),
]
def __init__(self, data: dict, message: Callable[[str], str]):
def __init__(self, data: dict, ):
super().__init__()
self.data = data
self.message = message
def compose(self) -> ComposeResult:
yield Header()
yield ScrollableContainer(
Label(self.message("作品数据 / 文件保存根路径"), classes="params", ),
Input(self.data["work_path"], placeholder=self.message("程序根路径"), valid_empty=True,
Label(_("作品数据 / 文件保存根路径"), classes="params", ),
Input(self.data["work_path"], placeholder=_("程序根路径"), valid_empty=True,
id="work_path", ),
Label(self.message("作品文件储存文件夹名称"), classes="params", ),
Label(_("作品文件储存文件夹名称"), classes="params", ),
Input(self.data["folder_name"], placeholder="Download", id="folder_name", ),
Label(self.message("作品文件名称格式"), classes="params", ),
Input(self.data["name_format"], placeholder=self.message("发布时间 作者昵称 作品标题"), valid_empty=True,
Label(_("作品文件名称格式"), classes="params", ),
Input(self.data["name_format"], placeholder=_("发布时间 作者昵称 作品标题"), valid_empty=True,
id="name_format", ),
Label(self.message("User-Agent"), classes="params", ),
Input(self.data["user_agent"], placeholder=self.message("内置 Chrome User Agent"), valid_empty=True,
Label(_("User-Agent"), classes="params", ),
Input(self.data["user_agent"], placeholder=_("内置 Chrome User Agent"), valid_empty=True,
id="user_agent", ),
Label(self.message("小红书网页版 Cookie"), classes="params", ),
Label(_("小红书网页版 Cookie"), classes="params", ),
Input(placeholder=self.__check_cookie(), valid_empty=True, id="cookie", ),
Label(self.message("网络代理"), classes="params", ),
Input(self.data["proxy"], placeholder=self.message("不使用代理"), valid_empty=True, id="proxy", ),
Label(self.message("请求数据超时限制,单位:秒"), classes="params", ),
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(self.message("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ),
Label(_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ),
Input(str(self.data["chunk"]), placeholder="1048576", type="integer", id="chunk", ),
Label(self.message("请求数据失败时,重试的最大次数"), classes="params", ),
Label(_("请求数据失败时,重试的最大次数"), classes="params", ),
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ),
Label(),
Container(
Checkbox(self.message("记录作品详细数据"), id="record_data", value=self.data["record_data"], ),
Checkbox(self.message("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ),
Checkbox(self.message("视频作品下载开关"), id="video_download", value=self.data["video_download"], ),
Checkbox(self.message("图文作品下载开关"), id="image_download", value=self.data["image_download"], ),
Checkbox(_("记录作品详细数据"), id="record_data", value=self.data["record_data"], ),
Checkbox(_("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ),
Checkbox(_("视频作品下载开关"), id="video_download", value=self.data["video_download"], ),
Checkbox(_("图文作品下载开关"), id="image_download", value=self.data["image_download"], ),
classes="horizontal-layout"),
Label(),
Container(
Checkbox(self.message("动图文件下载开关"), id="live_download", value=self.data["live_download"], ),
Checkbox(self.message("作品下载记录开关"), id="download_record", value=self.data["download_record"], ),
Checkbox(_("动图文件下载开关"), id="live_download", value=self.data["live_download"], ),
Checkbox(_("作品下载记录开关"), id="download_record", value=self.data["download_record"], ),
classes="horizontal-layout"),
Container(
Label(self.message("图片下载格式"), classes="params", ),
Label(self.message("程序语言"), classes="params", ),
Label(_("图片下载格式"), classes="params", ),
Label(_("程序语言"), classes="params", ),
classes="horizontal-layout",
),
Label(),
@ -83,19 +82,19 @@ class Setting(Screen):
id="language", ),
classes="horizontal-layout"),
Container(
Button(self.message("保存配置"), id="save", ),
Button(self.message("放弃更改"), id="abandon", ),
Button(_("保存配置"), id="save", ),
Button(_("放弃更改"), id="abandon", ),
classes="settings_button", ),
)
yield Footer()
def __check_cookie(self) -> str:
if self.data["cookie"]:
return self.message("小红书网页版 Cookie无需登录参数已设置")
return self.message("小红书网页版 Cookie无需登录参数未设置")
return _("小红书网页版 Cookie无需登录参数已设置")
return _("小红书网页版 Cookie无需登录参数未设置")
def on_mount(self) -> None:
self.title = self.message("程序设置")
self.title = _("程序设置")
@on(Button.Pressed, "#save")
def save_settings(self):
@ -117,7 +116,6 @@ class Setting(Screen):
"video_download": self.query_one("#video_download").value,
"live_download": self.query_one("#live_download").value,
"download_record": self.query_one("#download_record").value,
# "server": False,
})
@on(Button.Pressed, "#abandon")

View File

@ -1,5 +1,3 @@
from typing import Callable
from rich.text import Text
from textual import work
from textual.app import ComposeResult
@ -15,19 +13,19 @@ from ..module import (
INFO,
RELEASES,
)
from ..translation import _
__all__ = ["Update"]
class Update(ModalScreen):
def __init__(self, app: XHS, message: Callable[[str], str]):
def __init__(self, app: XHS, ):
super().__init__()
self.xhs = app
self.message = message
def compose(self) -> ComposeResult:
yield Grid(
Label(self.message("正在检查新版本,请稍等...")),
Label(_("正在检查新版本,请稍等...")),
LoadingIndicator(),
classes="loading",
)
@ -39,24 +37,24 @@ class Update(ModalScreen):
version = url.split("/")[-1]
match self.compare_versions(f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA):
case 4:
tip = Text(f"{self.message("检测到新版本:{0}.{1}").format(
tip = Text(f"{_("检测到新版本:{0}.{1}").format(
XHS.VERSION_MAJOR, XHS.VERSION_MINOR)}\n{RELEASES}", style=WARNING)
case 3:
tip = Text(
f"{self.message("当前版本为开发版, 可更新至正式版")}\n{RELEASES}",
f"{_("当前版本为开发版, 可更新至正式版")}\n{RELEASES}",
style=WARNING)
case 2:
tip = Text(
self.message("当前已是最新开发版"),
_("当前已是最新开发版"),
style=WARNING)
case 1:
tip = Text(
self.message("当前已是最新正式版"),
_("当前已是最新正式版"),
style=INFO)
case _:
raise ValueError
except ValueError:
tip = Text(self.message("检测新版本失败"), style=ERROR)
tip = Text(_("检测新版本失败"), style=ERROR)
self.dismiss(tip)
def on_mount(self) -> None:

View File

@ -6,7 +6,6 @@ from asyncio import sleep
from contextlib import suppress
from datetime import datetime
from re import compile
from typing import Callable
from urllib.parse import urlparse
from fastapi import FastAPI
@ -36,9 +35,9 @@ from source.module import (
VERSION_MINOR,
VERSION_BETA,
)
from source.module import Translate
from source.module import logging
from source.module import sleep_time
from source.translation import switch_language, _
from .download import Download
from .explore import Explore
from .image import Image
@ -80,8 +79,6 @@ class XHS:
work_path="",
folder_name="Download",
name_format="发布时间 作者昵称 作品标题",
# sec_ch_ua: str = "",
# sec_ch_ua_platform: str = "",
user_agent: str = None,
cookie: str = None,
proxy: str | dict = None,
@ -96,22 +93,18 @@ class XHS:
folder_mode=False,
download_record=True,
language="zh_CN",
# server=False,
transition: Callable[[str], str] = None,
read_cookie: int | str = None,
_print: bool = True,
*args,
**kwargs,
):
self.message = transition or Translate(language).message()
switch_language(language)
self.manager = Manager(
ROOT,
work_path,
folder_name,
name_format,
chunk,
# sec_ch_ua,
# sec_ch_ua_platform,
user_agent,
self.read_browser_cookie(read_cookie) or cookie,
proxy,
@ -124,8 +117,6 @@ class XHS:
live_download,
download_record,
folder_mode,
# server,
self.message,
_print,
)
self.html = Html(self.manager)
@ -163,7 +154,7 @@ class XHS:
if (u := container["下载地址"]) and download:
if await self.skip_download(i := container["作品ID"]):
logging(
log, self.message("作品 {0} 存在下载记录,跳过下载").format(i))
log, _("作品 {0} 存在下载记录,跳过下载").format(i))
else:
path, result = await self.download.run(
u,
@ -176,7 +167,7 @@ class XHS:
)
await self.__add_record(i, result)
elif not u:
logging(log, self.message("提取作品文件下载地址失败"), ERROR)
logging(log, _("提取作品文件下载地址失败"), ERROR)
await self.save_data(container)
@_data_cache
@ -202,10 +193,10 @@ class XHS:
# return # 调试代码
urls = await self.__extract_links(url, log)
if not urls:
logging(log, self.message("提取小红书作品链接失败"), WARNING)
logging(log, _("提取小红书作品链接失败"), WARNING)
else:
logging(
log, self.message("{0} 个小红书作品待处理...").format(len(urls)))
log, _("{0} 个小红书作品待处理...").format(len(urls)))
# return urls # 调试代码
return [await self.__deal_extract(i, download, index, log, bar, data, ) for i in urls]
@ -220,7 +211,7 @@ class XHS:
) -> None:
url = await self.__extract_links(url, log)
if not url:
logging(log, self.message("提取小红书作品链接失败"), WARNING)
logging(log, _("提取小红书作品链接失败"), WARNING)
else:
await self.__deal_extract(url[0], download, index, log, bar, data, )
@ -250,29 +241,29 @@ class XHS:
cookie: str = None,
):
if await self.skip_download(i := self.__extract_link_id(url)) and not data:
msg = self.message("作品 {0} 存在下载记录,跳过处理").format(i)
msg = _("作品 {0} 存在下载记录,跳过处理").format(i)
logging(log, msg)
return {"message": msg}
logging(log, self.message("开始处理作品:{0}").format(i))
logging(log, _("开始处理作品:{0}").format(i))
html = await self.html.request_url(url, log=log, cookie=cookie, )
namespace = self.__generate_data_object(html)
if not namespace:
logging(log, self.message("{0} 获取数据失败").format(i), ERROR)
logging(log, _("{0} 获取数据失败").format(i), ERROR)
return {}
data = self.explore.run(namespace)
# logging(log, data) # 调试代码
if not data:
logging(log, self.message("{0} 提取数据失败").format(i), ERROR)
logging(log, _("{0} 提取数据失败").format(i), ERROR)
return {}
match data["作品类型"]:
case "视频":
case _("视频"):
self.__extract_video(data, namespace)
case "图文":
case _("图文"):
self.__extract_image(data, namespace)
case _:
data["下载地址"] = []
await self.__download_files(data, download, index, log, bar)
logging(log, self.message("作品处理完成:{0}").format(i))
logging(log, _("作品处理完成:{0}").format(i))
await sleep_time()
return data
@ -332,7 +323,7 @@ class XHS:
) -> None:
logging(
None,
self.message(
_(
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"),
style=MASTER,
)
@ -392,13 +383,13 @@ class XHS:
# skip = data.get("skip", False)
# url = await self.__extract_links(url, None)
# if not url:
# msg = self.message("提取小红书作品链接失败")
# msg = _("提取小红书作品链接失败")
# data = None
# else:
# if data := await self.__deal_extract(url[0], download, index, None, None, not skip, ):
# msg = self.message("获取小红书作品数据成功")
# msg = _("获取小红书作品数据成功")
# else:
# msg = self.message("获取小红书作品数据失败")
# msg = _("获取小红书作品数据失败")
# data = None
# return web.json_response(dict(message=msg, url=url[0], data=data))
@ -420,12 +411,12 @@ class XHS:
# await self.runner.setup()
# self.site = web.TCPSite(self.runner, "0.0.0.0")
# await self.site.start()
# logging(log, self.message("Web API 服务器已启动!"))
# logging(log, self.message("服务器主机及端口: {0}".format(self.site.name, )))
# logging(log, _("Web API 服务器已启动!"))
# logging(log, _("服务器主机及端口: {0}".format(self.site.name, )))
# async def close_server(self, log=None, ):
# await self.runner.cleanup()
# logging(log, self.message("Web API 服务器已关闭!"))
# logging(log, _("Web API 服务器已关闭!"))
async def run_server(self, host="0.0.0.0", port=8000, log_level="info", ):
self.server = FastAPI(
@ -451,7 +442,7 @@ class XHS:
async def handle(extract: ExtractParams):
url = await self.__extract_links(extract.url, None)
if not url:
msg = self.message("提取小红书作品链接失败")
msg = _("提取小红书作品链接失败")
data = None
else:
if data := await self.__deal_extract(
@ -463,9 +454,9 @@ class XHS:
not extract.skip,
extract.cookie,
):
msg = self.message("获取小红书作品数据成功")
msg = _("获取小红书作品数据成功")
else:
msg = self.message("获取小红书作品数据失败")
msg = _("获取小红书作品数据失败")
data = None
return ExtractData(
message=msg,

View File

@ -18,6 +18,7 @@ from ..module import Manager
from ..module import logging
from ..module import retry as re_download
from ..module import sleep_time
from ..translation import _
if TYPE_CHECKING:
from httpx import AsyncClient
@ -44,7 +45,6 @@ class Download:
self.client: "AsyncClient" = manager.download_client
self.headers = manager.blank_headers
self.retry = manager.retry
self.message = manager.message
self.folder_mode = manager.folder_mode
self.video_format = "mp4"
self.live_format = "mp4"
@ -72,14 +72,14 @@ class Download:
) -> tuple[Path, list[Any]]:
path = self.__generate_path(name)
match type_:
case "视频":
case _("视频"):
tasks = self.__ready_download_video(
urls,
path,
name,
log,
)
case "图文":
case _("图文"):
tasks = self.__ready_download_image(
urls,
lives,
@ -115,7 +115,7 @@ class Download:
name: str,
log) -> list:
if not self.video_download:
logging(log, self.message("视频作品下载功能已关闭,跳过下载"))
logging(log, _("视频作品下载功能已关闭,跳过下载"))
return []
if self.__check_exists_path(path, f"{name}.{self.video_format}", log):
return []
@ -131,7 +131,7 @@ class Download:
log) -> list:
tasks = []
if not self.image_download:
logging(log, self.message("图文作品下载功能已关闭,跳过下载"))
logging(log, _("图文作品下载功能已关闭,跳过下载"))
return tasks
for i, j in enumerate(zip(urls, lives), start=1):
if index and i not in index:
@ -158,7 +158,7 @@ class Download:
def __check_exists_glob(self, path: Path, name: str, log, ) -> bool:
if any(path.glob(name)):
logging(
log, self.message(
log, _(
"{0} 文件已存在,跳过下载").format(name))
return True
return False
@ -166,7 +166,7 @@ class Download:
def __check_exists_path(self, path: Path, name: str, log, ) -> bool:
if path.joinpath(name).exists():
logging(
log, self.message(
log, _(
"{0} 文件已存在,跳过下载").format(name))
return True
return False
@ -192,7 +192,7 @@ class Download:
# except HTTPError as error:
# logging(
# log,
# self.message(
# _(
# "网络异常,{0} 请求失败,错误信息: {1}").format(name, repr(error)),
# ERROR,
# )
@ -205,7 +205,7 @@ class Download:
await sleep_time()
if response.status_code == 416:
raise CacheError(
self.message("文件 {0} 缓存异常,重新下载").format(temp.name),
_("文件 {0} 缓存异常,重新下载").format(temp.name),
)
response.raise_for_status()
# self.__create_progress(
@ -228,13 +228,13 @@ class Download:
)
self.manager.move(temp, real)
# self.__create_progress(bar, None)
logging(log, self.message("文件 {0} 下载成功").format(real.name))
logging(log, _("文件 {0} 下载成功").format(real.name))
return True
except HTTPError as error:
# self.__create_progress(bar, None)
logging(
log,
self.message(
_(
"网络异常,{0} 下载失败,错误信息: {1}").format(name, repr(error)),
ERROR,
)
@ -308,7 +308,7 @@ class Download:
except Exception as error:
logging(
log,
self.message("文件 {0} 格式判断失败,错误信息:{1}").format(temp.name, repr(error)),
_("文件 {0} 格式判断失败,错误信息:{1}").format(temp.name, repr(error)),
ERROR,
)
return path.joinpath(f"{name}.{default_suffix}")

View File

@ -1,13 +1,14 @@
from datetime import datetime
from source.expansion import Namespace
from ..expansion import Namespace
from ..translation import _
__all__ = ['Explore']
class Explore:
time_format = "%Y-%m-%d_%H:%M:%S"
explore_type = {"video": "视频", "normal": "图文"}
explore_type = {"video": _("视频"), "normal": _("图文")}
def run(self, data: Namespace) -> dict:
return self.__extract_data(data)
@ -44,7 +45,7 @@ class Explore:
container["作品标题"] = data.safe_extract("title")
container["作品描述"] = data.safe_extract("desc")
container["作品类型"] = self.explore_type.get(
data.safe_extract("type"), "未知")
data.safe_extract("type"), _("未知"))
# container["IP归属地"] = data.safe_extract("ipLocation")
def __extract_time(self, container: dict, data: Namespace):
@ -52,12 +53,12 @@ class Explore:
time /
1000).strftime(
self.time_format) if (
time := data.safe_extract("time")) else "未知"
time := data.safe_extract("time")) else _("未知")
container["最后更新时间"] = datetime.fromtimestamp(
last /
1000).strftime(
self.time_format) if (
last := data.safe_extract("lastUpdateTime")) else "未知"
last := data.safe_extract("lastUpdateTime")) else _("未知")
@staticmethod
def __extract_user(container: dict, data: Namespace):

View File

@ -1,10 +1,11 @@
from httpx import HTTPError
from source.module import ERROR
from source.module import Manager
from source.module import logging
from source.module import retry
from source.module import sleep_time
from ..module import ERROR
from ..module import Manager
from ..module import logging
from ..module import retry
from ..module import sleep_time
from ..translation import _
__all__ = ["Html"]
@ -12,7 +13,6 @@ __all__ = ["Html"]
class Html:
def __init__(self, manager: Manager, ):
self.retry = manager.retry
self.message = manager.message
self.client = manager.request_client
self.headers = manager.headers
self.blank_headers = manager.blank_headers
@ -41,7 +41,7 @@ class Html:
except HTTPError as error:
logging(
log,
self.message("网络异常,{0} 请求失败: {1}").format(url, repr(error)),
_("网络异常,{0} 请求失败: {1}").format(url, repr(error)),
ERROR
)
return ""

View File

@ -15,6 +15,11 @@ from rookiepy import (
vivaldi,
)
try:
from source.translation import _
except ImportError:
_ = lambda s: s
__all__ = ["BrowserCookie"]
@ -37,23 +42,23 @@ class BrowserCookie:
console = console or Console()
options = "\n".join(f"{i}. {k}: {v[1]}" for i, (k, v) in enumerate(cls.SUPPORT_BROWSER.items(), start=1))
if browser := console.input(
f"读取指定浏览器的 Cookie 并写入配置文件\n"
f"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie\n"
f"{options}\n请输入浏览器名称或序号:", ):
_("读取指定浏览器的 Cookie 并写入配置文件\n"
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie\n"
"{options}\n请输入浏览器名称或序号:").format(options=options), ):
return cls.get(browser, domains, console, )
console.print("未选择浏览器!")
console.print(_("未选择浏览器!"))
@classmethod
def get(cls, browser: str | int, domains: list[str], console: Console = None, ) -> str:
console = console or Console()
if not (browser := cls.__browser_object(browser)):
console.print("浏览器名称或序号输入错误!")
console.print(_("浏览器名称或序号输入错误!"))
return ""
try:
cookies = browser(domains=domains)
return "; ".join(f"{i["name"]}={i["value"]}" for i in cookies)
except RuntimeError:
console.print("获取 Cookie 失败,未找到 Cookie 数据!")
console.print(_("获取 Cookie 失败,未找到 Cookie 数据!"))
return ""
@classmethod
@ -91,4 +96,4 @@ match platform:
case "win32":
pass
case _:
print("从浏览器读取 Cookie 功能不支持当前平台!")
print(_("从浏览器读取 Cookie 功能不支持当前平台!"))

View File

@ -5,6 +5,11 @@ from warnings import warn
from emoji import replace_emoji
try:
from source.translation import _
except ImportError:
_ = lambda s: s
class Cleaner:
CONTROL_CHARACTERS = compile(r"[\x00-\x1F\x7F]")
@ -37,7 +42,7 @@ class Cleaner:
"\x00": "",
} # Linux 系统
else:
warn("不受支持的操作系统类型,可能无法正常去除非法字符!")
warn(_("不受支持的操作系统类型,可能无法正常去除非法字符!"))
rule = {}
cache = {i: "" for i in whitespace[1:]} # 补充换行符等非法字符
return rule | cache

View File

@ -26,8 +26,6 @@ from .static import (
HEADERS,
PROJECT,
USERAGENT,
SEC_CH_UA,
SEC_CH_UA_PLATFORM,
FILE_SIGNATURES,
FILE_SIGNATURES_LENGTH,
MAX_WORKERS,
@ -37,4 +35,3 @@ from .tools import (
logging,
sleep_time,
)
from .translator import Translate

View File

@ -3,7 +3,6 @@ from re import compile
from re import sub
from shutil import move
from shutil import rmtree
from typing import Callable
from httpx import AsyncClient
from httpx import AsyncHTTPTransport
@ -14,11 +13,10 @@ from httpx import get
from source.expansion import remove_empty_directories
from .static import HEADERS
# from .static import SEC_CH_UA
# from .static import SEC_CH_UA_PLATFORM
from .static import USERAGENT
from .static import WARNING
from .tools import logging
from ..translation import _
__all__ = ["Manager"]
@ -26,19 +24,19 @@ __all__ = ["Manager"]
class Manager:
NAME = compile(r"[^\u4e00-\u9fffa-zA-Z0-9-_“”《》]")
NAME_KEYS = (
'收藏数量',
'评论数量',
'分享数量',
'点赞数量',
'作品标签',
'作品ID',
'作品标题',
'作品描述',
'作品类型',
'发布时间',
'最后更新时间',
'作者昵称',
'作者ID',
"收藏数量",
"评论数量",
"分享数量",
"点赞数量",
"作品标签",
"作品ID",
"作品标题",
"作品描述",
"作品类型",
"发布时间",
"最后更新时间",
"作者昵称",
"作者ID",
)
NO_PROXY = {
"http://": None,
@ -55,8 +53,6 @@ class Manager:
folder: str,
name_format: str,
chunk: int,
# sec_ch_ua: str,
# sec_ch_ua_platform: str,
user_agent: str,
cookie: str,
proxy: str | dict,
@ -69,19 +65,14 @@ class Manager:
live_download: bool,
download_record: bool,
folder_mode: bool,
# server: bool,
transition: Callable[[str], str],
_print: bool,
):
self.root = root
self.temp = root.joinpath("./temp")
self.path = self.__check_path(path)
self.folder = self.__check_folder(folder)
self.message = transition
self.blank_headers = HEADERS | {
'user-agent': user_agent or USERAGENT,
# 'sec-ch-ua': sec_ch_ua or SEC_CH_UA,
# 'sec-ch-ua-platform': sec_ch_ua_platform or SEC_CH_UA_PLATFORM,
}
self.headers = self.blank_headers | {
'cookie': cookie,
@ -121,7 +112,6 @@ class Manager:
self.image_download = self.check_bool(image_download, True)
self.video_download = self.check_bool(video_download, True)
self.live_download = self.check_bool(live_download, True)
# self.server = self.check_bool(server, False)
def __check_path(self, path: str) -> Path:
if not path:
@ -211,11 +201,11 @@ class Manager:
}
)
response.raise_for_status()
self.proxy_tip = (self.message("代理 {0} 测试成功").format(proxy),)
self.proxy_tip = (_("代理 {0} 测试成功").format(proxy),)
return proxy
except TimeoutException:
self.proxy_tip = (
self.message("代理 {0} 测试超时").format(proxy),
_("代理 {0} 测试超时").format(proxy),
WARNING,
)
except (
@ -223,7 +213,7 @@ class Manager:
HTTPStatusError,
) as e:
self.proxy_tip = (
self.message("代理 {0} 测试失败:{1}").format(
_("代理 {0} 测试失败:{1}").format(
proxy,
e,
),

View File

@ -4,7 +4,7 @@ from re import compile
from aiosqlite import connect
from source.module import Manager
from ..module import Manager
__all__ = ["IDRecorder", "DataRecorder", ]

View File

@ -4,8 +4,6 @@ from pathlib import Path
from platform import system
from .static import ROOT
# from .static import SEC_CH_UA
# from .static import SEC_CH_UA_PLATFORM
from .static import USERAGENT
__all__ = ['Settings']
@ -16,8 +14,6 @@ class Settings:
"work_path": "",
"folder_name": "Download",
"name_format": "发布时间 作者昵称 作品标题",
# "sec_ch_ua": SEC_CH_UA,
# "sec_ch_ua_platform": SEC_CH_UA_PLATFORM,
"user_agent": USERAGENT,
"cookie": "",
"proxy": None,
@ -32,7 +28,6 @@ class Settings:
"folder_mode": False,
"download_record": True,
"language": "zh_CN",
# "server": False,
}
encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8"

View File

@ -15,24 +15,11 @@ USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master
USERAGENT = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 '
'Safari/537.36 Edg/128.0.0.0')
SEC_CH_UA = '"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"'
SEC_CH_UA_PLATFORM = '"Windows"'
HEADERS = {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
# 'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
# 'dnt': '1',
'pragma': 'no-cache',
# 'priority': 'u=0, i',
# 'sec-ch-ua': SEC_CH_UA,
# 'sec-ch-ua-mobile': '?0',
# 'sec-ch-ua-platform': SEC_CH_UA_PLATFORM,
# 'sec-fetch-dest': 'document',
# 'sec-fetch-mode': 'navigate',
# 'sec-fetch-site': 'none',
# 'sec-fetch-user': '?1',
# 'upgrade-insecure-requests': '1',
'user-agent': USERAGENT,
}

View File

@ -1,27 +0,0 @@
from gettext import translation
from ..module import ROOT
__all__ = ["Translate"]
class Translate:
SUPPORT = {
"zh_CN",
"en_US",
}
def __init__(self, language: str):
self.language = self.__check_language(language)
self.translate = translation(
"xhs",
localedir=ROOT.joinpath("locale"),
languages=[self.language],
fallback=True,
)
def __check_language(self, language: str) -> str:
return language if language in self.SUPPORT else "zh_CN"
def message(self):
return self.translate.gettext

View File

@ -0,0 +1 @@
from .translate import switch_language, _

View File

@ -0,0 +1,63 @@
from gettext import translation
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
class TranslationManager:
"""管理gettext翻译的类"""
_instance = None # 单例实例
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(TranslationManager, cls).__new__(cls)
return cls._instance
def __init__(self, domain="xhs", localedir=None):
self.domain = domain
if not localedir:
localedir = ROOT.joinpath('locale')
self.localedir = Path(localedir)
self.current_translator = self.setup_translation()
def setup_translation(self, language: str = "zh_CN"):
"""设置gettext翻译环境"""
try:
return translation(
self.domain,
localedir=self.localedir,
languages=[language],
fallback=True,
)
except FileNotFoundError as e:
print(f"Warning: Translation files for '{self.domain}' not found. Error: {e}")
return translation(self.domain, fallback=True)
def switch_language(self, language: str = "en_US"):
"""切换当前使用的语言"""
self.current_translator = self.setup_translation(language)
def gettext(self, message):
"""提供gettext方法"""
return self.current_translator.gettext(message)
# 初始化TranslationManager单例实例
translation_manager = TranslationManager()
def _translate(message):
"""辅助函数来简化翻译调用"""
return translation_manager.gettext(message)
def switch_language(language: str = "en_US"):
"""切换语言并刷新翻译函数"""
global _
translation_manager.switch_language(language)
_ = translation_manager.gettext
# 设置默认翻译函数
_ = _translate

View File

@ -1,8 +1,10 @@
**项目更新内容:**
1. 优化文件名称非法字符处理
2. 支持 API 模式传入 Cookie
3. 适配新版本 HTTPX 库
4. 更正英语语言代码
5. 优化文件下载功能
6. 移除内置延时机制
1. 修复命令行模式语言不生效的问题
2. 优化文件名称非法字符处理
3. 支持 API 模式传入 Cookie
4. 适配新版本 HTTPX 库
5. 重构项目翻译模块
6. 更正英语语言代码
7. 优化文件下载功能
8. 移除内置延时机制