style: 代码格式化和字符串处理优化

- 优化代码缩进和换行,提高可读性
- 统一使用单引号或双引号,保持一致性
- 移除冗余的空格和括号,精简代码
This commit is contained in:
2025-02-15 21:30:24 +08:00
parent 94198f5a51
commit 1570ba320c
34 changed files with 948 additions and 358 deletions

View File

@@ -60,7 +60,12 @@ async def example():
async def test(): async def test():
url = "" url = ""
async with XHS() as xhs: async with XHS() as xhs:
print(await xhs.extract(url, download=True, )) print(
await xhs.extract(
url,
download=True,
)
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -4,14 +4,14 @@ ROOT = Path(__file__).resolve().parent.parent
def find_python_files(dir_, file): def find_python_files(dir_, file):
with open(file, 'w', encoding='utf-8') as f: with open(file, "w", encoding="utf-8") as f:
for py_file in dir_.rglob('*.py'): # 递归查找所有 .py 文件 for py_file in dir_.rglob("*.py"): # 递归查找所有 .py 文件
f.write(str(py_file) + '\n') # 写入文件路径 f.write(str(py_file) + "\n") # 写入文件路径
# 设置源目录和输出文件 # 设置源目录和输出文件
source_directory = ROOT.joinpath("source") # 源目录 source_directory = ROOT.joinpath("source") # 源目录
output_file = 'py_files.txt' # 输出文件名 output_file = "py_files.txt" # 输出文件名
find_python_files(source_directory, output_file) find_python_files(source_directory, output_file)
print(f"所有 .py 文件路径已保存到 {output_file}") print(f"所有 .py 文件路径已保存到 {output_file}")

View File

@@ -6,9 +6,7 @@ ROOT = Path(__file__).resolve().parent
def scan_directory(): def scan_directory():
return [ return [
item.joinpath("LC_MESSAGES/xhs.po") item.joinpath("LC_MESSAGES/xhs.po") for item in ROOT.iterdir() if item.is_dir()
for item in ROOT.iterdir()
if item.is_dir()
] ]
@@ -18,7 +16,7 @@ def generate_map(files: list[Path]):
def generate_mo(maps: list[tuple[Path, Path]]): def generate_mo(maps: list[tuple[Path, Path]]):
for i, j in maps: for i, j in maps:
command = f"msgfmt --check -o \"{j}\" \"{i}\"" command = f'msgfmt --check -o "{j}" "{i}"'
print(run(command, shell=True, text=True)) print(run(command, shell=True, text=True))

14
main.py
View File

@@ -14,12 +14,20 @@ async def app():
await xhs.run_async() await xhs.run_async()
async def server(host="0.0.0.0", port=8000, log_level="info", ): async def server(
host="0.0.0.0",
port=8000,
log_level="info",
):
async with XHS(**Settings().run()) as xhs: async with XHS(**Settings().run()) as xhs:
await xhs.run_server(host, port, log_level, ) await xhs.run_server(
host,
port,
log_level,
)
if __name__ == '__main__': if __name__ == "__main__":
with suppress( with suppress(
KeyboardInterrupt, KeyboardInterrupt,
CancelledError, CancelledError,

View File

@@ -28,3 +28,82 @@ Repository = "https://github.com/JoeanAmier/XHS-Downloader"
[tool.uv.pip] [tool.uv.pip]
index-url = "https://mirrors.ustc.edu.cn/pypi/simple" index-url = "https://mirrors.ustc.edu.cn/pypi/simple"
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]
# Same as Black.
line-length = 88
indent-width = 4
# Assume Python 3.12
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"

View File

@@ -99,7 +99,12 @@ class CLI:
@staticmethod @staticmethod
@check_value @check_value
def read_cookie(ctx: Context, param, value) -> str: def read_cookie(ctx: Context, param, value) -> str:
return BrowserCookie.get(value, domains=["xiaohongshu.com", ]) return BrowserCookie.get(
value,
domains=[
"xiaohongshu.com",
],
)
@staticmethod @staticmethod
@check_value @check_value
@@ -110,12 +115,24 @@ class CLI:
table.add_column("parameter", no_wrap=True, style="bold") table.add_column("parameter", no_wrap=True, style="bold")
table.add_column("abbreviation", no_wrap=True, style="bold") table.add_column("abbreviation", no_wrap=True, style="bold")
table.add_column("type", no_wrap=True, style="bold") table.add_column("type", no_wrap=True, style="bold")
table.add_column("description", no_wrap=True, ) table.add_column(
"description",
no_wrap=True,
)
options = ( options = (
("--url", "-u", "str", _("小红书作品链接")), ("--url", "-u", "str", _("小红书作品链接")),
("--index", "-i", "str", (
fill(_("下载指定序号的图片文件,仅对图文作品生效;多个序号输入示例:\"1 3 5 7\""), width=55),), "--index",
"-i",
"str",
fill(
_(
'下载指定序号的图片文件,仅对图文作品生效;多个序号输入示例:"1 3 5 7"'
),
width=55,
),
),
("--work_path", "-wp", "str", _("作品数据 / 文件保存根路径")), ("--work_path", "-wp", "str", _("作品数据 / 文件保存根路径")),
("--folder_name", "-fn", "str", _("作品文件储存文件夹名称")), ("--folder_name", "-fn", "str", _("作品文件储存文件夹名称")),
("--name_format", "-nf", "str", _("作品文件名称格式")), ("--name_format", "-nf", "str", _("作品文件名称格式")),
@@ -125,22 +142,51 @@ class CLI:
("--cookie", "-ck", "str", _("小红书网页版 Cookie无需登录")), ("--cookie", "-ck", "str", _("小红书网页版 Cookie无需登录")),
("--proxy", "-p", "str", _("网络代理")), ("--proxy", "-p", "str", _("网络代理")),
("--timeout", "-t", "int", _("请求数据超时限制,单位:秒")), ("--timeout", "-t", "int", _("请求数据超时限制,单位:秒")),
("--chunk", "-c", "int", fill(_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), width=55),), (
"--chunk",
"-c",
"int",
fill(
_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), width=55
),
),
("--max_retry", "-mr", "int", _("请求数据失败时,重试的最大次数")), ("--max_retry", "-mr", "int", _("请求数据失败时,重试的最大次数")),
("--record_data", "-rd", "bool", _("是否记录作品数据至文件")), ("--record_data", "-rd", "bool", _("是否记录作品数据至文件")),
("--image_format", "-if", "choice", _("图文作品文件下载格式支持PNG、WEBP")), (
"--image_format",
"-if",
"choice",
_("图文作品文件下载格式支持PNG、WEBP"),
),
("--live_download", "-ld", "bool", _("动态图片下载开关")), ("--live_download", "-ld", "bool", _("动态图片下载开关")),
("--download_record", "-dr", "bool", _("作品下载记录开关")), ("--download_record", "-dr", "bool", _("作品下载记录开关")),
("--folder_mode", "-fm", "bool", _("是否将每个作品的文件储存至单独的文件夹")), (
"--folder_mode",
"-fm",
"bool",
_("是否将每个作品的文件储存至单独的文件夹"),
),
("--language", "-l", "choice", _("设置程序语言目前支持zh_CN、en_US")), ("--language", "-l", "choice", _("设置程序语言目前支持zh_CN、en_US")),
("--settings", "-s", "str", _("读取指定配置文件")), ("--settings", "-s", "str", _("读取指定配置文件")),
("--browser_cookie", "-bc", "choice", (
fill(_("从指定的浏览器读取小红书网页版 Cookie支持{0}; 输入浏览器名称或序号").format( "--browser_cookie",
", ".join(f"{i}: {j}" for i, j in enumerate( "-bc",
BrowserCookie.SUPPORT_BROWSER.keys(), "choice",
start=1, fill(
)) _(
), width=55)), "从指定的浏览器读取小红书网页版 Cookie支持{0}; 输入浏览器名称或序号"
).format(
", ".join(
f"{i}: {j}"
for i, j in enumerate(
BrowserCookie.SUPPORT_BROWSER.keys(),
start=1,
)
)
),
width=55,
),
),
("--update_settings", "-us", "flag", _("是否更新配置文件")), ("--update_settings", "-us", "flag", _("是否更新配置文件")),
("--help", "-h", "flag", _("查看详细参数说明")), ("--help", "-h", "flag", _("查看详细参数说明")),
("--version", "-v", "flag", _("查看 XHS-Downloader 版本")), ("--version", "-v", "flag", _("查看 XHS-Downloader 版本")),
@@ -154,45 +200,123 @@ class CLI:
table, table,
border_style="bold", border_style="bold",
title="XHS-Downloader CLI Parameters", title="XHS-Downloader CLI Parameters",
title_align="left")) title_align="left",
)
)
@command(name="XHS-Downloader", help=PROJECT) @command(name="XHS-Downloader", help=PROJECT)
@option("--url", "-u", ) @option(
@option("--index", "-i", ) "--url",
@option("--work_path", "-u",
"-wp", )
type=Path(file_okay=False), @option(
) "--index",
@option("--folder_name", "-fn", ) "-i",
@option("--name_format", "-nf", ) )
@option("--user_agent", "-ua", ) @option(
@option("--cookie", "-ck", ) "--work_path",
@option("--proxy", "-p", ) "-wp",
@option("--timeout", "-t", type=int, ) type=Path(file_okay=False),
@option("--chunk", "-c", type=int, ) )
@option("--max_retry", "-mr", type=int, ) @option(
@option("--record_data", "-rd", type=bool, ) "--folder_name",
@option("--image_format", "-if", type=Choice(["png", "PNG", "webp", "WEBP"]), ) "-fn",
@option("--live_download", "-ld", type=bool, ) )
@option("--download_record", "-dr", type=bool, ) @option(
@option("--folder_mode", "-fm", type=bool, ) "--name_format",
@option("--language", "-l", "-nf",
type=Choice(["zh_CN", "en_US"]), ) )
@option("--settings", "-s", type=Path(dir_okay=False), ) @option(
@option("--browser_cookie", "-bc", type=Choice( "--user_agent",
list(BrowserCookie.SUPPORT_BROWSER.keys() "-ua",
) + [str(i) for i in range(1, len(BrowserCookie.SUPPORT_BROWSER) + 1)]), callback=CLI.read_cookie, ) )
@option("--update_settings", "-us", type=bool, @option(
is_flag=True, ) "--cookie",
@option("-h", "-ck",
"--help", )
is_flag=True, ) @option(
@option("--version", "-v", "--proxy",
is_flag=True, "-p",
is_eager=True, )
expose_value=False, @option(
callback=CLI.version, ) "--timeout",
"-t",
type=int,
)
@option(
"--chunk",
"-c",
type=int,
)
@option(
"--max_retry",
"-mr",
type=int,
)
@option(
"--record_data",
"-rd",
type=bool,
)
@option(
"--image_format",
"-if",
type=Choice(["png", "PNG", "webp", "WEBP"]),
)
@option(
"--live_download",
"-ld",
type=bool,
)
@option(
"--download_record",
"-dr",
type=bool,
)
@option(
"--folder_mode",
"-fm",
type=bool,
)
@option(
"--language",
"-l",
type=Choice(["zh_CN", "en_US"]),
)
@option(
"--settings",
"-s",
type=Path(dir_okay=False),
)
@option(
"--browser_cookie",
"-bc",
type=Choice(
list(BrowserCookie.SUPPORT_BROWSER.keys())
+ [str(i) for i in range(1, len(BrowserCookie.SUPPORT_BROWSER) + 1)]
),
callback=CLI.read_cookie,
)
@option(
"--update_settings",
"-us",
type=bool,
is_flag=True,
)
@option(
"-h",
"--help",
is_flag=True,
)
@option(
"--version",
"-v",
is_flag=True,
is_eager=True,
expose_value=False,
callback=CLI.version,
)
@pass_context @pass_context
def cli(ctx, help, language, **kwargs): def cli(ctx, help, language, **kwargs):
# Step 1: 切换语言 # Step 1: 切换语言
@@ -217,4 +341,4 @@ if __name__ == "__main__":
from click.testing import CliRunner from click.testing import CliRunner
runner = CliRunner() runner = CliRunner()
result = runner.invoke(cli, ['-l', 'en_US', '-u', '']) result = runner.invoke(cli, ["-l", "en_US", "-u", ""])

