Compare commits
275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbbf24b5ba | ||
|
|
855113c94d | ||
|
|
8039c6a5db | ||
|
|
bbc5d61011 | ||
|
|
1e26d89dd8 | ||
|
|
80eee2bb16 | ||
|
|
8363d8a78d | ||
|
|
e53c15ddc1 | ||
|
|
9d2547885d | ||
|
|
6a182eb310 | ||
|
|
8382a9abc9 | ||
|
|
d7ab7595e6 | ||
|
|
9d1861b59e | ||
|
|
d67a1d233d | ||
|
|
a7e3ed24e7 | ||
|
|
1045d0aaf9 | ||
|
|
e747d94396 | ||
|
|
96be49116c | ||
|
|
41c83af184 | ||
|
|
1e5ffc1907 | ||
|
|
f72b163eb8 | ||
|
|
bd7078211b | ||
|
|
7f726ff726 | ||
|
|
0338e08e46 | ||
|
|
b9942ae332 | ||
|
|
afdb9ed511 | ||
|
|
f2c52cbe89 | ||
|
|
af63050292 | ||
|
|
8023dbd81f | ||
|
|
67479affb2 | ||
|
|
08c5443289 | ||
|
|
3fe81aa124 | ||
|
|
bcd6b97a63 | ||
|
|
ad2320a689 | ||
|
|
1ceb97e9c2 | ||
|
|
d5e0fa7b89 | ||
|
|
ef7c793c16 | ||
|
|
1e1a1a0257 | ||
|
|
e22944cc1c | ||
|
|
4895ddab25 | ||
|
|
5b54e78582 | ||
|
|
120e5d65c3 | ||
|
|
9ad005c381 | ||
|
|
f39f5a8552 | ||
|
|
99397db7cc | ||
|
|
71741fa962 | ||
|
|
7c523af567 | ||
|
|
b3f0c7afb6 | ||
|
|
7102565c64 | ||
|
|
348e8bd40d | ||
|
|
9e6b819eb5 | ||
|
|
20cdd13eee | ||
|
|
0698e5ba90 | ||
|
|
1b48e70d44 | ||
|
|
079fe6c1f7 | ||
|
|
0a9f2f3059 | ||
|
|
e5d628ba93 | ||
|
|
d5081f0ff9 | ||
|
|
0c7f5eb32c | ||
|
|
e0fbd7916d | ||
|
|
b63f546b3a | ||
|
|
5e6f4d9576 | ||
|
|
8dd8761f88 | ||
|
|
c5b1f01b02 | ||
|
|
1e163acc06 | ||
|
|
87044f150b | ||
|
|
4d77a12cd9 | ||
|
|
f56046c810 | ||
|
|
da0dffb890 | ||
|
|
bbd2fe22df | ||
|
|
1eea31d5bc | ||
|
|
a916180284 | ||
|
|
f3b674c637 | ||
|
|
1034efc551 | ||
|
|
600f2fb60c | ||
|
|
c1ad2e20a9 | ||
|
|
af823116b0 | ||
|
|
92dd28c815 | ||
|
|
a8669caeb9 | ||
|
|
8533ad6538 | ||
|
|
07bb06398c | ||
|
|
69490e526a | ||
|
|
39438ccb32 | ||
|
|
d11929e92d | ||
|
|
936b4f9075 | ||
|
|
d972420ab3 | ||
|
|
8420685019 | ||
|
|
c88ac3de3d | ||
|
|
da86da6bb4 | ||
|
|
99e4186ca2 | ||
|
|
7f07dbff3e | ||
|
|
54e197e0fd | ||
|
|
3b4f23c670 | ||
|
|
f332b3fb2d | ||
|
|
c43f3ad23e | ||
|
|
e1f4fdc716 | ||
|
|
c2fa98bb51 | ||
|
|
ea63059dbf | ||
|
|
86b5de6687 | ||
|
|
fca19144df | ||
|
|
14315e4657 | ||
|
|
71278527e1 | ||
|
|
7059603d46 | ||
|
|
1570ba320c | ||
|
|
94198f5a51 | ||
|
|
697ff3f7a3 | ||
|
|
cd036582c0 | ||
|
|
459e9b54f5 | ||
|
|
78060c55a5 | ||
|
|
6392bf8f3a | ||
|
|
6e8c46a094 | ||
|
|
2fc168f8fd | ||
|
|
69362e1f80 | ||
|
|
86439d5e8f | ||
|
|
ad740a1466 | ||
|
|
9cc8ff22e3 | ||
|
|
c20778c2e8 | ||
|
|
8f87bb4e5a | ||
|
|
8f344c8438 | ||
|
|
8a5d9ae836 | ||
|
|
bd7af3d0d1 | ||
|
|
b3974fe8f3 | ||
|
|
ef68346f28 | ||
|
|
1bd7cfcf88 | ||
|
|
5a334e9a34 | ||
|
|
ca7937e94e | ||
|
|
d05713878f | ||
|
|
e4554d2c57 | ||
|
|
7eb270b53e | ||
|
|
2d8d6781e6 | ||
|
|
63c8a9e325 | ||
|
|
1d2731b93a | ||
|
|
102e7adbd4 | ||
|
|
13707c425a | ||
|
|
8f820330dc | ||
|
|
fc58bc84f3 | ||
|
|
14d8ee3abe | ||
|
|
10a09b4c0b | ||
|
|
f5e9bc306d | ||
|
|
80a3997ee7 | ||
|
|
cdd574a00b | ||
|
|
ad73544e7e | ||
|
|
60d32d1327 | ||
|
|
b360eae802 | ||
|
|
0cd01018a4 | ||
|
|
4a8b0e57d4 | ||
|
|
561984356f | ||
|
|
3e85760b05 | ||
|
|
81b88711d8 | ||
|
|
2c9ca72f4d | ||
|
|
5463854906 | ||
|
|
de28b9de69 | ||
|
|
7e0b0aca30 | ||
|
|
856b4ece98 | ||
|
|
bc436682fb | ||
|
|
8979ba8e25 | ||
|
|
58b2067c68 | ||
|
|
04557b074f | ||
|
|
80c3e71649 | ||
|
|
1698e1c792 | ||
|
|
2042afcd1e | ||
|
|
99490d32f4 | ||
|
|
ab405f605c | ||
|
|
10164ba7b6 | ||
|
|
8bfec97d56 | ||
|
|
76c4699e05 | ||
|
|
5b85379d49 | ||
|
|
2359cf93a4 | ||
|
|
14bf951ca6 | ||
|
|
911293b99d | ||
|
|
0a52dc0ed2 | ||
|
|
44d5c61856 | ||
|
|
bc81d9700c | ||
|
|
15e412e6ae | ||
|
|
24c7a1eb3e | ||
|
|
5a0d3b3f4d | ||
|
|
06161391f1 | ||
|
|
76e40b6ddf | ||
|
|
77b1d05ad8 | ||
|
|
a4d1a40d11 | ||
|
|
0915574bc6 | ||
|
|
e2eee03feb | ||
|
|
ce01a7b2d7 | ||
|
|
169128de8d | ||
|
|
7cedd8d4b8 | ||
|
|
24dc8a1a53 | ||
|
|
4d531e85d9 | ||
|
|
e6ac8647a2 | ||
|
|
d1bca5a896 | ||
|
|
678afea954 | ||
|
|
3e8b69f8e1 | ||
|
|
dd5c09389a | ||
|
|
01111cc401 | ||
|
|
3e67dfb634 | ||
|
|
5e33081e76 | ||
|
|
2313cf1682 | ||
|
|
3d28a6f262 | ||
|
|
b87132e364 | ||
|
|
e7c4d6daee | ||
|
|
01711be950 | ||
|
|
54b7cf86a5 | ||
|
|
bd0780c344 | ||
|
|
e26d4875e3 | ||
|
|
ea349048c3 | ||
|
|
3b2f1bda9e | ||
|
|
f607be0aaf | ||
|
|
a997fe1a1d | ||
|
|
18f92134d8 | ||
|
|
2c8efd0c27 | ||
|
|
f6c232d14c | ||
|
|
c4a9307f24 | ||
|
|
8f4e8e20b8 | ||
|
|
dab9f0e704 | ||
|
|
5c913b6482 | ||
|
|
6f0cda3668 | ||
|
|
eba43f2172 | ||
|
|
0d22a8f4c0 | ||
|
|
039f9f9785 | ||
|
|
6ccac1f9b6 | ||
|
|
d364e32473 | ||
|
|
952cf3496a | ||
|
|
48cfa60862 | ||
|
|
8ed4e81a24 | ||
|
|
00355dafbe | ||
|
|
3584c17038 | ||
|
|
32f52a6457 | ||
|
|
fedd9fba70 | ||
|
|
e0ba7af1f8 | ||
|
|
84a0889c8f | ||
|
|
af8e3cabb1 | ||
|
|
5f1e4a6657 | ||
|
|
9fd803ac09 | ||
|
|
a1d296191b | ||
|
|
413168f122 | ||
|
|
395d81c522 | ||
|
|
981477fd95 | ||
|
|
ceb3757085 | ||
|
|
cc72e54302 | ||
|
|
88cc09c371 | ||
|
|
b9dd05652d | ||
|
|
e767f04151 | ||
|
|
9572c86c1a | ||
|
|
120689d061 | ||
|
|
72421f6d2b | ||
|
|
dd2cd73604 | ||
|
|
c5f3f303b2 | ||
|
|
584fbea79d | ||
|
|
f67a3dc800 | ||
|
|
7d71e849c7 | ||
|
|
bac405ad9a | ||
|
|
7c7a3d4411 | ||
|
|
3197feec57 | ||
|
|
d99fef17ad | ||
|
|
3387225d13 | ||
|
|
ae3da1e61e | ||
|
|
b43b2ef195 | ||
|
|
db196018f1 | ||
|
|
2352d55a7f | ||
|
|
a865468083 | ||
|
|
08f5bc1fac | ||
|
|
954d8d50b9 | ||
|
|
01d93707e2 | ||
|
|
67cb02bee7 | ||
|
|
38c93a551b | ||
|
|
ca0f71b493 | ||
|
|
5b5db2c3ed | ||
|
|
65e6ec375b | ||
|
|
1bf780d3b5 | ||
|
|
3d3ba8f321 | ||
|
|
732c5c797b | ||
|
|
8cf576f16b | ||
|
|
5966c970ba | ||
|
|
f3ba085910 | ||
|
|
b68020152c | ||
|
|
e7f9a93ca1 |
41
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: 报告项目问题
|
||||
title: '[功能异常] '
|
||||
labels: ''
|
||||
assignees: JoeanAmier
|
||||
|
||||
---
|
||||
|
||||
**问题描述**
|
||||
|
||||
清晰简洁地描述该错误是什么。
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**重现步骤**
|
||||
|
||||
重现该问题的步骤:
|
||||
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. ...
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
**预期结果**
|
||||
|
||||
清晰简洁地描述您预期会发生的情况。
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**补充信息**
|
||||
|
||||
在此添加有关该问题的任何其他上下文信息,例如:操作系统、运行方式、配置文件、错误截图、运行日志等。
|
||||
|
||||
请注意:提供配置文件时,请删除 Cookie 内容,避免敏感数据泄露!
|
||||
|
||||
Add any other contextual information about the issue here, such as operating system, runtime mode, configuration files,
|
||||
error screenshots, runtime logs, etc.
|
||||
|
||||
Please note: When providing configuration files, please delete cookie content to avoid sensitive data leakage!
|
||||
32
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: 功能优化建议
|
||||
title: '[优化建议] '
|
||||
labels: ''
|
||||
assignees: JoeanAmier
|
||||
|
||||
---
|
||||
|
||||
**功能请求**
|
||||
|
||||
清晰简洁地描述问题是什么。例如:当 [...] 时,我总是感到沮丧。
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**描述您希望的解决方案**
|
||||
|
||||
清晰简洁地描述您希望发生的情况。
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**描述您考虑过的替代方案**
|
||||
|
||||
清晰简洁地描述您考虑过的任何替代解决方案或功能。
|
||||
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**补充信息**
|
||||
|
||||
在此添加有关功能请求的任何其他上下文或截图。
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
40
.github/workflows/Close_Stale_Issues_and_PRs.yaml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: "自动管理过时的问题和PR"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 */4 * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: |
|
||||
⚠️ 此 Issue 已超过一定时间未活动,如果没有进一步更新,将在 14 天后关闭。
|
||||
⚠️ This issue has been inactive for a certain period of time. If there are no further updates, it will be closed in 14 days.
|
||||
close-issue-message: |
|
||||
🔒 由于长时间未响应,此 Issue 已被自动关闭。如有需要,请重新打开或提交新 issue。
|
||||
🔒 Due to prolonged inactivity, this issue has been automatically closed. If needed, please reopen it or submit a new issue.
|
||||
stale-pr-message: |
|
||||
⚠️ 此 PR 已超过一定时间未更新,请更新,否则将在 14 天后关闭。
|
||||
⚠️ This PR has not been updated for a certain period of time. Please update it, otherwise it will be closed in 14 days.
|
||||
close-pr-message: |
|
||||
🔒 此 PR 已因无更新而自动关闭。如仍需合并,请重新打开或提交新 PR。
|
||||
🔒 This PR has been automatically closed due to inactivity. If you still wish to merge it, please reopen it or submit a new PR.
|
||||
|
||||
days-before-stale: 28
|
||||
days-before-close: 14
|
||||
|
||||
ascending: true
|
||||
|
||||
stale-issue-label: "未跟进问题(Stale)"
|
||||
close-issue-label: "自动关闭(Close)"
|
||||
stale-pr-label: "未跟进问题(Stale)"
|
||||
close-pr-label: "自动关闭(Close)"
|
||||
exempt-issue-labels: "功能异常(bug),文档补充(docs),功能优化(enhancement),适合新手(good first issue),"
|
||||
exempt-pr-labels: "功能异常(bug),文档补充(docs),功能优化(enhancement),适合新手(good first issue),"
|
||||
21
.github/workflows/Delete_untagged_images.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: 删除 GHCR Untagged 镜像
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 15 * *"
|
||||
release:
|
||||
types: [ published ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
delete-untagged:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete all containers from package without tags
|
||||
uses: Chizkiyahu/delete-untagged-ghcr-action@v6
|
||||
with:
|
||||
token: ${{ secrets.PAT_TOKEN }}
|
||||
repository_owner: ${{ github.repository_owner }}
|
||||
repository: ${{ github.repository }}
|
||||
package_name: "xhs-downloader"
|
||||
untagged_only: true
|
||||
owner_type: user
|
||||
46
.github/workflows/Manually_build_executable_programs.yml
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
name: 构建可执行文件
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 构建于 ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest, windows-11-arm, macos-15-intel, macos-latest ]
|
||||
|
||||
steps:
|
||||
- name: 签出存储库
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: 安装依赖项
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: 构建 Win 可执行文件
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
echo "DATE=$(Get-Date -Format 'yyyyMMdd')" >> $env:GITHUB_ENV
|
||||
pyinstaller --icon=./static/XHS-Downloader.ico --add-data "static:static" --add-data "locale:locale" --copy-metadata fastmcp --runtime-hook ./source/expansion/pyi_rth_beartype.py main.py
|
||||
shell: pwsh
|
||||
|
||||
- name: 构建 Mac 可执行文件
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
echo "DATE=$(date +'%Y%m%d')" >> $GITHUB_ENV
|
||||
pyinstaller --icon=./static/XHS-Downloader.icns --add-data "static:static" --add-data "locale:locale" --copy-metadata fastmcp --runtime-hook ./source/expansion/pyi_rth_beartype.py main.py
|
||||
|
||||
- name: 上传文件
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: XHS-Downloader_${{ runner.os }}_${{ runner.arch }}_${{ env.DATE }}
|
||||
path: dist/main/
|
||||
93
.github/workflows/Manually_docker_image.yml
vendored
Normal file
@ -0,0 +1,93 @@
|
||||
name: 构建并发布 Docker 镜像
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
is_beta:
|
||||
type: boolean
|
||||
required: true
|
||||
description: "开发版"
|
||||
default: true
|
||||
custom_version:
|
||||
type: string
|
||||
required: false
|
||||
description: "版本号"
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/xhs-downloader
|
||||
GHCR_REPO: ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/xhs-downloader
|
||||
|
||||
jobs:
|
||||
publish-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 拉取源码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 获取最新的发布标签
|
||||
id: get-latest-release
|
||||
run: |
|
||||
if [ -z "${{ github.event.inputs.custom_version }}" ]; then
|
||||
LATEST_TAG=$(curl -s \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
https://api.github.com/repos/${{ github.repository }}/releases/latest \
|
||||
| jq -r '.tag_name')
|
||||
else
|
||||
LATEST_TAG=${{ github.event.inputs.custom_version }}
|
||||
fi
|
||||
if [ -z "$LATEST_TAG" ]; then
|
||||
exit 1
|
||||
fi
|
||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
|
||||
- name: 设置 QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 生成标签
|
||||
id: generate-tags
|
||||
run: |
|
||||
if [ "${{ inputs.is_beta }}" == "true" ]; then
|
||||
LATEST_TAG="${LATEST_TAG%.*}.$(( ${LATEST_TAG##*.} + 1 ))"
|
||||
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||
TAGS="${{ env.DOCKER_REPO }}:${LATEST_TAG}-dev,${{ env.GHCR_REPO }}:${LATEST_TAG}-dev"
|
||||
else
|
||||
TAGS="${{ env.DOCKER_REPO }}:${LATEST_TAG},${{ env.DOCKER_REPO }}:latest,${{ env.GHCR_REPO }}:${LATEST_TAG},${{ env.GHCR_REPO }}:latest"
|
||||
fi
|
||||
echo "TAGS=$TAGS" >> $GITHUB_ENV
|
||||
|
||||
- name: 登录到 Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 登录到 GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 构建和推送 Docker 镜像到 Docker Hub 和 GHCR
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ env.TAGS }}
|
||||
provenance: false
|
||||
sbom: false
|
||||
58
.github/workflows/Release_build_executable_program.yml
vendored
Normal file
@ -0,0 +1,58 @@
|
||||
name: 自动构建并发布可执行文件
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
discussions: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 构建于 ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ windows-latest, windows-11-arm, macos-15-intel, macos-latest ]
|
||||
|
||||
steps:
|
||||
- name: 签出存储库
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: 安装依赖项
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install pyinstaller
|
||||
|
||||
- name: 构建 Win 可执行文件
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
pyinstaller --icon=./static/XHS-Downloader.ico --add-data "static:static" --add-data "locale:locale" --copy-metadata fastmcp --runtime-hook ./source/expansion/pyi_rth_beartype.py main.py
|
||||
shell: pwsh
|
||||
|
||||
- name: 构建 Mac 可执行文件
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
pyinstaller --icon=./static/XHS-Downloader.icns --add-data "static:static" --add-data "locale:locale" --copy-metadata fastmcp --runtime-hook ./source/expansion/pyi_rth_beartype.py main.py
|
||||
|
||||
- name: 创建压缩包
|
||||
run: |
|
||||
7z a "XHS-Downloader_V${{ github.event.release.tag_name }}_${{ runner.os }}_${{ runner.arch }}.zip" ./dist/main/*
|
||||
shell: bash
|
||||
|
||||
- name: 上传文件到 release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
./XHS-Downloader_V*.zip
|
||||
name: XHS-Downloader V${{ github.event.release.tag_name }}
|
||||
body_path: ./static/Release_Notes.md
|
||||
draft: ${{ github.event.release.draft }}
|
||||
prerelease: ${{ github.event.release.prerelease }}
|
||||
59
.github/workflows/Release_docker_image.yml
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
name: 自动构建并发布 Docker 镜像
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/xhs-downloader
|
||||
GHCR_REPO: ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/xhs-downloader
|
||||
|
||||
jobs:
|
||||
publish-docker:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 拉取源码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: 设置 QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: 登录到 Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: 登录到 GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: 构建和推送 Docker 镜像到 Docker Hub 和 GHCR
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.DOCKER_REPO }}:${{ github.event.release.tag_name }}
|
||||
${{ env.DOCKER_REPO }}:latest
|
||||
${{ env.GHCR_REPO }}:${{ github.event.release.tag_name }}
|
||||
${{ env.GHCR_REPO }}:latest
|
||||
provenance: false
|
||||
sbom: false
|
||||
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
/.venv/
|
||||
/.ruff_cache/
|
||||
/.idea/
|
||||
/.run/
|
||||
/Volume/
|
||||
!/.github/
|
||||
1
.python-version
Normal file
@ -0,0 +1 @@
|
||||
3.12
|
||||
47
Dockerfile
Normal file
@ -0,0 +1,47 @@
|
||||
# ---- 阶段 1: 构建器 (Builder) ----
|
||||
# 使用一个功能完整的镜像,它包含编译工具或可以轻松安装它们
|
||||
FROM python:3.12-bullseye as builder
|
||||
|
||||
# 安装编译 uvloop 和 httptools 所需的系统依赖 (C编译器等)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制需求文件
|
||||
COPY requirements.txt .
|
||||
|
||||
# 在这个具备编译环境的阶段安装所有 Python 依赖
|
||||
# 安装到一个独立的目录 /install 中,以便后续复制
|
||||
RUN pip install --no-cache-dir --prefix="/install" -r requirements.txt
|
||||
|
||||
# ---- 阶段 2: 最终镜像 (Final Image) ----
|
||||
# 使用轻量级 slim 镜像作为最终的运行环境
|
||||
FROM python:3.12-slim
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 添加元数据标签
|
||||
LABEL name="XHS-Downloader" authors="JoeanAmier" repository="https://github.com/JoeanAmier/XHS-Downloader"
|
||||
|
||||
# 从构建器阶段,将已经安装好的依赖包复制到最终镜像的系统路径中
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# 复制项目代码和相关文件
|
||||
COPY locale /app/locale
|
||||
COPY source /app/source
|
||||
COPY static/XHS-Downloader.tcss /app/static/XHS-Downloader.tcss
|
||||
COPY LICENSE /app/LICENSE
|
||||
COPY main.py /app/main.py
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5556
|
||||
|
||||
# 创建挂载点
|
||||
VOLUME /app/Volume
|
||||
|
||||
# 设置容器启动命令
|
||||
CMD ["python", "main.py"]
|
||||
738
README.md
@ -1,60 +1,151 @@
|
||||
<div align="center">
|
||||
<img src="static/XHS_Downloader.png" alt="TikTokDownloader" height="256" width="256"><br>
|
||||
<h1>小红书作品采集工具</h1>
|
||||
</div>
|
||||
<h1>📝 功能清单</h1>
|
||||
<ul>
|
||||
<li>采集小红书图文/视频作品信息</li>
|
||||
<li>获取小红书图文/视频作品下载地址</li>
|
||||
<li>下载小红书图文/视频作品文件</li>
|
||||
<li>自动跳过已存在的作品文件</li>
|
||||
<li>作品文件完整性处理机制</li>
|
||||
<li>批量下载小红书作品文件</li>
|
||||
</ul>
|
||||
<h1>📸 程序截图</h1>
|
||||
<img src="static/XHS-Downloader.png" alt="XHS-Downloader" height="256" width="256"><br>
|
||||
<h1>XHS-Downloader</h1>
|
||||
<p>简体中文 | <a href="README_EN.md">English</a></p>
|
||||
<a href="https://trendshift.io/repositories/5435" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5435" alt="JoeanAmier%2FXHS-Downloader | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<br>
|
||||
<img src="static/程序截图.png" alt="">
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=flat-square">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=flat-square&color=55efc4">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=flat-square&color=fda7df">
|
||||
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JoeanAmier/XHS-Downloader?style=flat-square&color=a29bfe">
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=flat-square&color=48dbfb">
|
||||
<br>
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Python-3.12-b8e994?style=flat-square&logo=python&labelColor=3dc1d3">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/UserScript-ffec3d?style=flat-square&logo=tampermonkey&logoColor=%2300485B">
|
||||
<img src="https://img.shields.io/badge/Sourcery-enabled-884898?style=flat-square&color=1890ff" alt="">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Docker-badc58?style=flat-square&logo=docker">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=flat-square&color=ffdd59">
|
||||
</div>
|
||||
<br>
|
||||
<p>🔥 <b>小红书链接提取/作品采集工具</b>:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品链接、用户链接;采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件!</p>
|
||||
<p>🔥 “小红书”、“XiaoHongShu”、“RedNote” 含义相同,本项目统称为 “小红书”</p>
|
||||
<h1>📑 项目功能</h1>
|
||||
<details>
|
||||
<summary>项目程序与用户脚本功能清单(点击展开)</summary>
|
||||
<ul><b>程序功能</b>
|
||||
<li>✅ 采集小红书作品信息</li>
|
||||
<li>✅ 提取小红书作品下载地址</li>
|
||||
<li>✅ 下载小红书无水印作品文件</li>
|
||||
<li>✅ 下载小红书 livePhoto 文件(含水印)</li>
|
||||
<li>✅ 自动跳过已下载的作品文件</li>
|
||||
<li>✅ 作品文件完整性处理机制</li>
|
||||
<li>✅ 自定义图文作品文件下载格式</li>
|
||||
<li>✅ 持久化储存作品信息至文件</li>
|
||||
<li>✅ 作品文件储存至单独文件夹</li>
|
||||
<li>✅ 后台监听剪贴板下载作品</li>
|
||||
<li>✅ 记录已下载作品 ID</li>
|
||||
<li>✅ 支持命令行下载作品文件</li>
|
||||
<li>✅ 从浏览器读取 Cookie</li>
|
||||
<li>✅ 自定义文件名称格式</li>
|
||||
<li>✅ 支持 API 调用功能</li>
|
||||
<li>✅ 支持 MCP 调用功能</li>
|
||||
<li>✅ 支持文件断点续传下载</li>
|
||||
<li>✅ 智能识别作品文件类型</li>
|
||||
<li>✅ 支持设置作者备注</li>
|
||||
<li>✅ 自动更新作者昵称</li>
|
||||
</ul>
|
||||
<ul><a href="#user-scripts"><b>脚本功能</b></a>
|
||||
<li>✅ 下载小红书无水印作品文件</li>
|
||||
<li>✅ 提取推荐页面作品链接</li>
|
||||
<li>✅ 提取账号发布作品链接</li>
|
||||
<li>✅ 提取账号收藏作品链接</li>
|
||||
<li>✅ 提取账号点赞作品链接</li>
|
||||
<li>✅ 提取账号专辑作品链接</li>
|
||||
<li>✅ 提取搜索结果作品链接</li>
|
||||
<li>✅ 提取搜索结果用户链接</li>
|
||||
</ul>
|
||||
</details>
|
||||
<h1>📸 程序截图</h1>
|
||||
<p><a href="https://www.bilibili.com/video/BV1Fcb3zWEjt/">前往 bilibili 观看演示</a>;<a href="https://youtu.be/VIjDytHaopg">前往 YouTube 观看演示</a></p>
|
||||
<img src="static/screenshot/程序运行截图CN1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/程序运行截图CN2.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/程序运行截图CN3.png" alt="">
|
||||
<h1>🔗 支持链接</h1>
|
||||
<ul>
|
||||
<li><code>https://www.xiaohongshu.com/explore/作品ID?xsec_token=XXX</code></li>
|
||||
<li><code>https://www.xiaohongshu.com/discovery/item/作品ID?xsec_token=XXX</code></li>
|
||||
<li><code>https://www.xiaohongshu.com/user/profile/作者ID/作品ID?xsec_token=XXX</code></li>
|
||||
<li><code>https://xhslink.com/分享码</code></li>
|
||||
<br/>
|
||||
<p><b>支持单次输入多个作品链接,链接之间使用空格分隔;程序会自动提取有效链接,无需额外处理!</b></p>
|
||||
</ul>
|
||||
<h1>🪟 关于终端</h1>
|
||||
<p>⭐ 推荐使用 <a href="https://learn.microsoft.com/zh-cn/windows/terminal/install">Windows 终端</a> (Windows 11 默认终端)运行程序以便获得最佳显示效果!</p>
|
||||
<h1>🥣 使用方法</h1>
|
||||
<p>如果仅需下载作品文件,选择 <b>直接运行</b> 或者 <b>源码运行</b> 均可,如果需要获取作品信息,则需要进行二次开发进行调用。</p>
|
||||
<h2>🖱 直接运行</h2>
|
||||
<p>前往 Releases 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main.exe</code> 即可使用。</p>
|
||||
<p>如果仅需下载无水印作品文件,建议选择 <b>程序运行</b> 或 <b>Docker 运行</b>;如果有其他需求,建议选择 <b>源码运行</b>!</p>
|
||||
<p><code>2.2</code> 版本开始,项目功能无异常的情况下,无需额外处理 Cookie!</p>
|
||||
<h2>🖱 程序运行</h2>
|
||||
<p>⭐ Mac OS、Windows 10 及以上用户可前往 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 或者 <a href="https://github.com/JoeanAmier/XHS-Downloader/actions">Actions</a> 下载程序压缩包,解压后打开程序文件夹,双击运行 <code>main</code> 即可使用。</p>
|
||||
<p>⭐ 本项目包含自动构建可执行文件的 GitHub Actions,使用者可以随时使用 GitHub Actions 将最新源码构建为可执行文件!</p>
|
||||
<p>⭐ 自动构建可执行文件教程请查阅本文档的 <code>构建可执行文件指南</code> 部分;如果需要更加详细的图文教程,请 <a href="https://mp.weixin.qq.com/s/TorfoZKkf4-x8IBNLImNuw">查阅文章</a>!</p>
|
||||
<p><strong>注意:由于 Mac OS 平台的可执行文件 <code>main</code> 未经过代码签名,首次运行时会受到系统安全限制。请先在终端执行 <code>xattr -cr 项目文件夹路径</code> 命令移除安全标记,执行一次后即可正常运行。</strong></p>
|
||||
<p>若通过此方式使用程序,文件默认下载路径为:<code>.\_internal\Volume\Download</code>;配置文件路径为:<code>.\_internal\Volume\settings.json</code></p>
|
||||
<h3>程序更新</h3>
|
||||
<p><strong>方案一:</strong>下载并解压文件,将旧版本的 <code>_internal\Volume</code> 文件夹复制到新版本的 <code>_internal</code> 文件夹。</p>
|
||||
<p><strong>方案二:</strong>下载并解压文件(不要运行程序),复制全部文件,直接覆盖旧版本文件。</p>
|
||||
<h2>⌨️ 源码运行</h2>
|
||||
<ol>
|
||||
<li>安装版本号不低于 <code>3.10</code> 的 Python 解释器</li>
|
||||
<li>安装 <code>requirements.txt</code> 包含的第三方模块</li>
|
||||
<li>下载本项目最新的源码或 <code>Releases</code> 发布的源码至本地</li>
|
||||
<li>运行 <code>main.py</code> 即可使用</li>
|
||||
<li>安装 <code>3.12</code> 版本的 <a href="https://www.python.org/">Python</a> 解释器</li>
|
||||
<li>下载最新的源码或 <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> 发布的源码至本地</li>
|
||||
<ol><b>使用 pip 安装项目依赖</b>
|
||||
<li>运行 <code>python -m venv venv</code> 命令创建虚拟环境(可选)</li>
|
||||
<li>运行 <code>.\venv\Scripts\activate.ps1</code> 或者 <code>venv\Scripts\activate</code> 命令激活虚拟环境(可选)</li>
|
||||
<li>运行 <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> 命令安装程序所需模块</li>
|
||||
<li>运行 <code>python .\main.py</code> 或者 <code>python main.py</code> 命令启动 XHS-Downloader</li>
|
||||
</ol>
|
||||
<h2>💻 二次开发</h2>
|
||||
<p>如果想要获取小红书图文/视频作品信息,可以根据 <code>main.py</code> 的注释提示进行代码调用。</p>
|
||||
<pre>
|
||||
# 测试链接
|
||||
error_demo = "https://www.xiaohongshu.com/explore/"
|
||||
image_demo = "https://www.xiaohongshu.com/explore/64d1b406000000000103ee8d"
|
||||
video_demo = "https://www.xiaohongshu.com/explore/64c05652000000000c0378e7"
|
||||
# 实例对象
|
||||
path = "./" # 作品下载储存根路径,默认值:当前路径
|
||||
folder = "Download" # 作品下载文件夹名称(自动创建),默认值:Download
|
||||
proxies = None # 网络代理
|
||||
timeout = 5 # 网络请求超时限制,默认值:10
|
||||
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位字节
|
||||
xhs = XHS(
|
||||
path=path,
|
||||
folder=folder,
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
chunk=chunk, ) # 使用自定义参数
|
||||
# xhs = XHS() # 使用默认参数
|
||||
download = True # 是否下载作品文件
|
||||
# 返回作品详细信息,包括下载地址
|
||||
print(xhs.extract(error_demo)) # 获取数据失败时返回空字典
|
||||
print(xhs.extract(image_demo, download=download))
|
||||
print(xhs.extract(video_demo, download=download))
|
||||
</pre>
|
||||
<h1>⛓ 批量下载</h1>
|
||||
<p>在程序当前文件夹创建一个 <code>xhs.txt</code> 文本文件,然后将待处理的作品链接输入文件,每行输入一个作品链接,编辑完成后保存文件,然后运行程序,程序会自动读取 <code>xhs.txt</code> 文件内容,并批量下载每个作品的文件,下载完成后需要手动删除 <code>xhs.txt</code> 文件。</p>
|
||||
<h1>⚙️ 配置文件</h1>
|
||||
<p>根目录下的 <code>settings.json</code> 文件,可以自定义部分运行参数。</p>
|
||||
<ol><b>使用 uv 安装项目依赖(推荐)</b>
|
||||
<li>运行 <code>uv venv</code> 命令创建虚拟环境</li>
|
||||
<li>运行 <code>uv sync</code> 命令同步环境依赖</li>
|
||||
<li>运行 <code>uv run main.py</code> 命令启动 XHS-Downloader</li>
|
||||
</ol>
|
||||
</ol>
|
||||
<h2>⌨️ Docker 运行</h2>
|
||||
<ol>
|
||||
<li>获取镜像</li>
|
||||
<ul>
|
||||
<li>方式一:使用 <code>Dockerfile</code> 文件构建镜像</li>
|
||||
<li>方式二:使用 <code>docker pull joeanamier/xhs-downloader</code> 命令拉取镜像</li>
|
||||
<li>方式三:使用 <code>docker pull ghcr.io/joeanamier/xhs-downloader</code> 命令拉取镜像</li>
|
||||
</ul>
|
||||
<li>创建容器</li>
|
||||
<ul>
|
||||
<li>TUI 模式:<code>docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称></code></li>
|
||||
<li>API 模式:<code>docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称> python main.py api</code></li>
|
||||
<li>MCP 模式:<code>docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称> python main.py mcp</code></li>
|
||||
<br><b>注意:</b>此处的 <code><镜像名称></code> 需与您在第一步中使用的镜像名称保持一致(<code>joeanamier/xhs-downloader</code> 或 <code>ghcr.io/joeanamier/xhs-downloader</code>)
|
||||
</ul>
|
||||
<li>运行容器
|
||||
<ul>
|
||||
<li>启动容器:<code>docker start -i 容器名称/容器 ID</code></li>
|
||||
<li>重启容器:<code>docker restart -i 容器名称/容器 ID</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>Docker 运行项目时不支持 <b>命令行调用模式</b>,无法使用 <b>读取剪贴板</b> 与 <b>监听剪贴板</b> 功能,可以正常粘贴内容,其他功能如有异常请反馈!</p>
|
||||
<h1>🛠 命令行模式</h1>
|
||||
<p>项目支持命令行运行模式,若想要下载图文作品的部分图片,可以使用此模式设置需要下载的图片序号!</p>
|
||||
<p><strong>注意:</strong>未设置 <code>--index</code> 参数时,支持传入多个作品链接,全部链接需要使用引号包围,链接之间使用空格分隔;已设置 <code>--index</code> 参数时,不支持传入多个作品链接,即使传入多个作品链接,程序仅处理首个作品链接!</p>
|
||||
<p><code>bool</code> 类型参数支持使用 <code>true</code>、<code>false</code>、<code>1</code>、<code>0</code>、<code>yes</code>、<code>no</code>、<code>on</code> 或 <code>off</code>(不区分大小写)来设置。</p>
|
||||
<h2>从浏览器读取 Cookie</h2>
|
||||
<p>可以使用命令行实现 <b>从浏览器读取 Cookie 并写入配置文件!</b></p>
|
||||
<p>命令示例:<code>python .\main.py --browser_cookie Chrome --update_settings</code></p>
|
||||
<p>兼容性提醒:此功能依赖的第三方模块已长期未更新,可能无法正常支持最新浏览器版本。若功能出现异常,请尝试手动获取 Cookie!</p>
|
||||
<hr>
|
||||
<img src="static/screenshot/命令行模式截图CN1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/命令行模式截图CN2.png" alt="">
|
||||
<h1>🖥 服务器模式</h1>
|
||||
<p>服务器模式包含 API 模式和 MCP 模式!</p>
|
||||
<h2>API 模式</h2>
|
||||
<p><b>启动:</b>运行命令:<code>python .\main.py api</code></p>
|
||||
<p><b>关闭:</b>按下 <code>Ctrl</code> + <code>C</code> 关闭服务器</p>
|
||||
<p>访问 <code>http://127.0.0.1:5556/docs</code> 或者 <code>http://127.0.0.1:5556/redoc</code>;你会看到自动生成的交互式 API 文档!</p>
|
||||
<p><b>请求接口:</b><code>/xhs/detail</code></p>
|
||||
<p><b>请求方法:</b><code>POST</code></p>
|
||||
<p><b>请求格式:</b><code>JSON</code></p>
|
||||
<p><b>请求参数:</b></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@ -66,24 +157,256 @@ print(xhs.extract(video_demo, download=download))
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">path</td>
|
||||
<td align="center">url</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">文件储存根路径</td>
|
||||
<td align="center">当前路径</td>
|
||||
<td align="center">小红书作品链接,自动提取,不支持多链接;必需参数</td>
|
||||
<td align="center">无</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder</td>
|
||||
<td align="center">download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否下载作品文件;设置为 <code>true</code> 将会耗费更多时间;可选参数</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">index</td>
|
||||
<td align="center">list[int]</td>
|
||||
<td align="center">下载指定序号的图片文件,仅对图文作品生效;<code>download</code> 参数设置为 <code>false</code> 时不生效;可选参数</td>
|
||||
<td align="center">null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">cookie</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">文件储存文件夹</td>
|
||||
<td align="center">请求数据时使用的 Cookie;可选参数</td>
|
||||
<td align="center">配置文件 cookie 参数</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">proxy</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">请求数据时使用的代理;可选参数</td>
|
||||
<td align="center">配置文件 proxy 参数</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">skip</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否跳过存在下载记录的作品;设置为 <code>true</code> 将不会返回存在下载记录的作品数据;可选参数</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><b>代码示例:</b></p>
|
||||
<pre>
|
||||
async def example_api():
|
||||
"""通过 API 设置参数,适合二次开发"""
|
||||
server = "http://127.0.0.1:5556/xhs/detail"
|
||||
data = {
|
||||
"url": "", # 必需参数
|
||||
"download": True,
|
||||
"index": [
|
||||
3,
|
||||
6,
|
||||
9,
|
||||
],
|
||||
"proxy": "http://127.0.0.1:10808",
|
||||
}
|
||||
response = post(server, json=data, timeout=10)
|
||||
print(response.json())
|
||||
</pre>
|
||||
<h2>MCP 模式</h2>
|
||||
<p><b>启动:</b>运行命令:<code>python .\main.py mcp</code></p>
|
||||
<p><b>关闭:</b>按下 <code>Ctrl</code> + <code>C</code> 关闭服务器</p>
|
||||
<h3>MCP 配置示例</h3>
|
||||
|
||||
[//]: # (<h4>STDIO</h4>)
|
||||
<h4>Streamable HTTP</h4>
|
||||
<p><b>MCP URL:</b><code>http://127.0.0.1:5556/mcp/</code></p>
|
||||
<img src="static/screenshot/MCP配置示例.png" alt="MCP配置示例">
|
||||
<h3>MCP 调用示例</h3>
|
||||
<details>
|
||||
<summary>MCP 功能及调用示例(点击展开)</summary>
|
||||
<h4><strong>获取小红书作品信息</strong></h4>
|
||||
<img src="static/screenshot/MCP获取数据.png" alt="MCP获取数据">
|
||||
<hr>
|
||||
<h4><strong>下载小红书作品文件</strong></h4>
|
||||
<p>下载图文作品时可以指定需要下载的图片序号;默认不返回作品信息,如需返回作品信息,请在对话时明确表述。</p>
|
||||
<img src="static/screenshot/MCP下载文件1.png" alt="MCP下载文件">
|
||||
<hr>
|
||||
<img src="static/screenshot/MCP下载文件2.png" alt="MCP下载文件">
|
||||
</details>
|
||||
<h1>📜 其他说明</h1>
|
||||
<ul>
|
||||
<li>由于作品链接携带日期信息,使用先前日期获取的作品链接可能会被风控,建议下载作品文件时使用最新获取的作品链接</li>
|
||||
<li>Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie</li>
|
||||
<li>如果开启保存作品数据至文件功能,作品数据默认储存至 <code>./Volume/Download/ExploreData.db</code> 文件</li>
|
||||
<li>程序下载记录数据储存至 <code>./Volume/ExploreID.db</code> 文件</li>
|
||||
<li>为了避免请求频率过高对平台服务器造成影响,本项目内置请求延时机制</li>
|
||||
</ul>
|
||||
<h1 id="user-scripts">🕹 用户脚本</h1>
|
||||
<p>如果您的浏览器安装了 <a href="https://www.tampermonkey.net/">Tampermonkey</a> 浏览器扩展程序,可以使用用户脚本体验项目功能!</p>
|
||||
<p>用户脚本链接(右键单击复制链接):<a href="https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/refs/heads/master/static/XHS-Downloader.js">master 分支</a>、<a href="https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/refs/heads/develop/static/XHS-Downloader.js">develop 分支</a></p>
|
||||
<img src="static/screenshot/脚本安装教程.png" alt="">
|
||||
<hr>
|
||||
<details>
|
||||
<summary>查看 Tampermonkey 用户脚本截图(点击展开)</summary>
|
||||
<img src="static/screenshot/用户脚本截图1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图2.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图3.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图4.png" alt="">
|
||||
</details>
|
||||
<p>提示:使用 XHS-Downloader 用户脚本批量提取作品链接,搭配 XHS-Downloader 程序可以实现批量下载无水印作品文件!</p>
|
||||
<h2>🌏 连接服务器</h2>
|
||||
<p>⭐ 本项目支持通过浏览器用户脚本与主程序联动,实现一键推送下载任务。</p>
|
||||
<ul><b>功能说明:</b>
|
||||
<li>在项目程序的配置文件中,需要将 <code>script_server</code> 参数设置为 <code>true</code></li>
|
||||
<li>保持项目程序在后台运行,它将作为服务器,接收用户脚本的指令(TUI、MCP 和 API 模式均支持)</li>
|
||||
<li>当您在浏览器中访问作品页面时,点击用户脚本菜单中的 <code>推送下载任务</code> 选项</li>
|
||||
<li>用户脚本会将下载任务发送给项目程序,由项目程序负责处理和下载文件</li>
|
||||
</ul>
|
||||
<h2>📜 脚本说明</h2>
|
||||
<ul>
|
||||
<li>下载小红书无水印作品文件时,脚本需要花费时间处理文件,请等待片刻,请勿多次点击下载按钮</li>
|
||||
<li>无水印图片文件为 PNG 格式;无水印视频文件较大,可能需要较长的时间处理,页面跳转可能会导致下载失败</li>
|
||||
<li>提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品</li>
|
||||
<li>提取推荐作品链接、搜索作品、用户链接时,脚本可以自动滚动指定次数加载更多内容,默认滚动次数:50 次</li>
|
||||
<li>自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动页面次数,修改后立即生效</li>
|
||||
<li>如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作</li>
|
||||
<li>支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载</li>
|
||||
<li>使用全局代理工具可能会导致脚本下载文件失败,如有异常,请尝试关闭代理工具,必要时向作者反馈</li>
|
||||
<li>XHS-Downloader 用户脚本仅实现可见即可得的数据采集功能,无任何收费功能和破解功能</li>
|
||||
</ul>
|
||||
<p><strong>自动滚动页面功能代码已重构,该功能默认关闭!启用该功能可能会被小红书检测为自动化操作,从而导致账号受到风控或封禁风险!</strong></p>
|
||||
<h1>💻 二次开发</h1>
|
||||
<p>如果有其他需求,可以根据 <code>example.py</code> 的注释提示进行代码调用或修改!</p>
|
||||
<pre>
|
||||
async def example():
|
||||
"""通过代码设置参数,适合二次开发"""
|
||||
# 示例链接
|
||||
demo_link = "https://www.xiaohongshu.com/explore/XXX?xsec_token=XXX"
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
name_format = "作品标题 作品描述"
|
||||
user_agent = "" # User-Agent
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录,可选参数,登录状态对数据采集有影响
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
|
||||
record_data = False # 是否保存作品数据至文件
|
||||
image_format = "WEBP" # 图文作品文件下载格式,支持:AUTO、PNG、WEBP、JPEG、HEIC
|
||||
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
|
||||
image_download = True # 图文、图集作品文件下载开关
|
||||
video_download = True # 视频作品文件下载开关
|
||||
live_download = False # 图文动图文件下载开关
|
||||
download_record = True # 是否记录下载成功的作品 ID
|
||||
language = "zh_CN" # 设置程序提示语言
|
||||
author_archive = True # 是否将每个作者的作品存至单独的文件夹
|
||||
write_mtime = True # 是否将作品文件的 修改时间 修改为作品的发布时间
|
||||
read_cookie = None # 读取浏览器 Cookie,支持设置浏览器名称(字符串)或者浏览器序号(整数),设置为 None 代表不读取
|
||||
# async with XHS() as xhs:
|
||||
# pass # 使用默认参数
|
||||
async with XHS(
|
||||
work_path=work_path,
|
||||
folder_name=folder_name,
|
||||
name_format=name_format,
|
||||
user_agent=user_agent,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
chunk=chunk,
|
||||
max_retry=max_retry,
|
||||
record_data=record_data,
|
||||
image_format=image_format,
|
||||
folder_mode=folder_mode,
|
||||
image_download=image_download,
|
||||
video_download=video_download,
|
||||
live_download=live_download,
|
||||
download_record=download_record,
|
||||
language=language,
|
||||
read_cookie=read_cookie,
|
||||
author_archive=author_archive,
|
||||
write_mtime=write_mtime,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
# 返回作品详细信息,包括下载地址
|
||||
# 获取数据失败时返回空字典
|
||||
print(
|
||||
await xhs.extract(
|
||||
demo_link,
|
||||
download,
|
||||
index=[
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
],
|
||||
)
|
||||
)
|
||||
</pre>
|
||||
<h1>📋 读取剪贴板</h1>
|
||||
<p>项目使用 <code>pyperclip</code> 实现读取剪贴板功能,该模块在不同的系统上会有差异。</p>
|
||||
<p>在 Windows 上,不需要额外的模块。</p>
|
||||
<p>在 Mac 上,该模块使用 pbcopy 和 pbpaste 命令,这些命令应该随操作系统一起提供。</p>
|
||||
<p>在 Linux 上,该模块使用 xclip 或 xsel 命令,这些命令应该随操作系统一起提供。否则,请运行 "sudo apt-get install xclip" 或 "sudo apt-get install xsel"(注意:xsel 似乎并不总是有效)</p>
|
||||
<p>在其他 Linux 系统上,你需要安装 qtpy 或 PyQT5 模块。</p>
|
||||
<h1>⚙️ 配置文件</h1>
|
||||
<p>项目根目录下的 <code>./Volume/settings.json</code> 文件,首次运行自动生成,可以自定义程序运行参数;如果设置了无效的参数值,程序将会使用参数默认值!</p>
|
||||
<p>如果您在程序界面修改配置时无法正常交互,可以直接编辑配置文件;如果您的计算机没有合适的程序编辑 JSON 文件,建议使用 <a href="https://www.toolhelper.cn/JSON/JSONFormat">在线工具</a> 编辑配置文件内容,修改后需要重启软件才能生效。</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">参数</th>
|
||||
<th align="center">类型</th>
|
||||
<th align="center">含义</th>
|
||||
<th align="center">默认值</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">mapping_data</td>
|
||||
<td align="center">str: str</td>
|
||||
<td align="center"><sup><a href="#author_archive">#</a></sup>作者别名映射表,格式:<code>作者ID: 作者别名</code></td>
|
||||
<td align="center">无</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">work_path</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">作品数据 / 文件保存根路径</td>
|
||||
<td align="center">项目根路径/Volume</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder_name</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">作品文件储存文件夹名称</td>
|
||||
<td align="center">Download</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">proxies</td>
|
||||
<td align="center">name_format</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">设置代理</td>
|
||||
<td align="center">作品文件名称格式,字段之间使用空格分隔,支持字段:<code>收藏数量</code>、<code>评论数量</code>、<code>分享数量</code>、<code>点赞数量</code>、<code>作品标签</code>、<code>作品ID</code>、<code>作品标题</code>、<code>作品描述</code>、<code>作品类型</code>、<code>发布时间</code>、<code>最后更新时间</code>、<code>作者昵称</code>、<code>作者ID</code></td>
|
||||
<td align="center"><code>发布时间 作者昵称 作品标题</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">user_agent</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">浏览器 User Agent</td>
|
||||
<td align="center">内置 Chrome User Agent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">cookie</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">小红书网页版 Cookie,<b>无需登录,非必需参数!</b></td>
|
||||
<td align="center">无</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">proxy</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">设置程序代理</td>
|
||||
<td align="center">null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">timeout</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">请求数据超时限制,单位:秒</td>
|
||||
@ -93,29 +416,294 @@ print(xhs.extract(video_demo, download=download))
|
||||
<td align="center">chunk</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">下载文件时,每次从服务器获取的数据块大小,单位:字节</td>
|
||||
<td align="center">262144(256KB)</td>
|
||||
<td align="center">2097152(2 MB)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">max_retry</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">请求数据失败时,重试的最大次数,单位:秒</td>
|
||||
<td align="center">5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">record_data</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否保存作品数据至文件,保存格式:<code>SQLite</code></td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">image_format</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">图文作品文件下载格式,支持:<code>AUTO</code>、<code>PNG</code>、<code>WEBP</code>、<code>JPEG</code>、<code>HEIC</code><br><strong>部分作品没有 <code>HEIC</code> 格式的文件,此时下载的文件可能为 <code>WEBP</code> 格式!</strong><br><strong>设置为 <code>AUTO</code> 时表示动态格式,实际格式取决于服务器响应数据!</strong></td>
|
||||
<td align="center">PNG</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">image_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">图文、图集作品文件下载开关</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">video_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">视频作品文件下载开关</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">live_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">图文动图文件下载开关</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder_mode</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">download_record</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否记录下载成功的作品 ID,如果开启,程序将会自动跳过下载存在记录的作品</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">author_archive</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center"><sup><a href="#author_archive">#</a></sup>是否将每个作者的作品储存至单独的文件夹;文件夹名称为 <code>作者ID_作者昵称</code></td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">write_mtime</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否将作品文件的 <code>修改时间</code> 属性修改为作品的发布时间</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">language</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">设置程序语言,目前支持:<code>zh_CN</code>、<code>en_US</code></td>
|
||||
<td align="center">zh_CN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">script_server</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">是否开启用户脚本服务器,用于接收浏览器用户脚本的下载任务(TUI、MCP 和 API 模式生效)</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>⚠️ 免责声明</h1>
|
||||
<hr>
|
||||
<div id="author_archive">
|
||||
<p>如果 <code>author_archive</code> 参数设置为 <code>true</code>,程序会把每个作者的作品储存至单独的文件夹;当作者的昵称发生变化时,程序会自动更新已下载作品文件名称中的作者昵称部分!</p>
|
||||
<p>除此之外,你还可以通过设置 <code>mapping_data</code> 参数为某个作者设置别名;如果对某个作者设置了别名,程序会使用你设置的作者别名去替代作者昵称!</p>
|
||||
</div>
|
||||
<hr>
|
||||
<p><b>其他说明:<code>user_agent</code>参数获取示例;强烈建议根据实际浏览器信息进行设置!</b></p>
|
||||
<img src="static/screenshot/请求头示例图.png" alt="">
|
||||
<h1>🌐 Cookie</h1>
|
||||
<p><code>2.2</code> 版本开始,项目功能无异常的情况下,无需额外处理 Cookie!</p>
|
||||
<ol>
|
||||
<li>打开浏览器(可选无痕模式启动),访问 <code>https://www.xiaohongshu.com/explore</code></li>
|
||||
<li>登录小红书账号(可跳过)</li>
|
||||
<li>按下 <code>F12</code> 打开开发人员工具</li>
|
||||
<li>选择 <code>网络</code> 选项卡</li>
|
||||
<li>勾选 <code>保留日志</code></li>
|
||||
<li>在 <code>过滤</code> 输入框输入 <code>cookie-name:web_session</code></li>
|
||||
<li>选择 <code>Fetch/XHR</code> 筛选器</li>
|
||||
<li>点击小红书页面任意作品</li>
|
||||
<li>在 <code>网络</code> 选项卡选择任意数据包(如果无数据包,重复步骤7)</li>
|
||||
<li>全选复制 Cookie 写入程序或配置文件</li>
|
||||
</ol>
|
||||
<br>
|
||||
<img src="static/screenshot/获取Cookie示意图.png" alt="">
|
||||
<h1>🗳 下载记录</h1>
|
||||
<p>XHS-Downloader 会将下载过的作品 ID 储存至数据库,当重复下载相同的作品时,XHS-Downloader 会自动跳过该作品的文件下载(即使作品文件不存在),如果想要重新下载作品文件,请先删除数据库中对应的作品 ID,再使用 XHS-Downloader 下载作品文件!</p>
|
||||
<p>该功能默认开启,如果关闭该功能,XHS-Downloader 会检查文件是否存在,若文件存在则跳过下载!</p>
|
||||
<h2>构建可执行文件指南</h2>
|
||||
<details>
|
||||
<summary><b>构建可执行文件指南(点击展开)</b></summary>
|
||||
|
||||
本指南将引导您通过 Fork 本仓库并执行 GitHub Actions 自动完成基于最新源码的程序构建和打包!
|
||||
|
||||
---
|
||||
|
||||
## 使用步骤
|
||||
|
||||
### 1. Fork 本仓库
|
||||
|
||||
1. 点击项目仓库右上角的 **Fork** 按钮,将本仓库 Fork 到您的个人 GitHub 账户中
|
||||
2. 您的 Fork 仓库地址将类似于:`https://github.com/your-username/this-repo`
|
||||
|
||||
---
|
||||
|
||||
### 2. 启用 GitHub Actions
|
||||
|
||||
1. 前往您 Fork 的仓库页面
|
||||
2. 点击顶部的 **Settings** 选项卡
|
||||
3. 点击右侧的 **Actions** 选项卡
|
||||
4. 点击 **General** 选项
|
||||
5. 在 **Actions permissions** 下,选择 **Allow all actions and reusable workflows** 选项,点击 **Save** 按钮
|
||||
|
||||
---
|
||||
|
||||
### 3. 手动触发打包流程
|
||||
|
||||
1. 在您 Fork 的仓库中,点击顶部的 **Actions** 选项卡
|
||||
2. 找到名为 **构建可执行文件** 的工作流
|
||||
3. 点击右侧的 **Run workflow** 按钮:
|
||||
- 选择 **master** 或者 **develop** 分支
|
||||
- 点击 **Run workflow**
|
||||
|
||||
---
|
||||
|
||||
### 4. 查看打包进度
|
||||
|
||||
1. 在 **Actions** 页面中,您可以看到触发的工作流运行记录
|
||||
2. 点击运行记录,查看详细的日志以了解打包进度和状态
|
||||
|
||||
---
|
||||
|
||||
### 5. 下载打包结果
|
||||
|
||||
1. 打包完成后,进入对应的运行记录页面
|
||||
2. 在页面底部的 **Artifacts** 部分,您将看到打包的结果文件
|
||||
3. 点击下载并保存到本地,即可获得打包好的程序
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **资源使用**:
|
||||
- Actions 的运行环境由 GitHub 免费提供,普通用户每月有一定的免费使用额度(2000 分钟)
|
||||
|
||||
2. **代码修改**:
|
||||
- 您可以自由修改 Fork 仓库中的代码以定制程序打包流程
|
||||
- 修改后重新触发打包流程,您将得到自定义的构建版本
|
||||
|
||||
3. **与主仓库保持同步**:
|
||||
- 如果主仓库更新了代码或工作流,建议您定期同步 Fork 仓库以获取最新功能和修复
|
||||
|
||||
---
|
||||
|
||||
## Actions 常见问题
|
||||
|
||||
### Q1: 为什么我无法触发工作流?
|
||||
|
||||
A: 请确认您已按照步骤 **启用 Actions**,否则 GitHub 会禁止运行工作流
|
||||
|
||||
### Q2: 打包流程失败怎么办?
|
||||
|
||||
A:
|
||||
|
||||
- 检查运行日志,了解失败原因
|
||||
- 确保代码没有语法错误或依赖问题
|
||||
- 如果问题仍未解决,可以在本仓库的 [Issues 页面](https://github.com/JoeanAmier/XHS-Downloader/issues) 提出问题
|
||||
|
||||
### Q3: 我可以直接使用主仓库的 Actions 吗?
|
||||
|
||||
A: 由于权限限制,您无法直接触发主仓库的 Actions。请通过 Fork 仓库的方式执行打包流程
|
||||
|
||||
</details>
|
||||
<h1>⭐ Star 趋势</h1>
|
||||
<p>
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=JoeanAmier/XHS-Downloader&type=Timeline"/>
|
||||
</p>
|
||||
<h1>♥️ 支持项目</h1>
|
||||
<p>如果 <b>XHS-Downloader</b> 对您有帮助,请考虑为它点个 <b>Star</b> ⭐,感谢您的支持!</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">微信(WeChat)</th>
|
||||
<th align="center">支付宝(Alipay)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td align="center"><img src="./static/微信赞助二维码.png" alt="微信赞助二维码" height="200" width="200"></td>
|
||||
<td align="center"><img src="./static/支付宝赞助二维码.png" alt="支付宝赞助二维码" height="200" width="200"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>如果您愿意,可以考虑提供资助为 <b>XHS-Downloader</b> 提供额外的支持!</p>
|
||||
<h1>🌟 贡献指南</h1>
|
||||
<p><strong>欢迎对本项目做出贡献!为了保持代码库的整洁、高效和易于维护,请仔细阅读以下指南,以确保您的贡献能够顺利被接受和整合。</strong></p>
|
||||
<ul>
|
||||
<li>
|
||||
使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。
|
||||
</li>
|
||||
<li>
|
||||
本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者尽力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。
|
||||
</li>
|
||||
<li>使用者在使用本项目时必须严格遵守 <a href="https://github.com/JoeanAmier/XHS_Downloader/blob/master/LICENSE">GNU
|
||||
<li>在开始开发前,请从 <code>develop</code> 分支拉取最新的代码,以此为基础进行修改;这有助于避免合并冲突并保证您的改动基于最新的项目状态。</li>
|
||||
<li>如果您的更改涉及多个不相关的功能或问题,请将它们分成多个独立的提交或拉取请求。</li>
|
||||
<li>每个拉取请求应尽可能专注于单一功能或修复,以便于代码审查和测试。</li>
|
||||
<li>遵循现有的代码风格;请确保您的代码与项目中已有的代码风格保持一致;建议使用 Ruff 工具保持代码格式规范。</li>
|
||||
<li>编写可读性强的代码;添加适当的注释帮助他人理解您的意图。</li>
|
||||
<li>每个提交都应该包含一个清晰、简洁的提交信息,以描述所做的更改。提交信息应遵循以下格式:<code><类型>: <简短描述></code></li>
|
||||
<li>当您准备提交拉取请求时,请优先将它们提交到 <code>develop</code> 分支;这是为了给维护者一个缓冲区,在最终合并到 <code>master</code>
|
||||
分支之前进行额外的测试和审查。</li>
|
||||
<li>建议在开发前或遇到疑问时与作者沟通,确保开发方向一致,避免重复劳动或无效提交。</li>
|
||||
</ul>
|
||||
<p><strong>参考资料:</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://www.contributor-covenant.org/zh-cn/version/2/1/code_of_conduct/">贡献者公约</a></li>
|
||||
<li><a href="https://opensource.guide/zh-hans/how-to-contribute/">如何为开源做贡献</a></li>
|
||||
</ul>
|
||||
<h1>✉️ 联系作者</h1>
|
||||
<ul>
|
||||
<li>作者邮箱:yonglelolu@foxmail.com</li>
|
||||
<li>作者微信: Downloader_Tools</li>
|
||||
<li>微信公众号: Downloader Tools</li>
|
||||
<li><b>Discord 社区</b>: <a href="https://discord.com/invite/ZYtmgKud9Y">点击加入社区</a></li>
|
||||
<li>QQ 群聊: <a href="https://github.com/JoeanAmier/XHS-Downloader/blob/master/static/QQ%E7%BE%A4%E8%81%8A%E4%BA%8C%E7%BB%B4%E7%A0%81.png">扫码加入群聊</a></li>
|
||||
</ul>
|
||||
<p><b>说明:</b>QQ 群聊仅限于讨论项目使用问题,严禁发布任何广告,严禁讨论任何账号交易、账号流量、流量变现、灰色产业等相关的内容!</p>
|
||||
<p>✨ <b>作者的其他开源项目:</b></p>
|
||||
<ul>
|
||||
<li><b>DouK-Downloader(抖音、TikTok)</b>:<a href="https://github.com/JoeanAmier/TikTokDownloader">https://github.com/JoeanAmier/TikTokDownloader</a></li>
|
||||
<li><b>KS-Downloader(快手、KuaiShou)</b>:<a href="https://github.com/JoeanAmier/KS-Downloader">https://github.com/JoeanAmier/KS-Downloader</a></li>
|
||||
</ul>
|
||||
|
||||
# 💰 项目赞助
|
||||
|
||||
## DartNode
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
|
||||
***
|
||||
|
||||
## ZMTO
|
||||
|
||||
<a href="https://www.zmto.com/"><img src="https://console.zmto.com/templates/2019/dist/images/logo_dark.svg" alt="ZMTO"></a>
|
||||
<p><a href="https://www.zmto.com/">ZMTO</a>:一家专业的云基础设施提供商,以可靠的尖端技术与专业支持,提供高效的解决方案,并为符合条件的开源项目提供企业级VPS基础设施,支持开源生态系统的可持续发展与创新。</p>
|
||||
<h1>⚠️ 免责声明</h1>
|
||||
<ol>
|
||||
<li>使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。</li>
|
||||
<li>本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。</li>
|
||||
<li>本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可,使用者需自行查阅并遵守相应协议,作者不对第三方组件的稳定性、安全性及合规性承担任何责任。</li>
|
||||
<li>使用者在使用本项目时必须严格遵守 <a href="https://github.com/JoeanAmier/XHS-Downloader/blob/master/LICENSE">GNU
|
||||
General Public License v3.0</a> 的要求,并在适当的地方注明使用了 <a
|
||||
href="https://github.com/JoeanAmier/XHS_Downloader/blob/master/LICENSE">GNU General Public License
|
||||
href="https://github.com/JoeanAmier/XHS-Downloader/blob/master/LICENSE">GNU General Public License
|
||||
v3.0</a> 的代码。
|
||||
</li>
|
||||
<li>
|
||||
使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。
|
||||
</li>
|
||||
<li>
|
||||
使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。
|
||||
</li>
|
||||
<li>使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。</li>
|
||||
<li>使用者不得使用本工具从事任何侵犯知识产权的行为,包括但不限于未经授权下载、传播受版权保护的内容,开发者不参与、不支持、不认可任何非法内容的获取或分发。</li>
|
||||
<li>本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规,确保处理行为合法正当;因违规操作导致的法律责任由使用者自行承担。</li>
|
||||
<li>使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。</li>
|
||||
<li>本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。</li>
|
||||
<li>基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。</li>
|
||||
</ul>
|
||||
<li>本项目不授予使用者任何专利许可;若使用本项目导致专利纠纷或侵权,使用者自行承担全部风险和责任。未经作者或权利人书面授权,不得使用本项目进行任何商业宣传、推广或再授权。</li>
|
||||
<li>作者保留随时终止向任何违反本声明的使用者提供服务的权利,并可能要求其销毁已获取的代码及衍生作品。</li>
|
||||
<li>作者保留在不另行通知的情况下更新本声明的权利,使用者持续使用即视为接受修订后的条款。</li>
|
||||
</ol>
|
||||
<b>在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。</b>
|
||||
|
||||
# 💡 项目参考
|
||||
|
||||
* https://github.com/encode/httpx/
|
||||
* https://github.com/tiangolo/fastapi
|
||||
* https://github.com/textualize/textual/
|
||||
* https://github.com/pyinstaller/pyinstaller
|
||||
* https://github.com/zbowling/beartype-pyinstaller-repro
|
||||
* https://github.com/jlowin/fastmcp
|
||||
* https://github.com/omnilib/aiosqlite
|
||||
* https://github.com/carpedm20/emoji/
|
||||
* https://github.com/asweigart/pyperclip
|
||||
* https://github.com/lxml/lxml
|
||||
* https://github.com/yaml/pyyaml
|
||||
* https://github.com/pallets/click/
|
||||
* https://github.com/encode/uvicorn
|
||||
* https://github.com/Tinche/aiofiles
|
||||
|
||||
730
README_EN.md
Normal file
@ -0,0 +1,730 @@
|
||||
<div align="center">
|
||||
<img src="static/XHS-Downloader.png" alt="XHS-Downloader" height="256" width="256"><br>
|
||||
<h1>XHS-Downloader</h1>
|
||||
<p><a href="README.md">简体中文</a> | English</p>
|
||||
<a href="https://trendshift.io/repositories/5435" target="_blank"><img src="https://trendshift.io/api/badge/repositories/5435" alt="JoeanAmier%2FXHS-Downloader | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<br>
|
||||
<img alt="GitHub" src="https://img.shields.io/github/license/JoeanAmier/XHS-Downloader?style=flat-square">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/JoeanAmier/XHS-Downloader?style=flat-square&color=55efc4">
|
||||
<img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/JoeanAmier/XHS-Downloader?style=flat-square&color=fda7df">
|
||||
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/JoeanAmier/XHS-Downloader?style=flat-square&color=a29bfe">
|
||||
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/JoeanAmier/XHS-Downloader?style=flat-square&color=48dbfb">
|
||||
<br>
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Python-3.12-b8e994?style=flat-square&logo=python&labelColor=3dc1d3">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/UserScript-ffec3d?style=flat-square&logo=tampermonkey&logoColor=%2300485B">
|
||||
<img src="https://img.shields.io/badge/Sourcery-enabled-884898?style=flat-square&color=1890ff" alt="">
|
||||
<img alt="Static Badge" src="https://img.shields.io/badge/Docker-badc58?style=flat-square&logo=docker">
|
||||
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/JoeanAmier/XHS-Downloader/total?style=flat-square&color=ffdd59">
|
||||
</div>
|
||||
<br>
|
||||
<p>🔥 <b>RedNote Link Extraction/Content Collection Tool</b>:Extract account-published, favorites, and liked works links; extract search result works links and user links; collect RedNote works information; extract RedNote works download addresses; download RedNote watermark-free works files!</p>
|
||||
<p>🔥 "RedNote", "XiaoHongShu" and "小红书" have the same meaning, and this project is collectively referred to as "RedNote".</p>
|
||||
<p>⭐ Due to the author's limited energy, I was unable to update the English document in a timely manner, and the content may have become outdated, partial translation is machine translation, the translation result may be incorrect, Suggest referring to Chinese documentation. If you want to contribute to translation, we warmly welcome you.</p>
|
||||
<h1>📑 Project Features</h1>
|
||||
<details>
|
||||
<summary>Program Features and User Script Features (Click to Expand)</summary>
|
||||
<ul><b>Program Features</b>
|
||||
<li>✅ Collect RedNote works information</li>
|
||||
<li>✅ Extract RedNote works download addresses</li>
|
||||
<li>✅ Download RedNote watermark-free works files</li>
|
||||
<li>✅ Download RedNote livePhoto files (watermark)</li>
|
||||
<li>✅ Automatically skip already downloaded works files</li>
|
||||
<li>✅ works file integrity handling mechanism</li>
|
||||
<li>✅ Customizable image works file download format</li>
|
||||
<li>✅ Persistently store works information to files</li>
|
||||
<li>✅ Store works files to a separate folder</li>
|
||||
<li>✅ Background clipboard monitoring for works download</li>
|
||||
<li>✅ Record downloaded works IDs</li>
|
||||
<li>✅ Support command line for downloading works files</li>
|
||||
<li>✅ Read cookies from browser</li>
|
||||
<li>✅ Customizable file name format</li>
|
||||
<li>✅ Support API call functionality</li>
|
||||
<li>✅ Support MCP call functionality</li>
|
||||
<li>✅ Support file breakpoint resume download</li>
|
||||
<li>✅ Intelligent recognition of works file types</li>
|
||||
<li>✅ Supports author alias configuration</li>
|
||||
<li>✅ Automatic author nickname updates</li>
|
||||
</ul>
|
||||
<ul><a href="#user-scripts"><b>Script Features</b></a>
|
||||
<li>✅ Download RedNote watermark-free works files</li>
|
||||
<li>✅ Extract discovery page works links</li>
|
||||
<li>✅ Extract account-published works links</li>
|
||||
<li>✅ Extract account-favorited works links</li>
|
||||
<li>✅ Extract account-liked works links</li>
|
||||
<li>✅ Extract account-board works links</li>
|
||||
<li>✅ Extract search result works links</li>
|
||||
<li>✅ Extract search result user links</li>
|
||||
</ul>
|
||||
</details>
|
||||
<h1>📸 Program Screenshots</h1>
|
||||
<p><a href="https://www.bilibili.com/video/BV1Fcb3zWEjt/">Watch Demo on Bilibili</a>;<a href="https://youtu.be/VIjDytHaopg">Watch Demo on YouTube</a></p>
|
||||
<img src="static/screenshot/程序运行截图EN1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/程序运行截图EN2.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/程序运行截图EN3.png" alt="">
|
||||
<h1>🔗 Supported Links</h1>
|
||||
<ul>
|
||||
<li><code>https://www.xiaohongshu.com/explore/WorksID?xsec_token=XXX</code></li>
|
||||
<li><code>https://www.xiaohongshu.com/discovery/item/WorksID?xsec_token=XXX</code></li>
|
||||
<li><code>https://www.xiaohongshu.com/user/profile/AuthorID/WorksID?xsec_token=XXX</code></li>
|
||||
<li><code>https://xhslink.com/ShareCode</code></li>
|
||||
<br/>
|
||||
<p><b>Supports entering multiple works links at once, separated by spaces; the program will automatically extract valid links without additional processing!</b></p>
|
||||
</ul>
|
||||
<h1>🪟 About the Terminal</h1>
|
||||
<p>⭐ It is recommended to use the <a href="https://learn.microsoft.com/en-us/windows/terminal/install">Windows Terminal</a> (default terminal for Windows 11) to run the program for the best display effect!</p>
|
||||
<h1>🥣 Usage</h1>
|
||||
<p>If you only need to download watermark-free works files, it is recommended to choose <b>Program Run</b>; if you have other needs, it is recommended to choose <b>Source Code Run</b>!</p>
|
||||
<p>Starting from version <code>2.2</code>, if there are no abnormalities in project functionality, there is no need to handle cookies separately!</p>
|
||||
<h2>🖱 Program Run</h2>
|
||||
<p>⭐ Mac OS, Windows 10 and above users can go to <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> or <a href="https://github.com/JoeanAmier/XHS-Downloader/actions">Actions</a> to download the program package, unzip it, open the program folder, and double-click to run <code>main</code> to use.</p>
|
||||
<p>⭐ This project includes GitHub Actions for automatic building executable files. Users can use GitHub Actions to build the latest source code into executable files at any time!</p>
|
||||
<p>⭐ For the automatic building executable files tutorial, please refer to the <code>Build of Executable File Guide</code> section of this document. If you need a more detailed step-by-step tutorial with illustrations, please <a href="https://mp.weixin.qq.com/s/TorfoZKkf4-x8IBNLImNuw">check out this article</a>!</p>
|
||||
<p><strong>Note: Due to the macOS platform's executable file <code>main</code> not being code-signed, it will be restricted by system security measures on first run. Please execute the command <code>xattr -cr project_folder_path</code> in the terminal to remove the security flag, after which it can run normally.</strong></p>
|
||||
<p>If you use the program in this way, the default download path for files is: <code>.\_internal\Volume\Download</code>; the configuration file path is: <code>.\_internal\Volume\settings.json</code></p>
|
||||
<h3>Update Methods</h3>
|
||||
<p><strong>Method 1:</strong> Download and extract the files, then copy the old version of the <code>_internal\Volume</code> folder into the new version's <code>_internal</code> folder.</p>
|
||||
<p><strong>Method 2:</strong> Download and extract the files (do not run the program), then copy all files and directly overwrite the old version.</p>
|
||||
<h2>⌨️ Run from Source Code</h2>
|
||||
<ol>
|
||||
<li>Install <a href="https://www.python.org/">Python</a> interpreter version <code>3.12</code></li>
|
||||
<li>Download the latest source code or the source code released in <a href="https://github.com/JoeanAmier/XHS-Downloader/releases/latest">Releases</a> to your local machine</li>
|
||||
<ol><b>Install project dependencies using pip</b>
|
||||
<li>Run the command <code>python -m venv venv</code> to create a virtual environment (optional)</li>
|
||||
<li>Run the command <code>.\venv\Scripts\activate.ps1</code> or <code>venv\Scripts\activate</code> to activate the virtual environment (optional)</li>
|
||||
<li>Run the command <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> to install the required modules for the program</li>
|
||||
<li>Run the command <code>python .\main.py</code> or <code>python main.py</code> to start XHS-Downloader</li>
|
||||
</ol>
|
||||
<ol><b>Install project dependencies using uv (recommended)</b>
|
||||
<li>Run the command <code>uv venv</code> to create a virtual environment</li>
|
||||
<li>Run the command <code>uv sync</code> to synchronize environment dependencies</li>
|
||||
<li>Run the command <code>uv run main.py</code> to start XHS-Downloader</li>
|
||||
</ol>
|
||||
</ol>
|
||||
<h2>⌨️ Docker Run</h2>
|
||||
<ol>
|
||||
<li>Get Image</li>
|
||||
<ul>
|
||||
<li>Method 1: Build the image using the <code>Dockerfile</code></li>
|
||||
<li>Method 2: Pull the image using the command <code>docker pull joeanamier/xhs-downloader</code></li>
|
||||
<li>Method 3: Pull the image using the command <code>docker pull ghcr.io/joeanamier/xhs-downloader</code></li>
|
||||
</ul>
|
||||
<li>Create Container</li>
|
||||
<ul>
|
||||
<li>TUI Mode: <code>docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name></code></li>
|
||||
<li>API Mode: <code>docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name> python main.py api</code></li>
|
||||
<li>MCP Mode: <code>docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name> python main.py mcp</code></li>
|
||||
<br><b>Note:</b> The <code><image name></code> here must be consistent with the image name you used in the first step (<code>joeanamier/xhs-downloader</code> or <code>ghcr.io/joeanamier/xhs-downloader</code>)
|
||||
</ul>
|
||||
<li>Run Container
|
||||
<ul>
|
||||
<li>Start Container: <code>docker start -i ContainerName/ContainerID</code></li>
|
||||
<li>Restart Container: <code>docker restart -i ContainerName/ContainerID</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<p>When running the project via Docker, the <b>command line call mode</b> is not supported. The <b>clipboard reading</b> and <b>clipboard monitoring</b> functions are unavailable, but pasting content works fine. Please provide feedback if other features are not functioning properly!</p>
|
||||
<h1>🛠 Command Line Mode</h1>
|
||||
<p>The project supports command line mode. If you want to download specific images from a text and image works, you can use this mode to set the image sequence number you want to download!</p>
|
||||
<p><strong>Note:</strong> When the <code>--index</code> parameter is not set, multiple works links can be passed in. All links must be enclosed in quotation marks and separated by spaces. When the <code>--index</code> parameter is set, multiple works links are not supported. Even if multiple links are passed in, the program will only process the first link!</p>
|
||||
<p>The <code>bool</code> type parameters support setting with <code>true</code>, <code>false</code>, <code>1</code>, <code>0</code>, <code>yes</code>, <code>no</code>, <code>on</code> or <code>off</code> (case insensitive).</p>
|
||||
<h2>Read Browser Cookies</h2>
|
||||
<p>You can use the command line to <b>read cookies from browser and write them to the configuration file!</b></p>
|
||||
<p>Command example: <code>python .\main.py --browser_cookie Chrome --update_settings</code></p>
|
||||
<p>Compatibility note: The third-party module this feature depends on has not been updated for a long time and may not properly support the latest browser versions. If the feature is not working properly, please try obtaining cookies manually!</p>
|
||||
<hr>
|
||||
<img src="static/screenshot/命令行模式截图EN1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/命令行模式截图EN2.png" alt="">
|
||||
<h1>🖥 Server Mode</h1>
|
||||
<p>Server modes include API mode and MCP mode!</p>
|
||||
<h2>API Mode</h2>
|
||||
<p><b>Start:</b> Run the command: <code>python .\main.py api</code></p>
|
||||
<p><b>Stop:</b> Press <code>Ctrl</code> + <code>C</code> to stop the server</p>
|
||||
<p>Open <code>http://127.0.0.1:5556/docs</code> or <code>http://127.0.0.1:5556/redoc</code>; you will see automatically generated interactive API documentation!</p>
|
||||
<p><b>Request endpoint:</b>
|
||||
<code>/xhs/detail</code></p>
|
||||
<p><b>Request method:</b>
|
||||
<code>POST</code></p>
|
||||
<p><b>Request format:</b>
|
||||
<code>JSON</code></p>
|
||||
<p><b>Request parameters:</b></p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">Parameter</th>
|
||||
<th align="center">Type</th>
|
||||
<th align="center">Description</th>
|
||||
<th align="center">Default</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">url</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">RedNote works link, auto-extraction, does not support multiple links; Required parameter</td>
|
||||
<td align="center">None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to download the works file; set to <code>true</code> will take more time; Optional parameter</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">index</td>
|
||||
<td align="center">list[int]</td>
|
||||
<td align="center">Download specific image files by index, only effective for text and image works; not effective when the <code>download</code> parameter is set to <code>false</code>; Optional parameter</td>
|
||||
<td align="center">null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">cookie</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Cookie used when requesting data; Optional parameter</td>
|
||||
<td align="center">Settings cookie Value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">proxy</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Proxy used when requesting data; Optional parameter</td>
|
||||
<td align="center">Settings proxy Value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">skip</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to skip works with download records; set to <code>true</code> will not return works data with download records; Optional parameter</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><b>Code example:</b></p>
|
||||
<pre>
|
||||
async def example_api():
|
||||
"""通过 API 设置参数,适合二次开发"""
|
||||
server = "http://127.0.0.1:5556/xhs/detail"
|
||||
data = {
|
||||
"url": "", # 必需参数
|
||||
"download": True,
|
||||
"index": [
|
||||
3,
|
||||
6,
|
||||
9,
|
||||
],
|
||||
"proxy": "http://127.0.0.1:10808",
|
||||
}
|
||||
response = post(server, json=data, timeout=10)
|
||||
print(response.json())
|
||||
</pre>
|
||||
<h2>MCP Mode</h2>
|
||||
<p><b>Start:</b> Run the command: <code>python .\main.py mcp</code></p>
|
||||
<p><b>Stop:</b> Press <code>Ctrl</code> + <code>C</code> to stop the server</p>
|
||||
<h3>MCP Configuration Example</h3>
|
||||
|
||||
[//]: # (<h4>STDIO</h4>)
|
||||
<h4>Streamable HTTP</h4>
|
||||
<p><b>MCP URL:</b><code>http://127.0.0.1:5556/mcp/</code></p>
|
||||
<img src="static/screenshot/MCP配置示例.png" alt="MCP Configuration Example">
|
||||
<h3>MCP Invocation Example</h3>
|
||||
<details>
|
||||
<summary>MCP Function and Call Example (Click to Expand)</summary>
|
||||
<h4><strong>Retrieve RedNote Works Information</strong></h4>
|
||||
<img src="static/screenshot/MCP获取数据.png" alt="MCP Data Retrieval">
|
||||
<hr>
|
||||
<h4><strong>Download RedNote Works Files</strong></h4>
|
||||
<p>When downloading images, you can specify the sequence numbers of the images to download. By default, post information is not returned. If you need the post information, please explicitly state so during the conversation.</p>
|
||||
<img src="static/screenshot/MCP下载文件1.png" alt="MCP File Download">
|
||||
<hr>
|
||||
<img src="static/screenshot/MCP下载文件2.png" alt="MCP File Download">
|
||||
</details>
|
||||
<h1>📜 Others</h1>
|
||||
<ul>
|
||||
<li>Due to the date information carried in the links of RedNote works, using links obtained from previous dates may be subject to risk control. It is recommended to use the latest RedNote works links when downloading RedNote work files</li>
|
||||
<li>Windows system requires running programs as an administrator to read Chromium, Chrome, Edge browser cookies</li>
|
||||
<li>If the function to save works data to a file is enabled, the works data will be stored by default in the <code>./Volume/Download/ExploreData.db</code> file</li>
|
||||
<li>The program's download records will be stored in the <code>./Volume/ExploreID.db</code> file</li>
|
||||
<li>To prevent high-frequency requests from impacting the platform's servers, this project includes a built-in request delay mechanism</li>
|
||||
</ul>
|
||||
<h1 id="user-scripts">🕹 User Script</h1>
|
||||
<p>If your browser has the <a href="https://www.tampermonkey.net/">Tampermonkey</a> extension installed, you can use the userscript to try the project's features!</p>
|
||||
<p>Userscript links (right-click to copy the link): <a href="https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/refs/heads/master/static/XHS-Downloader.js">master branch</a>, <a href="https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/refs/heads/develop/static/XHS-Downloader.js">develop branch</a></p>
|
||||
<img src="static/screenshot/脚本安装教程.png" alt="">
|
||||
<hr>
|
||||
<details>
|
||||
<summary>View Tampermonkey userscript screenshots (click to expand)</summary>
|
||||
<img src="static/screenshot/用户脚本截图1.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图2.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图3.png" alt="">
|
||||
<hr>
|
||||
<img src="static/screenshot/用户脚本截图4.png" alt="">
|
||||
</details>
|
||||
<p>Note: Using the XHS-Downloader user script to batch extract works links, in combination with the XHS-Downloader program, can achieve batch downloading of watermark-free works files!</p>
|
||||
<h2>🌐 Connect to Server</h2>
|
||||
<p>⭐ This project supports interaction with the main program through a browser userscript, enabling one-click push of download tasks.</p>
|
||||
<ul><b>Function Description:</b>
|
||||
<li>In the project program's configuration file, you need to set the <code>script_server</code> parameter to <code>true</code></li>
|
||||
<li>Keep the project program running in the background, where it will act as a server to receive commands from the userscript (TUI, MCP, and API modes are all supported)</li>
|
||||
<li>When you visit a post page in your browser, click the <code>Push Download Task</code> option in the userscript menu</li>
|
||||
<li>The userscript will send the download task to the project program, which will handle and download the files</li>
|
||||
</ul>
|
||||
<h2>📜 Script Instructions</h2>
|
||||
<ul>
|
||||
<li>When downloading watermark-free works from Xiaohongshu, the script requires time to process the files. Please wait for a moment and do not click the download button multiple times.</li>
|
||||
<li>Watermark-free image files are in PNG format; watermark-free video files are larger and may take longer to process. Page redirects may cause download failures.</li>
|
||||
<li>When extracting links for posts, collects, likes, and board from an account, the script can automatically scroll the page until all works are loaded.</li>
|
||||
<li>When extracting recommended works links, search works, and user links, the script can automatically scroll a specified number of times to load more content. The default number of page scrolls is 50.</li>
|
||||
<li>The automatic scrolling page function is turned off by default; Users can freely open and modify the number of times the page is scrolled, and the modification will take effect immediately.</li>
|
||||
<li>If the automatic page scroll feature is not enabled, users need to manually scroll the page to load more content before performing other actions.</li>
|
||||
<li>Support packaging and downloading of work files; This feature is enabled by default, and works from multiple files will be downloaded in compressed file format</li>
|
||||
<li>Using global proxy tools may cause script download failures. If there are issues, please try disabling the proxy tool. If necessary, contact the author for feedback.</li>
|
||||
<li>XHS-Downloader userscript only implements the data collection functionality for visible content and does not include any paid or cracked features.</li>
|
||||
</ul>
|
||||
<p><strong>The automatic page scroll feature has been refactored and is turned off by default! Enabling this feature may be detected as automated behavior by Xiaohongshu, potentially resulting in account risk control or banning.</strong></p>
|
||||
<h1>💻 Secondary Development</h1>
|
||||
<p>If you have other needs, you can perform code calls or modifications based on the comments in <code>example.py</code>!</p>
|
||||
<pre>
|
||||
async def example():
|
||||
"""通过代码设置参数,适合二次开发"""
|
||||
# 示例链接
|
||||
demo_link = "https://www.xiaohongshu.com/explore/XXX?xsec_token=XXX"
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
name_format = "作品标题 作品描述"
|
||||
user_agent = "" # User-Agent
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录,可选参数,登录状态对数据采集有影响
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
|
||||
record_data = False # 是否保存作品数据至文件
|
||||
image_format = "WEBP" # 图文作品文件下载格式,支持:AUTO、PNG、WEBP、JPEG、HEIC
|
||||
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
|
||||
image_download = True # 图文、图集作品文件下载开关
|
||||
video_download = True # 视频作品文件下载开关
|
||||
live_download = False # 图文动图文件下载开关
|
||||
download_record = True # 是否记录下载成功的作品 ID
|
||||
language = "zh_CN" # 设置程序提示语言
|
||||
author_archive = True # 是否将每个作者的作品存至单独的文件夹
|
||||
write_mtime = True # 是否将作品文件的 修改时间 修改为作品的发布时间
|
||||
read_cookie = None # 读取浏览器 Cookie,支持设置浏览器名称(字符串)或者浏览器序号(整数),设置为 None 代表不读取
|
||||
# async with XHS() as xhs:
|
||||
# pass # 使用默认参数
|
||||
async with XHS(
|
||||
work_path=work_path,
|
||||
folder_name=folder_name,
|
||||
name_format=name_format,
|
||||
user_agent=user_agent,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
chunk=chunk,
|
||||
max_retry=max_retry,
|
||||
record_data=record_data,
|
||||
image_format=image_format,
|
||||
folder_mode=folder_mode,
|
||||
image_download=image_download,
|
||||
video_download=video_download,
|
||||
live_download=live_download,
|
||||
download_record=download_record,
|
||||
language=language,
|
||||
read_cookie=read_cookie,
|
||||
author_archive=author_archive,
|
||||
write_mtime=write_mtime,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
# 返回作品详细信息,包括下载地址
|
||||
# 获取数据失败时返回空字典
|
||||
print(
|
||||
await xhs.extract(
|
||||
demo_link,
|
||||
download,
|
||||
index=[
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
],
|
||||
)
|
||||
)
|
||||
</pre>
|
||||
<h1>📋 Read Clipboard</h1>
|
||||
<p>The project uses <code>pyperclip</code> to implement clipboard reading functionality, which varies across different systems.</p>
|
||||
<p>On Windows, no additional modules are needed.</p>
|
||||
<p>On Mac, this module makes use of the pbcopy and pbpaste commands, which should come with the os.</p>
|
||||
<p>On Linux, this module makes use of the xclip or xsel commands, which should come with the os. Otherwise run "sudo apt-get install xclip" or "sudo apt-get install xsel" (Note: xsel does not always seem to work.)</p>
|
||||
<p>Otherwise on Linux, you will need the qtpy or PyQT5 modules installed.</p>
|
||||
<h1>⚙️ Configuration File</h1>
|
||||
<p>The <code>./Volume/settings.json</code> file in the project's root directory is automatically generated on the first run. You can use it to customize the program's operating parameters. If an invalid parameter value is set, the program will revert to its default value.</p>
|
||||
<p>If you are unable to modify settings through the program's interface, you can edit this configuration file directly. If your computer lacks a suitable program for editing JSON files, we recommend using an <a href="https://www.toolhelper.cn/JSON/JSONFormat">online tool</a>. Remember to restart the software after making changes for them to take effect.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">Parameter</th>
|
||||
<th align="center">Type</th>
|
||||
<th align="center">Description</th>
|
||||
<th align="center">Default Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">mapping_data</td>
|
||||
<td align="center">str: str</td>
|
||||
<td align="center"><sup><a href="#author_archive">#</a></sup>Author alias mapping data, format: <code>author ID: author alias</code></td>
|
||||
<td align="center">null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">work_path</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Root path for saving works data/files</td>
|
||||
<td align="center">Project root path/Volume</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder_name</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Name of the folder for storing works files</td>
|
||||
<td align="center">Download</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">name_format</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center"><sup><a href="#fields">#</a></sup>Format of works file name, separated by spaces between fields, supports fields: <code>收藏数量</code>、<code>评论数量</code>、<code>分享数量</code>、<code>点赞数量</code>、<code>作品标签</code>、<code>作品ID</code>、<code>作品标题</code>、<code>作品描述</code>、<code>作品类型</code>、<code>发布时间</code>、<code>最后更新时间</code>、<code>作者昵称</code>、<code>作者ID</code></td>
|
||||
<td align="center"><code>发布时间 作者昵称 作品标题</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">user_agent</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Browser User Agent</td>
|
||||
<td align="center">Built-in Chrome User Agent</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">cookie</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">RedNote web version cookie, <b>No login required, non essential parameters!</b></td>
|
||||
<td align="center">None</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">proxy</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Set program proxy</td>
|
||||
<td align="center">null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">timeout</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">Request data timeout limit, in seconds</td>
|
||||
<td align="center">10</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">chunk</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">Size of data chunk to fetch from the server each time when downloading files, in bytes</td>
|
||||
<td align="center">2097152(2 MB)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">max_retry</td>
|
||||
<td align="center">int</td>
|
||||
<td align="center">Maximum number of retries when requesting data fails</td>
|
||||
<td align="center">5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">record_data</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to save works data to a file, saved in <code>SQLite</code> format</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">image_format</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Download format for image works files, supported: <code>AUTO</code>、<code>PNG</code>、<code>WEBP</code>、<code>JPEG</code>、<code>HEIC</code><br><strong>Some works do not have files in HEIC format, and the downloaded files may be in WEBP format</strong><br><strong>When set to<code>AUTO</code>, it represents dynamic format, and the actual format depends on the server's response data</strong></td>
|
||||
<td align="center">PNG</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">image_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Switch for downloading image and atlas works files</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">video_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Switch for downloading video works files</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">live_download</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Switch for downloading animated image files</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">folder_mode</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to store each works files in a separate folder; the folder name matches the file name</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">download_record</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Do record the ID of successfully downloaded works? If enabled, the program will automatically skip downloading works with records</td>
|
||||
<td align="center">true</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">author_archive</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center"><sup><a href="#author_archive">#</a></sup>Whether to save each author's works into a separate folder; The folder name is <code>authorID_nickname</code></td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">write_mtime</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to modify the <code>modified time</code> attribute of the works file to the publication time of the works.</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">language</td>
|
||||
<td align="center">str</td>
|
||||
<td align="center">Set program language. Currently supported: <code>zh_CN</code>, <code>en_US</code></td>
|
||||
<td align="center">zh_CN</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">script_server</td>
|
||||
<td align="center">bool</td>
|
||||
<td align="center">Whether to enable the user script server for receiving download tasks from the browser user script (effective in TUI, MCP, and API modes)</td>
|
||||
<td align="center">false</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<div id="fields">
|
||||
<p>name_format instructions (Currently only supports Chinese values) :</p>
|
||||
<ul>
|
||||
<li><code>收藏数量</code>: Number of Collections</li>
|
||||
<li><code>评论数量</code>: Number of Comments</li>
|
||||
<li><code>分享数量</code>: Number of Shares</li>
|
||||
<li><code>点赞数量</code>: Number of Likes</li>
|
||||
<li><code>作品标签</code>: Works Tags</li>
|
||||
<li><code>作品ID</code>: Works ID</li>
|
||||
<li><code>作品标题</code>: Works Title</li>
|
||||
<li><code>作品描述</code>: Works Description</li>
|
||||
<li><code>作品类型</code>: Works Type</li>
|
||||
<li><code>发布时间</code>: Publish Time</li>
|
||||
<li><code>最后更新时间</code>: Last Updated Time</li>
|
||||
<li><code>作者昵称</code>: Author Nickname</li>
|
||||
<li><code>作者ID</code>: Author ID</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr>
|
||||
<div id="author_archive">
|
||||
<p>When <code>author_archive</code> is set to <code>true</code>, the program will store each author's works in dedicated folders. If an author's nickname changes, the program automatically updates the nickname portion in existing downloaded filenames!</p>
|
||||
<p>Additionally, you can configure author aliases through the <code>mapping_data</code> parameter. When an alias is set, the program will use your custom alias instead of the original nickname in filenames!</p>
|
||||
</div>
|
||||
<hr>
|
||||
<p><b>Additional Notes: The parameters <code>user_agent</code> examples are provided for reference; Strongly recommend setting according to actual browser information!</b></p>
|
||||
<img src="static/screenshot/请求头示例图.png" alt="">
|
||||
<h1>🌐 Cookie</h1>
|
||||
<p>Starting from version <code>2.2</code>, if there are no abnormalities in project functionality, there is no need to handle cookies separately!</p>
|
||||
<ol>
|
||||
<li>Open the browser (optional: start in incognito mode) and visit <code>https://www.xiaohongshu.com/explore</code></li>
|
||||
<li>Log in to your RedNote account (can be skipped)</li>
|
||||
<li>Press <code>F12</code> to open the developer tools</li>
|
||||
<li>Select the <code>Network</code> tab</li>
|
||||
<li>Check <code>Preserve log</code></li>
|
||||
<li>In the <code>Filter</code> input box, enter <code>cookie-name:web_session</code></li>
|
||||
<li>Select the <code>Fetch/XHR</code> filter</li>
|
||||
<li>Click on any piece of works on the RedNote page</li>
|
||||
<li>In the <code>Network</code> tab, select any data packet (if no packets appear, repeat step 7)</li>
|
||||
<li>Copy and paste the entire Cookie into the program or configuration file</li>
|
||||
</ol>
|
||||
<br>
|
||||
<img src="static/screenshot/获取Cookie示意图.png" alt="">
|
||||
<h1>🗳 Download Records</h1>
|
||||
<p>XHS-Downloader will store the IDs of downloaded works in a database. When downloading the same works again, XHS-Downloader will automatically skip the file download (even if the works file does not exist). If you want to re-download the works file, please delete the corresponding works ID from the database and then use XHS-Downloader to download the works file again!</p>
|
||||
<p>This feature is enabled by default. If it is turned off, XHS-Downloader will check if the file exists. If the file exists, it will skip the download!</p>
|
||||
<h2>Build of Executable File Guide</h2>
|
||||
<details>
|
||||
<summary>Build of Executable File Guide (Click to Expand)</summary>
|
||||
|
||||
This guide will walk you through forking this repository and executing GitHub Actions to automatically build and package
|
||||
the program based on the latest source code!
|
||||
|
||||
---
|
||||
|
||||
## Steps to Use
|
||||
|
||||
### 1. Fork the Repository
|
||||
|
||||
1. Click the **Fork** button at the top right of the project repository to fork it to your personal GitHub account
|
||||
2. Your forked repository address will look like this: `https://github.com/your-username/this-repo`
|
||||
|
||||
---
|
||||
|
||||
### 2. Enable GitHub Actions
|
||||
|
||||
1. Go to the page of your forked repository
|
||||
2. Click the **Settings** tab at the top
|
||||
3. Click the **Actions** tab on the right
|
||||
4. Click the **General** option
|
||||
5. Under **Actions permissions**, select **Allow all actions and reusable workflows** and click the **Save** button
|
||||
|
||||
---
|
||||
|
||||
### 3. Manually Trigger the Build Process
|
||||
|
||||
1. In your forked repository, click the **Actions** tab at the top
|
||||
2. Find the workflow named **构建可执行文件**
|
||||
3. Click the **Run workflow** button on the right:
|
||||
- Select the **master** or **develop** branch
|
||||
- Click **Run workflow**
|
||||
|
||||
---
|
||||
|
||||
### 4. Check the Build Progress
|
||||
|
||||
1. On the **Actions** page, you can see the execution records of the triggered workflow
|
||||
2. Click on the run record to view detailed logs to check the build progress and status
|
||||
|
||||
---
|
||||
|
||||
### 5. Download the Build Result
|
||||
|
||||
1. Once the build is complete, go to the corresponding run record page
|
||||
2. In the **Artifacts** section at the bottom of the page, you will see the built result file
|
||||
3. Click to download and save it to your local machine to get the built program
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Resource Usage**:
|
||||
- GitHub provides free build environments for Actions, with a monthly usage limit (2000 minutes) for free-tier
|
||||
users
|
||||
|
||||
2. **Code Modifications**:
|
||||
- You are free to modify the code in your forked repository to customize the build process
|
||||
- After making changes, you can trigger the build process again to get your customized version
|
||||
|
||||
3. **Stay in Sync with the Main Repository**:
|
||||
- If the main repository is updated with new code or workflows, it is recommended that you periodically sync your
|
||||
forked repository to get the latest features and fixes
|
||||
|
||||
---
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### Q1: Why can't I trigger the workflow?
|
||||
|
||||
A: Please ensure that you have followed the steps to **Enable Actions**. Otherwise, GitHub will prevent the workflow
|
||||
from running
|
||||
|
||||
### Q2: What should I do if the build process fails?
|
||||
|
||||
A:
|
||||
|
||||
- Check the run logs to understand the cause of the failure
|
||||
- Ensure there are no syntax errors or dependency issues in the code
|
||||
- If the problem persists, please open an issue on
|
||||
the [Issues page](https://github.com/JoeanAmier/XHS-Downloader/issues)
|
||||
|
||||
### Q3: Can I directly use the Actions from the main repository?
|
||||
|
||||
A: Due to permission restrictions, you cannot directly trigger Actions from the main repository. Please use the forked
|
||||
repository to execute the build process
|
||||
|
||||
</details>
|
||||
<h1>⭐ Star History</h1>
|
||||
<p>
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=JoeanAmier/XHS-Downloader&type=Timeline"/>
|
||||
</p>
|
||||
<h1>♥️ Support the Project</h1>
|
||||
<p>If <b>XHS-Downloader</b> has been helpful to you, please consider giving it a <b>Star</b> ⭐, Thank you for your support!</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="center">微信(WeChat)</th>
|
||||
<th align="center">支付宝(Alipay)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody><tr>
|
||||
<td align="center"><img src="./static/微信赞助二维码.png" alt="微信赞助二维码" height="200" width="200"></td>
|
||||
<td align="center"><img src="./static/支付宝赞助二维码.png" alt="支付宝赞助二维码" height="200" width="200"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>If you are willing, you may consider making a donation to provide additional support for <b>XHS-Downloader</b>!</p>
|
||||
<h1>🌟 Contribution Guidelines</h1>
|
||||
<p><strong>Welcome to contributing to this project! To keep the codebase clean, efficient, and easy to maintain, please read the following guidelines carefully to ensure that your contributions can be accepted and integrated smoothly.</strong></p>
|
||||
<ul>
|
||||
<li>Before starting development, please pull the latest code from the <code>develop</code> branch as the basis for your modifications; this helps avoid merge conflicts and ensures your changes are based on the latest state of the project.</li>
|
||||
<li>If your changes involve multiple unrelated features or issues, please split them into several independent commits or pull requests.</li>
|
||||
<li>Each pull request should focus on a single feature or fix as much as possible, to facilitate code review and testing.</li>
|
||||
<li>Follow the existing coding style; make sure your code is consistent with the style already present in the project; please use the Ruff tool to maintain code formatting standards.</li>
|
||||
<li>Write code that is easy to read; add appropriate annotation to help others understand your intentions.</li>
|
||||
<li>Each commit should include a clear and concise commit message describing the changes made. The commit message should follow this format: <code><type>: <short description></code></li>
|
||||
<li>When you are ready to submit a pull request, please prioritize submitting them to the <code>develop</code> branch; this provides maintainers with a buffer zone for additional testing and review before final merging into the <code>master</code> branch.</li>
|
||||
<li>It is recommended to communicate with the author before starting development or when encountering questions to ensure alignment in direction and avoid redundant efforts or unnecessary commits.</li>
|
||||
</ul>
|
||||
<p><strong>Reference materials:</strong></p>
|
||||
<ul>
|
||||
<li><a href="https://www.contributor-covenant.org/version/2/1/code_of_conduct/">Contributor Covenant</a></li>
|
||||
<li><a href="https://opensource.guide/how-to-contribute/">How to Contribute to Open Source</a></li>
|
||||
</ul>
|
||||
<h1>✉️ Contact the Author</h1>
|
||||
<ul>
|
||||
<li>Author's Email:yonglelolu@foxmail.com</li>
|
||||
<li>Author's WeChat: Downloader_Tools</li>
|
||||
<li><b>Discord Community</b>: <a href="https://discord.com/invite/ZYtmgKud9Y">Click to Join the Community</a></li>
|
||||
</ul>
|
||||
<p>✨ <b>Other Open Source Projects by the Author:</b></p>
|
||||
<ul>
|
||||
<li><b>DouK-Downloader(抖音、TikTok)</b>:<a href="https://github.com/JoeanAmier/TikTokDownloader">https://github.com/JoeanAmier/TikTokDownloader</a></li>
|
||||
<li><b>KS-Downloader(快手、KuaiShou)</b>:<a href="https://github.com/JoeanAmier/KS-Downloader">https://github.com/JoeanAmier/KS-Downloader</a></li>
|
||||
</ul>
|
||||
|
||||
# 💰 Project Sponsorship
|
||||
|
||||
## DartNode
|
||||
|
||||
[](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
|
||||
|
||||
***
|
||||
|
||||
## ZMTO
|
||||
|
||||
<a href="https://www.zmto.com/"><img src="https://console.zmto.com/templates/2019/dist/images/logo_dark.svg" alt="ZMTO"></a>
|
||||
<p><a href="https://www.zmto.com/">ZMTO</a>: A professional cloud infrastructure provider offering sophisticated solutions with reliable technology and expert support. We also empower qualified open source initiatives with enterprise-grade VPS infrastructure, driving sustainable development and innovation in the open source ecosystem. </p>
|
||||
<h1>⚠️ Disclaimer</h1>
|
||||
<ol>
|
||||
<li>The user's use of this project is entirely at their own discretion and responsibility. The author assumes no liability for any losses, claims, or risks arising from the user's use of this project.</li>
|
||||
<li>The code and functionalities provided by the author of this project are based on current knowledge and technological developments. The author strives to ensure the correctness and security of the code according to existing technical capabilities but does not guarantee that the code is entirely free of errors or defects.</li>
|
||||
<li>All third-party libraries, plugins, or services relied upon by this project follow their respective open-source or commercial licenses. Users must review and comply with those license agreements. The author assumes no responsibility for the stability, security, or compliance of third-party components.</li>
|
||||
<li>Users must strictly comply with the requirements of the <a href="https://github.com/JoeanAmier/XHS-Downloader/blob/master/LICENSE">GNU General Public License v3.0</a> when using this project and properly indicate that the code was used under the <a href="https://github.com/JoeanAmier/XHS-Downloader/blob/master/LICENSE">GNU General Public License v3.0</a>.</li>
|
||||
<li>When using the code and features of this project, users must independently research relevant laws and regulations and ensure their actions are legal and compliant. Any legal liabilities or risks arising from violations of laws and regulations shall be borne solely by the user.</li>
|
||||
<li>Users must not use this tool to engage in any activities that infringe intellectual property rights, including but not limited to downloading or distributing copyright-protected content without authorization. The developers do not participate in, support, or endorse any unauthorized acquisition or distribution of illegal content.</li>
|
||||
<li>This project assumes no responsibility for the compliance of any data processing activities (including collection, storage, and transmission) conducted by users. Users must comply with relevant laws and regulations and ensure that their processing activities are lawful and proper. Legal liabilities resulting from non-compliant operations shall be borne by the user.</li>
|
||||
<li>Under no circumstances may users associate the author, contributors, or other related parties of this project with their usage of the project, nor may they hold these parties responsible for any loss or damage arising from such usage.</li>
|
||||
<li>The author of this project will not provide a paid version of the XHS-Downloader project, nor will they offer any commercial services related to the XHS-Downloader project.</li>
|
||||
<li>Any secondary development, modification, or compilation based on this project is unrelated to the original author. The original author assumes no liability for any consequences resulting from such secondary development. Users bear full responsibility for all outcomes arising from such modifications.</li>
|
||||
<li>This project grants no patent licenses; if the use of this project leads to patent disputes or infringement, the user bears all associated risks and responsibilities. Without written authorization from the author or rights holder, users may not use this project for any commercial promotion, marketing, or re-licensing.</li>
|
||||
<li>The author reserves the right to terminate service to any user who violates this disclaimer at any time and may require them to destroy all obtained code and derivative works.</li>
|
||||
<li>The author reserves the right to update this disclaimer at any time without prior notice. Continued use of the project constitutes acceptance of the revised terms.</li>
|
||||
</ol>
|
||||
<b>Before using the code and functionalities of this project, please carefully consider and accept the above disclaimer. If you have any questions or disagree with the statement, please do not use the code and functionalities of this project. If you use the code and functionalities of this project, it is considered that you fully understand and accept the above disclaimer, and willingly assume all risks and consequences associated with the use of this project.</b>
|
||||
|
||||
# 💡 Project References
|
||||
|
||||
* https://github.com/encode/httpx/
|
||||
* https://github.com/tiangolo/fastapi
|
||||
* https://github.com/textualize/textual/
|
||||
* https://github.com/pyinstaller/pyinstaller
|
||||
* https://github.com/zbowling/beartype-pyinstaller-repro
|
||||
* https://github.com/jlowin/fastmcp
|
||||
* https://github.com/omnilib/aiosqlite
|
||||
* https://github.com/carpedm20/emoji/
|
||||
* https://github.com/asweigart/pyperclip
|
||||
* https://github.com/lxml/lxml
|
||||
* https://github.com/yaml/pyyaml
|
||||
* https://github.com/pallets/click/
|
||||
* https://github.com/encode/uvicorn
|
||||
* https://github.com/Tinche/aiofiles
|
||||
118
example.py
Normal file
@ -0,0 +1,118 @@
|
||||
from asyncio import run
|
||||
from pyperclip import paste
|
||||
from httpx import post
|
||||
from rich import print
|
||||
|
||||
from source import XHS
|
||||
|
||||
|
||||
async def example():
|
||||
"""通过代码设置参数,适合二次开发"""
|
||||
# 示例链接
|
||||
demo_link = "https://www.xiaohongshu.com/explore/XXX?xsec_token=XXX"
|
||||
|
||||
# 实例对象
|
||||
work_path = "D:\\" # 作品数据/文件保存根路径,默认值:项目根路径
|
||||
folder_name = "Download" # 作品文件储存文件夹名称(自动创建),默认值:Download
|
||||
name_format = "作品标题 作品描述"
|
||||
user_agent = "" # User-Agent
|
||||
cookie = "" # 小红书网页版 Cookie,无需登录,可选参数,登录状态对数据采集有影响
|
||||
proxy = None # 网络代理
|
||||
timeout = 5 # 请求数据超时限制,单位:秒,默认值:10
|
||||
chunk = 1024 * 1024 * 10 # 下载文件时,每次从服务器获取的数据块大小,单位:字节
|
||||
max_retry = 2 # 请求数据失败时,重试的最大次数,单位:秒,默认值:5
|
||||
record_data = False # 是否保存作品数据至文件
|
||||
image_format = "WEBP" # 图文作品文件下载格式,支持:AUTO、PNG、WEBP、JPEG、HEIC
|
||||
folder_mode = False # 是否将每个作品的文件储存至单独的文件夹
|
||||
image_download = True # 图文、图集作品文件下载开关
|
||||
video_download = True # 视频作品文件下载开关
|
||||
live_download = False # 图文动图文件下载开关
|
||||
download_record = True # 是否记录下载成功的作品 ID
|
||||
language = "zh_CN" # 设置程序提示语言
|
||||
author_archive = True # 是否将每个作者的作品存至单独的文件夹
|
||||
write_mtime = True # 是否将作品文件的 修改时间 修改为作品的发布时间
|
||||
read_cookie = None # 读取浏览器 Cookie,支持设置浏览器名称(字符串)或者浏览器序号(整数),设置为 None 代表不读取
|
||||
|
||||
# async with XHS() as xhs:
|
||||
# pass # 使用默认参数
|
||||
|
||||
async with XHS(
|
||||
work_path=work_path,
|
||||
folder_name=folder_name,
|
||||
name_format=name_format,
|
||||
user_agent=user_agent,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
timeout=timeout,
|
||||
chunk=chunk,
|
||||
max_retry=max_retry,
|
||||
record_data=record_data,
|
||||
image_format=image_format,
|
||||
folder_mode=folder_mode,
|
||||
image_download=image_download,
|
||||
video_download=video_download,
|
||||
live_download=live_download,
|
||||
download_record=download_record,
|
||||
language=language,
|
||||
read_cookie=read_cookie,
|
||||
author_archive=author_archive,
|
||||
write_mtime=write_mtime,
|
||||
) as xhs: # 使用自定义参数
|
||||
download = True # 是否下载作品文件,默认值:False
|
||||
# 返回作品详细信息,包括下载地址
|
||||
# 获取数据失败时返回空字典
|
||||
print(
|
||||
await xhs.extract(
|
||||
demo_link,
|
||||
download,
|
||||
index=[
|
||||
1,
|
||||
2,
|
||||
5,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def example_api():
|
||||
"""通过 API 设置参数,适合二次开发"""
|
||||
server = "http://127.0.0.1:5556/xhs/detail"
|
||||
data = {
|
||||
"url": "", # 必需参数
|
||||
"download": True,
|
||||
"index": [
|
||||
3,
|
||||
6,
|
||||
9,
|
||||
],
|
||||
"proxy": "http://127.0.0.1:10808",
|
||||
}
|
||||
response = post(server, json=data, timeout=10)
|
||||
print(response.json())
|
||||
|
||||
|
||||
async def test():
|
||||
url = "" or paste()
|
||||
if not url:
|
||||
return
|
||||
async with XHS(
|
||||
download_record=False,
|
||||
# image_format="PNG",
|
||||
# image_format="WEBP",
|
||||
# image_format="JPEG",
|
||||
# image_format="HEIC",
|
||||
# image_format="AVIF",
|
||||
# image_format="AUTO",
|
||||
) as xhs:
|
||||
print(
|
||||
await xhs.extract(
|
||||
url,
|
||||
# download=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# run(example())
|
||||
# run(example_api())
|
||||
run(test())
|
||||
30
locale/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 命令参考
|
||||
|
||||
**运行命令前,确保已经安装了 `gettext` 软件包,并配置好环境变量。**
|
||||
|
||||
**Before running the command, ensure that the `gettext` package is installed and the environment variables are properly
|
||||
configured.**
|
||||
|
||||
* `xgettext --files-from=py_files.txt -d xhs -o xhs.pot`
|
||||
* `mkdir zh_CN\LC_MESSAGES`
|
||||
* `msginit -l zh_CN -o zh_CN/LC_MESSAGES/xhs.po -i xhs.pot`
|
||||
* `mkdir en_US\LC_MESSAGES`
|
||||
* `msginit -l en_US -o en_US/LC_MESSAGES/xhs.po -i xhs.pot`
|
||||
* `msgmerge -U zh_CN/LC_MESSAGES/xhs.po xhs.pot`
|
||||
* `msgmerge -U en_US/LC_MESSAGES/xhs.po xhs.pot`
|
||||
|
||||
# 翻译贡献指南
|
||||
|
||||
* 如果想要贡献支持更多语言,请在终端切换至 `locale` 文件夹,运行命令
|
||||
`msginit -l 语言代码 -o 语言代码/LC_MESSAGES/xhs.po -i xhs.pot`
|
||||
生成 po 文件并编辑翻译。
|
||||
* 如果想要贡献改进翻译结果,请直接编辑 `xhs.po` 文件内容。
|
||||
* 仅需提交 `xhs.po` 文件,作者会转换格式并合并。
|
||||
|
||||
# Translation Contribution Guide
|
||||
|
||||
* If you want to contribute support for more languages, please switch to the `locale` folder in the terminal and run the
|
||||
command `msginit -l language_code -o language_code/LC_MESSAGES/xhs.po -i xhs.pot` to generate the po file and edit the
|
||||
translation.
|
||||
* If you want to contribute to improving the translation, please directly edit the content of the `xhs.po` file.
|
||||
* Only the `xhs.po` file needs to be submitted, and the author will convert the format and merge it.
|
||||
BIN
locale/en_US/LC_MESSAGES/xhs.mo
Normal file
704
locale/en_US/LC_MESSAGES/xhs.po
Normal file
@ -0,0 +1,704 @@
|
||||
# English translations for XHS-Downloader package.
|
||||
# Copyright (C) 2024 THE XHS-Downloader'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the XHS-Downloader package.
|
||||
# FIRST AUTHOR <yonglelolu@foxmail.com>, 2024.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: XHS-Downloader 2.7\n"
|
||||
"Report-Msgid-Bugs-To: <yonglelolu@foxmail.com>\n"
|
||||
"POT-Creation-Date: 2025-08-10 23:34+0800\n"
|
||||
"PO-Revision-Date: 2024-12-22 14:14+0800\n"
|
||||
"Last-Translator: <yonglelolu@foxmail.com>\n"
|
||||
"Language-Team: English\n"
|
||||
"Language: en_US\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:181
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过下载"
|
||||
msgstr "works {0} has a download record, skip download"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:198
|
||||
msgid "提取作品文件下载地址失败"
|
||||
msgstr "Failed to extract the download address for the RedNote works files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:228
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:255
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:598
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:785
|
||||
msgid "提取小红书作品链接失败"
|
||||
msgstr "Failed to extract the links for RedNote works"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:230
|
||||
#, python-brace-format
|
||||
msgid "共 {0} 个小红书作品待处理..."
|
||||
msgstr "{0} works from RedNote are awaiting processing..."
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:317
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过处理"
|
||||
msgstr "Works {0} has a download record, skip processing"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:320
|
||||
#, python-brace-format
|
||||
msgid "开始处理作品:{0}"
|
||||
msgstr "Start processing the works: {0}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:329
|
||||
#, python-brace-format
|
||||
msgid "{0} 获取数据失败"
|
||||
msgstr "{0} failed to retrieve data"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:334
|
||||
#, python-brace-format
|
||||
msgid "{0} 提取数据失败"
|
||||
msgstr "{0} failed to extract data"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:336
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:83
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "视频"
|
||||
msgstr "video"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:339
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:91
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:80
|
||||
msgid "图文"
|
||||
msgstr "image"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:340
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:92
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "图集"
|
||||
msgstr "LivePhoto"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:344
|
||||
#, python-brace-format
|
||||
msgid "未知的作品类型:{0}"
|
||||
msgstr "Unknown works type: {0}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:349
|
||||
#, python-brace-format
|
||||
msgid "作品处理完成:{0}"
|
||||
msgstr "works processing completed: {0}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:427
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:57
|
||||
msgid ""
|
||||
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,"
|
||||
"如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
|
||||
msgstr ""
|
||||
"The program will automatically read and extract the link to RedNote works "
|
||||
"from the clipboard, and automatically download the corresponding work file. "
|
||||
"If you want to close it, please click the close button or write the "
|
||||
"\"close\" text to the clipboard!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:569
|
||||
msgid "跳转至项目 GitHub 仓库"
|
||||
msgstr "Jump to the project's GitHub repository"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:570
|
||||
msgid "重定向至项目 GitHub 仓库主页"
|
||||
msgstr "Redirect to the project's GitHub repository homepage"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:578
|
||||
msgid "获取作品数据及下载地址"
|
||||
msgstr "Fetch works data and download links"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:610
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:795
|
||||
msgid "获取小红书作品数据成功"
|
||||
msgstr "Successfully obtained data on RedNote works"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:612
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:797
|
||||
msgid "获取小红书作品数据失败"
|
||||
msgstr "Failed to obtain data on RedNote works"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:682
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:727
|
||||
msgid "小红书作品链接"
|
||||
msgstr "Link to RedNote works"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:730
|
||||
msgid "指定需要下载的图文作品序号"
|
||||
msgstr "Specify the serial number of the images works to be downloaded"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:734
|
||||
msgid "是否需要返回作品信息数据"
|
||||
msgstr "Whether to return works information data"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:748
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753
|
||||
msgid "作品文件下载任务执行完毕"
|
||||
msgstr "Works file download task completed"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:758
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:763
|
||||
msgid "作品文件下载任务未执行"
|
||||
msgstr "Works file download task not executed"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:133
|
||||
msgid "视频作品下载功能已关闭,跳过下载"
|
||||
msgstr "The video download function has been turned off, skip download"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:150
|
||||
msgid "图文作品下载功能已关闭,跳过下载"
|
||||
msgstr "The image download function has been turned off, skip download"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:185
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:196
|
||||
#, python-brace-format
|
||||
msgid "{0} 文件已存在,跳过下载"
|
||||
msgstr "{0} already exists, skipping download"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:242
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 缓存异常,重新下载"
|
||||
msgstr "File {0} cache exception, download again"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:270
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 下载成功"
|
||||
msgstr "file {0} download successful"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:276
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 下载失败,错误信息: {1}"
|
||||
msgstr "Network error, {0} download failed, error message: {1}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:353
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 格式判断失败,错误信息:{1}"
|
||||
msgstr "Format recognition failed for file {0}, error message: {1}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:53
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:58
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:77
|
||||
msgid "未知"
|
||||
msgstr "unknown"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:65
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 请求失败: {1}"
|
||||
msgstr "Network error, {0} request failed: {1}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:124
|
||||
msgid "小红书作品链接,多个链接使用空格分隔"
|
||||
msgstr "RedNote works links, separate multiple links with spaces"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:131
|
||||
msgid ""
|
||||
"下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\""
|
||||
msgstr ""
|
||||
"Download images files with specified serial numbers, only effective for images "
|
||||
"works; Example of multiple serial numbers input: \"1 3 5 7\""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136
|
||||
msgid "作品数据/文件保存根路径"
|
||||
msgstr "Root path for saving works data / files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:137
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40
|
||||
msgid "作品文件储存文件夹名称"
|
||||
msgstr "Name of the folder for storing works files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:138
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49
|
||||
msgid "作品文件名称格式"
|
||||
msgstr "Format of works file name"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:140
|
||||
msgid "小红书网页版 Cookie,无需登录"
|
||||
msgstr "RedNote web version cookie, no need to log in"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:78
|
||||
msgid "网络代理"
|
||||
msgstr "Network proxy"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:88
|
||||
msgid "请求数据超时限制,单位:秒"
|
||||
msgstr "Network request timeout limit, in seconds"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:148
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:98
|
||||
msgid "下载文件时,每次从服务器获取的数据块大小,单位:字节"
|
||||
msgstr ""
|
||||
"When downloading a file, the size of the data block obtained from the server "
|
||||
"each time, in bytes"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:151
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:108
|
||||
msgid "请求数据失败时,重试的最大次数"
|
||||
msgstr "The maximum number of retries when data request fails"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:152
|
||||
msgid "是否记录作品数据至文件"
|
||||
msgstr "Record works data to file"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:157
|
||||
msgid "图文作品文件下载格式,支持:PNG、WEBP"
|
||||
msgstr "Image works file download format, supporting: PNG, WEBP"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:159
|
||||
msgid "动态图片下载开关"
|
||||
msgstr "LivePhoto download switch"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:160
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:149
|
||||
msgid "作品下载记录开关"
|
||||
msgstr "Download record switch"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165
|
||||
msgid "是否将每个作品的文件储存至单独的文件夹"
|
||||
msgstr "Whether to save each work's files into separate folders"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:171
|
||||
msgid "是否将每个作者的作品储存至单独的文件夹"
|
||||
msgstr "Whether to save each author's works into separate folders"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:178
|
||||
msgid "是否将作品文件的修改时间属性修改为作品的发布时间"
|
||||
msgstr ""
|
||||
"Would you like to set the file's modified time attribute to match the work's "
|
||||
"publication time"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:182
|
||||
msgid "设置程序语言,目前支持:zh_CN、en_US"
|
||||
msgstr "Set the programming language, currently supports: zh_CN、en_US"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183
|
||||
msgid "读取指定配置文件"
|
||||
msgstr "Read specified configuration file"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:190
|
||||
#, python-brace-format
|
||||
msgid "从指定的浏览器读取小红书网页版 Cookie,支持:{0}; 输入浏览器名称或序号"
|
||||
msgstr ""
|
||||
"Read RedNote web version cookies from the specified browser, supporting: "
|
||||
"{0}; Enter browser name or serial number"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:203
|
||||
msgid "是否更新配置文件"
|
||||
msgstr "Do you need to update the configuration file"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:204
|
||||
msgid "查看详细参数说明"
|
||||
msgstr "View detailed parameter descriptions"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:205
|
||||
msgid "查看 XHS-Downloader 版本"
|
||||
msgstr "View XHS Downloader Version"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:53
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"读取指定浏览器的 Cookie 并写入配置文件\n"
|
||||
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 "
|
||||
"Cookie!\n"
|
||||
"{options}\n"
|
||||
"请输入浏览器名称或序号:"
|
||||
msgstr ""
|
||||
"Read cookies from the specified browser and write them to the configuration "
|
||||
"file\n"
|
||||
"The Windows system requires running programs as an administrator to read "
|
||||
"Chromium, Chrome, Edge browser cookies!\n"
|
||||
"{options}\n"
|
||||
"Please enter your browser name or serial number:"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:63
|
||||
msgid "未选择浏览器!"
|
||||
msgstr "Browser not selected!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:75
|
||||
msgid "浏览器名称或序号输入错误!"
|
||||
msgstr "Browser name or serial number input error!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:81
|
||||
msgid "获取 Cookie 失败,未找到 Cookie 数据!"
|
||||
msgstr "Failed to retrieve cookie, no cookie data found!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:119
|
||||
msgid "从浏览器读取 Cookie 功能不支持当前平台!"
|
||||
msgstr ""
|
||||
"The cookie reading function from the browser is not supported on the current "
|
||||
"platform!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\cleaner.py:45
|
||||
msgid "不受支持的操作系统类型,可能无法正常去除非法字符!"
|
||||
msgstr ""
|
||||
"Unsupported operating system type, may not be able to remove illegal "
|
||||
"characters properly!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:232
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试成功"
|
||||
msgstr "Agent {0} tested successfully"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:236
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试超时"
|
||||
msgstr "Agent {0} test timeout"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试失败:{1}"
|
||||
msgstr "Agent {0} test failed: {1}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:57
|
||||
#, python-brace-format
|
||||
msgid "{old_folder} 文件夹不存在,跳过处理"
|
||||
msgstr "{old_folder} directory does not exist, skipping processing"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:86
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:108
|
||||
msgid "文件夹"
|
||||
msgstr "folder"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:91
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_folder} 已重命名为 {new_folder}"
|
||||
msgstr "The folder {old_folder} has been renamed to {new_folder}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:113
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_} 重命名为 {new_}"
|
||||
msgstr "The folder {old_} has been renamed to {new_}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:186
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:201
|
||||
msgid "文件"
|
||||
msgstr "file"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:191
|
||||
#, python-brace-format
|
||||
msgid "文件 {old_file} 重命名为 {new_file}"
|
||||
msgstr "The file {old_file} has been renamed to {new_file}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:210
|
||||
#, python-brace-format
|
||||
msgid "{type} {old}被占用,重命名失败: {error}"
|
||||
msgstr "{type} {old} is occupied, renaming failed: {error}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:219
|
||||
#, python-brace-format
|
||||
msgid "{type} {new}名称重复,重命名失败: {error}"
|
||||
msgstr "{type} {new} already exists, renaming failed: {error}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:228
|
||||
#, python-brace-format
|
||||
msgid "处理{type} {old}时发生预期之外的错误: {error}"
|
||||
msgstr "An unexpected error occurred while processing {type} {old}: {error}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\tools.py:31
|
||||
msgid ""
|
||||
"如需重新尝试处理该对象,请关闭所有正在访问该对象的窗口或程序,然后直接按下回"
|
||||
"车键!\n"
|
||||
"如需跳过处理该对象,请输入任意字符后按下回车键!"
|
||||
msgstr ""
|
||||
"If you want to retry processing this object, please close all windows or "
|
||||
"programs currently accessing it, then press Enter directly!\n"
|
||||
"If you want to skip processing this object, please enter any character and "
|
||||
"then press Enter!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:20
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:29
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:15
|
||||
msgid "退出程序"
|
||||
msgstr "Quit"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:30
|
||||
msgid "检查更新"
|
||||
msgstr "Update"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:22
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:35
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:16
|
||||
msgid "返回首页"
|
||||
msgstr "Return"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:35
|
||||
msgid "如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"
|
||||
msgstr ""
|
||||
"If XHS-Downloader is helpful to you, please consider giving it Star. Thank "
|
||||
"you for your support!"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:42
|
||||
msgid "Discord 社区"
|
||||
msgstr "Discord Community"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:46
|
||||
msgid "邀请链接:"
|
||||
msgstr "Invitation link: "
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:48
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:61
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:70
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:56
|
||||
msgid "点击访问"
|
||||
msgstr "Click to visit"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:51
|
||||
msgid "作者的其他开源项目"
|
||||
msgstr "Other open-source projects of the author"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:31
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:212
|
||||
msgid "程序设置"
|
||||
msgstr "Settings"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:32
|
||||
msgid "下载记录"
|
||||
msgstr "Record"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:33
|
||||
msgid "开启监听"
|
||||
msgstr "Monitor"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:34
|
||||
msgid "关于项目"
|
||||
msgstr "About"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:49
|
||||
msgid "开源协议: "
|
||||
msgstr "Open source protocol: "
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:52
|
||||
msgid "项目地址: "
|
||||
msgstr "Repository link: "
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:59
|
||||
msgid "请输入小红书图文/视频作品链接"
|
||||
msgstr "Please enter the link to the RedNote image or video works"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:62
|
||||
msgid "多个链接之间使用空格分隔"
|
||||
msgstr "Separate multiple links with spaces"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:64
|
||||
msgid "下载无水印作品文件"
|
||||
msgstr "Download images/video files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:65
|
||||
msgid "读取剪贴板"
|
||||
msgstr "Read the clipboard"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:66
|
||||
msgid "清空输入框"
|
||||
msgstr "Clear the input box"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:81
|
||||
msgid "免责声明\n"
|
||||
msgstr ""
|
||||
"Disclaimer for XHS-Downloader:\n"
|
||||
"\n"
|
||||
"1.The use of this project is entirely at the user's own discretion and "
|
||||
"risk. The author assumes no responsibility or liability of any kind for any "
|
||||
"loss, damage, or risk arising from the user's use of this project.\n"
|
||||
"2.The code and functionalities provided by the author of this project are "
|
||||
"developed based on current knowledge and technology. The author makes every "
|
||||
"effort to ensure the correctness and security of the code according to "
|
||||
"current technical standards but does not guarantee that the code is "
|
||||
"completely free of errors or defects.\n"
|
||||
"3.All third-party libraries, plugins, or services used by this project "
|
||||
"follow their original open-source or commercial licenses. Users must review "
|
||||
"and comply with these license agreements accordingly. The author assumes no "
|
||||
"responsibility for the stability, security, or compliance of any third-party "
|
||||
"components.\n"
|
||||
"4.When using this project, users must strictly comply with the requirements "
|
||||
"of the GNU General Public License v3.0 and clearly indicate in appropriate "
|
||||
"places that the code was used under the GNU General Public License v3.0.\n"
|
||||
"5.When using the code and functionalities of this project, users must "
|
||||
"independently research relevant laws and regulations and ensure that their "
|
||||
"usage is legal and compliant. Any legal liabilities or risks arising from "
|
||||
"violations of laws and regulations shall be borne solely by the user.\n"
|
||||
"6.Users must not use this tool to engage in any activities that infringe "
|
||||
"intellectual property rights, including but not limited to downloading or "
|
||||
"distributing copyrighted content without authorization. Developers do not "
|
||||
"participate in, support, or endorse the acquisition or distribution of any "
|
||||
"illegal or unauthorized content.\n"
|
||||
"7.This project assumes no responsibility for the compliance of data "
|
||||
"processing activities (including collection, storage, and transmission) "
|
||||
"performed by users. Users must comply with relevant laws and regulations and "
|
||||
"ensure that such activities are lawful and proper. Legal liabilities "
|
||||
"resulting from non-compliant operations shall be borne by the user.\n"
|
||||
"8.Under no circumstances may users associate the author, contributors, or "
|
||||
"other related parties of this project with their usage of the project, nor "
|
||||
"may they hold these parties liable for any loss or damage resulting from "
|
||||
"such usage.\n"
|
||||
"9.The author of this project will not provide a paid version of the XHS-"
|
||||
"Downloader project, nor will they offer any commercial services related to "
|
||||
"it.\n"
|
||||
"10.Any secondary development, modification, or compilation based on this "
|
||||
"project is unrelated to the original author. The original author assumes no "
|
||||
"liability for any consequences resulting from such secondary development. "
|
||||
"Users bear full responsibility for all outcomes arising from such "
|
||||
"modifications.\n"
|
||||
"11.This project does not grant users any patent licenses. If the use of "
|
||||
"this project leads to patent disputes or infringement, the user assumes all "
|
||||
"associated risks and responsibilities. Without written authorization from "
|
||||
"the author or rights holder, users may not use this project for any "
|
||||
"commercial promotion, advertising, or re-licensing.\n"
|
||||
"12.The author reserves the right to terminate service to any user who "
|
||||
"violates this disclaimer at any time and may require them to destroy all "
|
||||
"obtained code and derivative works.\n"
|
||||
"13.The author reserves the right to update this disclaimer at any time "
|
||||
"without prior notice. Continued use of the project constitutes acceptance of "
|
||||
"the revised terms.\n"
|
||||
"\n"
|
||||
"Before using the code and functionalities of this project, please carefully "
|
||||
"consider and accept the above disclaimer. If you have any questions or "
|
||||
"disagree with the above statements, please do not use the code and "
|
||||
"functionalities of this project. If you do use the code and functionalities "
|
||||
"of this project, it shall be deemed that you have fully understood and "
|
||||
"accepted the above disclaimer and voluntarily assume all risks and "
|
||||
"consequences associated with its use.\n"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:94
|
||||
msgid "未输入任何小红书作品链接"
|
||||
msgstr "No RedNote works links provided"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:124
|
||||
msgid "下载小红书作品文件失败"
|
||||
msgstr "Failed to download the RedNote works files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\loading.py:19
|
||||
msgid "程序处理中..."
|
||||
msgstr "Processing..."
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:22
|
||||
msgid "关闭监听"
|
||||
msgstr "Close"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:34
|
||||
msgid "已启动监听剪贴板模式"
|
||||
msgstr "Currently in monitoring clipboard mode"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:36
|
||||
msgid "退出监听剪贴板模式"
|
||||
msgstr "Exit monitoring clipboard mode"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:23
|
||||
msgid "请输入待删除的小红书作品链接或作品 ID"
|
||||
msgstr "Please enter the link or ID of the RedNote works to be deleted"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:26
|
||||
msgid ""
|
||||
"支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
|
||||
msgstr ""
|
||||
"Support input of works ID or links containing works ID, with multiple links "
|
||||
"or IDs separated by spaces"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:32
|
||||
msgid "删除指定作品 ID"
|
||||
msgstr "Delete specified works ID"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:47
|
||||
msgid "删除下载记录成功"
|
||||
msgstr "Successfully deleted download record"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:30
|
||||
msgid "作品数据 / 文件保存根路径"
|
||||
msgstr "Root path for saving works data / files"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:35
|
||||
msgid "程序根路径"
|
||||
msgstr "Program root path"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:64
|
||||
msgid "内置 Chrome User Agent"
|
||||
msgstr "Chrome User Agent"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:69
|
||||
msgid "小红书网页版 Cookie"
|
||||
msgstr "RedNote Web Cookie"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:83
|
||||
msgid "不使用代理"
|
||||
msgstr "No proxy"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:120
|
||||
msgid "记录作品详细数据"
|
||||
msgstr "Record works data"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:125
|
||||
msgid "作品归档保存模式"
|
||||
msgstr "Works Archiving Mode"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:130
|
||||
msgid "视频作品下载开关"
|
||||
msgstr "Video download switch"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:135
|
||||
msgid "图文作品下载开关"
|
||||
msgstr "Image download switch"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:144
|
||||
msgid "动图文件下载开关"
|
||||
msgstr "LivePhoto download switch"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:154
|
||||
msgid "作者归档保存模式"
|
||||
msgstr "Author Archiving Mode"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:159
|
||||
msgid "更新文件修改时间"
|
||||
msgstr "Update File Modification Time"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:167
|
||||
msgid "图片下载格式"
|
||||
msgstr "Image download format"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:171
|
||||
msgid "程序语言"
|
||||
msgstr "Program language"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:194
|
||||
msgid "保存配置"
|
||||
msgstr "Save configuration"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:198
|
||||
msgid "放弃更改"
|
||||
msgstr "Discard changes"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:208
|
||||
msgid "小红书网页版 Cookie,无需登录,参数已设置"
|
||||
msgstr ""
|
||||
"RedNote web version cookie, no login required, parameters have been set"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:209
|
||||
msgid "小红书网页版 Cookie,无需登录,参数未设置"
|
||||
msgstr "RedNote web version cookie, no login required, parameters not set"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:26
|
||||
msgid "正在检查新版本,请稍等..."
|
||||
msgstr "Checking for new version, please wait..."
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:46
|
||||
#, python-brace-format
|
||||
msgid "检测到新版本:{0}.{1}"
|
||||
msgstr "Detected new version: {0} {1}"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:54
|
||||
msgid "当前版本为开发版, 可更新至正式版"
|
||||
msgstr "Detected a new official version"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:59
|
||||
msgid "当前已是最新开发版"
|
||||
msgstr "You are already using the latest development version"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:64
|
||||
msgid "当前已是最新正式版"
|
||||
msgstr "You are already using the latest official version"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:71
|
||||
msgid "检测新版本失败"
|
||||
msgstr "Failed to check for a new version"
|
||||
17
locale/generate_path.py
Normal file
@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def find_python_files(dir_, file):
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
for py_file in dir_.rglob("*.py"): # 递归查找所有 .py 文件
|
||||
f.write(str(py_file) + "\n") # 写入文件路径
|
||||
|
||||
|
||||
# 设置源目录和输出文件
|
||||
source_directory = ROOT.joinpath("source") # 源目录
|
||||
output_file = "py_files.txt" # 输出文件名
|
||||
|
||||
find_python_files(source_directory, output_file)
|
||||
print(f"所有 .py 文件路径已保存到 {output_file}")
|
||||
24
locale/po_to_mo.py
Normal file
@ -0,0 +1,24 @@
|
||||
from pathlib import Path
|
||||
from subprocess import run
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def scan_directory():
|
||||
return [
|
||||
item.joinpath("LC_MESSAGES/xhs.po") for item in ROOT.iterdir() if item.is_dir()
|
||||
]
|
||||
|
||||
|
||||
def generate_map(files: list[Path]):
|
||||
return [(i, i.with_suffix(".mo")) for i in files]
|
||||
|
||||
|
||||
def generate_mo(maps: list[tuple[Path, Path]]):
|
||||
for i, j in maps:
|
||||
command = f'msgfmt --check -o "{j}" "{i}"'
|
||||
print(run(command, shell=True, text=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_mo(generate_map(scan_directory()))
|
||||
610
locale/xhs.pot
Normal file
@ -0,0 +1,610 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: XHS-Downloader 2.7\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-08-10 23:34+0800\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:181
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:198
|
||||
msgid "提取作品文件下载地址失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:228
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:255
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:598
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:785
|
||||
msgid "提取小红书作品链接失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:230
|
||||
#, python-brace-format
|
||||
msgid "共 {0} 个小红书作品待处理..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:317
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过处理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:320
|
||||
#, python-brace-format
|
||||
msgid "开始处理作品:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:329
|
||||
#, python-brace-format
|
||||
msgid "{0} 获取数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:334
|
||||
#, python-brace-format
|
||||
msgid "{0} 提取数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:336
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:83
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "视频"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:339
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:91
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:80
|
||||
msgid "图文"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:340
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:92
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "图集"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:344
|
||||
#, python-brace-format
|
||||
msgid "未知的作品类型:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:349
|
||||
#, python-brace-format
|
||||
msgid "作品处理完成:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:427
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:57
|
||||
msgid ""
|
||||
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,"
|
||||
"如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:569
|
||||
msgid "跳转至项目 GitHub 仓库"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:570
|
||||
msgid "重定向至项目 GitHub 仓库主页"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:578
|
||||
msgid "获取作品数据及下载地址"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:610
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:795
|
||||
msgid "获取小红书作品数据成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:612
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:797
|
||||
msgid "获取小红书作品数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:682
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:727
|
||||
msgid "小红书作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:730
|
||||
msgid "指定需要下载的图文作品序号"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:734
|
||||
msgid "是否需要返回作品信息数据"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:748
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753
|
||||
msgid "作品文件下载任务执行完毕"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:758
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:763
|
||||
msgid "作品文件下载任务未执行"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:133
|
||||
msgid "视频作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:150
|
||||
msgid "图文作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:185
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:196
|
||||
#, python-brace-format
|
||||
msgid "{0} 文件已存在,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:242
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 缓存异常,重新下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:270
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 下载成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:276
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 下载失败,错误信息: {1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:353
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 格式判断失败,错误信息:{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:53
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:58
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:77
|
||||
msgid "未知"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:65
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 请求失败: {1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:124
|
||||
msgid "小红书作品链接,多个链接使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:131
|
||||
msgid ""
|
||||
"下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\""
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136
|
||||
msgid "作品数据/文件保存根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:137
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40
|
||||
msgid "作品文件储存文件夹名称"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:138
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49
|
||||
msgid "作品文件名称格式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:140
|
||||
msgid "小红书网页版 Cookie,无需登录"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:78
|
||||
msgid "网络代理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:88
|
||||
msgid "请求数据超时限制,单位:秒"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:148
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:98
|
||||
msgid "下载文件时,每次从服务器获取的数据块大小,单位:字节"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:151
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:108
|
||||
msgid "请求数据失败时,重试的最大次数"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:152
|
||||
msgid "是否记录作品数据至文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:157
|
||||
msgid "图文作品文件下载格式,支持:PNG、WEBP"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:159
|
||||
msgid "动态图片下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:160
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:149
|
||||
msgid "作品下载记录开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165
|
||||
msgid "是否将每个作品的文件储存至单独的文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:171
|
||||
msgid "是否将每个作者的作品储存至单独的文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:178
|
||||
msgid "是否将作品文件的修改时间属性修改为作品的发布时间"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:182
|
||||
msgid "设置程序语言,目前支持:zh_CN、en_US"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183
|
||||
msgid "读取指定配置文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:190
|
||||
#, python-brace-format
|
||||
msgid "从指定的浏览器读取小红书网页版 Cookie,支持:{0}; 输入浏览器名称或序号"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:203
|
||||
msgid "是否更新配置文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:204
|
||||
msgid "查看详细参数说明"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:205
|
||||
msgid "查看 XHS-Downloader 版本"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:53
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"读取指定浏览器的 Cookie 并写入配置文件\n"
|
||||
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 "
|
||||
"Cookie!\n"
|
||||
"{options}\n"
|
||||
"请输入浏览器名称或序号:"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:63
|
||||
msgid "未选择浏览器!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:75
|
||||
msgid "浏览器名称或序号输入错误!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:81
|
||||
msgid "获取 Cookie 失败,未找到 Cookie 数据!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:119
|
||||
msgid "从浏览器读取 Cookie 功能不支持当前平台!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\cleaner.py:45
|
||||
msgid "不受支持的操作系统类型,可能无法正常去除非法字符!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:232
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:236
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试超时"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试失败:{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:57
|
||||
#, python-brace-format
|
||||
msgid "{old_folder} 文件夹不存在,跳过处理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:86
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:108
|
||||
msgid "文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:91
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_folder} 已重命名为 {new_folder}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:113
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_} 重命名为 {new_}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:186
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:201
|
||||
msgid "文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:191
|
||||
#, python-brace-format
|
||||
msgid "文件 {old_file} 重命名为 {new_file}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:210
|
||||
#, python-brace-format
|
||||
msgid "{type} {old}被占用,重命名失败: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:219
|
||||
#, python-brace-format
|
||||
msgid "{type} {new}名称重复,重命名失败: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:228
|
||||
#, python-brace-format
|
||||
msgid "处理{type} {old}时发生预期之外的错误: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\tools.py:31
|
||||
msgid ""
|
||||
"如需重新尝试处理该对象,请关闭所有正在访问该对象的窗口或程序,然后直接按下回"
|
||||
"车键!\n"
|
||||
"如需跳过处理该对象,请输入任意字符后按下回车键!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:20
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:29
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:15
|
||||
msgid "退出程序"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:30
|
||||
msgid "检查更新"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:22
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:35
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:16
|
||||
msgid "返回首页"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:35
|
||||
msgid "如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:42
|
||||
msgid "Discord 社区"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:46
|
||||
msgid "邀请链接:"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:48
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:61
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:70
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:56
|
||||
msgid "点击访问"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:51
|
||||
msgid "作者的其他开源项目"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:31
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:212
|
||||
msgid "程序设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:32
|
||||
msgid "下载记录"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:33
|
||||
msgid "开启监听"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:34
|
||||
msgid "关于项目"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:49
|
||||
msgid "开源协议: "
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:52
|
||||
msgid "项目地址: "
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:59
|
||||
msgid "请输入小红书图文/视频作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:62
|
||||
msgid "多个链接之间使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:64
|
||||
msgid "下载无水印作品文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:65
|
||||
msgid "读取剪贴板"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:66
|
||||
msgid "清空输入框"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:81
|
||||
msgid "免责声明\n"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:94
|
||||
msgid "未输入任何小红书作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:124
|
||||
msgid "下载小红书作品文件失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\loading.py:19
|
||||
msgid "程序处理中..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:22
|
||||
msgid "关闭监听"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:34
|
||||
msgid "已启动监听剪贴板模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:36
|
||||
msgid "退出监听剪贴板模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:23
|
||||
msgid "请输入待删除的小红书作品链接或作品 ID"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:26
|
||||
msgid ""
|
||||
"支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:32
|
||||
msgid "删除指定作品 ID"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:47
|
||||
msgid "删除下载记录成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:30
|
||||
msgid "作品数据 / 文件保存根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:35
|
||||
msgid "程序根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:64
|
||||
msgid "内置 Chrome User Agent"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:69
|
||||
msgid "小红书网页版 Cookie"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:83
|
||||
msgid "不使用代理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:120
|
||||
msgid "记录作品详细数据"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:125
|
||||
msgid "作品归档保存模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:130
|
||||
msgid "视频作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:135
|
||||
msgid "图文作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:144
|
||||
msgid "动图文件下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:154
|
||||
msgid "作者归档保存模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:159
|
||||
msgid "更新文件修改时间"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:167
|
||||
msgid "图片下载格式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:171
|
||||
msgid "程序语言"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:194
|
||||
msgid "保存配置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:198
|
||||
msgid "放弃更改"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:208
|
||||
msgid "小红书网页版 Cookie,无需登录,参数已设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:209
|
||||
msgid "小红书网页版 Cookie,无需登录,参数未设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:26
|
||||
msgid "正在检查新版本,请稍等..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:46
|
||||
#, python-brace-format
|
||||
msgid "检测到新版本:{0}.{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:54
|
||||
msgid "当前版本为开发版, 可更新至正式版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:59
|
||||
msgid "当前已是最新开发版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:64
|
||||
msgid "当前已是最新正式版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:71
|
||||
msgid "检测新版本失败"
|
||||
msgstr ""
|
||||
BIN
locale/zh_CN/LC_MESSAGES/xhs.mo
Normal file
648
locale/zh_CN/LC_MESSAGES/xhs.po
Normal file
@ -0,0 +1,648 @@
|
||||
# Chinese translations for XHS-Downloader package
|
||||
# Copyright (C) 2024 THE XHS-Downloader'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the XHS-Downloader package.
|
||||
# FIRST AUTHOR <yonglelolu@foxmail.com>, 2024.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: XHS-Downloader 2.7\n"
|
||||
"Report-Msgid-Bugs-To: <yonglelolu@foxmail.com>\n"
|
||||
"POT-Creation-Date: 2025-08-10 23:34+0800\n"
|
||||
"PO-Revision-Date: 2024-12-22 14:14+0800\n"
|
||||
"Last-Translator: <yonglelolu@foxmail.com>\n"
|
||||
"Language-Team: Chinese (simplified)\n"
|
||||
"Language: zh_CN\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:181
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:198
|
||||
msgid "提取作品文件下载地址失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:228
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:255
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:598
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:785
|
||||
msgid "提取小红书作品链接失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:230
|
||||
#, python-brace-format
|
||||
msgid "共 {0} 个小红书作品待处理..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:317
|
||||
#, python-brace-format
|
||||
msgid "作品 {0} 存在下载记录,跳过处理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:320
|
||||
#, python-brace-format
|
||||
msgid "开始处理作品:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:329
|
||||
#, python-brace-format
|
||||
msgid "{0} 获取数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:334
|
||||
#, python-brace-format
|
||||
msgid "{0} 提取数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:336
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:83
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "视频"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:339
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:91
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:80
|
||||
msgid "图文"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:340
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:92
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:79
|
||||
msgid "图集"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:344
|
||||
#, python-brace-format
|
||||
msgid "未知的作品类型:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:349
|
||||
#, python-brace-format
|
||||
msgid "作品处理完成:{0}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:427
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:57
|
||||
msgid ""
|
||||
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,"
|
||||
"如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:569
|
||||
msgid "跳转至项目 GitHub 仓库"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:570
|
||||
msgid "重定向至项目 GitHub 仓库主页"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:578
|
||||
msgid "获取作品数据及下载地址"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:610
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:795
|
||||
msgid "获取小红书作品数据成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:612
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:797
|
||||
msgid "获取小红书作品数据失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:682
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:727
|
||||
msgid "小红书作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:730
|
||||
msgid "指定需要下载的图文作品序号"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:734
|
||||
msgid "是否需要返回作品信息数据"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:748
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753
|
||||
msgid "作品文件下载任务执行完毕"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:758
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:763
|
||||
msgid "作品文件下载任务未执行"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:133
|
||||
msgid "视频作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:150
|
||||
msgid "图文作品下载功能已关闭,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:185
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:196
|
||||
#, python-brace-format
|
||||
msgid "{0} 文件已存在,跳过下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:242
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 缓存异常,重新下载"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:270
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 下载成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:276
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 下载失败,错误信息: {1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:353
|
||||
#, python-brace-format
|
||||
msgid "文件 {0} 格式判断失败,错误信息:{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:53
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:58
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:77
|
||||
msgid "未知"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:65
|
||||
#, python-brace-format
|
||||
msgid "网络异常,{0} 请求失败: {1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:124
|
||||
msgid "小红书作品链接,多个链接使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:131
|
||||
msgid ""
|
||||
"下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\""
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136
|
||||
msgid "作品数据/文件保存根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:137
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40
|
||||
msgid "作品文件储存文件夹名称"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:138
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49
|
||||
msgid "作品文件名称格式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:140
|
||||
msgid "小红书网页版 Cookie,无需登录"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:78
|
||||
msgid "网络代理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:88
|
||||
msgid "请求数据超时限制,单位:秒"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:148
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:98
|
||||
msgid "下载文件时,每次从服务器获取的数据块大小,单位:字节"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:151
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:108
|
||||
msgid "请求数据失败时,重试的最大次数"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:152
|
||||
msgid "是否记录作品数据至文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:157
|
||||
msgid "图文作品文件下载格式,支持:PNG、WEBP"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:159
|
||||
msgid "动态图片下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:160
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:149
|
||||
msgid "作品下载记录开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165
|
||||
msgid "是否将每个作品的文件储存至单独的文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:171
|
||||
msgid "是否将每个作者的作品储存至单独的文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:178
|
||||
msgid "是否将作品文件的修改时间属性修改为作品的发布时间"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:182
|
||||
msgid "设置程序语言,目前支持:zh_CN、en_US"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183
|
||||
msgid "读取指定配置文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:190
|
||||
#, python-brace-format
|
||||
msgid "从指定的浏览器读取小红书网页版 Cookie,支持:{0}; 输入浏览器名称或序号"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:203
|
||||
msgid "是否更新配置文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:204
|
||||
msgid "查看详细参数说明"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:205
|
||||
msgid "查看 XHS-Downloader 版本"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:53
|
||||
#, python-brace-format
|
||||
msgid ""
|
||||
"读取指定浏览器的 Cookie 并写入配置文件\n"
|
||||
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 "
|
||||
"Cookie!\n"
|
||||
"{options}\n"
|
||||
"请输入浏览器名称或序号:"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:63
|
||||
msgid "未选择浏览器!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:75
|
||||
msgid "浏览器名称或序号输入错误!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:81
|
||||
msgid "获取 Cookie 失败,未找到 Cookie 数据!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\browser.py:119
|
||||
msgid "从浏览器读取 Cookie 功能不支持当前平台!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\expansion\cleaner.py:45
|
||||
msgid "不受支持的操作系统类型,可能无法正常去除非法字符!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:232
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:236
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试超时"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244
|
||||
#, python-brace-format
|
||||
msgid "代理 {0} 测试失败:{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:57
|
||||
#, python-brace-format
|
||||
msgid "{old_folder} 文件夹不存在,跳过处理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:86
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:108
|
||||
msgid "文件夹"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:91
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_folder} 已重命名为 {new_folder}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:113
|
||||
#, python-brace-format
|
||||
msgid "文件夹 {old_} 重命名为 {new_}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:186
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:201
|
||||
msgid "文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:191
|
||||
#, python-brace-format
|
||||
msgid "文件 {old_file} 重命名为 {new_file}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:210
|
||||
#, python-brace-format
|
||||
msgid "{type} {old}被占用,重命名失败: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:219
|
||||
#, python-brace-format
|
||||
msgid "{type} {new}名称重复,重命名失败: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:228
|
||||
#, python-brace-format
|
||||
msgid "处理{type} {old}时发生预期之外的错误: {error}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\tools.py:31
|
||||
msgid ""
|
||||
"如需重新尝试处理该对象,请关闭所有正在访问该对象的窗口或程序,然后直接按下回"
|
||||
"车键!\n"
|
||||
"如需跳过处理该对象,请输入任意字符后按下回车键!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:20
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:29
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:15
|
||||
msgid "退出程序"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:21
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:30
|
||||
msgid "检查更新"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:22
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:35
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:16
|
||||
msgid "返回首页"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:35
|
||||
msgid "如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:42
|
||||
msgid "Discord 社区"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:46
|
||||
msgid "邀请链接:"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:48
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:61
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:70
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:56
|
||||
msgid "点击访问"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\about.py:51
|
||||
msgid "作者的其他开源项目"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:31
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:212
|
||||
msgid "程序设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:32
|
||||
msgid "下载记录"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:33
|
||||
msgid "开启监听"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:34
|
||||
msgid "关于项目"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:49
|
||||
msgid "开源协议: "
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:52
|
||||
msgid "项目地址: "
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:59
|
||||
msgid "请输入小红书图文/视频作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:62
|
||||
msgid "多个链接之间使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:64
|
||||
msgid "下载无水印作品文件"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:65
|
||||
msgid "读取剪贴板"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:66
|
||||
msgid "清空输入框"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:81
|
||||
msgid "免责声明\n"
|
||||
msgstr ""
|
||||
"关于 XHS-Downloader 的 免责声明:\n"
|
||||
"\n"
|
||||
"1.使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项"
|
||||
"目所产生的任何损失、责任、或风险概不负责。\n"
|
||||
"2.本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术"
|
||||
"水平努力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。\n"
|
||||
"3.本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可,使用者需"
|
||||
"自行查阅并遵守相应协议,作者不对第三方组件的稳定性、安全性及合规性承担任何责"
|
||||
"任。\n"
|
||||
"4.使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,"
|
||||
"并在适当的地方注明使用了 GNU General Public License v3.0 的代码。\n"
|
||||
"5.使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行"
|
||||
"为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。\n"
|
||||
"6.使用者不得使用本工具从事任何侵犯知识产权的行为,包括但不限于未经授权下载、"
|
||||
"传播受版权保护的内容,开发者不参与、不支持、不认可任何非法内容的获取或分"
|
||||
"发。\n"
|
||||
"7.本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用"
|
||||
"者应自行遵守相关法律法规,确保处理行为合法正当;因违规操作导致的法律责任由使"
|
||||
"用者自行承担。\n"
|
||||
"8.使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行"
|
||||
"为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。\n"
|
||||
"9.本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-"
|
||||
"Downloader 项目相关的任何商业服务。\n"
|
||||
"10.基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不"
|
||||
"承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的"
|
||||
"各种情况负全部责任。\n"
|
||||
"11.本项目不授予使用者任何专利许可;若使用本项目导致专利纠纷或侵权,使用者自"
|
||||
"行承担全部风险和责任。未经作者或权利人书面授权,不得使用本项目进行任何商业宣"
|
||||
"传、推广或再授权。\n"
|
||||
"12.作者保留随时终止向任何违反本声明的使用者提供服务的权利,并可能要求其销毁"
|
||||
"已获取的代码及衍生作品。\n"
|
||||
"13.作者保留在不另行通知的情况下更新本声明的权利,使用者持续使用即视为接受修"
|
||||
"订后的条款。\n"
|
||||
"\n"
|
||||
"在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声"
|
||||
"明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码"
|
||||
"和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险"
|
||||
"和后果。\n"
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:94
|
||||
msgid "未输入任何小红书作品链接"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:124
|
||||
msgid "下载小红书作品文件失败"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\loading.py:19
|
||||
msgid "程序处理中..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:22
|
||||
msgid "关闭监听"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:34
|
||||
msgid "已启动监听剪贴板模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:36
|
||||
msgid "退出监听剪贴板模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:23
|
||||
msgid "请输入待删除的小红书作品链接或作品 ID"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:26
|
||||
msgid ""
|
||||
"支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:32
|
||||
msgid "删除指定作品 ID"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:47
|
||||
msgid "删除下载记录成功"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:30
|
||||
msgid "作品数据 / 文件保存根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:35
|
||||
msgid "程序根路径"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:64
|
||||
msgid "内置 Chrome User Agent"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:69
|
||||
msgid "小红书网页版 Cookie"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:83
|
||||
msgid "不使用代理"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:120
|
||||
msgid "记录作品详细数据"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:125
|
||||
msgid "作品归档保存模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:130
|
||||
msgid "视频作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:135
|
||||
msgid "图文作品下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:144
|
||||
msgid "动图文件下载开关"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:154
|
||||
msgid "作者归档保存模式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:159
|
||||
msgid "更新文件修改时间"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:167
|
||||
msgid "图片下载格式"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:171
|
||||
msgid "程序语言"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:194
|
||||
msgid "保存配置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:198
|
||||
msgid "放弃更改"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:208
|
||||
msgid "小红书网页版 Cookie,无需登录,参数已设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:209
|
||||
msgid "小红书网页版 Cookie,无需登录,参数未设置"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:26
|
||||
msgid "正在检查新版本,请稍等..."
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:46
|
||||
#, python-brace-format
|
||||
msgid "检测到新版本:{0}.{1}"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:54
|
||||
msgid "当前版本为开发版, 可更新至正式版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:59
|
||||
msgid "当前已是最新开发版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:64
|
||||
msgid "当前已是最新正式版"
|
||||
msgstr ""
|
||||
|
||||
#: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:71
|
||||
msgid "检测新版本失败"
|
||||
msgstr ""
|
||||
131
main.py
@ -1,94 +1,59 @@
|
||||
from textual.app import App
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.widgets import Button
|
||||
from textual.widgets import Footer
|
||||
from textual.widgets import Header
|
||||
from textual.widgets import Input
|
||||
from textual.widgets import Label
|
||||
from textual.widgets import Static
|
||||
from asyncio import run
|
||||
from asyncio.exceptions import CancelledError
|
||||
from contextlib import suppress
|
||||
from sys import argv
|
||||
|
||||
from source import Batch
|
||||
from source import Settings
|
||||
from source import XHS
|
||||
from source import XHSDownloader
|
||||
from source import cli
|
||||
|
||||
|
||||
def example():
|
||||
"""通过代码设置参数,适合二次开发"""
|
||||
# 测试链接
|
||||
error_demo = "https://www.xiaohongshu.com/explore/"
|
||||
image_demo = "https://www.xiaohongshu.com/explore/64d1b406000000000103ee8d"
|
||||
video_demo = "https://www.xiaohongshu.com/explore/64c05652000000000c0378e7"
|
||||
# 实例对象
|
||||
path = "./" # 作品下载储存根路径,默认值:当前路径
|
||||
folder = "Download" # 作品下载文件夹名称(自动创建),默认值:Download
|
||||
proxies = None # 网络代理
|
||||
timeout = 5 # 网络请求超时限制,默认值:10
|
||||
chunk = 1024 * 1024 # 下载文件时,每次从服务器获取的数据块大小,单位字节
|
||||
xhs = XHS(
|
||||
path=path,
|
||||
folder=folder,
|
||||
proxies=proxies,
|
||||
timeout=timeout,
|
||||
chunk=chunk, ) # 使用自定义参数
|
||||
# xhs = XHS() # 使用默认参数
|
||||
download = True # 是否下载作品文件
|
||||
# 返回作品详细信息,包括下载地址
|
||||
print(xhs.extract(error_demo)) # 获取数据失败时返回空字典
|
||||
print(xhs.extract(image_demo, download=download))
|
||||
print(xhs.extract(video_demo, download=download))
|
||||
async def app():
|
||||
async with XHSDownloader() as xhs:
|
||||
await xhs.run_async()
|
||||
|
||||
|
||||
def main():
|
||||
"""读取并应用配置文件设置的参数,适合一般作品文件下载需求"""
|
||||
xhs = XHS(**Settings().run())
|
||||
if ids := Batch().read_txt():
|
||||
for i in ids:
|
||||
xhs.extract(i.rstrip('\n'), download=True)
|
||||
else:
|
||||
while True:
|
||||
if url := input("请输入小红书作品链接:"):
|
||||
xhs.extract(url, download=True)
|
||||
else:
|
||||
break
|
||||
async def api_server(
|
||||
host="0.0.0.0",
|
||||
port=5556,
|
||||
log_level="info",
|
||||
):
|
||||
async with XHS(**Settings().run()) as xhs:
|
||||
await xhs.run_api_server(
|
||||
host,
|
||||
port,
|
||||
log_level,
|
||||
)
|
||||
|
||||
|
||||
class RunMenu(Static):
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Button("下载无水印图片/视频", id="run", variant="success")
|
||||
yield Button("读取文件并开始批量下载作品", id="batch", variant="success")
|
||||
yield Button("清空输入", id="reset", variant="error")
|
||||
async def mcp_server(
|
||||
transport="streamable-http",
|
||||
host="0.0.0.0",
|
||||
port=5556,
|
||||
log_level="INFO",
|
||||
):
|
||||
async with XHS(**Settings().run()) as xhs:
|
||||
await xhs.run_mcp_server(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level,
|
||||
)
|
||||
|
||||
|
||||
class XHSDownloader(App):
|
||||
CSS_PATH = "static/XHS_Downloader.tcss"
|
||||
BINDINGS = [
|
||||
Binding(key="q", action="quit", description="结束运行"),
|
||||
Binding(
|
||||
key="w",
|
||||
action="repository",
|
||||
description="获取源码",
|
||||
),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Label("请输入小红书图文/视频作品链接:")
|
||||
yield Input(placeholder="URL")
|
||||
yield RunMenu()
|
||||
yield Header()
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = "小红书作品采集工具"
|
||||
|
||||
@staticmethod
|
||||
def action_repository():
|
||||
yield Label("Github Repository")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# example()
|
||||
main()
|
||||
# app = XHSDownloader()
|
||||
# app.run()
|
||||
if __name__ == "__main__":
|
||||
with suppress(
|
||||
KeyboardInterrupt,
|
||||
CancelledError,
|
||||
):
|
||||
# TODO: 重构优化
|
||||
if len(argv) == 1:
|
||||
run(app())
|
||||
elif argv[1].upper() == "API":
|
||||
run(api_server())
|
||||
elif argv[1].upper() == "MCP":
|
||||
run(mcp_server())
|
||||
# run(mcp_server("stdio"))
|
||||
else:
|
||||
cli()
|
||||
|
||||
117
pyproject.toml
Normal file
@ -0,0 +1,117 @@
|
||||
[project]
|
||||
name = "XHS-Downloader"
|
||||
version = "2.7"
|
||||
description = "小红书(XiaoHongShu、RedNote)链接提取/作品采集工具:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品、用户链接;采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件"
|
||||
authors = [
|
||||
{ name = "JoeanAmier", email = "yonglelolu@foxmail.com" },
|
||||
]
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0"
|
||||
requires-python = ">=3.12,<3.13"
|
||||
dependencies = [
|
||||
"aiofiles>=25.1.0",
|
||||
"aiosqlite>=0.21.0",
|
||||
"click>=8.3.1",
|
||||
"emoji>=2.15.0",
|
||||
"fastapi>=0.124.2",
|
||||
"fastmcp>=2.14.0",
|
||||
"httpx[socks]>=0.28.1",
|
||||
"lxml>=6.0.2",
|
||||
"pyperclip>=1.11.0",
|
||||
"pyyaml>=6.0.3",
|
||||
"rookiepy>=0.5.6",
|
||||
"textual>=6.8.0",
|
||||
"uvicorn>=0.38.0",
|
||||
"websockets>=15.0.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/JoeanAmier/XHS-Downloader"
|
||||
|
||||
[tool.uv.pip]
|
||||
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"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pyinstaller>=6.17.0",
|
||||
"textual-dev>=1.7.0",
|
||||
]
|
||||
@ -1 +1,30 @@
|
||||
requests==2.31.0
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml --no-deps --no-strip-extras -o requirements.txt
|
||||
aiofiles==25.1.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
aiosqlite==0.21.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
click==8.3.1
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
emoji==2.15.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
fastapi==0.124.2
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
fastmcp>=2.14.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
httpx[socks]==0.28.1
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
lxml==6.0.2
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
pyperclip==1.11.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
pyyaml==6.0.3
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
rookiepy==0.5.6
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
textual==6.8.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
uvicorn==0.38.0
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
websockets==15.0.1
|
||||
# via xhs-downloader (pyproject.toml)
|
||||
|
||||
3
source/CLI/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .main import cli
|
||||
|
||||
__all__ = ["cli"]
|
||||
371
source/CLI/main.py
Normal file
@ -0,0 +1,371 @@
|
||||
from asyncio import run
|
||||
from contextlib import suppress
|
||||
from pathlib import Path as Root
|
||||
from textwrap import fill
|
||||
|
||||
from click import Context
|
||||
from click import (
|
||||
command,
|
||||
option,
|
||||
Path,
|
||||
Choice,
|
||||
pass_context,
|
||||
echo,
|
||||
)
|
||||
from rich import print
|
||||
from rich.panel import Panel
|
||||
from rich.table import Table
|
||||
|
||||
from source.application import XHS
|
||||
from source.expansion import BrowserCookie
|
||||
from source.module import (
|
||||
ROOT,
|
||||
PROJECT,
|
||||
)
|
||||
from source.module import Settings
|
||||
from source.translation import switch_language, _
|
||||
|
||||
__all__ = ["cli"]
|
||||
|
||||
|
||||
def check_value(function):
|
||||
def inner(ctx: Context, param, value):
|
||||
if not value:
|
||||
return
|
||||
return function(ctx, param, value)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class CLI:
|
||||
def __init__(self, ctx: Context, **kwargs):
|
||||
self.ctx = ctx
|
||||
self.url = ctx.params.pop("url")
|
||||
self.index = self.__format_index(ctx.params.pop("index"))
|
||||
self.path = ctx.params.pop("settings")
|
||||
self.update = ctx.params.pop("update_settings")
|
||||
self.settings = Settings(self.__check_settings_path())
|
||||
self.parameter = (
|
||||
self.settings.run()
|
||||
| self.__clean_params(ctx.params)
|
||||
| {"script_server": False}
|
||||
)
|
||||
self.APP = XHS(**self.parameter)
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.APP.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.APP.__aexit__(exc_type, exc_value, traceback)
|
||||
|
||||
async def run(self):
|
||||
if self.url:
|
||||
await self.APP.extract_cli(self.url, index=self.index)
|
||||
self.__update_settings()
|
||||
|
||||
def __update_settings(self):
|
||||
if self.update:
|
||||
self.settings.update(self.parameter)
|
||||
|
||||
def __check_settings_path(self) -> Path:
|
||||
if not self.path:
|
||||
return ROOT
|
||||
return s.parent if (s := Root(self.path)).is_file() else ROOT
|
||||
|
||||
@staticmethod
|
||||
def __merge_cookie(data: dict) -> None:
|
||||
if not data["cookie"] and (bc := data["browser_cookie"]):
|
||||
data["cookie"] = bc
|
||||
data.pop("browser_cookie")
|
||||
|
||||
def __clean_params(self, data: dict) -> dict:
|
||||
self.__merge_cookie(data)
|
||||
return {k: v for k, v in data.items() if v}
|
||||
|
||||
@staticmethod
|
||||
def __format_index(index: str) -> list:
|
||||
if index:
|
||||
result = []
|
||||
values = index.split()
|
||||
for i in values:
|
||||
with suppress(ValueError):
|
||||
result.append(int(i))
|
||||
return result
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
@check_value
|
||||
def version(ctx: Context, param, value) -> None:
|
||||
echo(PROJECT)
|
||||
ctx.exit()
|
||||
|
||||
@staticmethod
|
||||
@check_value
|
||||
def read_cookie(ctx: Context, param, value) -> str:
|
||||
return BrowserCookie.get(
|
||||
value,
|
||||
domains=[
|
||||
"xiaohongshu.com",
|
||||
],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@check_value
|
||||
def help_(ctx: Context, param, value) -> None:
|
||||
table = Table(highlight=True, box=None, show_header=True)
|
||||
|
||||
# 添加表格的列名
|
||||
table.add_column("parameter", 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(
|
||||
"description",
|
||||
no_wrap=True,
|
||||
)
|
||||
|
||||
options = (
|
||||
("--url", "-u", "str", _("小红书作品链接,多个链接使用空格分隔")),
|
||||
(
|
||||
"--index",
|
||||
"-i",
|
||||
"str",
|
||||
fill(
|
||||
_(
|
||||
'下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:"1 3 5 7"'
|
||||
),
|
||||
width=55,
|
||||
),
|
||||
),
|
||||
("--work_path", "-wp", "str", _("作品数据/文件保存根路径")),
|
||||
("--folder_name", "-fn", "str", _("作品文件储存文件夹名称")),
|
||||
("--name_format", "-nf", "str", _("作品文件名称格式")),
|
||||
("--user_agent", "-ua", "str", "User-Agent"),
|
||||
("--cookie", "-ck", "str", _("小红书网页版 Cookie,无需登录")),
|
||||
("--proxy", "-p", "str", _("网络代理")),
|
||||
("--timeout", "-t", "int", _("请求数据超时限制,单位:秒")),
|
||||
(
|
||||
"--chunk",
|
||||
"-c",
|
||||
"int",
|
||||
fill(
|
||||
_("下载文件时,每次从服务器获取的数据块大小,单位:字节"), width=55
|
||||
),
|
||||
),
|
||||
("--max_retry", "-mr", "int", _("请求数据失败时,重试的最大次数")),
|
||||
("--record_data", "-rd", "bool", _("是否记录作品数据至文件")),
|
||||
(
|
||||
"--image_format",
|
||||
"-if",
|
||||
"choice",
|
||||
_("图文作品文件下载格式,支持:PNG、WEBP"),
|
||||
),
|
||||
("--live_download", "-ld", "bool", _("动态图片下载开关")),
|
||||
("--download_record", "-dr", "bool", _("作品下载记录开关")),
|
||||
(
|
||||
"--folder_mode",
|
||||
"-fm",
|
||||
"bool",
|
||||
_("是否将每个作品的文件储存至单独的文件夹"),
|
||||
),
|
||||
(
|
||||
"--author_archive",
|
||||
"-aa",
|
||||
"bool",
|
||||
_("是否将每个作者的作品储存至单独的文件夹"),
|
||||
),
|
||||
(
|
||||
"--write_mtime",
|
||||
"-wm",
|
||||
"bool",
|
||||
fill(
|
||||
_("是否将作品文件的修改时间属性修改为作品的发布时间"),
|
||||
width=55,
|
||||
),
|
||||
),
|
||||
("--language", "-l", "choice", _("设置程序语言,目前支持:zh_CN、en_US")),
|
||||
("--settings", "-s", "str", _("读取指定配置文件")),
|
||||
(
|
||||
"--browser_cookie",
|
||||
"-bc",
|
||||
"choice",
|
||||
fill(
|
||||
_(
|
||||
"从指定的浏览器读取小红书网页版 Cookie,支持:{0}; 输入浏览器名称或序号"
|
||||
).format(
|
||||
", ".join(
|
||||
f"{i}: {j}"
|
||||
for i, j in enumerate(
|
||||
BrowserCookie.SUPPORT_BROWSER.keys(),
|
||||
start=1,
|
||||
)
|
||||
)
|
||||
),
|
||||
width=55,
|
||||
),
|
||||
),
|
||||
("--update_settings", "-us", "flag", _("是否更新配置文件")),
|
||||
("--help", "-h", "flag", _("查看详细参数说明")),
|
||||
("--version", "-v", "flag", _("查看 XHS-Downloader 版本")),
|
||||
)
|
||||
|
||||
for option in options:
|
||||
table.add_row(*option)
|
||||
|
||||
print(
|
||||
Panel(
|
||||
table,
|
||||
border_style="bold",
|
||||
title="XHS-Downloader CLI Parameters",
|
||||
title_align="left",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@command(name="XHS-Downloader", help=PROJECT)
|
||||
@option(
|
||||
"--url",
|
||||
"-u",
|
||||
)
|
||||
@option(
|
||||
"--index",
|
||||
"-i",
|
||||
)
|
||||
@option(
|
||||
"--work_path",
|
||||
"-wp",
|
||||
type=Path(file_okay=False),
|
||||
)
|
||||
@option(
|
||||
"--folder_name",
|
||||
"-fn",
|
||||
)
|
||||
@option(
|
||||
"--name_format",
|
||||
"-nf",
|
||||
)
|
||||
@option(
|
||||
"--user_agent",
|
||||
"-ua",
|
||||
)
|
||||
@option(
|
||||
"--cookie",
|
||||
"-ck",
|
||||
)
|
||||
@option(
|
||||
"--proxy",
|
||||
"-p",
|
||||
)
|
||||
@option(
|
||||
"--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(
|
||||
"--author_archive",
|
||||
"-aa",
|
||||
type=bool,
|
||||
)
|
||||
@option(
|
||||
"--write_mtime",
|
||||
"-wm",
|
||||
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
|
||||
def cli(ctx, help, language, **kwargs):
|
||||
# Step 1: 切换语言
|
||||
if language:
|
||||
switch_language(language)
|
||||
|
||||
# Step 2: 如果请求了帮助信息,则显示帮助并退出
|
||||
if help:
|
||||
ctx.obj = kwargs # 保留当前上下文的参数
|
||||
CLI.help_(ctx, None, help)
|
||||
return
|
||||
|
||||
# Step 3: 主逻辑
|
||||
async def main():
|
||||
async with CLI(ctx, **kwargs) as xhs:
|
||||
await xhs.run()
|
||||
|
||||
run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from click.testing import CliRunner
|
||||
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli, ["-l", "en_US", "-u", ""])
|
||||
@ -1,61 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
from requests import exceptions
|
||||
from requests import get
|
||||
|
||||
from .Manager import Manager
|
||||
|
||||
__all__ = ['Download']
|
||||
|
||||
|
||||
class Download:
|
||||
manager = Manager()
|
||||
temp = Path("./Temp")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
folder: str,
|
||||
headers: dict,
|
||||
proxies=None,
|
||||
chunk=256 * 1024, ):
|
||||
self.root = self.init_root(path, folder)
|
||||
self.headers = headers
|
||||
self.proxies = {
|
||||
"http": proxies,
|
||||
"https": proxies,
|
||||
"ftp": proxies,
|
||||
}
|
||||
self.chunk = chunk
|
||||
|
||||
def init_root(self, path: str, folder: str) -> Path:
|
||||
root = Path(path).joinpath(folder)
|
||||
if not root.is_dir():
|
||||
root.mkdir()
|
||||
if not self.temp.is_dir():
|
||||
self.temp.mkdir()
|
||||
return root
|
||||
|
||||
def run(self, urls: list, name: str):
|
||||
if (l := len(urls)) > 1:
|
||||
for index, url in enumerate(urls):
|
||||
self.download(url, f"{name}_{index + 1}.webp")
|
||||
elif l == 1:
|
||||
self.download(urls[0], f"{name}.mp4")
|
||||
|
||||
def download(self, url: str, name: str):
|
||||
temp = self.temp.joinpath(name)
|
||||
file = self.root.joinpath(name)
|
||||
if self.manager.is_exists(file):
|
||||
print(f"{name} 已存在,跳过下载!")
|
||||
return
|
||||
try:
|
||||
with get(url, headers=self.headers, proxies=self.proxies, stream=True) as response:
|
||||
with temp.open("wb") as f:
|
||||
for chunk in response.iter_content(chunk_size=self.chunk):
|
||||
f.write(chunk)
|
||||
self.manager.move(temp, file)
|
||||
print(f"{name} 下载成功!")
|
||||
except exceptions.ChunkedEncodingError:
|
||||
self.manager.delete(temp)
|
||||
print(f"网络异常,{name} 下载失败!")
|
||||
@ -1,63 +0,0 @@
|
||||
from datetime import datetime
|
||||
from json import loads
|
||||
from re import compile
|
||||
|
||||
__all__ = ['Explore']
|
||||
|
||||
|
||||
class Explore:
|
||||
explore_data = compile(
|
||||
r'"currentTime":\d{13},"note":(.*?)}},"serverRequestInfo"')
|
||||
time_format = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
def run(self, html: str) -> dict:
|
||||
data = self.__get_json_data(html)
|
||||
return self.__extract_data(data)
|
||||
|
||||
def __get_json_data(self, html: str) -> dict:
|
||||
data = self.explore_data.findall(html)
|
||||
return {} if len(data) != 1 else loads(data[0])
|
||||
|
||||
def __extract_data(self, data: dict) -> dict:
|
||||
result = {}
|
||||
if data:
|
||||
self.__extract_interact_info(result, data)
|
||||
self.__extract_tags(result, data)
|
||||
self.__extract_info(result, data)
|
||||
self.__extract_time(result, data)
|
||||
self.__extract_user(result, data)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def __extract_interact_info(container: dict, data: dict):
|
||||
interact_info = data["interactInfo"]
|
||||
container["收藏数量"] = interact_info["collectedCount"]
|
||||
container["评论数量"] = interact_info["commentCount"]
|
||||
container["分享数量"] = interact_info["shareCount"]
|
||||
container["点赞数量"] = interact_info["likedCount"]
|
||||
|
||||
@staticmethod
|
||||
def __extract_tags(container: dict, data: dict):
|
||||
tags = data["tagList"]
|
||||
container["作品标签"] = [i["name"] for i in tags]
|
||||
|
||||
@staticmethod
|
||||
def __extract_info(container: dict, data: dict):
|
||||
container["作品ID"] = data["noteId"]
|
||||
container["作品标题"] = data["title"]
|
||||
container["作品描述"] = data["desc"]
|
||||
container["作品类型"] = {"video": "视频", "normal": "图文"}[data["type"]]
|
||||
|
||||
def __extract_time(self, container: dict, data: dict):
|
||||
container["发布时间"] = datetime.fromtimestamp(
|
||||
data["time"] / 1000).strftime(self.time_format)
|
||||
container["最后更新时间"] = datetime.fromtimestamp(
|
||||
data["lastUpdateTime"] /
|
||||
1000).strftime(
|
||||
self.time_format)
|
||||
|
||||
@staticmethod
|
||||
def __extract_user(container: dict, data: dict):
|
||||
user = data["user"]
|
||||
container["作者昵称"] = user["nickname"]
|
||||
container["作者ID"] = user["userId"]
|
||||
@ -1,44 +0,0 @@
|
||||
from requests import ReadTimeout
|
||||
from requests import exceptions
|
||||
from requests import get
|
||||
|
||||
__all__ = ['Html']
|
||||
|
||||
|
||||
class Html:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
headers: dict,
|
||||
proxies=None,
|
||||
timeout=10, ):
|
||||
self.headers = headers | {"Referer": "https://www.xiaohongshu.com/", }
|
||||
self.proxies = {
|
||||
"http": proxies,
|
||||
"https": proxies,
|
||||
"ftp": proxies,
|
||||
}
|
||||
self.timeout = timeout
|
||||
|
||||
def get_html(
|
||||
self,
|
||||
url: str,
|
||||
params=None,
|
||||
headers=None, ) -> str:
|
||||
try:
|
||||
response = get(
|
||||
url,
|
||||
params=params,
|
||||
proxies=self.proxies,
|
||||
timeout=self.timeout,
|
||||
headers=headers or self.headers, )
|
||||
except (
|
||||
exceptions.ProxyError,
|
||||
exceptions.SSLError,
|
||||
exceptions.ChunkedEncodingError,
|
||||
exceptions.ConnectionError,
|
||||
ReadTimeout,
|
||||
):
|
||||
print("获取网页源码失败,请尝试设置 Cookie 后重试!")
|
||||
return ""
|
||||
return response.text
|
||||
@ -1,20 +0,0 @@
|
||||
from re import compile
|
||||
|
||||
__all__ = ['Image']
|
||||
|
||||
|
||||
class Image:
|
||||
IMAGE_API = "https://sns-img-qc.xhscdn.com/"
|
||||
IMAGE_ID = compile(r'"traceId":"(.*?)"')
|
||||
|
||||
def get_image_link(self, html: str):
|
||||
return self.__get_image_links(html)
|
||||
|
||||
def __get_id(self, html: str) -> list:
|
||||
return self.IMAGE_ID.findall(html)
|
||||
|
||||
def __generate_url(self, ids: list) -> list:
|
||||
return [self.IMAGE_API + i for i in ids]
|
||||
|
||||
def __get_image_links(self, html: str) -> list:
|
||||
return self.__generate_url(self.__get_id(html))
|
||||
@ -1,18 +0,0 @@
|
||||
from pathlib import Path
|
||||
from shutil import move
|
||||
|
||||
__all__ = ['Manager']
|
||||
|
||||
|
||||
class Manager:
|
||||
@staticmethod
|
||||
def is_exists(path: Path) -> bool:
|
||||
return path.exists()
|
||||
|
||||
@staticmethod
|
||||
def delete(path: Path):
|
||||
path.unlink()
|
||||
|
||||
@staticmethod
|
||||
def move(temp: Path, path: Path):
|
||||
move(temp.resolve(), path.resolve())
|
||||
@ -1,38 +0,0 @@
|
||||
from json import dump
|
||||
from json import load
|
||||
from pathlib import Path
|
||||
|
||||
__all__ = ['Settings', 'Batch']
|
||||
|
||||
|
||||
class Settings:
|
||||
path = Path("./settings.json")
|
||||
default = {
|
||||
"path": "./",
|
||||
"folder": "Download",
|
||||
"proxies": None,
|
||||
"timeout": 10,
|
||||
"chunk": 256 * 1024,
|
||||
}
|
||||
|
||||
def run(self):
|
||||
return self.read() if self.path.is_file() else self.create()
|
||||
|
||||
def read(self):
|
||||
with self.path.open("r", encoding="utf-8") as f:
|
||||
return load(f)
|
||||
|
||||
def create(self):
|
||||
with self.path.open("w", encoding="utf-8") as f:
|
||||
dump(self.default, f, indent=2)
|
||||
return self.default
|
||||
|
||||
|
||||
class Batch:
|
||||
file = Path("./xhs.txt")
|
||||
|
||||
def read_txt(self) -> list:
|
||||
if self.file.is_file():
|
||||
with self.file.open("r") as f:
|
||||
return f.readlines()
|
||||
return []
|
||||
3
source/TUI/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .app import XHSDownloader
|
||||
|
||||
__all__ = ["XHSDownloader"]
|
||||
84
source/TUI/about.py
Normal file
@ -0,0 +1,84 @@
|
||||
from rich.text import Text
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Footer, Header, Label, Link
|
||||
|
||||
from ..module import (
|
||||
INFO,
|
||||
MASTER,
|
||||
PROJECT,
|
||||
PROMPT,
|
||||
)
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["About"]
|
||||
|
||||
|
||||
class About(Screen):
|
||||
BINDINGS = [
|
||||
Binding(key="Q", action="quit", description=_("退出程序")),
|
||||
Binding(key="U", action="update", description=_("检查更新")),
|
||||
Binding(key="B", action="back", description=_("返回首页")),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(
|
||||
Text(
|
||||
_(
|
||||
"如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star,感谢您的支持!"
|
||||
),
|
||||
style=INFO,
|
||||
),
|
||||
classes="prompt",
|
||||
)
|
||||
yield Label(
|
||||
Text(_("Discord 社区"), style=PROMPT),
|
||||
classes="prompt",
|
||||
)
|
||||
yield Link(
|
||||
_("邀请链接:") + "https://discord.com/invite/ZYtmgKud9Y",
|
||||
url="https://discord.com/invite/ZYtmgKud9Y",
|
||||
tooltip=_("点击访问"),
|
||||
)
|
||||
yield Label(
|
||||
Text(_("作者的其他开源项目"), style=PROMPT),
|
||||
classes="prompt",
|
||||
)
|
||||
yield Label(
|
||||
Text("DouK-Downloader (抖音 / TikTok)", style=MASTER),
|
||||
classes="prompt",
|
||||
)
|
||||
yield Link(
|
||||
"https://github.com/JoeanAmier/TikTokDownloader",
|
||||
url="https://github.com/JoeanAmier/TikTokDownloader",
|
||||
tooltip=_("点击访问"),
|
||||
)
|
||||
yield Label(
|
||||
Text("KS-Downloader (快手)", style=MASTER),
|
||||
classes="prompt",
|
||||
)
|
||||
yield Link(
|
||||
"https://github.com/JoeanAmier/KS-Downloader",
|
||||
url="https://github.com/JoeanAmier/KS-Downloader",
|
||||
tooltip=_("点击访问"),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = PROJECT
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
await self.app.action_quit()
|
||||
|
||||
async def action_back(self) -> None:
|
||||
await self.app.action_back()
|
||||
|
||||
async def action_update(self):
|
||||
await self.app.run_action("update")
|
||||
125
source/TUI/app.py
Normal file
@ -0,0 +1,125 @@
|
||||
from textual.app import App
|
||||
|
||||
from ..application import XHS
|
||||
from ..module import (
|
||||
ROOT,
|
||||
Settings,
|
||||
)
|
||||
from .about import About
|
||||
from .index import Index
|
||||
from .loading import Loading
|
||||
from .record import Record
|
||||
from .setting import Setting
|
||||
from .update import Update
|
||||
|
||||
__all__ = ["XHSDownloader"]
|
||||
|
||||
|
||||
class XHSDownloader(App):
|
||||
CSS_PATH = ROOT.parent.joinpath("static/XHS-Downloader.tcss")
|
||||
SETTINGS = Settings(ROOT)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.parameter: dict
|
||||
self.APP: XHS
|
||||
self.__initialization()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.APP.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.APP.__aexit__(exc_type, exc_value, traceback)
|
||||
|
||||
def __initialization(self) -> None:
|
||||
self.parameter = self.SETTINGS.run()
|
||||
self.APP = XHS(
|
||||
**self.parameter,
|
||||
_print=False,
|
||||
)
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
self.theme = "nord"
|
||||
self.install_screen(
|
||||
Setting(
|
||||
self.parameter,
|
||||
),
|
||||
name="setting",
|
||||
)
|
||||
self.install_screen(
|
||||
Index(
|
||||
self.APP,
|
||||
),
|
||||
name="index",
|
||||
)
|
||||
self.install_screen(Loading(), name="loading")
|
||||
self.install_screen(About(), name="about")
|
||||
self.install_screen(
|
||||
Record(
|
||||
self.APP,
|
||||
),
|
||||
name="record",
|
||||
)
|
||||
await self.push_screen("index")
|
||||
|
||||
async def action_settings(self):
|
||||
async def save_settings(data: dict) -> None:
|
||||
self.SETTINGS.update(data)
|
||||
await self.refresh_screen()
|
||||
|
||||
await self.push_screen("setting", save_settings)
|
||||
|
||||
async def refresh_screen(self):
|
||||
await self.action_back()
|
||||
await self.close_database()
|
||||
await self.APP.close()
|
||||
self.__initialization()
|
||||
await self.__aenter__()
|
||||
await self.APP.switch_script_server()
|
||||
self.uninstall_screen("index")
|
||||
self.uninstall_screen("setting")
|
||||
self.uninstall_screen("loading")
|
||||
self.uninstall_screen("about")
|
||||
self.uninstall_screen("record")
|
||||
self.install_screen(
|
||||
Index(
|
||||
self.APP,
|
||||
),
|
||||
name="index",
|
||||
)
|
||||
self.install_screen(
|
||||
Setting(
|
||||
self.parameter,
|
||||
),
|
||||
name="setting",
|
||||
)
|
||||
self.install_screen(Loading(), name="loading")
|
||||
self.install_screen(About(), name="about")
|
||||
self.install_screen(
|
||||
Record(
|
||||
self.APP,
|
||||
),
|
||||
name="record",
|
||||
)
|
||||
await self.push_screen("index")
|
||||
|
||||
def update_result(self, args: tuple[str, str]) -> None:
|
||||
self.notify(
|
||||
args[0],
|
||||
severity=args[1],
|
||||
)
|
||||
|
||||
async def action_update(self):
|
||||
await self.push_screen(
|
||||
Update(
|
||||
self.APP,
|
||||
),
|
||||
callback=self.update_result,
|
||||
)
|
||||
|
||||
async def close_database(self):
|
||||
await self.APP.id_recorder.cursor.close()
|
||||
await self.APP.id_recorder.database.close()
|
||||
await self.APP.data_recorder.cursor.close()
|
||||
await self.APP.data_recorder.database.close()
|
||||
152
source/TUI/index.py
Normal file
@ -0,0 +1,152 @@
|
||||
from pyperclip import paste
|
||||
from rich.text import Text
|
||||
from textual import on, work
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import HorizontalScroll, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Input, Label, Link, RichLog
|
||||
|
||||
from ..application import XHS
|
||||
from ..module import (
|
||||
ERROR,
|
||||
GENERAL,
|
||||
LICENCE,
|
||||
MASTER,
|
||||
PROJECT,
|
||||
PROMPT,
|
||||
REPOSITORY,
|
||||
WARNING,
|
||||
)
|
||||
from ..translation import _
|
||||
from .monitor import Monitor
|
||||
|
||||
__all__ = ["Index"]
|
||||
|
||||
|
||||
class Index(Screen):
|
||||
BINDINGS = [
|
||||
Binding(key="Q", action="quit", description=_("退出程序")),
|
||||
Binding(key="U", action="update", description=_("检查更新")),
|
||||
Binding(key="S", action="settings", description=_("程序设置")),
|
||||
Binding(key="R", action="record", description=_("下载记录")),
|
||||
Binding(key="M", action="monitor", description=_("开启监听")),
|
||||
Binding(key="A", action="about", description=_("关于项目")),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: XHS,
|
||||
):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
self.url = None
|
||||
self.tip = None
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(
|
||||
Label(Text(_("开源协议: ") + LICENCE, style=MASTER)),
|
||||
Link(
|
||||
Text(
|
||||
_("项目地址: ") + REPOSITORY,
|
||||
style=MASTER,
|
||||
),
|
||||
url=REPOSITORY,
|
||||
tooltip=_("点击访问"),
|
||||
),
|
||||
Label(
|
||||
Text(_("请输入小红书图文/视频作品链接"), style=PROMPT),
|
||||
classes="prompt",
|
||||
),
|
||||
Input(placeholder=_("多个链接之间使用空格分隔")),
|
||||
HorizontalScroll(
|
||||
Button(_("下载无水印作品文件"), id="deal"),
|
||||
Button(_("读取剪贴板"), id="paste"),
|
||||
Button(_("清空输入框"), id="reset"),
|
||||
),
|
||||
)
|
||||
yield RichLog(
|
||||
markup=True,
|
||||
wrap=True,
|
||||
auto_scroll=True,
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = PROJECT
|
||||
self.url = self.query_one(Input)
|
||||
self.tip = self.query_one(RichLog)
|
||||
self.xhs.print.func = self.tip
|
||||
self.tip.write(
|
||||
Text(_("免责声明\n") + f"\n{'>' * 50}", style=MASTER),
|
||||
scroll_end=True,
|
||||
)
|
||||
self.xhs.manager.print_proxy_tip()
|
||||
|
||||
@on(Button.Pressed, "#deal")
|
||||
async def deal_button(self):
|
||||
if self.url.value:
|
||||
self.deal()
|
||||
else:
|
||||
self.tip.write(
|
||||
Text(_("未输入任何小红书作品链接"), style=WARNING),
|
||||
scroll_end=True,
|
||||
)
|
||||
self.tip.write(
|
||||
Text(">" * 50, style=GENERAL),
|
||||
scroll_end=True,
|
||||
)
|
||||
|
||||
@on(Button.Pressed, "#reset")
|
||||
def reset_button(self):
|
||||
self.query_one(Input).value = ""
|
||||
|
||||
@on(Button.Pressed, "#paste")
|
||||
def paste_button(self):
|
||||
self.query_one(Input).value = paste()
|
||||
|
||||
@work(exclusive=True)
|
||||
async def deal(self):
|
||||
await self.app.push_screen("loading")
|
||||
if any(
|
||||
await self.xhs.extract(
|
||||
self.url.value,
|
||||
True,
|
||||
data=False,
|
||||
)
|
||||
):
|
||||
self.url.value = ""
|
||||
else:
|
||||
self.tip.write(
|
||||
Text(_("下载小红书作品文件失败"), style=ERROR),
|
||||
animate=True,
|
||||
scroll_end=True,
|
||||
)
|
||||
self.tip.write(
|
||||
Text(">" * 50, style=GENERAL),
|
||||
scroll_end=True,
|
||||
)
|
||||
await self.app.action_back()
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
await self.app.action_quit()
|
||||
|
||||
async def action_update(self) -> None:
|
||||
await self.app.run_action("update")
|
||||
|
||||
async def action_settings(self):
|
||||
await self.app.run_action("settings")
|
||||
|
||||
async def action_monitor(self):
|
||||
await self.app.push_screen(
|
||||
Monitor(
|
||||
self.xhs,
|
||||
)
|
||||
)
|
||||
|
||||
async def action_about(self):
|
||||
await self.app.push_screen("about")
|
||||
|
||||
async def action_record(self):
|
||||
await self.app.push_screen("record")
|
||||
22
source/TUI/loading.py
Normal file
@ -0,0 +1,22 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Label, LoadingIndicator
|
||||
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Loading"]
|
||||
|
||||
|
||||
class Loading(ModalScreen):
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(_("程序处理中...")),
|
||||
LoadingIndicator(),
|
||||
classes="loading",
|
||||
)
|
||||
58
source/TUI/monitor.py
Normal file
@ -0,0 +1,58 @@
|
||||
from rich.text import Text
|
||||
from textual import on, work
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Footer, Header, Label, RichLog
|
||||
|
||||
from ..application import XHS
|
||||
from ..module import (
|
||||
INFO,
|
||||
PROJECT,
|
||||
)
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Monitor"]
|
||||
|
||||
|
||||
class Monitor(Screen):
|
||||
BINDINGS = [
|
||||
Binding(key="Q", action="quit", description=_("退出程序")),
|
||||
Binding(key="C", action="close", description=_("关闭监听")),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app: XHS,
|
||||
):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Label(Text(_("已启动监听剪贴板模式"), style=INFO), classes="prompt")
|
||||
yield RichLog(markup=True, wrap=True)
|
||||
yield Button(_("退出监听剪贴板模式"), id="close")
|
||||
yield Footer()
|
||||
|
||||
@on(Button.Pressed, "#close")
|
||||
async def close_button(self):
|
||||
await self.action_close()
|
||||
|
||||
@work(exclusive=True)
|
||||
async def run_monitor(self):
|
||||
await self.xhs.monitor()
|
||||
await self.action_close()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = PROJECT
|
||||
self.xhs.print.func = self.query_one(RichLog)
|
||||
self.run_monitor()
|
||||
|
||||
async def action_close(self):
|
||||
self.xhs.stop_monitor()
|
||||
await self.app.action_back()
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
await self.action_close()
|
||||
await self.app.action_quit()
|
||||
9
source/TUI/progress.py
Normal file
@ -0,0 +1,9 @@
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
|
||||
__all__ = ["Progress"]
|
||||
|
||||
|
||||
class Progress(Screen):
|
||||
def compose(self) -> ComposeResult:
|
||||
pass
|
||||
56
source/TUI/record.py
Normal file
@ -0,0 +1,56 @@
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid, HorizontalScroll
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Button, Input, Label
|
||||
|
||||
from ..application import XHS
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Record"]
|
||||
|
||||
|
||||
class Record(ModalScreen):
|
||||
def __init__(
|
||||
self,
|
||||
app: XHS,
|
||||
):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(_("请输入待删除的小红书作品链接或作品 ID"), classes="prompt"),
|
||||
Input(
|
||||
placeholder=_(
|
||||
"支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔"
|
||||
),
|
||||
id="id",
|
||||
),
|
||||
HorizontalScroll(
|
||||
Button(
|
||||
_("删除指定作品 ID"),
|
||||
id="enter",
|
||||
),
|
||||
Button(_("返回首页"), id="close"),
|
||||
),
|
||||
id="record",
|
||||
)
|
||||
|
||||
async def delete(self, text: str):
|
||||
text = await self.xhs.extract_links(
|
||||
text,
|
||||
)
|
||||
text = self.xhs.extract_id(text)
|
||||
await self.xhs.id_recorder.delete(text)
|
||||
self.app.notify(_("删除下载记录成功"))
|
||||
|
||||
@on(Button.Pressed, "#enter")
|
||||
async def save_settings(self):
|
||||
text = self.query_one(Input)
|
||||
await self.delete(text.value)
|
||||
text.value = ""
|
||||
|
||||
@on(Button.Pressed, "#close")
|
||||
def reset(self):
|
||||
self.dismiss()
|
||||
259
source/TUI/setting.py
Normal file
@ -0,0 +1,259 @@
|
||||
from textual import on
|
||||
from textual.app import ComposeResult
|
||||
from textual.binding import Binding
|
||||
from textual.containers import Container, ScrollableContainer
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Button, Checkbox, Footer, Header, Input, Label, Select
|
||||
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Setting"]
|
||||
|
||||
|
||||
class Setting(Screen):
|
||||
BINDINGS = [
|
||||
Binding(key="Q", action="quit", description=_("退出程序")),
|
||||
Binding(key="B", action="index", description=_("返回首页")),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: dict,
|
||||
):
|
||||
super().__init__()
|
||||
self.data = data
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield ScrollableContainer(
|
||||
Label(
|
||||
_("作品数据 / 文件保存根路径"),
|
||||
classes="params",
|
||||
),
|
||||
Input(
|
||||
self.data["work_path"],
|
||||
placeholder=_("程序根路径"),
|
||||
valid_empty=True,
|
||||
id="work_path",
|
||||
),
|
||||
Label(
|
||||
_("作品文件储存文件夹名称"),
|
||||
classes="params",
|
||||
),
|
||||
Input(
|
||||
self.data["folder_name"],
|
||||
placeholder="Download",
|
||||
id="folder_name",
|
||||
),
|
||||
Label(
|
||||
_("作品文件名称格式"),
|
||||
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(),
|
||||
Container(
|
||||
Checkbox(
|
||||
_("记录作品详细数据"),
|
||||
id="record_data",
|
||||
value=self.data["record_data"],
|
||||
),
|
||||
Checkbox(
|
||||
_("作品归档保存模式"),
|
||||
id="folder_mode",
|
||||
value=self.data["folder_mode"],
|
||||
),
|
||||
Checkbox(
|
||||
_("视频作品下载开关"),
|
||||
id="video_download",
|
||||
value=self.data["video_download"],
|
||||
),
|
||||
Checkbox(
|
||||
_("图文作品下载开关"),
|
||||
id="image_download",
|
||||
value=self.data["image_download"],
|
||||
),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Label(),
|
||||
Container(
|
||||
Checkbox(
|
||||
_("动图文件下载开关"),
|
||||
id="live_download",
|
||||
value=self.data["live_download"],
|
||||
),
|
||||
Checkbox(
|
||||
_("作品下载记录开关"),
|
||||
id="download_record",
|
||||
value=self.data["download_record"],
|
||||
),
|
||||
Checkbox(
|
||||
_("作者归档保存模式"),
|
||||
id="author_archive",
|
||||
value=self.data["author_archive"],
|
||||
),
|
||||
Checkbox(
|
||||
_("更新文件修改时间"),
|
||||
id="write_mtime",
|
||||
value=self.data["write_mtime"],
|
||||
),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Label(),
|
||||
Container(
|
||||
Checkbox(
|
||||
_("脚本服务器开关"),
|
||||
id="script_server",
|
||||
value=self.data["script_server"],
|
||||
),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Container(
|
||||
Label(
|
||||
_("图片下载格式"),
|
||||
classes="params",
|
||||
),
|
||||
Label(
|
||||
_("程序语言"),
|
||||
classes="params",
|
||||
),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Label(),
|
||||
Container(
|
||||
Select.from_values(
|
||||
("AUTO", "PNG", "WEBP", "JPEG", "HEIC"),
|
||||
value=self.data["image_format"].upper(),
|
||||
allow_blank=False,
|
||||
id="image_format",
|
||||
),
|
||||
Select.from_values(
|
||||
["zh_CN", "en_US"],
|
||||
value=self.data["language"],
|
||||
allow_blank=False,
|
||||
id="language",
|
||||
),
|
||||
classes="horizontal-layout",
|
||||
),
|
||||
Container(
|
||||
Button(
|
||||
_("保存配置"),
|
||||
id="save",
|
||||
),
|
||||
Button(
|
||||
_("放弃更改"),
|
||||
id="abandon",
|
||||
),
|
||||
classes="settings_button",
|
||||
),
|
||||
)
|
||||
yield Footer()
|
||||
|
||||
def __check_cookie(self) -> str:
|
||||
if self.data["cookie"]:
|
||||
return _("小红书网页版 Cookie,无需登录,参数已设置")
|
||||
return _("小红书网页版 Cookie,无需登录,参数未设置")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.title = _("程序设置")
|
||||
|
||||
@on(Button.Pressed, "#save")
|
||||
def save_settings(self):
|
||||
self.dismiss(
|
||||
{
|
||||
"mapping_data": self.data.get("mapping_data", {}),
|
||||
"work_path": self.query_one("#work_path").value,
|
||||
"folder_name": self.query_one("#folder_name").value,
|
||||
"name_format": self.query_one("#name_format").value,
|
||||
"user_agent": self.query_one("#user_agent").value,
|
||||
"cookie": self.query_one("#cookie").value or self.data["cookie"],
|
||||
"proxy": self.query_one("#proxy").value or None,
|
||||
"timeout": int(self.query_one("#timeout").value),
|
||||
"chunk": int(self.query_one("#chunk").value),
|
||||
"max_retry": int(self.query_one("#max_retry").value),
|
||||
"record_data": self.query_one("#record_data").value,
|
||||
"image_format": self.query_one("#image_format").value,
|
||||
"folder_mode": self.query_one("#folder_mode").value,
|
||||
"language": self.query_one("#language").value,
|
||||
"image_download": self.query_one("#image_download").value,
|
||||
"video_download": self.query_one("#video_download").value,
|
||||
"live_download": self.query_one("#live_download").value,
|
||||
"download_record": self.query_one("#download_record").value,
|
||||
"author_archive": self.query_one("#author_archive").value,
|
||||
"write_mtime": self.query_one("#write_mtime").value,
|
||||
"script_server": self.query_one("#script_server").value,
|
||||
}
|
||||
)
|
||||
|
||||
@on(Button.Pressed, "#abandon")
|
||||
def reset(self):
|
||||
self.dismiss(self.data)
|
||||
|
||||
async def action_quit(self) -> None:
|
||||
await self.app.action_quit()
|
||||
|
||||
async def action_index(self):
|
||||
await self.app.action_back()
|
||||
92
source/TUI/update.py
Normal file
@ -0,0 +1,92 @@
|
||||
from textual import work
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Grid
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Label, LoadingIndicator
|
||||
|
||||
from ..application import XHS
|
||||
from ..module import (
|
||||
RELEASES,
|
||||
)
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Update"]
|
||||
|
||||
|
||||
class Update(ModalScreen):
|
||||
def __init__(
|
||||
self,
|
||||
app: XHS,
|
||||
):
|
||||
super().__init__()
|
||||
self.xhs = app
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Grid(
|
||||
Label(_("正在检查新版本,请稍等...")),
|
||||
LoadingIndicator(),
|
||||
classes="loading",
|
||||
)
|
||||
|
||||
@work(exclusive=True)
|
||||
async def check_update(self) -> None:
|
||||
try:
|
||||
url = await self.xhs.html.request_url(
|
||||
RELEASES,
|
||||
False,
|
||||
timeout=5,
|
||||
)
|
||||
version = url.split("/")[-1]
|
||||
match self.compare_versions(
|
||||
f"{XHS.VERSION_MAJOR}.{XHS.VERSION_MINOR}", version, XHS.VERSION_BETA
|
||||
):
|
||||
case 4:
|
||||
args = (
|
||||
_("检测到新版本:{0}.{1}").format(
|
||||
XHS.VERSION_MAJOR,
|
||||
XHS.VERSION_MINOR,
|
||||
),
|
||||
"warning",
|
||||
)
|
||||
case 3:
|
||||
args = (
|
||||
_("当前版本为开发版, 可更新至正式版"),
|
||||
"warning",
|
||||
)
|
||||
case 2:
|
||||
args = (
|
||||
_("当前已是最新开发版"),
|
||||
"warning",
|
||||
)
|
||||
case 1:
|
||||
args = (
|
||||
_("当前已是最新正式版"),
|
||||
"information",
|
||||
)
|
||||
case _:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
args = (
|
||||
_("检测新版本失败"),
|
||||
"error",
|
||||
)
|
||||
self.dismiss(args)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.check_update()
|
||||
|
||||
@staticmethod
|
||||
def compare_versions(
|
||||
current_version: str, target_version: str, is_development: bool
|
||||
) -> int:
|
||||
current_major, current_minor = map(int, current_version.split("."))
|
||||
target_major, target_minor = map(int, target_version.split("."))
|
||||
|
||||
if target_major > current_major:
|
||||
return 4
|
||||
if target_major == current_major:
|
||||
if target_minor > current_minor:
|
||||
return 4
|
||||
if target_minor == current_minor:
|
||||
return 3 if is_development else 1
|
||||
return 2
|
||||
@ -1,17 +0,0 @@
|
||||
from re import compile
|
||||
|
||||
__all__ = ['Video']
|
||||
|
||||
|
||||
class Video:
|
||||
VIDEO_ID = compile(r'"masterUrl":"(.*?)"')
|
||||
|
||||
def get_video_link(self, html: str):
|
||||
return self.__get_video_link(html)
|
||||
|
||||
def __get_video_link(self, html: str) -> list:
|
||||
return [self.clean_url(u) for u in self.VIDEO_ID.findall(html)]
|
||||
|
||||
@staticmethod
|
||||
def clean_url(url: str) -> str:
|
||||
return bytes(url, "utf-8").decode("unicode_escape")
|
||||
@ -1,74 +1,11 @@
|
||||
from re import compile
|
||||
from .CLI import cli
|
||||
from .TUI import XHSDownloader
|
||||
from .application import XHS
|
||||
from .module import Settings
|
||||
|
||||
from .Download import Download
|
||||
from .Explore import Explore
|
||||
from .Html import Html
|
||||
from .Image import Image
|
||||
from .Settings import Batch
|
||||
from .Settings import Settings
|
||||
from .Video import Video
|
||||
|
||||
__all__ = ['XHS', 'Settings', 'Batch']
|
||||
|
||||
|
||||
class XHS:
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
|
||||
}
|
||||
links = compile(r"https://www.xiaohongshu.com/explore/[0-9a-z]+")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path="./",
|
||||
folder="Download",
|
||||
proxies=None,
|
||||
timeout=10,
|
||||
chunk=256 * 1024,
|
||||
):
|
||||
self.html = Html(self.headers, proxies, timeout)
|
||||
self.image = Image()
|
||||
self.video = Video()
|
||||
self.explore = Explore()
|
||||
self.download = Download(
|
||||
path,
|
||||
folder,
|
||||
self.headers,
|
||||
proxies,
|
||||
chunk)
|
||||
|
||||
def __get_image(self, container: dict, html: str, download):
|
||||
urls = self.image.get_image_link(html)
|
||||
if download:
|
||||
self.download.run(urls, self.__naming_rules(container))
|
||||
container["下载地址"] = urls
|
||||
|
||||
def __get_video(self, container: dict, html: str, download):
|
||||
url = self.video.get_video_link(html)
|
||||
if download:
|
||||
self.download.run(url, self.__naming_rules(container))
|
||||
container["下载地址"] = url
|
||||
|
||||
def extract(self, url: str, download=False) -> dict:
|
||||
if not self.__check(url):
|
||||
print(f"无效的作品链接: {url}")
|
||||
return {}
|
||||
html = self.html.get_html(url)
|
||||
if not html:
|
||||
return {}
|
||||
data = self.explore.run(html)
|
||||
if not data:
|
||||
print(f"获取作品数据失败: {url}")
|
||||
return {}
|
||||
if data["作品类型"] == "视频":
|
||||
self.__get_video(data, html, download)
|
||||
else:
|
||||
self.__get_image(data, html, download)
|
||||
return data
|
||||
|
||||
def __check(self, url: str):
|
||||
return self.links.match(url)
|
||||
|
||||
@staticmethod
|
||||
def __naming_rules(data: dict) -> str:
|
||||
"""下载文件默认使用作品 ID 作为文件名,可修改此方法自定义文件名格式"""
|
||||
return data["作品ID"]
|
||||
__all__ = [
|
||||
"XHS",
|
||||
"XHSDownloader",
|
||||
"cli",
|
||||
"Settings",
|
||||
]
|
||||
|
||||
3
source/application/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from .app import XHS
|
||||
|
||||
__all__ = ["XHS"]
|
||||
981
source/application/app.py
Normal file
@ -0,0 +1,981 @@
|
||||
from asyncio import (
|
||||
Event,
|
||||
Queue,
|
||||
QueueEmpty,
|
||||
create_task,
|
||||
gather,
|
||||
sleep,
|
||||
Future,
|
||||
CancelledError,
|
||||
)
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
from re import compile
|
||||
from urllib.parse import urlparse
|
||||
from textwrap import dedent
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastmcp import FastMCP
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
from types import SimpleNamespace
|
||||
from pyperclip import copy, paste
|
||||
from uvicorn import Config, Server
|
||||
from typing import Callable
|
||||
|
||||
from ..expansion import (
|
||||
BrowserCookie,
|
||||
Cleaner,
|
||||
Converter,
|
||||
Namespace,
|
||||
beautify_string,
|
||||
)
|
||||
from ..module import (
|
||||
__VERSION__,
|
||||
ERROR,
|
||||
MASTER,
|
||||
REPOSITORY,
|
||||
ROOT,
|
||||
VERSION_BETA,
|
||||
VERSION_MAJOR,
|
||||
VERSION_MINOR,
|
||||
WARNING,
|
||||
DataRecorder,
|
||||
ExtractData,
|
||||
ExtractParams,
|
||||
IDRecorder,
|
||||
Manager,
|
||||
MapRecorder,
|
||||
logging,
|
||||
# sleep_time,
|
||||
ScriptServer,
|
||||
INFO,
|
||||
)
|
||||
from ..translation import _, switch_language
|
||||
|
||||
from ..module import Mapping
|
||||
from .download import Download
|
||||
from .explore import Explore
|
||||
from .image import Image
|
||||
from .request import Html
|
||||
from .video import Video
|
||||
from rich import print
|
||||
|
||||
__all__ = ["XHS"]
|
||||
|
||||
|
||||
def data_cache(function):
|
||||
async def inner(
|
||||
self,
|
||||
data: dict,
|
||||
):
|
||||
if self.manager.record_data:
|
||||
download = data["下载地址"]
|
||||
lives = data["动图地址"]
|
||||
await function(
|
||||
self,
|
||||
data,
|
||||
)
|
||||
data["下载地址"] = download
|
||||
data["动图地址"] = lives
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
class Print:
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable = print,
|
||||
):
|
||||
self.func = func
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
):
|
||||
return self.func
|
||||
|
||||
|
||||
class XHS:
|
||||
VERSION_MAJOR = VERSION_MAJOR
|
||||
VERSION_MINOR = VERSION_MINOR
|
||||
VERSION_BETA = VERSION_BETA
|
||||
LINK = compile(r"(?:https?://)?www\.xiaohongshu\.com/explore/\S+")
|
||||
USER = compile(r"(?:https?://)?www\.xiaohongshu\.com/user/profile/[a-z0-9]+/\S+")
|
||||
SHARE = compile(r"(?:https?://)?www\.xiaohongshu\.com/discovery/item/\S+")
|
||||
SHORT = compile(r"(?:https?://)?xhslink\.com/[^\s\"<>\\^`{|},。;!?、【】《》]+")
|
||||
ID = compile(r"(?:explore|item)/(\S+)?\?")
|
||||
ID_USER = compile(r"user/profile/[a-z0-9]+/(\S+)?\?")
|
||||
__INSTANCE = None
|
||||
CLEANER = Cleaner()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = super().__new__(cls)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mapping_data: dict = None,
|
||||
work_path="",
|
||||
folder_name="Download",
|
||||
name_format="发布时间 作者昵称 作品标题",
|
||||
user_agent: str = None,
|
||||
cookie: str = "",
|
||||
proxy: str | dict = None,
|
||||
timeout=10,
|
||||
chunk=1024 * 1024,
|
||||
max_retry=5,
|
||||
record_data=False,
|
||||
image_format="PNG",
|
||||
image_download=True,
|
||||
video_download=True,
|
||||
live_download=False,
|
||||
folder_mode=False,
|
||||
download_record=True,
|
||||
author_archive=False,
|
||||
write_mtime=False,
|
||||
language="zh_CN",
|
||||
read_cookie: int | str = None,
|
||||
script_server: bool = False,
|
||||
script_host="0.0.0.0",
|
||||
script_port=5558,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
switch_language(language)
|
||||
self.print = Print()
|
||||
self.manager = Manager(
|
||||
ROOT,
|
||||
work_path,
|
||||
folder_name,
|
||||
name_format,
|
||||
chunk,
|
||||
user_agent,
|
||||
self.read_browser_cookie(read_cookie) or cookie,
|
||||
proxy,
|
||||
timeout,
|
||||
max_retry,
|
||||
record_data,
|
||||
image_format,
|
||||
image_download,
|
||||
video_download,
|
||||
live_download,
|
||||
download_record,
|
||||
folder_mode,
|
||||
author_archive,
|
||||
write_mtime,
|
||||
script_server,
|
||||
self.CLEANER,
|
||||
self.print,
|
||||
)
|
||||
self.mapping_data = mapping_data or {}
|
||||
self.map_recorder = MapRecorder(
|
||||
self.manager,
|
||||
)
|
||||
self.mapping = Mapping(self.manager, self.map_recorder)
|
||||
self.html = Html(self.manager)
|
||||
self.image = Image()
|
||||
self.video = Video()
|
||||
self.explore = Explore()
|
||||
self.convert = Converter()
|
||||
self.download = Download(self.manager)
|
||||
self.id_recorder = IDRecorder(self.manager)
|
||||
self.data_recorder = DataRecorder(self.manager)
|
||||
self.clipboard_cache: str = ""
|
||||
self.queue = Queue()
|
||||
self.event = Event()
|
||||
self.script = None
|
||||
self.init_script_server(
|
||||
script_host,
|
||||
script_port,
|
||||
)
|
||||
|
||||
def __extract_image(self, container: dict, data: Namespace):
|
||||
container["下载地址"], container["动图地址"] = self.image.get_image_link(
|
||||
data, self.manager.image_format
|
||||
)
|
||||
|
||||
def __extract_video(self, container: dict, data: Namespace):
|
||||
container["下载地址"] = self.video.get_video_link(data)
|
||||
container["动图地址"] = [
|
||||
None,
|
||||
]
|
||||
|
||||
async def __download_files(
|
||||
self,
|
||||
container: dict,
|
||||
download: bool,
|
||||
index,
|
||||
count: SimpleNamespace,
|
||||
):
|
||||
name = self.__naming_rules(container)
|
||||
if (u := container["下载地址"]) and download:
|
||||
if await self.skip_download(i := container["作品ID"]):
|
||||
self.logging(_("作品 {0} 存在下载记录,跳过下载").format(i))
|
||||
count.skip += 1
|
||||
else:
|
||||
__, result = await self.download.run(
|
||||
u,
|
||||
container["动图地址"],
|
||||
index,
|
||||
container["作者ID"]
|
||||
+ "_"
|
||||
+ self.CLEANER.filter_name(container["作者昵称"]),
|
||||
name,
|
||||
container["作品类型"],
|
||||
container["时间戳"],
|
||||
)
|
||||
if result:
|
||||
count.success += 1
|
||||
await self.__add_record(
|
||||
i,
|
||||
)
|
||||
else:
|
||||
count.fail += 1
|
||||
elif not u:
|
||||
self.logging(_("提取作品文件下载地址失败"), ERROR)
|
||||
count.fail += 1
|
||||
await self.save_data(container)
|
||||
|
||||
@data_cache
|
||||
async def save_data(
|
||||
self,
|
||||
data: dict,
|
||||
):
|
||||
data["采集时间"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
data["下载地址"] = " ".join(data["下载地址"])
|
||||
data["动图地址"] = " ".join(i or "NaN" for i in data["动图地址"])
|
||||
data.pop("时间戳", None)
|
||||
await self.data_recorder.add(**data)
|
||||
|
||||
async def __add_record(
|
||||
self,
|
||||
id_: str,
|
||||
) -> None:
|
||||
await self.id_recorder.add(id_)
|
||||
|
||||
async def extract(
|
||||
self,
|
||||
url: str,
|
||||
download=False,
|
||||
index: list | tuple = None,
|
||||
data=True,
|
||||
) -> list[dict]:
|
||||
if not (
|
||||
urls := await self.extract_links(
|
||||
url,
|
||||
)
|
||||
):
|
||||
self.logging(_("提取小红书作品链接失败"), WARNING)
|
||||
return []
|
||||
statistics = SimpleNamespace(
|
||||
all=len(urls),
|
||||
success=0,
|
||||
fail=0,
|
||||
skip=0,
|
||||
)
|
||||
self.logging(_("共 {0} 个小红书作品待处理...").format(statistics.all))
|
||||
result = [
|
||||
await self.__deal_extract(
|
||||
i,
|
||||
download,
|
||||
index,
|
||||
data,
|
||||
count=statistics,
|
||||
)
|
||||
for i in urls
|
||||
]
|
||||
self.show_statistics(
|
||||
statistics,
|
||||
)
|
||||
return result
|
||||
|
||||
def show_statistics(
|
||||
self,
|
||||
statistics: SimpleNamespace,
|
||||
) -> None:
|
||||
self.logging(
|
||||
_("共处理 {0} 个作品,成功 {1} 个,失败 {2} 个,跳过 {3} 个").format(
|
||||
statistics.all,
|
||||
statistics.success,
|
||||
statistics.fail,
|
||||
statistics.skip,
|
||||
),
|
||||
)
|
||||
|
||||
async def extract_cli(
|
||||
self,
|
||||
url: str,
|
||||
download=True,
|
||||
index: list | tuple = None,
|
||||
data=False,
|
||||
) -> None:
|
||||
url = await self.extract_links(
|
||||
url,
|
||||
)
|
||||
if not url:
|
||||
self.logging(_("提取小红书作品链接失败"), WARNING)
|
||||
return
|
||||
if index:
|
||||
await self.__deal_extract(
|
||||
url[0],
|
||||
download,
|
||||
index,
|
||||
data,
|
||||
)
|
||||
else:
|
||||
statistics = SimpleNamespace(
|
||||
all=len(url),
|
||||
success=0,
|
||||
fail=0,
|
||||
skip=0,
|
||||
)
|
||||
[
|
||||
await self.__deal_extract(
|
||||
u,
|
||||
download,
|
||||
index,
|
||||
data,
|
||||
count=statistics,
|
||||
)
|
||||
for u in url
|
||||
]
|
||||
self.show_statistics(
|
||||
statistics,
|
||||
)
|
||||
|
||||
async def extract_links(
|
||||
self,
|
||||
url: str,
|
||||
) -> list:
|
||||
urls = []
|
||||
for i in url.split():
|
||||
if u := self.SHORT.search(i):
|
||||
i = await self.html.request_url(
|
||||
u.group(),
|
||||
False,
|
||||
)
|
||||
if u := self.SHARE.search(i):
|
||||
urls.append(u.group())
|
||||
elif u := self.LINK.search(i):
|
||||
urls.append(u.group())
|
||||
elif u := self.USER.search(i):
|
||||
urls.append(u.group())
|
||||
return urls
|
||||
|
||||
def extract_id(self, links: list[str]) -> list[str]:
|
||||
ids = []
|
||||
for i in links:
|
||||
if j := self.ID.search(i):
|
||||
ids.append(j.group(1))
|
||||
elif j := self.ID_USER.search(i):
|
||||
ids.append(j.group(1))
|
||||
return ids
|
||||
|
||||
async def _get_html_data(
|
||||
self,
|
||||
url: str,
|
||||
data: bool,
|
||||
cookie: str = None,
|
||||
proxy: str = None,
|
||||
count=SimpleNamespace(
|
||||
all=0,
|
||||
success=0,
|
||||
fail=0,
|
||||
skip=0,
|
||||
),
|
||||
) -> tuple[str, Namespace | dict]:
|
||||
if await self.skip_download(id_ := self.__extract_link_id(url)) and not data:
|
||||
msg = _("作品 {0} 存在下载记录,跳过处理").format(id_)
|
||||
self.logging(msg)
|
||||
count.skip += 1
|
||||
return id_, {"message": msg}
|
||||
self.logging(_("开始处理作品:{0}").format(id_))
|
||||
html = await self.html.request_url(
|
||||
url,
|
||||
cookie=cookie,
|
||||
proxy=proxy,
|
||||
)
|
||||
namespace = self.__generate_data_object(html)
|
||||
if not namespace:
|
||||
self.logging(_("{0} 获取数据失败").format(id_), ERROR)
|
||||
count.fail += 1
|
||||
return id_, {}
|
||||
return id_, namespace
|
||||
|
||||
def _extract_data(
|
||||
self,
|
||||
namespace: Namespace,
|
||||
id_: str,
|
||||
count,
|
||||
):
|
||||
data = self.explore.run(namespace)
|
||||
if not data:
|
||||
self.logging(_("{0} 提取数据失败").format(id_), ERROR)
|
||||
count.fail += 1
|
||||
return {}
|
||||
return data
|
||||
|
||||
async def _deal_download_tasks(
|
||||
self,
|
||||
data: dict,
|
||||
namespace: Namespace,
|
||||
id_: str,
|
||||
download: bool,
|
||||
index: list | tuple | None,
|
||||
count: SimpleNamespace,
|
||||
):
|
||||
if data["作品类型"] == _("视频"):
|
||||
self.__extract_video(data, namespace)
|
||||
elif data["作品类型"] in {
|
||||
_("图文"),
|
||||
_("图集"),
|
||||
}:
|
||||
self.__extract_image(data, namespace)
|
||||
else:
|
||||
self.logging(_("未知的作品类型:{0}").format(id_), WARNING)
|
||||
data["下载地址"] = []
|
||||
data["动图地址"] = []
|
||||
await self.update_author_nickname(
|
||||
data,
|
||||
)
|
||||
await self.__download_files(
|
||||
data,
|
||||
download,
|
||||
index,
|
||||
count,
|
||||
)
|
||||
# await sleep_time()
|
||||
return data
|
||||
|
||||
async def __deal_extract(
|
||||
self,
|
||||
url: str,
|
||||
download: bool,
|
||||
index: list | tuple | None,
|
||||
data: bool,
|
||||
cookie: str = None,
|
||||
proxy: str = None,
|
||||
count=SimpleNamespace(
|
||||
all=0,
|
||||
success=0,
|
||||
fail=0,
|
||||
skip=0,
|
||||
),
|
||||
):
|
||||
id_, namespace = await self._get_html_data(
|
||||
url,
|
||||
data,
|
||||
cookie,
|
||||
proxy,
|
||||
count,
|
||||
)
|
||||
if not isinstance(namespace, Namespace):
|
||||
return namespace
|
||||
if not (
|
||||
data := self._extract_data(
|
||||
namespace,
|
||||
id_,
|
||||
count,
|
||||
)
|
||||
):
|
||||
return data
|
||||
data = await self._deal_download_tasks(
|
||||
data,
|
||||
namespace,
|
||||
id_,
|
||||
download,
|
||||
index,
|
||||
count,
|
||||
)
|
||||
self.logging(_("作品处理完成:{0}").format(id_))
|
||||
return data
|
||||
|
||||
async def deal_script_tasks(
|
||||
self,
|
||||
data: dict,
|
||||
index: list | tuple | None,
|
||||
count=SimpleNamespace(
|
||||
all=0,
|
||||
success=0,
|
||||
fail=0,
|
||||
skip=0,
|
||||
),
|
||||
):
|
||||
namespace = self.json_to_namespace(data)
|
||||
id_ = namespace.safe_extract("noteId", "")
|
||||
if not (
|
||||
data := self._extract_data(
|
||||
namespace,
|
||||
id_,
|
||||
count,
|
||||
)
|
||||
):
|
||||
return data
|
||||
return await self._deal_download_tasks(
|
||||
data,
|
||||
namespace,
|
||||
id_,
|
||||
True,
|
||||
index,
|
||||
count,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def json_to_namespace(data: dict) -> Namespace:
|
||||
return Namespace(data)
|
||||
|
||||
async def update_author_nickname(
|
||||
self,
|
||||
container: dict,
|
||||
):
|
||||
if a := self.CLEANER.filter_name(
|
||||
self.mapping_data.get(i := container["作者ID"], "")
|
||||
):
|
||||
container["作者昵称"] = a
|
||||
else:
|
||||
container["作者昵称"] = self.manager.filter_name(container["作者昵称"]) or i
|
||||
await self.mapping.update_cache(
|
||||
i,
|
||||
container["作者昵称"],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __extract_link_id(url: str) -> str:
|
||||
link = urlparse(url)
|
||||
return link.path.split("/")[-1]
|
||||
|
||||
def __generate_data_object(self, html: str) -> Namespace:
|
||||
data = self.convert.run(html)
|
||||
return Namespace(data)
|
||||
|
||||
def __naming_rules(self, data: dict) -> str:
|
||||
keys = self.manager.name_format.split()
|
||||
values = []
|
||||
for key in keys:
|
||||
match key:
|
||||
case "发布时间":
|
||||
values.append(self.__get_name_time(data))
|
||||
case "作品标题":
|
||||
values.append(self.__get_name_title(data))
|
||||
case _:
|
||||
values.append(data[key])
|
||||
return beautify_string(
|
||||
self.CLEANER.filter_name(
|
||||
self.manager.SEPARATE.join(values),
|
||||
default=self.manager.SEPARATE.join(
|
||||
(
|
||||
data["作者ID"],
|
||||
data["作品ID"],
|
||||
)
|
||||
),
|
||||
),
|
||||
length=128,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __get_name_time(data: dict) -> str:
|
||||
return data["发布时间"].replace(":", ".")
|
||||
|
||||
def __get_name_title(self, data: dict) -> str:
|
||||
return (
|
||||
beautify_string(
|
||||
self.manager.filter_name(data["作品标题"]),
|
||||
64,
|
||||
)
|
||||
or data["作品ID"]
|
||||
)
|
||||
|
||||
async def monitor(
|
||||
self,
|
||||
delay=1,
|
||||
download=True,
|
||||
data=False,
|
||||
) -> None:
|
||||
self.logging(
|
||||
_(
|
||||
"程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件,如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!"
|
||||
),
|
||||
style=MASTER,
|
||||
)
|
||||
self.event.clear()
|
||||
copy("")
|
||||
await gather(
|
||||
self.__get_link(delay),
|
||||
self.__receive_link(delay, download=download, index=None, data=data),
|
||||
)
|
||||
|
||||
async def __get_link(self, delay: int):
|
||||
while not self.event.is_set():
|
||||
if (t := paste()).lower() == "close":
|
||||
self.stop_monitor()
|
||||
elif t != self.clipboard_cache:
|
||||
self.clipboard_cache = t
|
||||
create_task(self.__push_link(t))
|
||||
await sleep(delay)
|
||||
|
||||
async def __push_link(
|
||||
self,
|
||||
content: str,
|
||||
):
|
||||
await gather(
|
||||
*[self.queue.put(i) for i in await self.extract_links(content, None)]
|
||||
)
|
||||
|
||||
async def __receive_link(self, delay: int, *args, **kwargs):
|
||||
while not self.event.is_set() or self.queue.qsize() > 0:
|
||||
with suppress(QueueEmpty):
|
||||
await self.__deal_extract(self.queue.get_nowait(), *args, **kwargs)
|
||||
await sleep(delay)
|
||||
|
||||
def stop_monitor(self):
|
||||
self.event.set()
|
||||
|
||||
async def skip_download(self, id_: str) -> bool:
|
||||
return bool(await self.id_recorder.select(id_))
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.id_recorder.__aenter__()
|
||||
await self.data_recorder.__aenter__()
|
||||
await self.map_recorder.__aenter__()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.id_recorder.__aexit__(exc_type, exc_value, traceback)
|
||||
await self.data_recorder.__aexit__(exc_type, exc_value, traceback)
|
||||
await self.map_recorder.__aexit__(exc_type, exc_value, traceback)
|
||||
await self.close()
|
||||
|
||||
async def close(self):
|
||||
await self.stop_script_server()
|
||||
await self.manager.close()
|
||||
|
||||
@staticmethod
|
||||
def read_browser_cookie(value: str | int) -> str:
|
||||
return (
|
||||
BrowserCookie.get(
|
||||
value,
|
||||
domains=[
|
||||
"xiaohongshu.com",
|
||||
],
|
||||
)
|
||||
if value
|
||||
else ""
|
||||
)
|
||||
|
||||
async def run_api_server(
|
||||
self,
|
||||
host="0.0.0.0",
|
||||
port=5556,
|
||||
log_level="info",
|
||||
):
|
||||
api = FastAPI(
|
||||
debug=self.VERSION_BETA,
|
||||
title="XHS-Downloader",
|
||||
version=__VERSION__,
|
||||
)
|
||||
self.setup_routes(api)
|
||||
config = Config(
|
||||
api,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level,
|
||||
)
|
||||
server = Server(config)
|
||||
await server.serve()
|
||||
|
||||
def setup_routes(
|
||||
self,
|
||||
server: FastAPI,
|
||||
):
|
||||
@server.get(
|
||||
"/",
|
||||
summary=_("跳转至项目 GitHub 仓库"),
|
||||
description=_("重定向至项目 GitHub 仓库主页"),
|
||||
tags=["API"],
|
||||
)
|
||||
async def index():
|
||||
return RedirectResponse(url=REPOSITORY)
|
||||
|
||||
@server.post(
|
||||
"/xhs/detail",
|
||||
summary=_("获取作品数据及下载地址"),
|
||||
description=_(
|
||||
dedent("""
|
||||
**参数**:
|
||||
|
||||
- **url**: 小红书作品链接,自动提取,不支持多链接;必需参数
|
||||
- **download**: 是否下载作品文件;设置为 true 将会耗费更多时间;可选参数
|
||||
- **index**: 下载指定序号的图片文件,仅对图文作品生效;download 参数设置为 false 时不生效;可选参数
|
||||
- **cookie**: 请求数据时使用的 Cookie;可选参数
|
||||
- **proxy**: 请求数据时使用的代理;可选参数
|
||||
- **skip**: 是否跳过存在下载记录的作品;设置为 true 将不会返回存在下载记录的作品数据;可选参数
|
||||
""")
|
||||
),
|
||||
tags=["API"],
|
||||
response_model=ExtractData,
|
||||
)
|
||||
async def handle(extract: ExtractParams):
|
||||
data = None
|
||||
url = await self.extract_links(extract.url, None)
|
||||
if not url:
|
||||
msg = _("提取小红书作品链接失败")
|
||||
else:
|
||||
if data := await self.__deal_extract(
|
||||
url[0],
|
||||
extract.download,
|
||||
extract.index,
|
||||
None,
|
||||
None,
|
||||
not extract.skip,
|
||||
extract.cookie,
|
||||
extract.proxy,
|
||||
):
|
||||
msg = _("获取小红书作品数据成功")
|
||||
else:
|
||||
msg = _("获取小红书作品数据失败")
|
||||
return ExtractData(message=msg, params=extract, data=data)
|
||||
|
||||
async def run_mcp_server(
|
||||
self,
|
||||
transport="streamable-http",
|
||||
host="0.0.0.0",
|
||||
port=5556,
|
||||
log_level="INFO",
|
||||
):
|
||||
mcp = FastMCP(
|
||||
"XHS-Downloader",
|
||||
instructions=dedent("""
|
||||
本服务器提供两个 MCP 接口,分别用于获取小红书作品信息数据和下载小红书作品文件,二者互不依赖,可独立调用。
|
||||
|
||||
支持的作品链接格式:
|
||||
- https://www.xiaohongshu.com/explore/...
|
||||
- https://www.xiaohongshu.com/discovery/item/...
|
||||
- https://xhslink.com/...
|
||||
|
||||
get_detail_data
|
||||
功能:输入小红书作品链接,返回该作品的信息数据,不会下载文件。
|
||||
参数:
|
||||
- url(必填):小红书作品链接
|
||||
返回:
|
||||
- message:结果提示
|
||||
- data:作品信息数据
|
||||
|
||||
download_detail
|
||||
功能:输入小红书作品链接,下载作品文件,默认不返回作品信息数据。
|
||||
参数:
|
||||
- url(必填):小红书作品链接
|
||||
- index(选填):根据用户指定的图片序号(如用户说“下载第1和第3张”时,index应为 [1, 3]),生成由所需图片序号组成的列表;如果用户未指定序号,则该字段为 None
|
||||
- return_data(可选):是否返回作品信息数据;如需返回作品信息数据,设置此参数为 true,默认值为 false
|
||||
返回:
|
||||
- message:结果提示
|
||||
- data:作品信息数据,不需要返回作品信息数据时固定为 None
|
||||
"""),
|
||||
version=__VERSION__,
|
||||
)
|
||||
|
||||
@mcp.tool(
|
||||
name="get_detail_data",
|
||||
description=dedent("""
|
||||
功能:输入小红书作品链接,返回该作品的信息数据,不会下载文件。
|
||||
|
||||
参数:
|
||||
url(必填):小红书作品链接,格式如:
|
||||
- https://www.xiaohongshu.com/explore/...
|
||||
- https://www.xiaohongshu.com/discovery/item/...
|
||||
- https://xhslink.com/...
|
||||
|
||||
返回:
|
||||
- message:结果提示
|
||||
- data:作品信息数据
|
||||
"""),
|
||||
tags={
|
||||
"小红书",
|
||||
"XiaoHongShu",
|
||||
"RedNote",
|
||||
},
|
||||
annotations={
|
||||
"title": "获取小红书作品信息数据",
|
||||
"readOnlyHint": False,
|
||||
"destructiveHint": False,
|
||||
"idempotentHint": True,
|
||||
"openWorldHint": True,
|
||||
},
|
||||
)
|
||||
async def get_detail_data(
|
||||
url: Annotated[str, Field(description=_("小红书作品链接"))],
|
||||
) -> dict:
|
||||
msg, data = await self.deal_detail_mcp(
|
||||
url,
|
||||
False,
|
||||
None,
|
||||
)
|
||||
return {
|
||||
"message": msg,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
@mcp.tool(
|
||||
name="download_detail",
|
||||
description=dedent("""
|
||||
功能:输入小红书作品链接,下载作品文件,默认不返回作品信息数据。
|
||||
|
||||
参数:
|
||||
url(必填):小红书作品链接,格式如:
|
||||
- https://www.xiaohongshu.com/explore/...
|
||||
- https://www.xiaohongshu.com/discovery/item/...
|
||||
- https://xhslink.com/...
|
||||
index(选填):根据用户指定的图片序号(如用户说“下载第1和第3张”时,index应为 [1, 3]),生成由所需图片序号组成的列表;如果用户未指定序号,则该字段为 None
|
||||
return_data(可选):是否返回作品信息数据;如需返回作品信息数据,设置此参数为 true,默认值为 false
|
||||
|
||||
返回:
|
||||
- message:结果提示
|
||||
- data:作品信息数据,不需要返回作品信息数据时固定为 None
|
||||
"""),
|
||||
tags={
|
||||
"小红书",
|
||||
"XiaoHongShu",
|
||||
"RedNote",
|
||||
"Download",
|
||||
"下载",
|
||||
},
|
||||
annotations={
|
||||
"title": "下载小红书作品文件,可以返回作品信息数据",
|
||||
"readOnlyHint": False,
|
||||
"destructiveHint": False,
|
||||
"idempotentHint": True,
|
||||
"openWorldHint": True,
|
||||
},
|
||||
)
|
||||
async def download_detail(
|
||||
url: Annotated[str, Field(description=_("小红书作品链接"))],
|
||||
index: Annotated[
|
||||
list[str | int] | None,
|
||||
Field(default=None, description=_("指定需要下载的图文作品序号")),
|
||||
],
|
||||
return_data: Annotated[
|
||||
bool,
|
||||
Field(default=False, description=_("是否需要返回作品信息数据")),
|
||||
],
|
||||
) -> dict:
|
||||
msg, data = await self.deal_detail_mcp(
|
||||
url,
|
||||
True,
|
||||
index,
|
||||
)
|
||||
match (
|
||||
bool(data),
|
||||
return_data,
|
||||
):
|
||||
case (True, True):
|
||||
return {
|
||||
"message": msg + ", " + _("作品文件下载任务执行完毕"),
|
||||
"data": data,
|
||||
}
|
||||
case (True, False):
|
||||
return {
|
||||
"message": _("作品文件下载任务执行完毕"),
|
||||
"data": None,
|
||||
}
|
||||
case (False, True):
|
||||
return {
|
||||
"message": msg + ", " + _("作品文件下载任务未执行"),
|
||||
"data": None,
|
||||
}
|
||||
case (False, False):
|
||||
return {
|
||||
"message": msg + ", " + _("作品文件下载任务未执行"),
|
||||
"data": None,
|
||||
}
|
||||
case _:
|
||||
raise ValueError
|
||||
|
||||
await mcp.run_async(
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
log_level=log_level,
|
||||
)
|
||||
|
||||
async def deal_detail_mcp(
|
||||
self,
|
||||
url: str,
|
||||
download: bool,
|
||||
index: list[str | int] | None,
|
||||
):
|
||||
data = None
|
||||
url = await self.extract_links(url, None)
|
||||
if not url:
|
||||
msg = _("提取小红书作品链接失败")
|
||||
else:
|
||||
if data := await self.__deal_extract(
|
||||
url[0],
|
||||
download,
|
||||
index,
|
||||
None,
|
||||
None,
|
||||
True,
|
||||
):
|
||||
msg = _("获取小红书作品数据成功")
|
||||
else:
|
||||
msg = _("获取小红书作品数据失败")
|
||||
return msg, data
|
||||
|
||||
def init_script_server(
|
||||
self,
|
||||
host="0.0.0.0",
|
||||
port=5558,
|
||||
):
|
||||
if self.manager.script_server:
|
||||
self.run_script_server(host, port)
|
||||
|
||||
async def switch_script_server(
|
||||
self,
|
||||
host="0.0.0.0",
|
||||
port=5558,
|
||||
switch: bool = None,
|
||||
):
|
||||
if switch is None:
|
||||
switch = self.manager.script_server
|
||||
if switch:
|
||||
self.run_script_server(
|
||||
host,
|
||||
port,
|
||||
)
|
||||
else:
|
||||
await self.stop_script_server()
|
||||
|
||||
def run_script_server(
|
||||
self,
|
||||
host="0.0.0.0",
|
||||
port=5558,
|
||||
):
|
||||
if not self.script:
|
||||
self.script = create_task(self._run_script_server(host, port))
|
||||
|
||||
async def _run_script_server(
|
||||
self,
|
||||
host="0.0.0.0",
|
||||
port=5558,
|
||||
):
|
||||
async with ScriptServer(self, host, port):
|
||||
await Future()
|
||||
|
||||
async def stop_script_server(self):
|
||||
if self.script:
|
||||
self.script.cancel()
|
||||
with suppress(CancelledError):
|
||||
await self.script
|
||||
self.script = None
|
||||
|
||||
async def _script_server_debug(self):
|
||||
await self.switch_script_server(
|
||||
switch=self.manager.script_server,
|
||||
)
|
||||
|
||||
def logging(self, text, style=INFO):
|
||||
logging(
|
||||
self.print,
|
||||
text,
|
||||
style,
|
||||
)
|
||||
337
source/application/download.py
Normal file
@ -0,0 +1,337 @@
|
||||
from asyncio import Semaphore, gather
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiofiles import open
|
||||
from httpx import HTTPError
|
||||
|
||||
from ..expansion import CacheError
|
||||
|
||||
# from ..module import WARNING
|
||||
from ..module import (
|
||||
ERROR,
|
||||
FILE_SIGNATURES,
|
||||
FILE_SIGNATURES_LENGTH,
|
||||
MAX_WORKERS,
|
||||
logging,
|
||||
# sleep_time,
|
||||
)
|
||||
from ..module import retry as re_download
|
||||
from ..translation import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from httpx import AsyncClient
|
||||
|
||||
from ..module import Manager
|
||||
|
||||
__all__ = ["Download"]
|
||||
|
||||
|
||||
class Download:
|
||||
SEMAPHORE = Semaphore(MAX_WORKERS)
|
||||
CONTENT_TYPE_MAP = {
|
||||
"image/png": "png",
|
||||
"image/jpeg": "jpeg",
|
||||
"image/webp": "webp",
|
||||
"video/mp4": "mp4",
|
||||
"video/quicktime": "mov",
|
||||
"audio/mp4": "m4a",
|
||||
"audio/mpeg": "mp3",
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
manager: "Manager",
|
||||
):
|
||||
self.manager = manager
|
||||
self.print = manager.print
|
||||
self.folder = manager.folder
|
||||
self.temp = manager.temp
|
||||
self.chunk = manager.chunk
|
||||
self.client: "AsyncClient" = manager.download_client
|
||||
self.headers = manager.blank_headers
|
||||
self.retry = manager.retry
|
||||
self.folder_mode = manager.folder_mode
|
||||
self.video_format = "mp4"
|
||||
self.live_format = "mp4"
|
||||
self.image_format = manager.image_format
|
||||
self.image_format_list = (
|
||||
"jpeg",
|
||||
"png",
|
||||
"webp",
|
||||
"avif",
|
||||
"heic",
|
||||
)
|
||||
self.image_download = manager.image_download
|
||||
self.video_download = manager.video_download
|
||||
self.live_download = manager.live_download
|
||||
self.author_archive = manager.author_archive
|
||||
self.write_mtime = manager.write_mtime
|
||||
|
||||
async def run(
|
||||
self,
|
||||
urls: list,
|
||||
lives: list,
|
||||
index: list | tuple | None,
|
||||
nickname: str,
|
||||
filename: str,
|
||||
type_: str,
|
||||
mtime: int,
|
||||
) -> tuple[Path, list[Any]]:
|
||||
path = self.__generate_path(nickname, filename)
|
||||
if type_ == _("视频"):
|
||||
tasks = self.__ready_download_video(
|
||||
urls,
|
||||
path,
|
||||
filename,
|
||||
)
|
||||
elif type_ in {
|
||||
_("图文"),
|
||||
_("图集"),
|
||||
}:
|
||||
tasks = self.__ready_download_image(
|
||||
urls,
|
||||
lives,
|
||||
index,
|
||||
path,
|
||||
filename,
|
||||
)
|
||||
else:
|
||||
raise ValueError
|
||||
tasks = [
|
||||
self.__download(
|
||||
url,
|
||||
path,
|
||||
name,
|
||||
format_,
|
||||
mtime,
|
||||
)
|
||||
for url, name, format_ in tasks
|
||||
]
|
||||
tasks = await gather(*tasks)
|
||||
return path, tasks # 未解之谜
|
||||
|
||||
def __generate_path(self, nickname: str, filename: str):
|
||||
if self.author_archive:
|
||||
folder = self.folder.joinpath(nickname)
|
||||
folder.mkdir(exist_ok=True)
|
||||
else:
|
||||
folder = self.folder
|
||||
path = self.manager.archive(folder, filename, self.folder_mode)
|
||||
path.mkdir(exist_ok=True)
|
||||
return path
|
||||
|
||||
def __ready_download_video(
|
||||
self,
|
||||
urls: list[str],
|
||||
path: Path,
|
||||
name: str,
|
||||
) -> list:
|
||||
if not self.video_download:
|
||||
logging(self.print, _("视频作品下载功能已关闭,跳过下载"))
|
||||
return []
|
||||
if self.__check_exists_path(
|
||||
path,
|
||||
f"{name}.{self.video_format}",
|
||||
):
|
||||
return []
|
||||
return [(urls[0], name, self.video_format)]
|
||||
|
||||
def __ready_download_image(
|
||||
self,
|
||||
urls: list[str],
|
||||
lives: list[str],
|
||||
index: list | tuple | None,
|
||||
path: Path,
|
||||
name: str,
|
||||
) -> list:
|
||||
tasks = []
|
||||
if not self.image_download:
|
||||
logging(self.print, _("图文作品下载功能已关闭,跳过下载"))
|
||||
return tasks
|
||||
for i, j in enumerate(zip(urls, lives), start=1):
|
||||
if index and i not in index:
|
||||
continue
|
||||
file = f"{name}_{i}"
|
||||
if not any(
|
||||
self.__check_exists_path(
|
||||
path,
|
||||
f"{file}.{s}",
|
||||
)
|
||||
for s in self.image_format_list
|
||||
):
|
||||
tasks.append([j[0], file, self.image_format])
|
||||
if (
|
||||
not self.live_download
|
||||
or not j[1]
|
||||
or self.__check_exists_path(
|
||||
path,
|
||||
f"{file}.{self.live_format}",
|
||||
)
|
||||
):
|
||||
continue
|
||||
tasks.append([j[1], file, self.live_format])
|
||||
return tasks
|
||||
|
||||
def __check_exists_glob(
|
||||
self,
|
||||
path: Path,
|
||||
name: str,
|
||||
) -> bool:
|
||||
if any(path.glob(name)):
|
||||
logging(self.print, _("{0} 文件已存在,跳过下载").format(name))
|
||||
return True
|
||||
return False
|
||||
|
||||
def __check_exists_path(
|
||||
self,
|
||||
path: Path,
|
||||
name: str,
|
||||
) -> bool:
|
||||
if path.joinpath(name).exists():
|
||||
logging(self.print, _("{0} 文件已存在,跳过下载").format(name))
|
||||
return True
|
||||
return False
|
||||
|
||||
@re_download
|
||||
async def __download(
|
||||
self,
|
||||
url: str,
|
||||
path: Path,
|
||||
name: str,
|
||||
format_: str,
|
||||
mtime: int,
|
||||
):
|
||||
async with self.SEMAPHORE:
|
||||
headers = self.headers.copy()
|
||||
temp = self.temp.joinpath(f"{name}.{format_}")
|
||||
self.__update_headers_range(
|
||||
headers,
|
||||
temp,
|
||||
)
|
||||
try:
|
||||
async with self.client.stream(
|
||||
"GET",
|
||||
url,
|
||||
headers=headers,
|
||||
) as response:
|
||||
# await sleep_time()
|
||||
if response.status_code == 416:
|
||||
raise CacheError(
|
||||
_("文件 {0} 缓存异常,重新下载").format(temp.name),
|
||||
)
|
||||
response.raise_for_status()
|
||||
# self.__create_progress(
|
||||
# bar,
|
||||
# int(
|
||||
# response.headers.get(
|
||||
# 'content-length', 0)) or None,
|
||||
# )
|
||||
async with open(temp, "ab") as f:
|
||||
async for chunk in response.aiter_bytes(self.chunk):
|
||||
await f.write(chunk)
|
||||
# self.__update_progress(bar, len(chunk))
|
||||
real = await self.__suffix_with_file(
|
||||
temp,
|
||||
path,
|
||||
name,
|
||||
# suffix,
|
||||
format_,
|
||||
)
|
||||
self.manager.move(
|
||||
temp,
|
||||
real,
|
||||
mtime,
|
||||
self.write_mtime,
|
||||
)
|
||||
# self.__create_progress(bar, None)
|
||||
logging(self.print, _("文件 {0} 下载成功").format(real.name))
|
||||
return True
|
||||
except HTTPError as error:
|
||||
# self.__create_progress(bar, None)
|
||||
logging(
|
||||
self.print,
|
||||
_("网络异常,{0} 下载失败,错误信息: {1}").format(
|
||||
name, repr(error)
|
||||
),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
except CacheError as error:
|
||||
self.manager.delete(temp)
|
||||
logging(
|
||||
self.print,
|
||||
str(error),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __create_progress(
|
||||
bar,
|
||||
total: int | None,
|
||||
completed=0,
|
||||
):
|
||||
if bar:
|
||||
bar.update(total=total, completed=completed)
|
||||
|
||||
@staticmethod
|
||||
def __update_progress(bar, advance: int):
|
||||
if bar:
|
||||
bar.advance(advance)
|
||||
|
||||
@classmethod
|
||||
def __extract_type(cls, content: str) -> str:
|
||||
return cls.CONTENT_TYPE_MAP.get(content, "")
|
||||
|
||||
async def __head_file(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict[str, str],
|
||||
suffix: str,
|
||||
) -> tuple[int, str]:
|
||||
"""未使用"""
|
||||
response = await self.client.head(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
# await sleep_time()
|
||||
response.raise_for_status()
|
||||
suffix = self.__extract_type(response.headers.get("Content-Type")) or suffix
|
||||
length = response.headers.get("Content-Length", 0)
|
||||
return int(length), suffix
|
||||
|
||||
@staticmethod
|
||||
def __get_resume_byte_position(file: Path) -> int:
|
||||
return file.stat().st_size if file.is_file() else 0
|
||||
|
||||
def __update_headers_range(
|
||||
self,
|
||||
headers: dict[str, str],
|
||||
file: Path,
|
||||
) -> int:
|
||||
headers["Range"] = f"bytes={(p := self.__get_resume_byte_position(file))}-"
|
||||
return p
|
||||
|
||||
async def __suffix_with_file(
|
||||
self,
|
||||
temp: Path,
|
||||
path: Path,
|
||||
name: str,
|
||||
default_suffix: str,
|
||||
) -> Path:
|
||||
try:
|
||||
async with open(temp, "rb") as f:
|
||||
file_start = await f.read(FILE_SIGNATURES_LENGTH)
|
||||
for offset, signature, suffix in FILE_SIGNATURES:
|
||||
if file_start[offset : offset + len(signature)] == signature:
|
||||
return path.joinpath(f"{name}.{suffix}")
|
||||
except Exception as error:
|
||||
logging(
|
||||
self.print,
|
||||
_("文件 {0} 格式判断失败,错误信息:{1}").format(
|
||||
temp.name, repr(error)
|
||||
),
|
||||
ERROR,
|
||||
)
|
||||
return path.joinpath(f"{name}.{default_suffix}")
|
||||
80
source/application/explore.py
Normal file
@ -0,0 +1,80 @@
|
||||
from datetime import datetime
|
||||
|
||||
from ..expansion import Namespace
|
||||
from ..translation import _
|
||||
|
||||
__all__ = ["Explore"]
|
||||
|
||||
|
||||
class Explore:
|
||||
time_format = "%Y-%m-%d_%H:%M:%S"
|
||||
|
||||
def run(self, data: Namespace) -> dict:
|
||||
return self.__extract_data(data)
|
||||
|
||||
def __extract_data(self, data: Namespace) -> dict:
|
||||
result = {}
|
||||
if data:
|
||||
self.__extract_interact_info(result, data)
|
||||
self.__extract_tags(result, data)
|
||||
self.__extract_info(result, data)
|
||||
self.__extract_time(result, data)
|
||||
self.__extract_user(result, data)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def __extract_interact_info(container: dict, data: Namespace) -> None:
|
||||
container["收藏数量"] = data.safe_extract("interactInfo.collectedCount", "-1")
|
||||
container["评论数量"] = data.safe_extract("interactInfo.commentCount", "-1")
|
||||
container["分享数量"] = data.safe_extract("interactInfo.shareCount", "-1")
|
||||
container["点赞数量"] = data.safe_extract("interactInfo.likedCount", "-1")
|
||||
|
||||
@staticmethod
|
||||
def __extract_tags(container: dict, data: Namespace):
|
||||
tags = data.safe_extract("tagList", [])
|
||||
container["作品标签"] = " ".join(
|
||||
Namespace.object_extract(i, "name") for i in tags
|
||||
)
|
||||
|
||||
def __extract_info(self, container: dict, data: Namespace):
|
||||
container["作品ID"] = data.safe_extract("noteId")
|
||||
container["作品链接"] = (
|
||||
f"https://www.xiaohongshu.com/explore/{container['作品ID']}"
|
||||
)
|
||||
container["作品标题"] = data.safe_extract("title")
|
||||
container["作品描述"] = data.safe_extract("desc")
|
||||
container["作品类型"] = self.__classify_works(data)
|
||||
# container["IP归属地"] = data.safe_extract("ipLocation")
|
||||
|
||||
def __extract_time(self, container: dict, data: Namespace):
|
||||
container["发布时间"] = (
|
||||
datetime.fromtimestamp(time / 1000).strftime(self.time_format)
|
||||
if (time := data.safe_extract("time"))
|
||||
else _("未知")
|
||||
)
|
||||
container["最后更新时间"] = (
|
||||
datetime.fromtimestamp(last / 1000).strftime(self.time_format)
|
||||
if (last := data.safe_extract("lastUpdateTime"))
|
||||
else _("未知")
|
||||
)
|
||||
container["时间戳"] = (
|
||||
(time / 1000) if (time := data.safe_extract("time")) else None
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __extract_user(container: dict, data: Namespace):
|
||||
container["作者昵称"] = data.safe_extract("user.nickname")
|
||||
container["作者ID"] = data.safe_extract("user.userId")
|
||||
container["作者链接"] = (
|
||||
f"https://www.xiaohongshu.com/user/profile/{container['作者ID']}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def __classify_works(data: Namespace) -> str:
|
||||
type_ = data.safe_extract("type")
|
||||
list_ = data.safe_extract("imageList", [])
|
||||
if type_ not in {"video", "normal"} or len(list_) == 0:
|
||||
return _("未知")
|
||||
if type_ == "video":
|
||||
return _("视频") if len(list_) == 1 else _("图集")
|
||||
return _("图文")
|
||||
60
source/application/image.py
Normal file
@ -0,0 +1,60 @@
|
||||
from source.expansion import Namespace
|
||||
|
||||
from .request import Html
|
||||
|
||||
__all__ = ["Image"]
|
||||
|
||||
|
||||
class Image:
|
||||
@classmethod
|
||||
def get_image_link(cls, data: Namespace, format_: str) -> tuple[list, list]:
|
||||
images = data.safe_extract("imageList", [])
|
||||
live_link = cls.__get_live_link(images)
|
||||
token_list = [
|
||||
cls.__extract_image_token(Namespace.object_extract(i, "urlDefault"))
|
||||
for i in images
|
||||
]
|
||||
match format_:
|
||||
case "png" | "webp" | "jpeg" | "heic" | "avif":
|
||||
return [
|
||||
Html.format_url(
|
||||
cls.__generate_fixed_link(
|
||||
i,
|
||||
format_,
|
||||
)
|
||||
)
|
||||
for i in token_list
|
||||
], live_link
|
||||
case "auto":
|
||||
return [
|
||||
Html.format_url(cls.__generate_auto_link(i)) for i in token_list
|
||||
], live_link
|
||||
case _:
|
||||
raise ValueError
|
||||
|
||||
@staticmethod
|
||||
def __generate_auto_link(token: str) -> str:
|
||||
return f"https://sns-img-bd.xhscdn.com/{token}"
|
||||
|
||||
@staticmethod
|
||||
def __generate_fixed_link(
|
||||
token: str,
|
||||
format_: str,
|
||||
) -> str:
|
||||
return f"https://ci.xiaohongshu.com/{token}?imageView2/format/{format_}"
|
||||
|
||||
@staticmethod
|
||||
def __extract_image_token(url: str) -> str:
|
||||
return "/".join(url.split("/")[5:]).split("!")[0]
|
||||
|
||||
@staticmethod
|
||||
def __get_live_link(items: list) -> list:
|
||||
return [
|
||||
(
|
||||
Html.format_url(
|
||||
Namespace.object_extract(item, "stream.h264[0].masterUrl")
|
||||
)
|
||||
or None
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
137
source/application/request.py
Normal file
@ -0,0 +1,137 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from httpx import HTTPError
|
||||
from httpx import get
|
||||
|
||||
from ..module import ERROR, Manager, logging, retry, sleep_time
|
||||
from ..translation import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..module import Manager
|
||||
|
||||
__all__ = ["Html"]
|
||||
|
||||
|
||||
class Html:
|
||||
def __init__(
|
||||
self,
|
||||
manager: "Manager",
|
||||
):
|
||||
self.print = manager.print
|
||||
self.retry = manager.retry
|
||||
self.client = manager.request_client
|
||||
self.headers = manager.headers
|
||||
self.timeout = manager.timeout
|
||||
|
||||
@retry
|
||||
async def request_url(
|
||||
self,
|
||||
url: str,
|
||||
content=True,
|
||||
cookie: str = None,
|
||||
proxy: str = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
if not url.startswith("http"):
|
||||
url = f"https://{url}"
|
||||
headers = self.update_cookie(
|
||||
cookie,
|
||||
)
|
||||
try:
|
||||
match bool(proxy):
|
||||
case False:
|
||||
response = await self.__request_url_get(
|
||||
url,
|
||||
headers,
|
||||
**kwargs,
|
||||
)
|
||||
await sleep_time()
|
||||
response.raise_for_status()
|
||||
return response.text if content else str(response.url)
|
||||
case True:
|
||||
response = await self.__request_url_get_proxy(
|
||||
url,
|
||||
headers,
|
||||
proxy,
|
||||
**kwargs,
|
||||
)
|
||||
await sleep_time()
|
||||
response.raise_for_status()
|
||||
return response.text if content else str(response.url)
|
||||
case _:
|
||||
raise ValueError
|
||||
except HTTPError as error:
|
||||
logging(
|
||||
self.print,
|
||||
_("网络异常,{0} 请求失败: {1}").format(url, repr(error)),
|
||||
ERROR,
|
||||
)
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def format_url(url: str) -> str:
|
||||
return bytes(url, "utf-8").decode("unicode_escape")
|
||||
|
||||
def update_cookie(
|
||||
self,
|
||||
cookie: str = None,
|
||||
) -> dict:
|
||||
return self.headers | {"Cookie": cookie} if cookie else self.headers.copy()
|
||||
|
||||
async def __request_url_head(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict,
|
||||
**kwargs,
|
||||
):
|
||||
return await self.client.head(
|
||||
url,
|
||||
headers=headers,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def __request_url_head_proxy(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict,
|
||||
proxy: str,
|
||||
**kwargs,
|
||||
):
|
||||
return await self.client.head(
|
||||
url,
|
||||
headers=headers,
|
||||
proxy=proxy,
|
||||
follow_redirects=True,
|
||||
verify=False,
|
||||
timeout=self.timeout,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def __request_url_get(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict,
|
||||
**kwargs,
|
||||
):
|
||||
return await self.client.get(
|
||||
url,
|
||||
headers=headers,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
async def __request_url_get_proxy(
|
||||
self,
|
||||
url: str,
|
||||
headers: dict,
|
||||
proxy: str,
|
||||
**kwargs,
|
||||
):
|
||||
return get(
|
||||
url,
|
||||
headers=headers,
|
||||
proxy=proxy,
|
||||
follow_redirects=True,
|
||||
verify=False,
|
||||
timeout=self.timeout,
|
||||
**kwargs,
|
||||
)
|
||||
20
source/application/video.py
Normal file
@ -0,0 +1,20 @@
|
||||
from source.expansion import Namespace
|
||||
from .request import Html
|
||||
|
||||
__all__ = ["Video"]
|
||||
|
||||
|
||||
class Video:
|
||||
VIDEO_LINK = (
|
||||
"video",
|
||||
"consumer",
|
||||
"originVideoKey",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_video_link(cls, data: Namespace) -> list:
|
||||
return (
|
||||
[Html.format_url(f"https://sns-video-bd.xhscdn.com/{t}")]
|
||||
if (t := data.safe_extract(".".join(cls.VIDEO_LINK)))
|
||||
else []
|
||||
)
|
||||
10
source/expansion/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
from .browser import BrowserCookie
|
||||
from .cleaner import Cleaner
|
||||
from .converter import Converter
|
||||
from .error import CacheError
|
||||
from .file_folder import file_switch
|
||||
from .file_folder import remove_empty_directories
|
||||
from .namespace import Namespace
|
||||
from .truncate import beautify_string
|
||||
from .truncate import trim_string
|
||||
from .truncate import truncate_string
|
||||
119
source/expansion/browser.py
Normal file
@ -0,0 +1,119 @@
|
||||
from contextlib import suppress
|
||||
from sys import platform
|
||||
|
||||
from rich.console import Console
|
||||
from rookiepy import (
|
||||
arc,
|
||||
brave,
|
||||
chrome,
|
||||
chromium,
|
||||
edge,
|
||||
firefox,
|
||||
librewolf,
|
||||
opera,
|
||||
opera_gx,
|
||||
vivaldi,
|
||||
)
|
||||
|
||||
try:
|
||||
from source.translation import _
|
||||
except ImportError:
|
||||
_ = lambda s: s
|
||||
|
||||
__all__ = ["BrowserCookie"]
|
||||
|
||||
|
||||
class BrowserCookie:
|
||||
SUPPORT_BROWSER = {
|
||||
"Arc": (arc, "Linux, macOS, Windows"),
|
||||
"Chrome": (chrome, "Linux, macOS, Windows"),
|
||||
"Chromium": (chromium, "Linux, macOS, Windows"),
|
||||
"Opera": (opera, "Linux, macOS, Windows"),
|
||||
"OperaGX": (opera_gx, "macOS, Windows"),
|
||||
"Brave": (brave, "Linux, macOS, Windows"),
|
||||
"Edge": (edge, "Linux, macOS, Windows"),
|
||||
"Vivaldi": (vivaldi, "Linux, macOS, Windows"),
|
||||
"Firefox": (firefox, "Linux, macOS, Windows"),
|
||||
"LibreWolf": (librewolf, "Linux, macOS, Windows"),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def run(
|
||||
cls,
|
||||
domains: list[str],
|
||||
console: Console = None,
|
||||
) -> str | None:
|
||||
console = console or Console()
|
||||
options = "\n".join(
|
||||
f"{i}. {k}: {v[1]}"
|
||||
for i, (k, v) in enumerate(cls.SUPPORT_BROWSER.items(), start=1)
|
||||
)
|
||||
if browser := console.input(
|
||||
_(
|
||||
"读取指定浏览器的 Cookie 并写入配置文件\n"
|
||||
"Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie!\n"
|
||||
"{options}\n请输入浏览器名称或序号:"
|
||||
).format(options=options),
|
||||
):
|
||||
return cls.get(
|
||||
browser,
|
||||
domains,
|
||||
console,
|
||||
)
|
||||
console.print(_("未选择浏览器!"))
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def get(
|
||||
cls,
|
||||
browser: str | int,
|
||||
domains: list[str],
|
||||
console: Console = None,
|
||||
) -> str:
|
||||
console = console or Console()
|
||||
if not (browser := cls.__browser_object(browser)):
|
||||
console.print(_("浏览器名称或序号输入错误!"))
|
||||
return ""
|
||||
try:
|
||||
cookies = browser(domains=domains)
|
||||
return "; ".join(f"{i['name']}={i['value']}" for i in cookies)
|
||||
except RuntimeError:
|
||||
console.print(_("获取 Cookie 失败,未找到 Cookie 数据!"))
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def __browser_object(cls, browser: str | int):
|
||||
with suppress(ValueError):
|
||||
browser = int(browser) - 1
|
||||
if isinstance(browser, int):
|
||||
try:
|
||||
return list(cls.SUPPORT_BROWSER.values())[browser][0]
|
||||
except IndexError:
|
||||
return None
|
||||
if isinstance(browser, str):
|
||||
try:
|
||||
return cls.__match_browser(browser)
|
||||
except KeyError:
|
||||
return None
|
||||
raise TypeError
|
||||
|
||||
@classmethod
|
||||
def __match_browser(cls, browser: str):
|
||||
for i, j in cls.SUPPORT_BROWSER.items():
|
||||
if i.lower() == browser.lower():
|
||||
return j[0]
|
||||
|
||||
|
||||
match platform:
|
||||
case "darwin":
|
||||
from rookiepy import safari
|
||||
|
||||
BrowserCookie.SUPPORT_BROWSER |= {
|
||||
"Safari": (safari, "macOS"),
|
||||
}
|
||||
case "linux":
|
||||
BrowserCookie.SUPPORT_BROWSER.pop("OperaGX")
|
||||
case "win32":
|
||||
pass
|
||||
case _:
|
||||
print(_("从浏览器读取 Cookie 功能不支持当前平台!"))
|
||||
116
source/expansion/cleaner.py
Normal file
@ -0,0 +1,116 @@
|
||||
from platform import system
|
||||
from re import compile
|
||||
from string import whitespace
|
||||
from warnings import warn
|
||||
|
||||
from emoji import replace_emoji
|
||||
|
||||
try:
|
||||
from source.translation import _
|
||||
except ImportError:
|
||||
_ = lambda s: s
|
||||
|
||||
|
||||
class Cleaner:
|
||||
CONTROL_CHARACTERS = compile(r"[\x00-\x1F\x7F]")
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
替换字符串中包含的非法字符,默认根据系统类型生成对应的非法字符字典,也可以自行设置非法字符字典
|
||||
"""
|
||||
self.rule = self.default_rule() # 默认非法字符字典
|
||||
|
||||
@staticmethod
|
||||
def default_rule():
|
||||
"""根据系统类型生成默认非法字符字典"""
|
||||
if (s := system()) in ("Windows", "Darwin"):
|
||||
rule = {
|
||||
"/": "",
|
||||
"\\": "",
|
||||
"|": "",
|
||||
"<": "",
|
||||
">": "",
|
||||
'"': "",
|
||||
"?": "",
|
||||
":": "",
|
||||
"*": "",
|
||||
"\x00": "",
|
||||
} # Windows 系统和 Mac 系统
|
||||
elif s == "Linux":
|
||||
rule = {
|
||||
"/": "",
|
||||
"\x00": "",
|
||||
} # Linux 系统
|
||||
else:
|
||||
warn(_("不受支持的操作系统类型,可能无法正常去除非法字符!"))
|
||||
rule = {}
|
||||
cache = {i: "" for i in whitespace[1:]} # 补充换行符等非法字符
|
||||
return rule | cache
|
||||
|
||||
def set_rule(self, rule: dict[str, str], update=True):
|
||||
"""
|
||||
设置非法字符字典
|
||||
|
||||
:param rule: 替换规则,字典格式,键为非法字符,值为替换后的内容
|
||||
:param update: 如果是 True,则与原有规则字典合并,否则替换原有规则字典
|
||||
"""
|
||||
self.rule = {**self.rule, **rule} if update else rule
|
||||
|
||||
def filter(self, text: str) -> str:
|
||||
"""
|
||||
去除非法字符
|
||||
|
||||
:param text: 待处理的字符串
|
||||
:return: 替换后的字符串,如果替换后字符串为空,则返回 None
|
||||
"""
|
||||
for i in self.rule:
|
||||
text = text.replace(i, self.rule[i])
|
||||
return text
|
||||
|
||||
def filter_name(
|
||||
self,
|
||||
text: str,
|
||||
replace: str = "",
|
||||
default: str = "",
|
||||
) -> str:
|
||||
"""过滤文件夹名称中的非法字符"""
|
||||
text = text.replace(":", ".")
|
||||
|
||||
text = self.remove_control_characters(text)
|
||||
|
||||
text = self.filter(text)
|
||||
|
||||
text = replace_emoji(
|
||||
text,
|
||||
replace,
|
||||
)
|
||||
|
||||
text = self.clear_spaces(text)
|
||||
|
||||
text = text.strip().strip(".").strip("_")
|
||||
|
||||
return text or default
|
||||
|
||||
@staticmethod
|
||||
def clear_spaces(string: str):
|
||||
"""将连续的空格转换为单个空格"""
|
||||
return " ".join(string.split())
|
||||
|
||||
@classmethod
|
||||
def remove_control_characters(
|
||||
cls,
|
||||
text,
|
||||
replace="",
|
||||
):
|
||||
# 使用正则表达式匹配所有控制字符
|
||||
return cls.CONTROL_CHARACTERS.sub(
|
||||
replace,
|
||||
text,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo = Cleaner()
|
||||
print(demo.rule)
|
||||
print(demo.filter_name(""))
|
||||
print(demo.remove_control_characters("hello \x08world"))
|
||||
64
source/expansion/converter.py
Normal file
@ -0,0 +1,64 @@
|
||||
from typing import Union
|
||||
|
||||
from lxml.etree import HTML
|
||||
from yaml import safe_load
|
||||
|
||||
__all__ = ["Converter"]
|
||||
|
||||
|
||||
class Converter:
|
||||
INITIAL_STATE = "//script/text()"
|
||||
KEYS_LINK = (
|
||||
"note",
|
||||
"noteDetailMap",
|
||||
"[-1]",
|
||||
"note",
|
||||
)
|
||||
|
||||
def run(self, content: str) -> dict:
|
||||
return self._filter_object(self._convert_object(self._extract_object(content)))
|
||||
|
||||
def _extract_object(self, html: str) -> str:
|
||||
if not html:
|
||||
return ""
|
||||
html_tree = HTML(html)
|
||||
scripts = html_tree.xpath(self.INITIAL_STATE)
|
||||
return self.get_script(scripts)
|
||||
|
||||
@staticmethod
|
||||
def _convert_object(text: str) -> dict:
|
||||
return safe_load(text.lstrip("window.__INITIAL_STATE__="))
|
||||
|
||||
@classmethod
|
||||
def _filter_object(cls, data: dict) -> dict:
|
||||
return cls.deep_get(data, cls.KEYS_LINK) or {}
|
||||
|
||||
@classmethod
|
||||
def deep_get(cls, data: dict, keys: list | tuple, default=None):
|
||||
if not data:
|
||||
return default
|
||||
try:
|
||||
for key in keys:
|
||||
if key.startswith("[") and key.endswith("]"):
|
||||
data = cls.safe_get(data, int(key[1:-1]))
|
||||
else:
|
||||
data = data[key]
|
||||
return data
|
||||
except (KeyError, IndexError, ValueError, TypeError):
|
||||
return default
|
||||
|
||||
@staticmethod
|
||||
def safe_get(data: Union[dict, list, tuple, set], index: int):
|
||||
if isinstance(data, dict):
|
||||
return list(data.values())[index]
|
||||
elif isinstance(data, list | tuple | set):
|
||||
return data[index]
|
||||
raise TypeError
|
||||
|
||||
@staticmethod
|
||||
def get_script(scripts: list) -> str:
|
||||
scripts.reverse()
|
||||
for script in scripts:
|
||||
if script.startswith("window.__INITIAL_STATE__"):
|
||||
return script
|
||||
return ""
|
||||
7
source/expansion/error.py
Normal file
@ -0,0 +1,7 @@
|
||||
class CacheError(Exception):
|
||||
def __init__(self, message: str):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
25
source/expansion/file_folder.py
Normal file
@ -0,0 +1,25 @@
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def file_switch(path: Path) -> None:
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
else:
|
||||
path.touch()
|
||||
|
||||
|
||||
def remove_empty_directories(path: Path) -> None:
|
||||
exclude = {
|
||||
"\\.",
|
||||
"\\_",
|
||||
"\\__",
|
||||
}
|
||||
for dir_path, dir_names, file_names in path.walk(
|
||||
top_down=False,
|
||||
):
|
||||
if any(i in str(dir_path) for i in exclude):
|
||||
continue
|
||||
if not dir_names and not file_names:
|
||||
with suppress(OSError):
|
||||
dir_path.rmdir()
|
||||
84
source/expansion/namespace.py
Normal file
@ -0,0 +1,84 @@
|
||||
from copy import deepcopy
|
||||
from types import SimpleNamespace
|
||||
from typing import Union
|
||||
|
||||
__all__ = ["Namespace"]
|
||||
|
||||
|
||||
class Namespace:
|
||||
def __init__(self, data: dict) -> None:
|
||||
self.data: SimpleNamespace = self.generate_data_object(data)
|
||||
|
||||
@staticmethod
|
||||
def generate_data_object(data: dict) -> SimpleNamespace:
|
||||
def depth_conversion(element):
|
||||
if isinstance(element, dict):
|
||||
return SimpleNamespace(
|
||||
**{k: depth_conversion(v) for k, v in element.items()}
|
||||
)
|
||||
elif isinstance(element, list):
|
||||
return [depth_conversion(item) for item in element]
|
||||
else:
|
||||
return element
|
||||
|
||||
return depth_conversion(data)
|
||||
|
||||
def safe_extract(
|
||||
self,
|
||||
attribute_chain: str,
|
||||
default: Union[str, int, list, dict, SimpleNamespace] = "",
|
||||
):
|
||||
return self.__safe_extract(self.data, attribute_chain, default)
|
||||
|
||||
@staticmethod
|
||||
def __safe_extract(
|
||||
data_object: SimpleNamespace,
|
||||
attribute_chain: str,
|
||||
default: Union[str, int, list, dict, SimpleNamespace] = "",
|
||||
):
|
||||
data = deepcopy(data_object)
|
||||
attributes = attribute_chain.split(".")
|
||||
for attribute in attributes:
|
||||
if "[" in attribute:
|
||||
parts = attribute.split("[", 1)
|
||||
attribute = parts[0]
|
||||
index = parts[1][:-1]
|
||||
try:
|
||||
index = int(index)
|
||||
data = getattr(data, attribute, None)[index]
|
||||
except (IndexError, TypeError, ValueError):
|
||||
return default
|
||||
else:
|
||||
data = getattr(data, attribute, None)
|
||||
if not data:
|
||||
return default
|
||||
return data or default
|
||||
|
||||
@classmethod
|
||||
def object_extract(
|
||||
cls,
|
||||
data_object: SimpleNamespace,
|
||||
attribute_chain: str,
|
||||
default: Union[str, int, list, dict, SimpleNamespace] = "",
|
||||
):
|
||||
return cls.__safe_extract(
|
||||
data_object,
|
||||
attribute_chain,
|
||||
default,
|
||||
)
|
||||
|
||||
@property
|
||||
def __dict__(self):
|
||||
return self.convert_to_dict(self.data)
|
||||
|
||||
@classmethod
|
||||
def convert_to_dict(cls, data) -> dict:
|
||||
return {
|
||||
key: cls.convert_to_dict(value)
|
||||
if isinstance(value, SimpleNamespace)
|
||||
else value
|
||||
for key, value in vars(data).items()
|
||||
}
|
||||
|
||||
def __bool__(self):
|
||||
return bool(vars(self.data))
|
||||
88
source/expansion/pyi_rth_beartype.py
Normal file
@ -0,0 +1,88 @@
|
||||
"""
|
||||
PyInstaller runtime hook for beartype compatibility.
|
||||
|
||||
This runtime hook runs BEFORE any user code and patches beartype's import hook
|
||||
system to be compatible with PyInstaller's frozen import mechanism.
|
||||
|
||||
The Problem:
|
||||
-----------
|
||||
Beartype's `beartype.claw` module installs a custom path hook into
|
||||
`sys.path_hooks` that uses `SourceFileLoader` to load and transform Python
|
||||
source files. In PyInstaller's frozen environment:
|
||||
|
||||
1. There are no source .py files - only compiled bytecode in a PYZ archive
|
||||
2. Beartype's hook is prepended to `sys.path_hooks`, taking precedence over
|
||||
PyInstaller's `PyiFrozenFinder`
|
||||
3. Beartype clears `sys.path_importer_cache`, invalidating PyInstaller's
|
||||
cached finders
|
||||
|
||||
This causes `ModuleNotFoundError` for any module imported AFTER beartype's
|
||||
hook is installed.
|
||||
|
||||
The Fix:
|
||||
--------
|
||||
This runtime hook monkey-patches beartype's `add_beartype_pathhook` function
|
||||
to detect PyInstaller's frozen environment and skip installing the problematic
|
||||
path hook.
|
||||
|
||||
See Also:
|
||||
---------
|
||||
- https://github.com/beartype/beartype/issues/599
|
||||
- https://github.com/pyinstaller/pyinstaller/issues/9324
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
|
||||
def _is_pyinstaller_frozen():
|
||||
"""Check if running in a PyInstaller frozen environment."""
|
||||
return getattr(sys, "frozen", False) or hasattr(sys, "_MEIPASS")
|
||||
|
||||
|
||||
def _patch_beartype_claw():
|
||||
"""
|
||||
Patch beartype's add_beartype_pathhook to skip in frozen environments.
|
||||
|
||||
This patches the function at import time before any user code can call it.
|
||||
"""
|
||||
# Only patch if we're actually frozen
|
||||
if not _is_pyinstaller_frozen():
|
||||
return
|
||||
|
||||
try:
|
||||
# Import the module containing the function to patch
|
||||
from beartype.claw._importlib import clawimpmain
|
||||
|
||||
# Store the original function
|
||||
_original_add_beartype_pathhook = clawimpmain.add_beartype_pathhook
|
||||
|
||||
def _patched_add_beartype_pathhook():
|
||||
"""
|
||||
Patched version of add_beartype_pathhook that skips in frozen env.
|
||||
"""
|
||||
if _is_pyinstaller_frozen():
|
||||
# In frozen environment, skip installing the path hook entirely
|
||||
# This prevents breaking PyInstaller's frozen import system
|
||||
return
|
||||
# Otherwise, call the original
|
||||
return _original_add_beartype_pathhook()
|
||||
|
||||
# Replace the function in clawimpmain module
|
||||
clawimpmain.add_beartype_pathhook = _patched_add_beartype_pathhook
|
||||
|
||||
# CRITICAL: Also patch clawpkgmain which does `from ... import add_beartype_pathhook`
|
||||
# and thus has its own local reference to the original function
|
||||
from beartype.claw._package import clawpkgmain
|
||||
|
||||
clawpkgmain.add_beartype_pathhook = _patched_add_beartype_pathhook
|
||||
|
||||
except ImportError:
|
||||
# beartype not installed or not using claw module
|
||||
pass
|
||||
except Exception:
|
||||
# Don't crash on patch failure
|
||||
pass
|
||||
|
||||
|
||||
# Apply the patch when this runtime hook is loaded
|
||||
_patch_beartype_claw()
|
||||
35
source/expansion/truncate.py
Normal file
@ -0,0 +1,35 @@
|
||||
from unicodedata import name
|
||||
|
||||
|
||||
def is_chinese_char(char: str) -> bool:
|
||||
return "CJK" in name(char, "")
|
||||
|
||||
|
||||
def truncate_string(s: str, length: int = 64) -> str:
|
||||
count = 0
|
||||
result = ""
|
||||
for char in s:
|
||||
count += 2 if is_chinese_char(char) else 1
|
||||
if count > length:
|
||||
break
|
||||
result += char
|
||||
return result
|
||||
|
||||
|
||||
def trim_string(s: str, length: int = 64) -> str:
|
||||
length = length // 2 - 2
|
||||
return f"{s[:length]}...{s[-length:]}" if len(s) > length else s
|
||||
|
||||
|
||||
def beautify_string(s: str, length: int = 64) -> str:
|
||||
count = 0
|
||||
for char in s:
|
||||
count += 2 if is_chinese_char(char) else 1
|
||||
if count > length:
|
||||
break
|
||||
else:
|
||||
return s
|
||||
length //= 2
|
||||
start = truncate_string(s, length)
|
||||
end = truncate_string(s[::-1], length)[::-1]
|
||||
return f"{start}...{end}"
|
||||
42
source/module/__init__.py
Normal file
@ -0,0 +1,42 @@
|
||||
from .extend import Account
|
||||
from .manager import Manager
|
||||
from .model import (
|
||||
ExtractData,
|
||||
ExtractParams,
|
||||
)
|
||||
from .recorder import DataRecorder
|
||||
from .recorder import IDRecorder
|
||||
from .recorder import MapRecorder
|
||||
from .mapping import Mapping
|
||||
from .settings import Settings
|
||||
from .static import (
|
||||
VERSION_MAJOR,
|
||||
VERSION_MINOR,
|
||||
VERSION_BETA,
|
||||
ROOT,
|
||||
REPOSITORY,
|
||||
LICENCE,
|
||||
RELEASES,
|
||||
MASTER,
|
||||
PROMPT,
|
||||
GENERAL,
|
||||
PROGRESS,
|
||||
ERROR,
|
||||
WARNING,
|
||||
INFO,
|
||||
USERSCRIPT,
|
||||
HEADERS,
|
||||
PROJECT,
|
||||
USERAGENT,
|
||||
FILE_SIGNATURES,
|
||||
FILE_SIGNATURES_LENGTH,
|
||||
MAX_WORKERS,
|
||||
__VERSION__,
|
||||
)
|
||||
from .tools import (
|
||||
retry,
|
||||
logging,
|
||||
sleep_time,
|
||||
retry_limited,
|
||||
)
|
||||
from .script import ScriptServer
|
||||
5
source/module/extend.py
Normal file
@ -0,0 +1,5 @@
|
||||
__all__ = ["Account"]
|
||||
|
||||
|
||||
class Account:
|
||||
pass
|
||||
293
source/module/manager.py
Normal file
@ -0,0 +1,293 @@
|
||||
from pathlib import Path
|
||||
from re import compile, sub
|
||||
from shutil import move, rmtree
|
||||
from os import utime
|
||||
from httpx import (
|
||||
AsyncClient,
|
||||
AsyncHTTPTransport,
|
||||
HTTPStatusError,
|
||||
RequestError,
|
||||
TimeoutException,
|
||||
get,
|
||||
)
|
||||
|
||||
from source.expansion import remove_empty_directories
|
||||
|
||||
from ..translation import _
|
||||
from .static import HEADERS, USERAGENT, WARNING
|
||||
from .tools import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..expansion import Cleaner
|
||||
|
||||
__all__ = ["Manager"]
|
||||
|
||||
|
||||
class Manager:
|
||||
NAME = compile(r"[^\u4e00-\u9fffa-zA-Z0-9-_!?,。;:“”()《》]")
|
||||
NAME_KEYS = (
|
||||
"收藏数量",
|
||||
"评论数量",
|
||||
"分享数量",
|
||||
"点赞数量",
|
||||
"作品标签",
|
||||
"作品ID",
|
||||
"作品标题",
|
||||
"作品描述",
|
||||
"作品类型",
|
||||
"发布时间",
|
||||
"最后更新时间",
|
||||
"作者昵称",
|
||||
"作者ID",
|
||||
)
|
||||
NO_PROXY = {
|
||||
"http://": None,
|
||||
"https://": None,
|
||||
}
|
||||
SEPARATE = "_"
|
||||
WEB_ID = r"(?:^|; )webId=[^;]+"
|
||||
WEB_SESSION = r"(?:^|; )web_session=[^;]+"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: Path,
|
||||
path: str,
|
||||
folder: str,
|
||||
name_format: str,
|
||||
chunk: int,
|
||||
user_agent: str,
|
||||
cookie: str,
|
||||
proxy: str | dict,
|
||||
timeout: int,
|
||||
retry: int,
|
||||
record_data: bool,
|
||||
image_format: str,
|
||||
image_download: bool,
|
||||
video_download: bool,
|
||||
live_download: bool,
|
||||
download_record: bool,
|
||||
folder_mode: bool,
|
||||
author_archive: bool,
|
||||
write_mtime: bool,
|
||||
script_server: bool,
|
||||
cleaner: "Cleaner",
|
||||
print_object,
|
||||
):
|
||||
self.print = print_object
|
||||
self.root = root
|
||||
self.cleaner = cleaner
|
||||
self.temp = root.joinpath("Temp")
|
||||
self.path = self.__check_path(path)
|
||||
self.folder = self.__check_folder(folder)
|
||||
self.compatible()
|
||||
self.blank_headers = HEADERS | {
|
||||
"user-agent": user_agent or USERAGENT,
|
||||
}
|
||||
self.headers = self.blank_headers | {
|
||||
"cookie": cookie,
|
||||
}
|
||||
self.retry = retry
|
||||
self.chunk = chunk
|
||||
self.name_format = self.__check_name_format(name_format)
|
||||
self.record_data = self.check_bool(record_data, False)
|
||||
self.image_format = self.__check_image_format(image_format)
|
||||
self.folder_mode = self.check_bool(folder_mode, False)
|
||||
self.download_record = self.check_bool(download_record, True)
|
||||
self.proxy_tip = None
|
||||
self.proxy = self.__check_proxy(proxy)
|
||||
self.print_proxy_tip()
|
||||
self.timeout = timeout
|
||||
self.request_client = AsyncClient(
|
||||
headers=self.headers
|
||||
| {
|
||||
"referer": "https://www.xiaohongshu.com/",
|
||||
},
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
follow_redirects=True,
|
||||
mounts={
|
||||
"http://": AsyncHTTPTransport(proxy=self.proxy),
|
||||
"https://": AsyncHTTPTransport(proxy=self.proxy),
|
||||
},
|
||||
)
|
||||
self.download_client = AsyncClient(
|
||||
headers=self.blank_headers,
|
||||
timeout=timeout,
|
||||
verify=False,
|
||||
follow_redirects=True,
|
||||
mounts={
|
||||
"http://": AsyncHTTPTransport(proxy=self.proxy),
|
||||
"https://": AsyncHTTPTransport(proxy=self.proxy),
|
||||
},
|
||||
)
|
||||
self.image_download = self.check_bool(image_download, True)
|
||||
self.video_download = self.check_bool(video_download, True)
|
||||
self.live_download = self.check_bool(live_download, True)
|
||||
self.author_archive = self.check_bool(author_archive, False)
|
||||
self.write_mtime = self.check_bool(write_mtime, False)
|
||||
self.script_server = self.check_bool(script_server, False)
|
||||
self.create_folder()
|
||||
|
||||
def __check_path(self, path: str) -> Path:
|
||||
if not path:
|
||||
return self.root
|
||||
if (r := Path(path)).is_dir():
|
||||
return r
|
||||
return r if (r := self.__check_root_again(r)) else self.root
|
||||
|
||||
def __check_folder(self, folder: str) -> Path:
|
||||
folder = self.cleaner.filter_name(folder, default="Download")
|
||||
return self.path.joinpath(folder)
|
||||
|
||||
@staticmethod
|
||||
def __check_root_again(root: Path) -> bool | Path:
|
||||
if root.parent.is_dir():
|
||||
root.mkdir(exist_ok=True)
|
||||
return root
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def __check_image_format(image_format) -> str:
|
||||
if (i := image_format.lower()) in {
|
||||
"auto",
|
||||
"png",
|
||||
"webp",
|
||||
"jpeg",
|
||||
"heic",
|
||||
"avif",
|
||||
}:
|
||||
return i
|
||||
return "png"
|
||||
|
||||
@staticmethod
|
||||
def is_exists(path: Path) -> bool:
|
||||
return path.exists()
|
||||
|
||||
@staticmethod
|
||||
def delete(path: Path):
|
||||
if path.exists():
|
||||
path.unlink()
|
||||
|
||||
@staticmethod
|
||||
def archive(root: Path, name: str, folder_mode: bool) -> Path:
|
||||
return root.joinpath(name) if folder_mode else root
|
||||
|
||||
@classmethod
|
||||
def move(
|
||||
cls,
|
||||
temp: Path,
|
||||
path: Path,
|
||||
mtime: int = None,
|
||||
rewrite: bool = False,
|
||||
):
|
||||
move(temp.resolve(), path.resolve())
|
||||
if rewrite and mtime:
|
||||
cls.update_mtime(path.resolve(), mtime)
|
||||
|
||||
@staticmethod
|
||||
def update_mtime(file: Path, mtime: int):
|
||||
utime(file, (mtime, mtime))
|
||||
|
||||
def __clean(self):
|
||||
rmtree(self.temp.resolve())
|
||||
|
||||
def filter_name(self, name: str) -> str:
|
||||
name = self.NAME.sub("_", name)
|
||||
return sub(r"_+", "_", name).strip("_")
|
||||
|
||||
@staticmethod
|
||||
def check_bool(value: bool, default: bool) -> bool:
|
||||
return value if isinstance(value, bool) else default
|
||||
|
||||
async def close(self):
|
||||
await self.request_client.aclose()
|
||||
await self.download_client.aclose()
|
||||
# self.__clean()
|
||||
remove_empty_directories(self.root)
|
||||
remove_empty_directories(self.folder)
|
||||
|
||||
def __check_name_format(self, format_: str) -> str:
|
||||
keys = format_.split()
|
||||
return next(
|
||||
("发布时间 作者昵称 作品标题" for key in keys if key not in self.NAME_KEYS),
|
||||
format_,
|
||||
)
|
||||
|
||||
def __check_proxy(
|
||||
self,
|
||||
proxy: str,
|
||||
url="https://www.xiaohongshu.com/explore",
|
||||
) -> str | None:
|
||||
if proxy:
|
||||
try:
|
||||
response = get(
|
||||
url,
|
||||
proxy=proxy,
|
||||
timeout=10,
|
||||
headers={
|
||||
"User-Agent": USERAGENT,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
self.proxy_tip = (_("代理 {0} 测试成功").format(proxy),)
|
||||
return proxy
|
||||
except TimeoutException:
|
||||
self.proxy_tip = (
|
||||
_("代理 {0} 测试超时").format(proxy),
|
||||
WARNING,
|
||||
)
|
||||
except (
|
||||
RequestError,
|
||||
HTTPStatusError,
|
||||
) as e:
|
||||
self.proxy_tip = (
|
||||
_("代理 {0} 测试失败:{1}").format(
|
||||
proxy,
|
||||
e,
|
||||
),
|
||||
WARNING,
|
||||
)
|
||||
return None
|
||||
|
||||
def print_proxy_tip(
|
||||
self,
|
||||
) -> None:
|
||||
if self.proxy_tip:
|
||||
logging(self.print, *self.proxy_tip)
|
||||
|
||||
@classmethod
|
||||
def clean_cookie(cls, cookie_string: str) -> str:
|
||||
return cls.delete_cookie(
|
||||
cookie_string,
|
||||
(
|
||||
cls.WEB_ID,
|
||||
cls.WEB_SESSION,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def delete_cookie(cls, cookie_string: str, patterns: list | tuple) -> str:
|
||||
for pattern in patterns:
|
||||
# 使用空字符串替换匹配到的部分
|
||||
cookie_string = sub(pattern, "", cookie_string)
|
||||
# 去除多余的分号和空格
|
||||
cookie_string = sub(r";\s*$", "", cookie_string) # 删除末尾的分号和空格
|
||||
cookie_string = sub(r";\s*;", ";", cookie_string) # 删除中间多余分号后的空格
|
||||
return cookie_string.strip("; ")
|
||||
|
||||
def create_folder(
|
||||
self,
|
||||
):
|
||||
self.folder.mkdir(exist_ok=True)
|
||||
self.temp.mkdir(exist_ok=True)
|
||||
|
||||
def compatible(
|
||||
self,
|
||||
):
|
||||
if (
|
||||
self.path == self.root
|
||||
and (old := self.path.parent.joinpath(self.folder.name)).exists()
|
||||
and not self.folder.exists()
|
||||
):
|
||||
move(old, self.folder)
|
||||
216
source/module/mapping.py
Normal file
@ -0,0 +1,216 @@
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ..translation import _
|
||||
from .static import ERROR
|
||||
from .tools import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manager import Manager
|
||||
from recorder import MapRecorder
|
||||
|
||||
|
||||
__all__ = ["Mapping"]
|
||||
|
||||
|
||||
class Mapping:
|
||||
def __init__(
|
||||
self,
|
||||
manager: "Manager",
|
||||
mapping: "MapRecorder",
|
||||
):
|
||||
self.root = manager.folder
|
||||
self.folder_mode = manager.folder_mode
|
||||
self.database = mapping
|
||||
self.switch = manager.author_archive
|
||||
self.print = manager.print
|
||||
|
||||
async def update_cache(
|
||||
self,
|
||||
id_: str,
|
||||
alias: str,
|
||||
):
|
||||
if not self.switch:
|
||||
return
|
||||
if (a := await self.has_mapping(id_)) and a != alias:
|
||||
self.__check_file(
|
||||
id_,
|
||||
alias,
|
||||
a,
|
||||
)
|
||||
await self.database.add(id_, alias)
|
||||
|
||||
async def has_mapping(self, id_: str) -> str:
|
||||
return d[0] if (d := await self.database.select(id_)) else ""
|
||||
|
||||
def __check_file(
|
||||
self,
|
||||
id_: str,
|
||||
alias: str,
|
||||
old_alias: str,
|
||||
):
|
||||
if not (old_folder := self.root.joinpath(f"{id_}_{old_alias}")).is_dir():
|
||||
logging(
|
||||
self.print,
|
||||
_("{old_folder} 文件夹不存在,跳过处理").format(
|
||||
old_folder=old_folder.name
|
||||
),
|
||||
)
|
||||
return
|
||||
self.__rename_folder(
|
||||
old_folder,
|
||||
id_,
|
||||
alias,
|
||||
)
|
||||
self.__scan_file(
|
||||
id_,
|
||||
alias,
|
||||
old_alias,
|
||||
)
|
||||
|
||||
def __rename_folder(
|
||||
self,
|
||||
old_folder: Path,
|
||||
id_: str,
|
||||
alias: str,
|
||||
):
|
||||
new_folder = self.root.joinpath(f"{id_}_{alias}")
|
||||
self.__rename(
|
||||
old_folder,
|
||||
new_folder,
|
||||
_("文件夹"),
|
||||
)
|
||||
logging(
|
||||
self.print,
|
||||
_("文件夹 {old_folder} 已重命名为 {new_folder}").format(
|
||||
old_folder=old_folder.name, new_folder=new_folder.name
|
||||
),
|
||||
)
|
||||
|
||||
def __rename_works_folder(
|
||||
self,
|
||||
old_: Path,
|
||||
alias: str,
|
||||
old_alias: str,
|
||||
) -> Path:
|
||||
if old_alias in old_.name:
|
||||
new_ = old_.parent / old_.name.replace(old_alias, alias, 1)
|
||||
self.__rename(
|
||||
old_,
|
||||
new_,
|
||||
_("文件夹"),
|
||||
)
|
||||
logging(
|
||||
self.print,
|
||||
_("文件夹 {old_} 重命名为 {new_}").format(
|
||||
old_=old_.name, new_=new_.name
|
||||
),
|
||||
)
|
||||
return new_
|
||||
return old_
|
||||
|
||||
def __scan_file(
|
||||
self,
|
||||
id_: str,
|
||||
alias: str,
|
||||
old_alias: str,
|
||||
):
|
||||
root = self.root.joinpath(f"{id_}_{alias}")
|
||||
item_list = root.iterdir()
|
||||
if self.folder_mode:
|
||||
for f in item_list:
|
||||
if f.is_dir():
|
||||
f = self.__rename_works_folder(
|
||||
f,
|
||||
alias,
|
||||
old_alias,
|
||||
)
|
||||
files = f.iterdir()
|
||||
self.__batch_rename(
|
||||
f,
|
||||
files,
|
||||
alias,
|
||||
old_alias,
|
||||
)
|
||||
else:
|
||||
self.__batch_rename(
|
||||
root,
|
||||
item_list,
|
||||
alias,
|
||||
old_alias,
|
||||
)
|
||||
|
||||
def __batch_rename(
|
||||
self,
|
||||
root: Path,
|
||||
files,
|
||||
alias: str,
|
||||
old_alias: str,
|
||||
):
|
||||
for old_file in files:
|
||||
if old_alias not in old_file.name:
|
||||
break
|
||||
self.__rename_file(
|
||||
root,
|
||||
old_file,
|
||||
alias,
|
||||
old_alias,
|
||||
)
|
||||
|
||||
def __rename_file(
|
||||
self,
|
||||
root: Path,
|
||||
old_file: Path,
|
||||
alias: str,
|
||||
old_alias: str,
|
||||
):
|
||||
new_file = root.joinpath(old_file.name.replace(old_alias, alias, 1))
|
||||
self.__rename(
|
||||
old_file,
|
||||
new_file,
|
||||
_("文件"),
|
||||
)
|
||||
logging(
|
||||
self.print,
|
||||
_("文件 {old_file} 重命名为 {new_file}").format(
|
||||
old_file=old_file.name, new_file=new_file.name
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
def __rename(
|
||||
self,
|
||||
old_: Path,
|
||||
new_: Path,
|
||||
type_=_("文件"),
|
||||
) -> bool:
|
||||
try:
|
||||
old_.rename(new_)
|
||||
return True
|
||||
except PermissionError as e:
|
||||
logging(
|
||||
self.print,
|
||||
_("{type} {old}被占用,重命名失败: {error}").format(
|
||||
type=type_, old=old_.name, error=e
|
||||
),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
except FileExistsError as e:
|
||||
logging(
|
||||
self.print,
|
||||
_("{type} {new}名称重复,重命名失败: {error}").format(
|
||||
type=type_, new=new_.name, error=e
|
||||
),
|
||||
ERROR,
|
||||
)
|
||||
return False
|
||||
except OSError as e:
|
||||
logging(
|
||||
self.print,
|
||||
_("处理{type} {old}时发生预期之外的错误: {error}").format(
|
||||
type=type_, old=old_.name, error=e
|
||||
),
|
||||
ERROR,
|
||||
)
|
||||
return True
|
||||
16
source/module/model.py
Normal file
@ -0,0 +1,16 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ExtractParams(BaseModel):
|
||||
url: str
|
||||
download: bool = False
|
||||
index: list[str | int] | None = None
|
||||
cookie: str = None
|
||||
proxy: str = None
|
||||
skip: bool = False
|
||||
|
||||
|
||||
class ExtractData(BaseModel):
|
||||
message: str
|
||||
params: ExtractParams
|
||||
data: dict | None
|
||||
191
source/module/recorder.py
Normal file
@ -0,0 +1,191 @@
|
||||
from asyncio import CancelledError
|
||||
from contextlib import suppress
|
||||
from typing import TYPE_CHECKING
|
||||
from shutil import move
|
||||
from aiosqlite import connect
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..module import Manager
|
||||
|
||||
__all__ = ["IDRecorder", "DataRecorder", "MapRecorder"]
|
||||
|
||||
|
||||
class IDRecorder:
|
||||
def __init__(self, manager: "Manager"):
|
||||
self.name = "ExploreID.db"
|
||||
self.file = manager.root.joinpath(self.name)
|
||||
self.changed = False
|
||||
self.switch = manager.download_record
|
||||
self.database = None
|
||||
self.cursor = None
|
||||
|
||||
async def _connect_database(self):
|
||||
self.database = await connect(self.file)
|
||||
self.cursor = await self.database.cursor()
|
||||
await self.database.execute(
|
||||
"CREATE TABLE IF NOT EXISTS explore_id (ID TEXT PRIMARY KEY);"
|
||||
)
|
||||
await self.database.commit()
|
||||
|
||||
async def select(self, id_: str):
|
||||
if self.switch:
|
||||
await self.cursor.execute("SELECT ID FROM explore_id WHERE ID=?", (id_,))
|
||||
return await self.cursor.fetchone()
|
||||
|
||||
async def add(
|
||||
self,
|
||||
id_: str,
|
||||
name: str = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
if self.switch:
|
||||
await self.database.execute("REPLACE INTO explore_id VALUES (?);", (id_,))
|
||||
await self.database.commit()
|
||||
|
||||
async def __delete(self, id_: str) -> None:
|
||||
if id_:
|
||||
await self.database.execute("DELETE FROM explore_id WHERE ID=?", (id_,))
|
||||
await self.database.commit()
|
||||
|
||||
async def delete(self, ids: list[str]):
|
||||
if self.switch:
|
||||
[await self.__delete(i) for i in ids]
|
||||
|
||||
async def all(self):
|
||||
if self.switch:
|
||||
await self.cursor.execute("SELECT ID FROM explore_id")
|
||||
return [i[0] for i in await self.cursor.fetchmany()]
|
||||
|
||||
async def __aenter__(self):
|
||||
self.compatible()
|
||||
await self._connect_database()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
with suppress(CancelledError):
|
||||
await self.cursor.close()
|
||||
await self.database.close()
|
||||
|
||||
def compatible(
|
||||
self,
|
||||
):
|
||||
if (
|
||||
not self.changed
|
||||
and (old := self.file.parent.parent.joinpath(self.name)).exists()
|
||||
and not self.file.exists()
|
||||
):
|
||||
move(old, self.file)
|
||||
|
||||
|
||||
class DataRecorder(IDRecorder):
|
||||
DATA_TABLE = (
|
||||
("采集时间", "TEXT"),
|
||||
("作品ID", "TEXT PRIMARY KEY"),
|
||||
("作品类型", "TEXT"),
|
||||
("作品标题", "TEXT"),
|
||||
("作品描述", "TEXT"),
|
||||
("作品标签", "TEXT"),
|
||||
("发布时间", "TEXT"),
|
||||
("最后更新时间", "TEXT"),
|
||||
("收藏数量", "TEXT"),
|
||||
("评论数量", "TEXT"),
|
||||
("分享数量", "TEXT"),
|
||||
("点赞数量", "TEXT"),
|
||||
("作者昵称", "TEXT"),
|
||||
("作者ID", "TEXT"),
|
||||
("作者链接", "TEXT"),
|
||||
("作品链接", "TEXT"),
|
||||
("下载地址", "TEXT"),
|
||||
("动图地址", "TEXT"),
|
||||
)
|
||||
|
||||
def __init__(self, manager: "Manager"):
|
||||
super().__init__(manager)
|
||||
self.name = "ExploreData.db"
|
||||
self.file = manager.folder.joinpath(self.name)
|
||||
self.changed = True
|
||||
self.switch = manager.record_data
|
||||
|
||||
async def _connect_database(self):
|
||||
self.database = await connect(self.file)
|
||||
self.cursor = await self.database.cursor()
|
||||
await self.database.execute(f"""CREATE TABLE IF NOT EXISTS explore_data (
|
||||
{",".join(" ".join(i) for i in self.DATA_TABLE)}
|
||||
);""")
|
||||
await self.database.commit()
|
||||
|
||||
async def select(self, id_: str):
|
||||
pass
|
||||
|
||||
async def add(self, **kwargs) -> None:
|
||||
if self.switch:
|
||||
await self.database.execute(
|
||||
f"""REPLACE INTO explore_data (
|
||||
{", ".join(i[0] for i in self.DATA_TABLE)}
|
||||
) VALUES (
|
||||
{", ".join("?" for _ in kwargs)}
|
||||
);""",
|
||||
self.__generate_values(kwargs),
|
||||
)
|
||||
await self.database.commit()
|
||||
|
||||
async def __delete(self, id_: str) -> None:
|
||||
pass
|
||||
|
||||
async def delete(self, ids: list | tuple):
|
||||
pass
|
||||
|
||||
async def all(self):
|
||||
pass
|
||||
|
||||
def __generate_values(self, data: dict) -> tuple:
|
||||
return tuple(data[i] for i, _ in self.DATA_TABLE)
|
||||
|
||||
|
||||
class MapRecorder(IDRecorder):
|
||||
def __init__(self, manager: "Manager"):
|
||||
super().__init__(manager)
|
||||
self.name = "MappingData.db"
|
||||
self.file = manager.root.joinpath(self.name)
|
||||
self.switch = manager.author_archive
|
||||
|
||||
async def _connect_database(self):
|
||||
self.database = await connect(self.file)
|
||||
self.cursor = await self.database.cursor()
|
||||
await self.database.execute(
|
||||
"CREATE TABLE IF NOT EXISTS mapping_data ("
|
||||
"ID TEXT PRIMARY KEY,"
|
||||
"NAME TEXT NOT NULL"
|
||||
");"
|
||||
)
|
||||
await self.database.commit()
|
||||
|
||||
async def select(self, id_: str):
|
||||
if self.switch:
|
||||
await self.cursor.execute(
|
||||
"SELECT NAME FROM mapping_data WHERE ID=?", (id_,)
|
||||
)
|
||||
return await self.cursor.fetchone()
|
||||
|
||||
async def add(self, id_: str, name: str, *args, **kwargs) -> None:
|
||||
if self.switch:
|
||||
await self.database.execute(
|
||||
"REPLACE INTO mapping_data VALUES (?, ?);",
|
||||
(
|
||||
id_,
|
||||
name,
|
||||
),
|
||||
)
|
||||
await self.database.commit()
|
||||
|
||||
async def __delete(self, id_: str) -> None:
|
||||
pass
|
||||
|
||||
async def delete(self, ids: list[str]):
|
||||
pass
|
||||
|
||||
async def all(self):
|
||||
if self.switch:
|
||||
await self.cursor.execute("SELECT ID, NAME FROM mapping_data")
|
||||
return [i[0] for i in await self.cursor.fetchmany()]
|
||||
47
source/module/script.py
Normal file
@ -0,0 +1,47 @@
|
||||
from contextlib import suppress
|
||||
from json import loads
|
||||
from websockets import ConnectionClosed, serve
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..application import XHS
|
||||
|
||||
|
||||
class ScriptServer:
|
||||
def __init__(
|
||||
self,
|
||||
core: "XHS",
|
||||
host="0.0.0.0",
|
||||
port=5558,
|
||||
):
|
||||
self.core = core
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server = None
|
||||
|
||||
async def handler(self, websocket):
|
||||
with suppress(ConnectionClosed):
|
||||
async for message in websocket:
|
||||
data = loads(message)
|
||||
await self.core.deal_script_tasks(**data)
|
||||
|
||||
async def start(self):
|
||||
"""启动服务器"""
|
||||
self.server = await serve(
|
||||
self.handler,
|
||||
self.host,
|
||||
self.port,
|
||||
)
|
||||
|
||||
async def stop(self):
|
||||
"""停止服务器"""
|
||||
if self.server:
|
||||
self.server.close()
|
||||
await self.server.wait_closed()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
await self.stop()
|
||||
120
source/module/settings.py
Normal file
@ -0,0 +1,120 @@
|
||||
from json import dump, load
|
||||
from pathlib import Path
|
||||
from platform import system
|
||||
from shutil import move
|
||||
from .static import ROOT, USERAGENT
|
||||
|
||||
__all__ = ["Settings"]
|
||||
|
||||
|
||||
class Settings:
|
||||
# 默认配置参数
|
||||
default = {
|
||||
"mapping_data": {}, # 账号备注映射数据
|
||||
"work_path": "", # 工作目录路径
|
||||
"folder_name": "Download", # 下载文件夹名称
|
||||
"name_format": "发布时间 作者昵称 作品标题", # 文件命名格式
|
||||
"user_agent": USERAGENT, # 请求头
|
||||
"cookie": "", # Cookie
|
||||
"proxy": None, # 代理设置
|
||||
"timeout": 10, # 超时时间(秒)
|
||||
"chunk": 1024 * 1024 * 2, # 下载块大小(字节)
|
||||
"max_retry": 5, # 最大重试次数
|
||||
"record_data": False, # 是否记录作品数据
|
||||
"image_format": "PNG", # 图文作品格式
|
||||
"image_download": True, # 是否下载图文
|
||||
"video_download": True, # 是否下载视频
|
||||
"live_download": False, # 是否下载动图
|
||||
"folder_mode": False, # 文件夹归档模式
|
||||
"download_record": True, # 是否记录下载历史
|
||||
"author_archive": False, # 是否按作者归档
|
||||
"write_mtime": False, # 是否写入修改时间
|
||||
"language": "zh_CN", # 语言设置
|
||||
"script_server": False, # 是否启用脚本服务器
|
||||
}
|
||||
# 根据操作系统设置编码格式
|
||||
encode = "UTF-8-SIG" if system() == "Windows" else "UTF-8"
|
||||
|
||||
def __init__(self, root: Path = ROOT):
|
||||
"""初始化Settings类
|
||||
|
||||
Args:
|
||||
root: 设置文件的根目录路径,默认为ROOT
|
||||
"""
|
||||
# 设置文件路径
|
||||
self.name = "settings.json"
|
||||
self.root = root
|
||||
self.path = root.joinpath(self.name)
|
||||
|
||||
def run(self):
|
||||
"""运行设置管理
|
||||
|
||||
Returns:
|
||||
dict: 设置参数字典
|
||||
"""
|
||||
self.migration_file()
|
||||
# 如果文件存在则读取,否则创建新文件
|
||||
return self.read() if self.path.is_file() else self.create()
|
||||
|
||||
def read(self) -> dict:
|
||||
"""读取设置文件
|
||||
|
||||
Returns:
|
||||
dict: 读取的设置参数字典
|
||||
"""
|
||||
# 读取设置文件
|
||||
with self.path.open("r", encoding=self.encode) as f:
|
||||
return self.compatible(load(f))
|
||||
|
||||
def create(self) -> dict:
|
||||
"""创建新的设置文件
|
||||
|
||||
Returns:
|
||||
dict: 默认设置参数字典
|
||||
"""
|
||||
# 创建新的设置文件
|
||||
with self.path.open("w", encoding=self.encode) as f:
|
||||
dump(self.default, f, indent=4, ensure_ascii=False)
|
||||
return self.default
|
||||
|
||||
def update(self, data: dict):
|
||||
"""更新设置文件内容
|
||||
|
||||
Args:
|
||||
data: 要更新的设置参数字典
|
||||
"""
|
||||
# 更新设置文件
|
||||
with self.path.open("w", encoding=self.encode) as f:
|
||||
dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def compatible(
|
||||
self,
|
||||
data: dict,
|
||||
) -> dict:
|
||||
"""兼容性检查,确保所有默认配置都存在
|
||||
|
||||
Args:
|
||||
data: 要检查的设置参数字典
|
||||
|
||||
Returns:
|
||||
dict: 经过兼容性检查后的设置参数字典
|
||||
"""
|
||||
# 兼容性检查: 确保所有默认配置都存在
|
||||
update = False
|
||||
for i, j in self.default.items():
|
||||
if i not in data:
|
||||
data[i] = j
|
||||
update = True
|
||||
if update:
|
||||
self.update(data)
|
||||
return data
|
||||
|
||||
def migration_file(self):
|
||||
"""迁移设置文件
|
||||
|
||||
如果旧的设置文件存在且新路径下不存在,则移动旧文件到新路径
|
||||
"""
|
||||
if (
|
||||
old := self.root.parent.joinpath(self.name)
|
||||
).exists() and not self.path.exists():
|
||||
move(old, self.path)
|
||||
69
source/module/static.py
Normal file
@ -0,0 +1,69 @@
|
||||
from pathlib import Path
|
||||
|
||||
VERSION_MAJOR = 2
|
||||
VERSION_MINOR = 7
|
||||
VERSION_BETA = True
|
||||
__VERSION__ = f"{VERSION_MAJOR}.{VERSION_MINOR}.{'beta' if VERSION_BETA else 'stable'}"
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent.joinpath("Volume")
|
||||
ROOT.mkdir(exist_ok=True)
|
||||
PROJECT = f"XHS-Downloader V{VERSION_MAJOR}.{VERSION_MINOR} {
|
||||
'Beta' if VERSION_BETA else 'Stable'
|
||||
}"
|
||||
|
||||
REPOSITORY = "https://github.com/JoeanAmier/XHS-Downloader"
|
||||
LICENCE = "GNU General Public License v3.0"
|
||||
RELEASES = "https://github.com/JoeanAmier/XHS-Downloader/releases/latest"
|
||||
|
||||
USERSCRIPT = "https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js"
|
||||
|
||||
USERAGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
|
||||
|
||||
HEADERS = {
|
||||
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
|
||||
"application/signed-exchange;v=b3;q=0.7",
|
||||
"referer": "https://www.xiaohongshu.com/explore",
|
||||
"user-agent": USERAGENT,
|
||||
}
|
||||
|
||||
MASTER = "#fff200"
|
||||
PROMPT = "turquoise2"
|
||||
GENERAL = "bright_white"
|
||||
PROGRESS = "bright_magenta"
|
||||
ERROR = "bright_red"
|
||||
WARNING = "bright_yellow"
|
||||
INFO = "bright_green"
|
||||
|
||||
FILE_SIGNATURES: tuple[
|
||||
tuple[
|
||||
int,
|
||||
bytes,
|
||||
str,
|
||||
],
|
||||
...,
|
||||
] = (
|
||||
# 分别为偏移量(字节)、十六进制签名、后缀
|
||||
# 参考:https://en.wikipedia.org/wiki/List_of_file_signatures
|
||||
# 参考:https://www.garykessler.net/library/file_sigs.html
|
||||
(0, b"\xff\xd8\xff", "jpeg"),
|
||||
(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\x68\x65\x69\x63", "heic"),
|
||||
(8, b"\x57\x45\x42\x50", "webp"),
|
||||
(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\x6d\x70\x34\x32", "m4v"),
|
||||
(4, b"\x66\x74\x79\x70\x71\x74\x20\x20", "mov"),
|
||||
(0, b"\x1a\x45\xdf\xa3", "mkv"),
|
||||
(0, b"\x00\x00\x01\xb3", "mpg"),
|
||||
(0, b"\x00\x00\x01\xba", "mpg"),
|
||||
(0, b"\x46\x4c\x56\x01", "flv"),
|
||||
(8, b"\x41\x56\x49\x20", "avi"),
|
||||
)
|
||||
FILE_SIGNATURES_LENGTH = max(
|
||||
offset + len(signature) for offset, signature, _ in FILE_SIGNATURES
|
||||
)
|
||||
|
||||
MAX_WORKERS: int = 4
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(__VERSION__)
|
||||
57
source/module/tools.py
Normal file
@ -0,0 +1,57 @@
|
||||
from asyncio import sleep
|
||||
from random import uniform
|
||||
from typing import Callable
|
||||
|
||||
from rich import print
|
||||
from rich.text import Text
|
||||
|
||||
from ..translation import _
|
||||
from .static import INFO
|
||||
|
||||
|
||||
def retry(function):
|
||||
async def inner(self, *args, **kwargs):
|
||||
if result := await function(self, *args, **kwargs):
|
||||
return result
|
||||
for __ in range(self.retry):
|
||||
if result := await function(self, *args, **kwargs):
|
||||
return result
|
||||
return result
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def retry_limited(function):
|
||||
# TODO: 不支持 TUI
|
||||
def inner(self, *args, **kwargs):
|
||||
while True:
|
||||
if function(self, *args, **kwargs):
|
||||
return
|
||||
if self.console.input(
|
||||
_(
|
||||
"如需重新尝试处理该对象,请关闭所有正在访问该对象的窗口或程序,然后直接按下回车键!\n"
|
||||
"如需跳过处理该对象,请输入任意字符后按下回车键!"
|
||||
),
|
||||
):
|
||||
return
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
def logging(log: Callable, text, style=INFO):
|
||||
string = Text(text, style=style)
|
||||
func = log()
|
||||
if func is print:
|
||||
func(string)
|
||||
else:
|
||||
func.write(
|
||||
string,
|
||||
scroll_end=True,
|
||||
)
|
||||
|
||||
|
||||
async def sleep_time(
|
||||
min_time: int | float = 2.0,
|
||||
max_time: int | float = 4.0,
|
||||
):
|
||||
await sleep(uniform(min_time, max_time))
|
||||
1
source/translation/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .translate import switch_language, _
|
||||
87
source/translation/translate.py
Normal file
@ -0,0 +1,87 @@
|
||||
from gettext import translation
|
||||
from locale import getlocale
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
|
||||
class TranslationManager:
|
||||
"""管理gettext翻译的类"""
|
||||
|
||||
_instance = None # 单例实例
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not cls._instance:
|
||||
cls._instance = super(TranslationManager, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, domain="xhs", localedir=None):
|
||||
self.domain = domain
|
||||
if not localedir:
|
||||
localedir = ROOT.joinpath("locale")
|
||||
self.localedir = Path(localedir)
|
||||
self.current_translator = self.setup_translation(
|
||||
self.get_language_code(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_language_code() -> str:
|
||||
# 获取当前系统的语言和区域设置
|
||||
language_code, __ = getlocale()
|
||||
if not language_code:
|
||||
return "en_US"
|
||||
return (
|
||||
"zh_CN"
|
||||
if any(
|
||||
s in language_code.upper()
|
||||
for s in (
|
||||
"CHINESE",
|
||||
"ZH",
|
||||
"CHINA",
|
||||
)
|
||||
)
|
||||
else "en_US"
|
||||
)
|
||||
|
||||
def setup_translation(self, language: str = "zh_CN"):
|
||||
"""设置gettext翻译环境"""
|
||||
try:
|
||||
return translation(
|
||||
self.domain,
|
||||
localedir=self.localedir,
|
||||
languages=[language],
|
||||
fallback=True,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
print(
|
||||
f"Warning: Translation files for '{self.domain}' not found. Error: {e}"
|
||||
)
|
||||
return translation(self.domain, fallback=True)
|
||||
|
||||
def switch_language(self, language: str = "en_US"):
|
||||
"""切换当前使用的语言"""
|
||||
self.current_translator = self.setup_translation(language)
|
||||
|
||||
def gettext(self, message):
|
||||
"""提供gettext方法"""
|
||||
return self.current_translator.gettext(message)
|
||||
|
||||
|
||||
# 初始化TranslationManager单例实例
|
||||
translation_manager = TranslationManager()
|
||||
|
||||
|
||||
def _translate(message):
|
||||
"""辅助函数来简化翻译调用"""
|
||||
return translation_manager.gettext(message)
|
||||
|
||||
|
||||
def switch_language(language: str = "en_US"):
|
||||
"""切换语言并刷新翻译函数"""
|
||||
global _
|
||||
translation_manager.switch_language(language)
|
||||
_ = translation_manager.gettext
|
||||
|
||||
|
||||
# 设置默认翻译函数
|
||||
_ = _translate
|
||||
2429
static/20250619.js
Normal file
BIN
static/DartNode_AD.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/QQ群聊二维码.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
18
static/Release_Notes.md
Normal file
@ -0,0 +1,18 @@
|
||||
**项目更新内容:**
|
||||
|
||||
1. 修复 MCP 模式无法下载文件的问题
|
||||
2. 新增 `script_server` 配置参数
|
||||
3. 新增连接用户脚本下载作品功能
|
||||
4. 新增作品处理统计功能
|
||||
5. 调整内置延时机制
|
||||
|
||||
*****
|
||||
|
||||
**用户脚本更新内容:**
|
||||
|
||||
**版本号:2.2.3**
|
||||
|
||||
1. 新增链接提取/图片下载选择模式开关
|
||||
2. 修复合辑作品链接提取失败的问题
|
||||
3. 链接提取选择界面添加序号显示
|
||||
4. 新增推送下载任务至服务器功能
|
||||
BIN
static/XHS-Downloader.icns
Normal file
BIN
static/XHS-Downloader.ico
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
static/XHS-Downloader.jpg
Normal file
|
After Width: | Height: | Size: 59 KiB |
2241
static/XHS-Downloader.js
Normal file
BIN
static/XHS-Downloader.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
52
static/XHS-Downloader.tcss
Normal file
@ -0,0 +1,52 @@
|
||||
Button {
|
||||
width: 1fr;
|
||||
margin: 1 1;
|
||||
}
|
||||
.vertical-layout {
|
||||
layout: vertical;
|
||||
height: auto;
|
||||
}
|
||||
.horizontal-layout, .settings_button {
|
||||
layout: horizontal;
|
||||
height: auto;
|
||||
}
|
||||
.horizontal-layout > * {
|
||||
width: 25vw;
|
||||
}
|
||||
Button#deal, Button#paste, Button#save, Button#enter {
|
||||
color: $success;
|
||||
}
|
||||
Button#reset, Button#abandon, Button#close {
|
||||
color: $error;
|
||||
}
|
||||
Label, Link {
|
||||
width: 100%;
|
||||
content-align-horizontal: center;
|
||||
content-align-vertical: middle;
|
||||
}
|
||||
Link {
|
||||
color: $accent;
|
||||
}
|
||||
Label.params {
|
||||
margin: 1 0 0 0;
|
||||
color: $primary;
|
||||
}
|
||||
Label.prompt {
|
||||
padding: 1;
|
||||
}
|
||||
.loading {
|
||||
grid-size: 1 2;
|
||||
grid-gutter: 1;
|
||||
width: 40vw;
|
||||
height: 5;
|
||||
border: double $primary;
|
||||
}
|
||||
#record {
|
||||
grid-size: 1 3;
|
||||
width: 80vw;
|
||||
height: 12;
|
||||
border: double $primary;
|
||||
}
|
||||
ModalScreen {
|
||||
align: center middle;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 25 KiB |
@ -1,6 +0,0 @@
|
||||
Button {
|
||||
width: auto;
|
||||
}
|
||||
RunMenu {
|
||||
layout: horizontal;
|
||||
}
|
||||
BIN
static/screenshot/MCP下载文件1.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
static/screenshot/MCP下载文件2.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
static/screenshot/MCP获取数据.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
static/screenshot/MCP配置示例.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
static/screenshot/命令行模式截图CN1.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
static/screenshot/命令行模式截图CN2.png
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
static/screenshot/命令行模式截图EN1.png
Normal file
|
After Width: | Height: | Size: 343 KiB |
BIN
static/screenshot/命令行模式截图EN2.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
static/screenshot/用户脚本截图1.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
static/screenshot/用户脚本截图2.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
static/screenshot/用户脚本截图3.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
static/screenshot/用户脚本截图4.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
static/screenshot/程序运行截图CN1.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
static/screenshot/程序运行截图CN2.png
Normal file
|
After Width: | Height: | Size: 102 KiB |