mirror of
https://github.com/JoeanAmier/XHS-Downloader.git
synced 2026-03-22 06:57:16 +08:00
style: 代码格式化和字符串处理优化
- 优化代码缩进和换行,提高可读性 - 统一使用单引号或双引号,保持一致性 - 移除冗余的空格和括号,精简代码
This commit is contained in:
@@ -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__":
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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
14
main.py
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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",
|
||||||
|
"choice",
|
||||||
|
fill(
|
||||||
|
_(
|
||||||
|
"从指定的浏览器读取小红书网页版 Cookie,支持:{0}; 输入浏览器名称或序号"
|
||||||
|
).format(
|
||||||
|
", ".join(
|
||||||
|
f"{i}: {j}"
|
||||||
|
for i, j in enumerate(
|
||||||
BrowserCookie.SUPPORT_BROWSER.keys(),
|
BrowserCookie.SUPPORT_BROWSER.keys(),
|
||||||
start=1,
|
start=1,
|
||||||
))
|
)
|
||||||
), width=55)),
|
)
|
||||||
|
),
|
||||||
|
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",
|
||||||
|
)
|
||||||
|
@option(
|
||||||
|
"--index",
|
||||||
|
"-i",
|
||||||
|
)
|
||||||
|
@option(
|
||||||
|
"--work_path",
|
||||||
"-wp",
|
"-wp",
|
||||||
type=Path(file_okay=False),
|
type=Path(file_okay=False),
|
||||||
)
|
)
|
||||||
@option("--folder_name", "-fn", )
|
@option(
|
||||||
@option("--name_format", "-nf", )
|
"--folder_name",
|
||||||
@option("--user_agent", "-ua", )
|
"-fn",
|
||||||
@option("--cookie", "-ck", )
|
)
|
||||||
@option("--proxy", "-p", )
|
@option(
|
||||||
@option("--timeout", "-t", type=int, )
|
"--name_format",
|
||||||
@option("--chunk", "-c", type=int, )
|
"-nf",
|
||||||
@option("--max_retry", "-mr", type=int, )
|
)
|
||||||
@option("--record_data", "-rd", type=bool, )
|
@option(
|
||||||
@option("--image_format", "-if", type=Choice(["png", "PNG", "webp", "WEBP"]), )
|
"--user_agent",
|
||||||
@option("--live_download", "-ld", type=bool, )
|
"-ua",
|
||||||
@option("--download_record", "-dr", type=bool, )
|
)
|
||||||
@option("--folder_mode", "-fm", type=bool, )
|
@option(
|
||||||
@option("--language", "-l",
|
"--cookie",
|
||||||
type=Choice(["zh_CN", "en_US"]), )
|
"-ck",
|
||||||
@option("--settings", "-s", type=Path(dir_okay=False), )
|
)
|
||||||
@option("--browser_cookie", "-bc", type=Choice(
|
@option(
|
||||||
list(BrowserCookie.SUPPORT_BROWSER.keys()
|
"--proxy",
|
||||||
) + [str(i) for i in range(1, len(BrowserCookie.SUPPORT_BROWSER) + 1)]), callback=CLI.read_cookie, )
|
"-p",
|
||||||
@option("--update_settings", "-us", type=bool,
|
)
|
||||||
is_flag=True, )
|
@option(
|
||||||
@option("-h",
|
"--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",
|
"--help",
|
||||||
is_flag=True, )
|
is_flag=True,
|
||||||
@option("--version", "-v",
|
)
|
||||||
|
@option(
|
||||||
|
"--version",
|
||||||
|
"-v",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
is_eager=True,
|
is_eager=True,
|
||||||
expose_value=False,
|
expose_value=False,
|
||||||
callback=CLI.version, )
|
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", ""])
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from .app import XHSDownloader
|
from .app import XHSDownloader
|
||||||
|
|
||||||
__all__ = ['XHSDownloader']
|
__all__ = ["XHSDownloader"]
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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(_("删除下载记录成功"))
|
||||||
|
|||||||
@@ -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,7 +210,8 @@ 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,
|
"work_path": self.query_one("#work_path").value,
|
||||||
"folder_name": self.query_one("#folder_name").value,
|
"folder_name": self.query_one("#folder_name").value,
|
||||||
"name_format": self.query_one("#name_format").value,
|
"name_format": self.query_one("#name_format").value,
|
||||||
@@ -116,7 +229,8 @@ class Setting(Screen):
|
|||||||
"video_download": self.query_one("#video_download").value,
|
"video_download": self.query_one("#video_download").value,
|
||||||
"live_download": self.query_one("#live_download").value,
|
"live_download": self.query_one("#live_download").value,
|
||||||
"download_record": self.query_one("#download_record").value,
|
"download_record": self.query_one("#download_record").value,
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@on(Button.Pressed, "#abandon")
|
@on(Button.Pressed, "#abandon")
|
||||||
def reset(self):
|
def reset(self):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
beautify_string(
|
||||||
self.manager.filter_name(data["作品标题"]),
|
self.manager.filter_name(data["作品标题"]),
|
||||||
64,
|
64,
|
||||||
) or data["作品ID"]
|
)
|
||||||
|
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 (
|
||||||
|
BrowserCookie.get(
|
||||||
value,
|
value,
|
||||||
domains=["xiaohongshu.com", ],
|
domains=[
|
||||||
) if value else ""
|
"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)
|
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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']}"
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 []
|
||||||
|
)
|
||||||
|
|||||||
@@ -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"
|
_(
|
||||||
|
"读取指定浏览器的 Cookie 并写入配置文件\n"
|
||||||
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie!\n"
|
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie!\n"
|
||||||
"{options}\n请输入浏览器名称或序号:").format(options=options), ):
|
"{options}\n请输入浏览器名称或序号:"
|
||||||
return cls.get(browser, domains, console, )
|
).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 ""
|
||||||
|
|||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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("; ")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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"):
|
||||||
|
|||||||
Reference in New Issue
Block a user