View File

@@ -1,3 +1,3 @@
from .app import XHSDownloader from .app import XHSDownloader
__all__ = ['XHSDownloader'] __all__ = ["XHSDownloader"]

View File

@@ -20,46 +20,53 @@ __all__ = ["About"]
class About(Screen): class About(Screen):
BINDINGS = [ BINDINGS = [
Binding( Binding(key="Q", action="quit", description=_("退出程序")),
key="Q", Binding(key="U", action="update", description=_("检查更新")),
action="quit", Binding(key="B", action="back", description=_("返回首页")),
description=_("退出程序")),
Binding(
key="U",
action="update",
description=_("检查更新")),
Binding(
key="B",
action="back",
description=_("返回首页")),
] ]
def __init__(self, ): def __init__(
self,
):
super().__init__() super().__init__()
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield Label( yield Label(
Text( Text(
_("如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star感谢您的支持"), _(
"如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star感谢您的支持"
),
style=INFO, style=INFO,
), ),
classes="prompt", classes="prompt",
) )
yield Label(Text(_("Discord 社区"), style=PROMPT), classes="prompt", ) yield Label(
Text(_("Discord 社区"), style=PROMPT),
classes="prompt",
)
yield Link( yield Link(
_("邀请链接:") + "https://discord.com/invite/ZYtmgKud9Y", _("邀请链接:") + "https://discord.com/invite/ZYtmgKud9Y",
url="https://discord.com/invite/ZYtmgKud9Y", url="https://discord.com/invite/ZYtmgKud9Y",
tooltip=_("点击访问"), tooltip=_("点击访问"),
) )
yield Label(Text(_("作者的其他开源项目"), style=PROMPT), classes="prompt", ) yield Label(
yield Label(Text("TikTokDownloader (抖音 / TikTok)", style=MASTER), classes="prompt", ) Text(_("作者的其他开源项目"), style=PROMPT),
classes="prompt",
)
yield Label(
Text("TikTokDownloader (抖音 / TikTok)", style=MASTER),
classes="prompt",
)
yield Link( yield Link(
"https://github.com/JoeanAmier/TikTokDownloader", "https://github.com/JoeanAmier/TikTokDownloader",
url="https://github.com/JoeanAmier/TikTokDownloader", url="https://github.com/JoeanAmier/TikTokDownloader",
tooltip=_("点击访问"), tooltip=_("点击访问"),
) )
yield Label(Text("KS-Downloader (快手)", style=MASTER), classes="prompt", ) yield Label(
Text("KS-Downloader (快手)", style=MASTER),
classes="prompt",
)
yield Link( yield Link(
"https://github.com/JoeanAmier/KS-Downloader", "https://github.com/JoeanAmier/KS-Downloader",
url="https://github.com/JoeanAmier/KS-Downloader", url="https://github.com/JoeanAmier/KS-Downloader",

View File

@@ -49,20 +49,31 @@ class XHSDownloader(App):
Setting( Setting(
self.parameter, self.parameter,
), ),
name="setting") name="setting",
self.install_screen(Index(self.APP, ), name="index") )
self.install_screen(
Index(
self.APP,
),
name="index",
)
self.install_screen(Loading(), name="loading") self.install_screen(Loading(), name="loading")
self.install_screen(About(), name="about") self.install_screen(About(), name="about")
self.install_screen(Record(self.APP, ), name="record") self.install_screen(
Record(
self.APP,
),
name="record",
)
await self.push_screen("index") await self.push_screen("index")
self.SETTINGS.check_keys( self.SETTINGS.check_keys(
self.parameter, self.parameter,
logging, logging,
self.query_one(RichLog), self.query_one(RichLog),
_("配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!") + _(
f"\n{ "配置文件 settings.json 缺少必要的参数,请删除该文件,然后重新运行程序,自动生成默认配置文件!"
">" * )
50}", + f"\n{'>' * 50}",
ERROR, ERROR,
) )
@@ -84,22 +95,41 @@ class XHSDownloader(App):
self.uninstall_screen("loading") self.uninstall_screen("loading")
self.uninstall_screen("about") self.uninstall_screen("about")
self.uninstall_screen("record") self.uninstall_screen("record")
self.install_screen(Index(self.APP, ), name="index") self.install_screen(
Index(
self.APP,
),
name="index",
)
self.install_screen( self.install_screen(
Setting( Setting(
self.parameter, self.parameter,
), ),
name="setting") name="setting",
)
self.install_screen(Loading(), name="loading") self.install_screen(Loading(), name="loading")
self.install_screen(About(), name="about") self.install_screen(About(), name="about")
self.install_screen(Record(self.APP, ), name="record") self.install_screen(
Record(
self.APP,
),
name="record",
)
await self.push_screen("index") await self.push_screen("index")
def update_result(self, args: tuple[str, str]) -> None: def update_result(self, args: tuple[str, str]) -> None:
self.notify(args[0], severity=args[1], ) self.notify(
args[0],
severity=args[1],
)
async def action_update(self): async def action_update(self):
await self.push_screen(Update(self.APP, ), callback=self.update_result) await self.push_screen(
Update(
self.APP,
),
callback=self.update_result,
)
async def close_database(self): async def close_database(self):
await self.APP.id_recorder.cursor.close() await self.APP.id_recorder.cursor.close()

View File

