mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2025-12-26 04:48:05 +08:00
refactor: 重构项目翻译模块
This commit is contained in:
parent
cdd574a00b
commit
80a3997ee7
@ -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.
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
__all__ = []
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
|
||||
@ -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'])
|
||||
|
||||
@ -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", )
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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",
|
||||
)
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}")
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 功能不支持当前平台!"))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
@ -4,7 +4,7 @@ from re import compile
|
||||
|
||||
from aiosqlite import connect
|
||||
|
||||
from source.module import Manager
|
||||
from ..module import Manager
|
||||
|
||||
__all__ = ["IDRecorder", "DataRecorder", ]
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
1
source/translation/__init__.py
Normal file
1
source/translation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .translate import switch_language, _
|
||||
63
source/translation/translate.py
Normal file
63
source/translation/translate.py
Normal 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
|
||||
@ -1,8 +1,10 @@
|
||||
**项目更新内容:**
|
||||
|
||||
1. 优化文件名称非法字符处理
|
||||
2. 支持 API 模式传入 Cookie
|
||||
3. 适配新版本 HTTPX 库
|
||||
4. 更正英语语言代码
|
||||
5. 优化文件下载功能
|
||||
6. 移除内置延时机制
|
||||
1. 修复命令行模式语言不生效的问题
|
||||
2. 优化文件名称非法字符处理
|
||||
3. 支持 API 模式传入 Cookie
|
||||
4. 适配新版本 HTTPX 库
|
||||
5. 重构项目翻译模块
|
||||
6. 更正英语语言代码
|
||||
7. 优化文件下载功能
|
||||
8. 移除内置延时机制
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user