@@ -42,7 +42,10 @@ class Index(Screen):
Binding(key="A", action="about", description=_("关于项目")), Binding(key="A", action="about", description=_("关于项目")),
] ]
def __init__(self, app: XHS, ): def __init__(
self,
app: XHS,
):
super().__init__() super().__init__()
self.xhs = app self.xhs = app
self.url = None self.url = None
@@ -51,11 +54,7 @@ class Index(Screen):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield ScrollableContainer( yield ScrollableContainer(
Label( Label(Text(_("开源协议: ") + LICENCE, style=MASTER)),
Text(
_("开源协议: ") + LICENCE,
style=MASTER)
),
Link( Link(
Text( Text(
_("项目地址: ") + REPOSITORY, _("项目地址: ") + REPOSITORY,
@@ -65,9 +64,8 @@ class Index(Screen):
tooltip=_("点击访问"), tooltip=_("点击访问"),
), ),
Label( Label(
Text( Text(_("请输入小红书图文/视频作品链接"), style=PROMPT),
_("请输入小红书图文/视频作品链接"), classes="prompt",
style=PROMPT), classes="prompt",
), ),
Input(placeholder=_("多个链接之间使用空格分隔")), Input(placeholder=_("多个链接之间使用空格分隔")),
HorizontalScroll( HorizontalScroll(
@@ -76,7 +74,11 @@ class Index(Screen):
Button(_("清空输入框"), id="reset"), Button(_("清空输入框"), id="reset"),
), ),
) )
yield RichLog(markup=True, wrap=True, auto_scroll=True, ) yield RichLog(
markup=True,
wrap=True,
auto_scroll=True,
)
yield Footer() yield Footer()
def on_mount(self) -> None: def on_mount(self) -> None:
@@ -84,15 +86,12 @@ class Index(Screen):
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.tip.write( self.tip.write(
Text( Text(_("免责声明\n") + f"\n{'>' * 50}", style=MASTER),
_("免责声明\n") +
f"\n{
">" *
50}",
style=MASTER),
scroll_end=True, scroll_end=True,
) )
self.xhs.manager.print_proxy_tip(log=self.tip, ) self.xhs.manager.print_proxy_tip(
log=self.tip,
)
@on(Button.Pressed, "#deal") @on(Button.Pressed, "#deal")
async def deal_button(self): async def deal_button(self):
@@ -119,7 +118,14 @@ class Index(Screen):
@work(exclusive=True) @work(exclusive=True)
async def deal(self): async def deal(self):
await self.app.push_screen("loading") await self.app.push_screen("loading")
if any(await self.xhs.extract(self.url.value, True, log=self.tip, data=False, )): if any(
await self.xhs.extract(
self.url.value,
True,
log=self.tip,
data=False,
)
):
self.url.value = "" self.url.value = ""
else: else:
self.tip.write( self.tip.write(
@@ -143,7 +149,11 @@ class Index(Screen):
await self.app.run_action("settings") await self.app.run_action("settings")
async def action_monitor(self): async def action_monitor(self):
await self.app.push_screen(Monitor(self.xhs, )) await self.app.push_screen(
Monitor(
self.xhs,
)
)
async def action_about(self): async def action_about(self):
await self.app.push_screen("about") await self.app.push_screen("about")

View File

@@ -10,7 +10,9 @@ __all__ = ["Loading"]
class Loading(ModalScreen): class Loading(ModalScreen):
def __init__(self, ): def __init__(
self,
):
super().__init__() super().__init__()
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:

View File

@@ -27,7 +27,10 @@ class Monitor(Screen):
Binding(key="C", action="close", description=_("关闭监听")), Binding(key="C", action="close", description=_("关闭监听")),
] ]
def __init__(self, app: XHS, ): def __init__(
self,
app: XHS,
):
super().__init__() super().__init__()
self.xhs = app self.xhs = app
@@ -44,15 +47,22 @@ class Monitor(Screen):
@work(exclusive=True) @work(exclusive=True)
async def run_monitor(self): async def run_monitor(self):
await self.xhs.monitor(download=True, log=self.query_one(RichLog), data=False, ) await self.xhs.monitor(
download=True,
log=self.query_one(RichLog),
data=False,
)
await self.action_close() await self.action_close()
def on_mount(self) -> None: def on_mount(self) -> None:
self.title = PROJECT self.title = PROJECT
self.query_one(RichLog).write( self.query_one(RichLog).write(
Text(_( Text(
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"), _(
style=MASTER), "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
),
style=MASTER,
),
scroll_end=True, scroll_end=True,
) )
self.run_monitor() self.run_monitor()

View File

@@ -14,23 +14,37 @@ __all__ = ["Record"]
class Record(ModalScreen): class Record(ModalScreen):
def __init__(self, app: XHS, ): def __init__(
self,
app: XHS,
):
super().__init__() super().__init__()
self.xhs = app self.xhs = app
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Grid( yield Grid(
Label(_("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"), Label(_("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"),
Input(placeholder=_("支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"), Input(
id="id", ), placeholder=_(
"支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
),
id="id",
),
HorizontalScroll( HorizontalScroll(
Button(_("删除指定作品 ID"), id="enter", ), Button(
Button(_("返回首页"), id="close"), ), _("删除指定作品 ID"),
id="enter",
),
Button(_("返回首页"), id="close"),
),
id="record", id="record",
) )
async def delete(self, text: str): async def delete(self, text: str):
text = await self.xhs.extract_links(text, None, ) text = await self.xhs.extract_links(
text,
None,
)
text = self.xhs.extract_id(text) text = self.xhs.extract_id(text)
await self.xhs.id_recorder.delete(text) await self.xhs.id_recorder.delete(text)
self.app.notify(_("删除下载记录成功")) self.app.notify(_("删除下载记录成功"))

View File

@@ -23,49 +23,151 @@ class Setting(Screen):
Binding(key="B", action="index", description=_("返回首页")), Binding(key="B", action="index", description=_("返回首页")),
] ]
def __init__(self, data: dict, ): def __init__(
self,
data: dict,
):
super().__init__() super().__init__()
self.data = data self.data = data
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield ScrollableContainer( yield ScrollableContainer(
Label(_("作品数据 / 文件保存根路径"), classes="params", ), Label(
Input(self.data["work_path"], placeholder=_("程序根路径"), valid_empty=True, _("作品数据 / 文件保存根路径"),
id="work_path", ), classes="params",
Label(_("作品文件储存文件夹名称"), classes="params", ), ),
Input(self.data["folder_name"], placeholder="Download", id="folder_name", ), Input(
Label(_("作品文件名称格式"), classes="params", ), self.data["work_path"],
Input(self.data["name_format"], placeholder="发布时间 作者昵称 作品标题", valid_empty=True, placeholder=_("程序根路径"),
id="name_format", ), valid_empty=True,
Label("User-Agent", classes="params", ), id="work_path",
Input(self.data["user_agent"], placeholder=_("内置 Chrome User Agent"), valid_empty=True, ),
id="user_agent", ), Label(
Label(_("小红书网页版 Cookie"), classes="params", ), _("作品文件储存文件夹名称"),
Input(placeholder=self.__check_cookie(), valid_empty=True, id="cookie", ), classes="params",
Label(_("网络代理"), classes="params", ), ),
Input(self.data["proxy"], placeholder=_("不使用代理"), valid_empty=True, id="proxy", ), Input(
Label(_("请求数据超时限制,单位:秒"), classes="params", ), self.data["folder_name"],
Input(str(self.data["timeout"]), placeholder="10", type="integer", id="timeout", ), placeholder="Download",
Label(_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), classes="params", ), id="folder_name",
Input(str(self.data["chunk"]), placeholder="1048576", type="integer", id="chunk", ), ),
Label(_("请求数据失败时,重试的最大次数"), classes="params", ), Label(
Input(str(self.data["max_retry"]), placeholder="5", type="integer", id="max_retry", ), _("作品文件名称格式"),
classes="params",
),
Input(
self.data["name_format"],
placeholder="发布时间 作者昵称 作品标题",
valid_empty=True,
id="name_format",
),
Label(
"User-Agent",
classes="params",
),
Input(
self.data["user_agent"],
placeholder=_("内置 Chrome User Agent"),
valid_empty=True,
id="user_agent",
),
Label(
_("小红书网页版 Cookie"),
classes="params",
),
Input(
placeholder=self.__check_cookie(),
valid_empty=True,
id="cookie",
),
Label(
_("网络代理"),
classes="params",
),
Input(
self.data["proxy"],
placeholder=_("不使用代理"),
valid_empty=True,
id="proxy",
),
Label(
_("请求数据超时限制,单位:秒"),
classes="params",
),
Input(
str(self.data["timeout"]),
placeholder="10",
type="integer",
id="timeout",
),
Label(
_("下载文件时,每次从服务器获取的数据块大小,单位:字节"),
classes="params",
),
Input(
str(self.data["chunk"]),
placeholder="1048576",
type="integer",
id="chunk",
),
Label(
_("请求数据失败时,重试的最大次数"),
classes="params",
),
Input(
str(self.data["max_retry"]),
placeholder="5",
type="integer",
id="max_retry",
),
Label(), Label(),
Container( Container(
Checkbox(_("记录作品详细数据"), id="record_data", value=self.data["record_data"], ), Checkbox(
Checkbox(_("作品文件夹归档模式"), id="folder_mode", value=self.data["folder_mode"], ), _("记录作品详细数据"),
Checkbox(_("视频作品下载开关"), id="video_download", value=self.data["video_download"], ), id="record_data",
Checkbox(_("图文作品下载开关"), id="image_download", value=self.data["image_download"], ), value=self.data["record_data"],
classes="horizontal-layout"), ),
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(), Label(),
Container( Container(
Checkbox(_("动图文件下载开关"), id="live_download", value=self.data["live_download"], ), Checkbox(
Checkbox(_("作品下载记录开关"), id="download_record", value=self.data["download_record"], ), _("动图文件下载开关"),
classes="horizontal-layout"), id="live_download",
value=self.data["live_download"],
),
Checkbox(
_("作品下载记录开关"),
id="download_record",
value=self.data["download_record"],
),
classes="horizontal-layout",
),
Container( Container(
Label(_("图片下载格式"), classes="params", ), Label(
Label(_("程序语言"), classes="params", ), _("图片下载格式"),
classes="params",
),
Label(
_("程序语言"),
classes="params",
),
classes="horizontal-layout", classes="horizontal-layout",
), ),
Label(), Label(),
@@ -74,17 +176,27 @@ class Setting(Screen):
("PNG", "WEBP"), ("PNG", "WEBP"),
value=self.data["image_format"].upper(), value=self.data["image_format"].upper(),
allow_blank=False, allow_blank=False,
id="image_format"), id="image_format",
),
Select.from_values( Select.from_values(
["zh_CN", "en_US"], ["zh_CN", "en_US"],
value=self.data["language"], value=self.data["language"],
allow_blank=False, allow_blank=False,
id="language", ), id="language",
classes="horizontal-layout"), ),
classes="horizontal-layout",
),
Container( Container(
Button(_("保存配置"), id="save", ), Button(
Button(_("放弃更改"), id="abandon", ), _("保存配置"),
classes="settings_button", ), id="save",
),
Button(
_("放弃更改"),
id="abandon",
),
classes="settings_button",
),
) )
yield Footer() yield Footer()
@@ -98,25 +210,27 @@ class Setting(Screen):
@on(Button.Pressed, "#save") @on(Button.Pressed, "#save")
def save_settings(self): def save_settings(self):
self.dismiss({ self.dismiss(
"work_path": self.query_one("#work_path").value, {
"folder_name": self.query_one("#folder_name").value, "work_path": self.query_one("#work_path").value,
"name_format": self.query_one("#name_format").value, "folder_name": self.query_one("#folder_name").value,
"user_agent": self.query_one("#user_agent").value, "name_format": self.query_one("#name_format").value,
"cookie": self.query_one("#cookie").value or self.data["cookie"], "user_agent": self.query_one("#user_agent").value,
"proxy": self.query_one("#proxy").value or None, "cookie": self.query_one("#cookie").value or self.data["cookie"],
"timeout": int(self.query_one("#timeout").value), "proxy": self.query_one("#proxy").value or None,
"chunk": int(self.query_one("#chunk").value), "timeout": int(self.query_one("#timeout").value),
"max_retry": int(self.query_one("#max_retry").value), "chunk": int(self.query_one("#chunk").value),
"record_data": self.query_one("#record_data").value, "max_retry": int(self.query_one("#max_retry").value),
"image_format": self.query_one("#image_format").value, "record_data": self.query_one("#record_data").value,
"folder_mode": self.query_one("#folder_mode").value, "image_format": self.query_one("#image_format").value,
"language": self.query_one("#language").value, "folder_mode": self.query_one("#folder_mode").value,
"image_download": self.query_one("#image_download").value, "language": self.query_one("#language").value,
"video_download": self.query_one("#video_download").value, "image_download": self.query_one("#image_download").value,
"live_download": self.query_one("#live_download").value, "video_download": self.query_one("#video_download").value,
"download_record": self.query_one("#download_record").value, "live_download": self.query_one("#live_download").value,
}) "download_record": self.query_one("#download_record").value,
}
)
@on(Button.Pressed, "#abandon") @on(Button.Pressed, "#abandon")
def reset(self): def reset(self):

View File

@@ -15,7 +15,10 @@ __all__ = ["Update"]
class Update(ModalScreen): class Update(ModalScreen):
def __init__(self, app: XHS, ): def __init__(
self,
app: XHS,
):
super().__init__() super().__init__()
self.xhs = app self.xhs = app
@@ -29,9 +32,16 @@ class Update(ModalScreen):
@work(exclusive=True) @work(exclusive=True)
async def check_update(self) -> None: async def check_update(self) -> None:
try: try:
url = await self.xhs.html.request_url(RELEASES, False, None, timeout=5, ) url = await self.xhs.html.request_url(
RELEASES,
False,
None,
timeout=5,
)
version = url.split("/")[-1] version = url.split("/")[-1]
match self.compare_versions(f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA): match self.compare_versions(
f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA
):
case 4: case 4:
args = ( args = (
_("检测到新版本:{0}.{1}").format( _("检测到新版本:{0}.{1}").format(
@@ -58,7 +68,10 @@ class Update(ModalScreen):
case _: case _:
raise ValueError raise ValueError
except ValueError: except ValueError:
args = (_("检测新版本失败"), "error",) args = (
_("检测新版本失败"),
"error",
)
self.dismiss(args) self.dismiss(args)
def on_mount(self) -> None: def on_mount(self) -> None:
@@ -66,11 +79,10 @@ class Update(ModalScreen):
@staticmethod @staticmethod
def compare_versions( def compare_versions(
current_version: str, current_version: str, target_version: str, is_development: bool
target_version: str, ) -> int:
is_development: bool) -> int: current_major, current_minor = map(int, current_version.split("."))
current_major, current_minor = map(int, current_version.split('.')) target_major, target_minor = map(int, target_version.split("."))
target_major, target_minor = map(int, target_version.split('.'))
if target_major > current_major: if target_major > current_major:
return 4 return 4

View File

@@ -3,4 +3,9 @@ from .TUI import XHSDownloader
from .application import XHS from .application import XHS
from .module import Settings from .module import Settings
__all__ = ['XHS', 'XHSDownloader', 'cli', 'Settings', ] __all__ = [
"XHS",
"XHSDownloader",
"cli",
"Settings",
]

View File

@@ -48,11 +48,17 @@ __all__ = ["XHS"]
def _data_cache(function): def _data_cache(function):
async def inner(self, data: dict, ): async def inner(
self,
data: dict,
):
if self.manager.record_data: if self.manager.record_data:
download = data["下载地址"] download = data["下载地址"]
lives = data["动图地址"] lives = data["动图地址"]
await function(self, data, ) await function(
self,
data,
)
data["下载地址"] = download data["下载地址"] = download
data["动图地址"] = lives data["动图地址"] = lives
@@ -137,11 +143,14 @@ class XHS:
def __extract_image(self, container: dict, data: Namespace): def __extract_image(self, container: dict, data: Namespace):
container["下载地址"], container["动图地址"] = self.image.get_image_link( container["下载地址"], container["动图地址"] = self.image.get_image_link(
data, self.manager.image_format) data, self.manager.image_format
)
def __extract_video(self, container: dict, data: Namespace): def __extract_video(self, container: dict, data: Namespace):
container["下载地址"] = self.video.get_video_link(data) container["下载地址"] = self.video.get_video_link(data)
container["动图地址"] = [None, ] container["动图地址"] = [
None,
]
async def __download_files( async def __download_files(
self, self,
@@ -154,8 +163,7 @@ class XHS:
name = self.__naming_rules(container) name = self.__naming_rules(container)
if (u := container["下载地址"]) and download: if (u := container["下载地址"]) and download:
if await self.skip_download(i := container["作品ID"]): if await self.skip_download(i := container["作品ID"]):
logging( logging(log, _("作品 {0} 存在下载记录,跳过下载").format(i))
log, _("作品 {0} 存在下载记录,跳过下载").format(i))
else: else:
path, result = await self.download.run( path, result = await self.download.run(
u, u,
@@ -172,7 +180,10 @@ class XHS:
await self.save_data(container) await self.save_data(container)
@_data_cache @_data_cache
async def save_data(self, data: dict, ): async def save_data(
self,
data: dict,
):
data["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") data["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data["下载地址"] = " ".join(data["下载地址"]) data["下载地址"] = " ".join(data["下载地址"])
data["动图地址"] = " ".join(i or "NaN" for i in data["动图地址"]) data["动图地址"] = " ".join(i or "NaN" for i in data["动图地址"])
@@ -196,10 +207,19 @@ class XHS:
if not urls: if not urls:
logging(log, _("提取小红书作品链接失败"), WARNING) logging(log, _("提取小红书作品链接失败"), WARNING)
else: else:
logging( logging(log, _("{0} 个小红书作品待处理...").format(len(urls)))
log, _("{0} 个小红书作品待处理...").format(len(urls)))
# return urls # 调试代码 # return urls # 调试代码
return [await self.__deal_extract(i, download, index, log, bar, data, ) for i in urls] return [
await self.__deal_extract(
i,
download,
index,
log,
bar,
data,
)
for i in urls
]
async def extract_cli( async def extract_cli(
self, self,
@@ -214,7 +234,14 @@ class XHS:
if not url: if not url:
logging(log, _("提取小红书作品链接失败"), WARNING) logging(log, _("提取小红书作品链接失败"), WARNING)
else: else:
await self.__deal_extract(url[0], download, index, log, bar, data, ) await self.__deal_extract(
url[0],
download,
index,
log,
bar,
data,
)
async def extract_links(self, url: str, log) -> list: async def extract_links(self, url: str, log) -> list:
urls = [] urls = []
@@ -253,7 +280,11 @@ class XHS:
logging(log, msg) logging(log, msg)
return {"message": msg} return {"message": msg}
logging(log, _("开始处理作品:{0}").format(i)) logging(log, _("开始处理作品:{0}").format(i))
html = await self.html.request_url(url, log=log, cookie=cookie, ) html = await self.html.request_url(
url,
log=log,
cookie=cookie,
)
namespace = self.__generate_data_object(html) namespace = self.__generate_data_object(html)
if not namespace: if not namespace:
logging(log, _("{0} 获取数据失败").format(i), ERROR) logging(log, _("{0} 获取数据失败").format(i), ERROR)
@@ -299,10 +330,12 @@ class XHS:
return beautify_string( return beautify_string(
self.CLEANER.filter_name( self.CLEANER.filter_name(
self.manager.SEPARATE.join(values), self.manager.SEPARATE.join(values),
default=self.manager.SEPARATE.join(( default=self.manager.SEPARATE.join(
data["作者ID"], (
data["ID"], data["ID"],
)), data["作品ID"],
)
),
), ),
length=128, length=128,
) )
@@ -315,10 +348,13 @@ class XHS:
return self.manager.filter_name(data["作者昵称"]) or data["作者ID"] return self.manager.filter_name(data["作者昵称"]) or data["作者ID"]
def __get_name_title(self, data: dict) -> str: def __get_name_title(self, data: dict) -> str:
return beautify_string( return (
self.manager.filter_name(data["作品标题"]), beautify_string(
64, self.manager.filter_name(data["作品标题"]),
) or data["作品ID"] 64,
)
or data["作品ID"]
)
async def monitor( async def monitor(
self, self,
@@ -331,11 +367,15 @@ class XHS:
logging( logging(
None, None,
_( _(
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"), "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
),
style=MASTER, style=MASTER,
) )
self.event.clear() self.event.clear()
await gather(self.__push_link(delay), self.__receive_link(delay, download, None, log, bar, data)) await gather(
self.__push_link(delay),
self.__receive_link(delay, download, None, log, bar, data),
)
async def __push_link(self, delay: int): async def __push_link(self, delay: int):
while not self.event.is_set(): while not self.event.is_set():
@@ -373,10 +413,16 @@ class XHS:
@staticmethod @staticmethod
def read_browser_cookie(value: str | int) -> str: def read_browser_cookie(value: str | int) -> str:
return BrowserCookie.get( return (
value, BrowserCookie.get(
domains=["xiaohongshu.com", ], value,
) if value else "" domains=[
"xiaohongshu.com",
],
)
if value
else ""
)
# @staticmethod # @staticmethod
# async def index(request): # async def index(request):
@@ -425,11 +471,17 @@ class XHS:
# await self.runner.cleanup() # await self.runner.cleanup()
# logging(log, _("Web API 服务器已关闭!")) # logging(log, _("Web API 服务器已关闭!"))
async def run_server(self, host="0.0.0.0", port=8000, log_level="info", ): async def run_server(
self,
host="0.0.0.0",
port=8000,
log_level="info",
):
self.server = FastAPI( self.server = FastAPI(
debug=self.VERSION_BETA, debug=self.VERSION_BETA,
title="XHS-Downloader", title="XHS-Downloader",
version=f"{self.VERSION_MAJOR}.{self.VERSION_MINOR}") version=f"{self.VERSION_MAJOR}.{self.VERSION_MINOR}",
)
self.setup_routes() self.setup_routes()
config = Config( config = Config(
self.server, self.server,
@@ -445,7 +497,10 @@ class XHS:
async def index(): async def index():
return RedirectResponse(url=REPOSITORY) return RedirectResponse(url=REPOSITORY)
@self.server.post("/xhs/", response_model=ExtractData, ) @self.server.post(
"/xhs/",
response_model=ExtractData,
)
async def handle(extract: ExtractParams): async def handle(extract: ExtractParams):
url = await self.extract_links(extract.url, None) url = await self.extract_links(extract.url, None)
if not url: if not url:
@@ -466,6 +521,5 @@ class XHS:
msg = _("获取小红书作品数据失败") msg = _("获取小红书作品数据失败")
data = None data = None
return ExtractData( return ExtractData(
message=msg, message=msg, url=url[0] if url else extract.url, data=data
url=url[0] if url else extract.url, )
data=data)

View File

@@ -23,7 +23,7 @@ from ..translation import _
if TYPE_CHECKING: if TYPE_CHECKING:
from httpx import AsyncClient from httpx import AsyncClient
__all__ = ['Download'] __all__ = ["Download"]
class Download: class Download:
@@ -38,7 +38,10 @@ class Download:
"audio/mpeg": "mp3", "audio/mpeg": "mp3",
} }
def __init__(self, manager: Manager, ): def __init__(
self,
manager: Manager,
):
self.manager = manager self.manager = manager
self.folder = manager.folder self.folder = manager.folder
self.temp = manager.temp self.temp = manager.temp
@@ -98,7 +101,8 @@ class Download:
format_, format_,
log, log,
bar, bar,
) for url, name, format_ in tasks )
for url, name, format_ in tasks
] ]
tasks = await gather(*tasks) tasks = await gather(*tasks)
return path, tasks return path, tasks
@@ -109,11 +113,8 @@ class Download:
return path return path
def __ready_download_video( def __ready_download_video(
self, self, urls: list[str], path: Path, name: str, log
urls: list[str], ) -> list:
path: Path,
name: str,
log) -> list:
if not self.video_download: if not self.video_download:
logging(log, _("视频作品下载功能已关闭,跳过下载")) logging(log, _("视频作品下载功能已关闭,跳过下载"))
return [] return []
@@ -128,7 +129,8 @@ class Download:
index: list | tuple | None, index: list | tuple | None,
path: Path, path: Path,
name: str, name: str,
log) -> list: log,
) -> list:
tasks = [] tasks = []
if not self.image_download: if not self.image_download:
logging(log, _("图文作品下载功能已关闭,跳过下载")) logging(log, _("图文作品下载功能已关闭,跳过下载"))
@@ -146,28 +148,38 @@ class Download:
for s in self.image_format_list for s in self.image_format_list
): ):
tasks.append([j[0], file, self.image_format]) tasks.append([j[0], file, self.image_format])
if not self.live_download or not j[1] or self.__check_exists_path( if (
not self.live_download
or not j[1]
or self.__check_exists_path(
path, path,
f"{file}.{self.live_format}", f"{file}.{self.live_format}",
log, log,
)
): ):
continue continue
tasks.append([j[1], file, self.live_format]) tasks.append([j[1], file, self.live_format])
return tasks return tasks
def __check_exists_glob(self, path: Path, name: str, log, ) -> bool: def __check_exists_glob(
self,
path: Path,
name: str,
log,
) -> bool:
if any(path.glob(name)): if any(path.glob(name)):
logging( logging(log, _("{0} 文件已存在,跳过下载").format(name))
log, _(
"{0} 文件已存在,跳过下载").format(name))
return True return True
return False return False
def __check_exists_path(self, path: Path, name: str, log, ) -> bool: def __check_exists_path(
self,
path: Path,
name: str,
log,
) -> bool:
if path.joinpath(name).exists(): if path.joinpath(name).exists():
logging( logging(log, _("{0} 文件已存在,跳过下载").format(name))
log, _(
"{0} 文件已存在,跳过下载").format(name))
return True return True
return False return False
@@ -199,9 +211,16 @@ class Download:
# return False # return False
# temp = self.temp.joinpath(f"{name}.{suffix}") # temp = self.temp.joinpath(f"{name}.{suffix}")
temp = self.temp.joinpath(f"{name}.{format_}") temp = self.temp.joinpath(f"{name}.{format_}")
self.__update_headers_range(headers, temp, ) self.__update_headers_range(
headers,
temp,
)
try: try:
async with self.client.stream("GET", url, headers=headers, ) as response: async with self.client.stream(
"GET",
url,
headers=headers,
) as response:
await sleep_time() await sleep_time()
if response.status_code == 416: if response.status_code == 416:
raise CacheError( raise CacheError(
@@ -234,8 +253,9 @@ class Download:
# self.__create_progress(bar, None) # self.__create_progress(bar, None)
logging( logging(
log, log,
_( _("网络异常,{0} 下载失败,错误信息: {1}").format(
"网络异常,{0} 下载失败,错误信息: {1}").format(name, repr(error)), name, repr(error)
),
ERROR, ERROR,
) )
return False return False
@@ -248,7 +268,11 @@ class Download:
) )
@staticmethod @staticmethod
def __create_progress(bar, total: int | None, completed=0, ): def __create_progress(
bar,
total: int | None,
completed=0,
):
if bar: if bar:
bar.update(total=total, completed=completed) bar.update(total=total, completed=completed)
@@ -273,10 +297,8 @@ class Download:
) )
await sleep_time() await sleep_time()
response.raise_for_status() response.raise_for_status()
suffix = self.__extract_type( suffix = self.__extract_type(response.headers.get("Content-Type")) or suffix
response.headers.get("Content-Type")) or suffix length = response.headers.get("Content-Length", 0)
length = response.headers.get(
"Content-Length", 0)
return int(length), suffix return int(length), suffix
@staticmethod @staticmethod
@@ -303,12 +325,14 @@ class Download:
async with open(temp, "rb") as f: async with open(temp, "rb") as f:
file_start = await f.read(FILE_SIGNATURES_LENGTH) file_start = await f.read(FILE_SIGNATURES_LENGTH)
for offset, signature, suffix in FILE_SIGNATURES: for offset, signature, suffix in FILE_SIGNATURES:
if file_start[offset:offset + len(signature)] == signature: if file_start[offset: offset + len(signature)] == signature:
return path.joinpath(f"{name}.{suffix}") return path.joinpath(f"{name}.{suffix}")
except Exception as error: except Exception as error:
logging( logging(
log, log,
_("文件 {0} 格式判断失败,错误信息:{1}").format(temp.name, repr(error)), _("文件 {0} 格式判断失败,错误信息:{1}").format(
temp.name, repr(error)
),
ERROR, ERROR,
) )
return path.joinpath(f"{name}.{default_suffix}") return path.joinpath(f"{name}.{default_suffix}")

View File

@@ -3,7 +3,7 @@ from datetime import datetime
from ..expansion import Namespace from ..expansion import Namespace
from ..translation import _ from ..translation import _
__all__ = ['Explore'] __all__ = ["Explore"]
class Explore: class Explore:
@@ -27,10 +27,8 @@ class Explore:
@staticmethod @staticmethod
def __extract_interact_info(container: dict, data: Namespace) -> None: def __extract_interact_info(container: dict, data: Namespace) -> None:
container["收藏数量"] = data.safe_extract( container["收藏数量"] = data.safe_extract("interactInfo.collectedCount", "-1")
"interactInfo.collectedCount", "-1") container["评论数量"] = data.safe_extract("interactInfo.commentCount", "-1")
container["评论数量"] = data.safe_extract(
"interactInfo.commentCount", "-1")
container["分享数量"] = data.safe_extract("interactInfo.shareCount", "-1") container["分享数量"] = data.safe_extract("interactInfo.shareCount", "-1")
container["点赞数量"] = data.safe_extract("interactInfo.likedCount", "-1") container["点赞数量"] = data.safe_extract("interactInfo.likedCount", "-1")
@@ -38,33 +36,37 @@ class Explore:
def __extract_tags(container: dict, data: Namespace): def __extract_tags(container: dict, data: Namespace):
tags = data.safe_extract("tagList", []) tags = data.safe_extract("tagList", [])
container["作品标签"] = " ".join( container["作品标签"] = " ".join(
Namespace.object_extract( Namespace.object_extract(i, "name") for i in tags
i, "name") for i in tags) )
def __extract_info(self, container: dict, data: Namespace): def __extract_info(self, container: dict, data: Namespace):
container["作品ID"] = data.safe_extract("noteId") container["作品ID"] = data.safe_extract("noteId")
container["作品链接"] = f"https://www.xiaohongshu.com/explore/{container["作品ID"]}" container["作品链接"] = (
f"https://www.xiaohongshu.com/explore/{container['作品ID']}"
)
container["作品标题"] = data.safe_extract("title") container["作品标题"] = data.safe_extract("title")
container["作品描述"] = data.safe_extract("desc") container["作品描述"] = data.safe_extract("desc")
container["作品类型"] = self.explore_type.get( container["作品类型"] = self.explore_type.get(
data.safe_extract("type"), _("未知")) data.safe_extract("type"), _("未知")
)
# container["IP归属地"] = data.safe_extract("ipLocation") # container["IP归属地"] = data.safe_extract("ipLocation")
def __extract_time(self, container: dict, data: Namespace): def __extract_time(self, container: dict, data: Namespace):
container["发布时间"] = datetime.fromtimestamp( container["发布时间"] = (
time / datetime.fromtimestamp(time / 1000).strftime(self.time_format)
1000).strftime( if (time := data.safe_extract("time"))
self.time_format) if ( else _("未知")
time := data.safe_extract("time")) else _("未知") )
container["最后更新时间"] = datetime.fromtimestamp( container["最后更新时间"] = (
last / datetime.fromtimestamp(last / 1000).strftime(self.time_format)
1000).strftime( if (last := data.safe_extract("lastUpdateTime"))
self.time_format) if ( else _("未知")
last := data.safe_extract("lastUpdateTime")) else _("未知") )
@staticmethod @staticmethod
def __extract_user(container: dict, data: Namespace): def __extract_user(container: dict, data: Namespace):
container["作者昵称"] = data.safe_extract("user.nickname") container["作者昵称"] = data.safe_extract("user.nickname")
container["作者ID"] = data.safe_extract("user.userId") container["作者ID"] = data.safe_extract("user.userId")
container["作者链接"] = f"https://www.xiaohongshu.com/user/profile/{ container["作者链接"] = (
container["作者ID"]}" f"https://www.xiaohongshu.com/user/profile/{container['作者ID']}"
)

View File

@@ -1,7 +1,7 @@
from source.expansion import Namespace from source.expansion import Namespace
from .request import Html from .request import Html
__all__ = ['Image'] __all__ = ["Image"]
class Image: class Image:
@@ -10,16 +10,18 @@ class Image:
images = data.safe_extract("imageList", []) images = data.safe_extract("imageList", [])
live_link = cls.__get_live_link(images) live_link = cls.__get_live_link(images)
token_list = [ token_list = [
cls.__extract_image_token( cls.__extract_image_token(Namespace.object_extract(i, "urlDefault"))
Namespace.object_extract( for i in images
i, "urlDefault")) for i in images] ]
match format_: match format_:
case "png": case "png":
return [Html.format_url(cls.__generate_png_link(i)) return [
for i in token_list], live_link Html.format_url(cls.__generate_png_link(i)) for i in token_list
], live_link
case "webp": case "webp":
return [Html.format_url(cls.__generate_webp_link(i)) return [
for i in token_list], live_link Html.format_url(cls.__generate_webp_link(i)) for i in token_list
], live_link
case _: case _:
raise ValueError raise ValueError

View File

@@ -11,7 +11,10 @@ __all__ = ["Html"]
class Html: class Html:
def __init__(self, manager: Manager, ): def __init__(
self,
manager: Manager,
):
self.retry = manager.retry self.retry = manager.retry
self.client = manager.request_client self.client = manager.request_client
self.headers = manager.headers self.headers = manager.headers
@@ -26,23 +29,32 @@ class Html:
cookie: str = None, cookie: str = None,
**kwargs, **kwargs,
) -> str: ) -> str:
headers = self.select_headers(url, cookie, ) headers = self.select_headers(
url,
cookie,
)
try: try:
match content: match content:
case True: case True:
response = await self.__request_url_get(url, headers, **kwargs, ) response = await self.__request_url_get(
url,
headers,
**kwargs,
)
await sleep_time() await sleep_time()
response.raise_for_status() response.raise_for_status()
return response.text return response.text
case False: case False:
response = await self.__request_url_head(url, headers, **kwargs, ) response = await self.__request_url_head(
url,
headers,
**kwargs,
)
await sleep_time() await sleep_time()
return str(response.url) return str(response.url)
except HTTPError as error: except HTTPError as error:
logging( logging(
log, log, _("网络异常,{0} 请求失败: {1}").format(url, repr(error)), ERROR
_("网络异常,{0} 请求失败: {1}").format(url, repr(error)),
ERROR
) )
return "" return ""
@@ -50,19 +62,33 @@ class Html:
def format_url(url: str) -> str: def format_url(url: str) -> str:
return bytes(url, "utf-8").decode("unicode_escape") return bytes(url, "utf-8").decode("unicode_escape")
def select_headers(self, url: str, cookie: str = None, ) -> dict: def select_headers(
self,
url: str,
cookie: str = None,
) -> dict:
if "explore" not in url: if "explore" not in url:
return self.blank_headers return self.blank_headers
return self.headers | {"Cookie": cookie} if cookie else self.headers return self.headers | {"Cookie": cookie} if cookie else self.headers
async def __request_url_head(self, url: str, headers: dict, **kwargs, ): async def __request_url_head(
self,
url: str,
headers: dict,
**kwargs,
):
return await self.client.head( return await self.client.head(
url, url,
headers=headers, headers=headers,
**kwargs, **kwargs,
) )
async def __request_url_get(self, url: str, headers: dict, **kwargs, ): async def __request_url_get(
self,
url: str,
headers: dict,
**kwargs,
):
return await self.client.get( return await self.client.get(
url, url,
headers=headers, headers=headers,

View File

@@ -1,7 +1,7 @@
from source.expansion import Namespace from source.expansion import Namespace
from .request import Html from .request import Html
__all__ = ['Video'] __all__ = ["Video"]
class Video: class Video:
@@ -13,5 +13,8 @@ class Video:
@classmethod @classmethod
def get_video_link(cls, data: Namespace) -> list: def get_video_link(cls, data: Namespace) -> list:
return [Html.format_url(f"https://sns-video-bd.xhscdn.com/{t}")] if ( return (
t := data.safe_extract(".".join(cls.VIDEO_LINK))) else [] [Html.format_url(f"https://sns-video-bd.xhscdn.com/{t}")]
if (t := data.safe_extract(".".join(cls.VIDEO_LINK)))
else []
)

View File

@@ -38,25 +38,44 @@ class BrowserCookie:
} }
@classmethod @classmethod
def run(cls, domains: list[str], console: Console = None, ) -> str: def run(
cls,
domains: list[str],
console: Console = None,
) -> str:
console = console or Console() console = console or Console()
options = "\n".join(f"{i}. {k}: {v[1]}" for i, (k, v) in enumerate(cls.SUPPORT_BROWSER.items(), start=1)) options = "\n".join(
f"{i}. {k}: {v[1]}"
for i, (k, v) in enumerate(cls.SUPPORT_BROWSER.items(), start=1)
)
if browser := console.input( if browser := console.input(
_("读取指定浏览器的 Cookie 并写入配置文件\n" _(
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie\n" "读取指定浏览器 Cookie 并写入配置文件\n"
"{options}\n请输入浏览器名称或序号:").format(options=options), ): "Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie\n"
return cls.get(browser, domains, console, ) "{options}\n请输入浏览器名称或序号:"
).format(options=options),
):
return cls.get(
browser,
domains,
console,
)
console.print(_("未选择浏览器!")) console.print(_("未选择浏览器!"))
@classmethod @classmethod
def get(cls, browser: str | int, domains: list[str], console: Console = None, ) -> str: def get(
cls,
browser: str | int,
domains: list[str],
console: Console = None,
) -> str:
console = console or Console() console = console or Console()
if not (browser := cls.__browser_object(browser)): if not (browser := cls.__browser_object(browser)):
console.print(_("浏览器名称或序号输入错误!")) console.print(_("浏览器名称或序号输入错误!"))
return "" return ""
try: try:
cookies = browser(domains=domains) cookies = browser(domains=domains)
return "; ".join(f"{i["name"]}={i["value"]}" for i in cookies) return "; ".join(f"{i['name']}={i['value']}" for i in cookies)
except RuntimeError: except RuntimeError:
console.print(_("获取 Cookie 失败,未找到 Cookie 数据!")) console.print(_("获取 Cookie 失败,未找到 Cookie 数据!"))
return "" return ""

View File

@@ -30,7 +30,7 @@ class Cleaner:
"|": "", "|": "",
"<": "", "<": "",
">": "", ">": "",
"\"": "", '"': "",
"?": "", "?": "",
":": "", ":": "",
"*": "", "*": "",
@@ -80,7 +80,10 @@ class Cleaner:
text = self.filter(text) text = self.filter(text)
text = replace_emoji(text, replace, ) text = replace_emoji(
text,
replace,
)
text = self.clear_spaces(text) text = self.clear_spaces(text)
@@ -94,9 +97,16 @@ class Cleaner:
return " ".join(string.split()) return " ".join(string.split())
@classmethod @classmethod
def remove_control_characters(cls, text, replace="", ): def remove_control_characters(
cls,
text,
replace="",
):
# 使用正则表达式匹配所有控制字符 # 使用正则表达式匹配所有控制字符
return cls.CONTROL_CHARACTERS.sub(replace, text, ) return cls.CONTROL_CHARACTERS.sub(
replace,
text,
)
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -16,9 +16,7 @@ class Converter:
) )
def run(self, content: str) -> dict: def run(self, content: str) -> dict:
return self._filter_object( return self._filter_object(self._convert_object(self._extract_object(content)))
self._convert_object(
self._extract_object(content)))
def _extract_object(self, html: str) -> str: def _extract_object(self, html: str) -> str:
if not html: if not html:

View File

@@ -15,7 +15,9 @@ def remove_empty_directories(path: Path) -> None:
"\\_", "\\_",
"\\__", "\\__",
} }
for dir_path, dir_names, file_names in path.walk(top_down=False, ): for dir_path, dir_names, file_names in path.walk(
top_down=False,
):
if any(i in str(dir_path) for i in exclude): if any(i in str(dir_path) for i in exclude):
continue continue
if not dir_names and not file_names: if not dir_names and not file_names:

View File

@@ -14,7 +14,8 @@ class Namespace:
def depth_conversion(element): def depth_conversion(element):
if isinstance(element, dict): if isinstance(element, dict):
return SimpleNamespace( return SimpleNamespace(
**{k: depth_conversion(v) for k, v in element.items()}) **{k: depth_conversion(v) for k, v in element.items()}
)
elif isinstance(element, list): elif isinstance(element, list):
return [depth_conversion(item) for item in element] return [depth_conversion(item) for item in element]
else: else:
@@ -25,14 +26,16 @@ class Namespace:
def safe_extract( def safe_extract(
self, self,
attribute_chain: str, attribute_chain: str,
default: Union[str, int, list, dict, SimpleNamespace] = ""): default: Union[str, int, list, dict, SimpleNamespace] = "",
):
return self.__safe_extract(self.data, attribute_chain, default) return self.__safe_extract(self.data, attribute_chain, default)
@staticmethod @staticmethod
def __safe_extract( def __safe_extract(
data_object: SimpleNamespace, data_object: SimpleNamespace,
attribute_chain: str, attribute_chain: str,
default: Union[str, int, list, dict, SimpleNamespace] = "", ): default: Union[str, int, list, dict, SimpleNamespace] = "",
):
data = deepcopy(data_object) data = deepcopy(data_object)
attributes = attribute_chain.split(".") attributes = attribute_chain.split(".")
for attribute in attributes: for attribute in attributes:
@@ -61,7 +64,8 @@ class Namespace:
return cls.__safe_extract( return cls.__safe_extract(
data_object, data_object,
attribute_chain, attribute_chain,
default, ) default,
)
@property @property
def __dict__(self): def __dict__(self):
@@ -70,10 +74,11 @@ class Namespace:
@classmethod @classmethod
def convert_to_dict(cls, data) -> dict: def convert_to_dict(cls, data) -> dict:
return { return {
key: cls.convert_to_dict(value) if isinstance( key: cls.convert_to_dict(value)
value, if isinstance(value, SimpleNamespace)
SimpleNamespace) else value for key, else value
value in vars(data).items()} for key, value in vars(data).items()
}
def __bool__(self): def __bool__(self):
return bool(vars(self.data)) return bool(vars(self.data))

View File

@@ -2,7 +2,7 @@ from unicodedata import name
def is_chinese_char(char: str) -> bool: def is_chinese_char(char: str) -> bool:
return 'CJK' in name(char, "") return "CJK" in name(char, "")
def truncate_string(s: str, length: int = 64) -> str: def truncate_string(s: str, length: int = 64) -> str:

View File

@@ -72,10 +72,10 @@ class Manager:
self.path = self.__check_path(path) self.path = self.__check_path(path)
self.folder = self.__check_folder(folder) self.folder = self.__check_folder(folder)
self.blank_headers = HEADERS | { self.blank_headers = HEADERS | {
'user-agent': user_agent or USERAGENT, "user-agent": user_agent or USERAGENT,
} }
self.headers = self.blank_headers | { self.headers = self.blank_headers | {
'cookie': cookie, "cookie": cookie,
} }
self.retry = retry self.retry = retry
self.chunk = chunk self.chunk = chunk
@@ -86,10 +86,13 @@ class Manager:
self.download_record = self.check_bool(download_record, True) self.download_record = self.check_bool(download_record, True)
self.proxy_tip = None self.proxy_tip = None
self.proxy = self.__check_proxy(proxy) self.proxy = self.__check_proxy(proxy)
self.print_proxy_tip(_print, ) self.print_proxy_tip(
_print,
)
self.request_client = AsyncClient( self.request_client = AsyncClient(
headers=self.headers | { headers=self.headers
'referer': 'https://www.xiaohongshu.com/', | {
"referer": "https://www.xiaohongshu.com/",
}, },
timeout=timeout, timeout=timeout,
verify=False, verify=False,
@@ -177,11 +180,7 @@ class Manager:
def __check_name_format(self, format_: str) -> str: def __check_name_format(self, format_: str) -> str:
keys = format_.split() keys = format_.split()
return next( return next(
( ("发布时间 作者昵称 作品标题" for key in keys if key not in self.NAME_KEYS),
"发布时间 作者昵称 作品标题"
for key in keys
if key not in self.NAME_KEYS
),
format_, format_,
) )
@@ -198,7 +197,7 @@ class Manager:
timeout=10, timeout=10,
headers={ headers={
"User-Agent": USERAGENT, "User-Agent": USERAGENT,
} },
) )
response.raise_for_status() response.raise_for_status()
self.proxy_tip = (_("代理 {0} 测试成功").format(proxy),) self.proxy_tip = (_("代理 {0} 测试成功").format(proxy),)
@@ -220,7 +219,11 @@ class Manager:
WARNING, WARNING,
) )
def print_proxy_tip(self, _print: bool = True, log=None, ) -> None: def print_proxy_tip(
self,
_print: bool = True,
log=None,
) -> None:
if _print and self.proxy_tip: if _print and self.proxy_tip:
logging(log, *self.proxy_tip) logging(log, *self.proxy_tip)
@@ -240,6 +243,6 @@ class Manager:
# 使用空字符串替换匹配到的部分 # 使用空字符串替换匹配到的部分
cookie_string = sub(pattern, "", cookie_string) cookie_string = sub(pattern, "", cookie_string)
# 去除多余的分号和空格 # 去除多余的分号和空格
cookie_string = sub(r';\s*$', "", cookie_string) # 删除末尾的分号和空格 cookie_string = sub(r";\s*$", "", cookie_string) # 删除末尾的分号和空格
cookie_string = sub(r';\s*;', ";", cookie_string) # 删除中间多余分号后的空格 cookie_string = sub(r";\s*;", ";", cookie_string) # 删除中间多余分号后的空格
return cookie_string.strip('; ') return cookie_string.strip("; ")

View File

@@ -5,7 +5,10 @@ from aiosqlite import connect
from ..module import Manager from ..module import Manager
__all__ = ["IDRecorder", "DataRecorder", ] __all__ = [
"IDRecorder",
"DataRecorder",
]
class IDRecorder: class IDRecorder:
@@ -18,7 +21,9 @@ class IDRecorder:
async def _connect_database(self): async def _connect_database(self):
self.database = await connect(self.file) self.database = await connect(self.file)
self.cursor = await self.database.cursor() self.cursor = await self.database.cursor()
await self.database.execute("CREATE TABLE IF NOT EXISTS explore_id (ID TEXT PRIMARY KEY);") await self.database.execute(
"CREATE TABLE IF NOT EXISTS explore_id (ID TEXT PRIMARY KEY);"
)
await self.database.commit() await self.database.commit()
async def select(self, id_: str): async def select(self, id_: str):
@@ -95,11 +100,14 @@ class DataRecorder(IDRecorder):
async def add(self, **kwargs) -> None: async def add(self, **kwargs) -> None:
if self.switch: if self.switch:
await self.database.execute(f"""REPLACE INTO explore_data ( await self.database.execute(
f"""REPLACE INTO explore_data (
{", ".join(i[0] for i in self.DATA_TABLE)} {", ".join(i[0] for i in self.DATA_TABLE)}
) VALUES ( ) VALUES (
{", ".join("?" for _ in kwargs)} {", ".join("?" for _ in kwargs)}
);""", self.__generate_values(kwargs)) );""",
self.__generate_values(kwargs),
)
await self.database.commit() await self.database.commit()
async def __delete(self, id_: str) -> None: async def __delete(self, id_: str) -> None:

View File

@@ -6,7 +6,7 @@ from platform import system
from .static import ROOT from .static import ROOT
from .static import USERAGENT from .static import USERAGENT
__all__ = ['Settings'] __all__ = ["Settings"]
class Settings: class Settings:

View File

@@ -3,10 +3,11 @@ from pathlib import Path
VERSION_MAJOR = 2 VERSION_MAJOR = 2
VERSION_MINOR = 5 VERSION_MINOR = 5
VERSION_BETA = True VERSION_BETA = True
__version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{"beta" if VERSION_BETA else "stable"}" __version__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{'beta' if VERSION_BETA else 'stable'}"
ROOT = Path(__file__).resolve().parent.parent.parent ROOT = Path(__file__).resolve().parent.parent.parent
PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{ PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{VERSION_MINOR} {
VERSION_MINOR} {"Beta" if VERSION_BETA else "Stable"}" 'Beta' if VERSION_BETA else 'Stable'
}"
REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader" REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader"
LICENCE = "GNU General Public License v3.0" LICENCE = "GNU General Public License v3.0"
@@ -14,8 +15,10 @@ RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest"
USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js" 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/131.0.0.0 " USERAGENT = (
"Safari/537.36") "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 "
"Safari/537.36"
)
HEADERS = { HEADERS = {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8," "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
@@ -32,26 +35,35 @@ ERROR = "b bright_red"
WARNING = "b bright_yellow" WARNING = "b bright_yellow"
INFO = "b bright_green" INFO = "b bright_green"
FILE_SIGNATURES: tuple[tuple[int, bytes, str,], ...] = ( FILE_SIGNATURES: tuple[
tuple[
int,
bytes,
str,
],
...,
] = (
# 分别为偏移量(字节)、十六进制签名、后缀 # 分别为偏移量(字节)、十六进制签名、后缀
# 参考https://en.wikipedia.org/wiki/List_of_file_signatures # 参考https://en.wikipedia.org/wiki/List_of_file_signatures
# 参考https://www.garykessler.net/library/file_sigs.html # 参考https://www.garykessler.net/library/file_sigs.html
(0, b"\xFF\xD8\xFF", "jpeg"), (0, b"\xff\xd8\xff", "jpeg"),
(0, b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A", "png"), (0, b"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", "png"),
(4, b"\x66\x74\x79\x70\x61\x76\x69\x66", "avif"), (4, b"\x66\x74\x79\x70\x61\x76\x69\x66", "avif"),
(4, b"\x66\x74\x79\x70\x68\x65\x69\x63", "heic"), (4, b"\x66\x74\x79\x70\x68\x65\x69\x63", "heic"),
(8, b"\x57\x45\x42\x50", "webp"), (8, b"\x57\x45\x42\x50", "webp"),
(4, b"\x66\x74\x79\x70\x4D\x53\x4E\x56", "mp4"), (4, b"\x66\x74\x79\x70\x4d\x53\x4e\x56", "mp4"),
(4, b"\x66\x74\x79\x70\x69\x73\x6F\x6D", "mp4"), (4, b"\x66\x74\x79\x70\x69\x73\x6f\x6d", "mp4"),
(4, b"\x66\x74\x79\x70\x6D\x70\x34\x32", "m4v"), (4, b"\x66\x74\x79\x70\x6d\x70\x34\x32", "m4v"),
(4, b"\x66\x74\x79\x70\x71\x74\x20\x20", "mov"), (4, b"\x66\x74\x79\x70\x71\x74\x20\x20", "mov"),
(0, b"\x1A\x45\xDF\xA3", "mkv"), (0, b"\x1a\x45\xdf\xa3", "mkv"),
(0, b"\x00\x00\x01\xB3", "mpg"), (0, b"\x00\x00\x01\xb3", "mpg"),
(0, b"\x00\x00\x01\xBA", "mpg"), (0, b"\x00\x00\x01\xba", "mpg"),
(0, b"\x46\x4c\x56\x01", "flv"), (0, b"\x46\x4c\x56\x01", "flv"),
(8, b"\x41\x56\x49\x20", "avi"), (8, b"\x41\x56\x49\x20", "avi"),
) )
FILE_SIGNATURES_LENGTH = max(offset + len(signature) for offset, signature, _ in FILE_SIGNATURES) FILE_SIGNATURES_LENGTH = max(
offset + len(signature) for offset, signature, _ in FILE_SIGNATURES
)
MAX_WORKERS: int = 4 MAX_WORKERS: int = 4

View File

@@ -22,7 +22,10 @@ def retry(function):
def logging(log, text, style=INFO): def logging(log, text, style=INFO):
string = Text(text, style=style) string = Text(text, style=style)
if log: if log:
log.write(string, scroll_end=True, ) log.write(
string,
scroll_end=True,
)
else: else:
print(string) print(string)

View File

@@ -18,7 +18,7 @@ class TranslationManager:
def __init__(self, domain="xhs", localedir=None): def __init__(self, domain="xhs", localedir=None):
self.domain = domain self.domain = domain
if not localedir: if not localedir:
localedir = ROOT.joinpath('locale') localedir = ROOT.joinpath("locale")
self.localedir = Path(localedir) self.localedir = Path(localedir)
self.current_translator = self.setup_translation( self.current_translator = self.setup_translation(
self.get_language_code(), self.get_language_code(),
@@ -41,7 +41,8 @@ class TranslationManager:
) )
except FileNotFoundError as e: except FileNotFoundError as e:
print( print(
f"Warning: Translation files for '{self.domain}' not found. Error: {e}") f"Warning: Translation files for '{self.domain}' not found. Error: {e}"
)
return translation(self.domain, fallback=True) return translation(self.domain, fallback=True)
def switch_language(self, language: str = "en_US"): def switch_language(self, language: str = "en_US"):