Repository: JoeanAmier/XHS_Downloader Branch: master Commit: 8a8c1acb4b84 Files: 75 Total size: 615.4 KB Directory structure: gitextract_832couqt/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── Close_Stale_Issues_and_PRs.yaml │ ├── Delete_untagged_images.yml │ ├── Manually_build_executable_programs.yml │ ├── Manually_docker_image.yml │ ├── Release_build_executable_program.yml │ └── Release_docker_image.yml ├── .gitignore ├── .python-version ├── Dockerfile ├── LICENSE ├── README.md ├── README_EN.md ├── example.py ├── locale/ │ ├── README.md │ ├── en_US/ │ │ └── LC_MESSAGES/ │ │ ├── xhs.mo │ │ └── xhs.po │ ├── generate_path.py │ ├── po_to_mo.py │ ├── xhs.pot │ └── zh_CN/ │ └── LC_MESSAGES/ │ ├── xhs.mo │ └── xhs.po ├── main.py ├── pyproject.toml ├── requirements.txt ├── source/ │ ├── CLI/ │ │ ├── __init__.py │ │ └── main.py │ ├── TUI/ │ │ ├── __init__.py │ │ ├── about.py │ │ ├── app.py │ │ ├── index.py │ │ ├── loading.py │ │ ├── monitor.py │ │ ├── progress.py │ │ ├── record.py │ │ ├── setting.py │ │ └── update.py │ ├── __init__.py │ ├── application/ │ │ ├── __init__.py │ │ ├── app.py │ │ ├── download.py │ │ ├── explore.py │ │ ├── image.py │ │ ├── request.py │ │ ├── user_posted.py │ │ └── video.py │ ├── expansion/ │ │ ├── __init__.py │ │ ├── browser.py │ │ ├── cleaner.py │ │ ├── converter.py │ │ ├── error.py │ │ ├── file_folder.py │ │ ├── namespace.py │ │ ├── pyi_rth_beartype.py │ │ └── truncate.py │ ├── module/ │ │ ├── __init__.py │ │ ├── extend.py │ │ ├── manager.py │ │ ├── mapping.py │ │ ├── model.py │ │ ├── recorder.py │ │ ├── script.py │ │ ├── settings.py │ │ ├── static.py │ │ └── tools.py │ └── translation/ │ ├── __init__.py │ └── translate.py └── static/ ├── 20250619.js ├── Release_Notes.md ├── XHS-Downloader.icns ├── XHS-Downloader.js ├── XHS-Downloader.tcss └── 自动滚动页面.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- 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! ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- 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. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 registries: pip_mirror: type: python-index url: https://mirrors.ustc.edu.cn/pypi/simple updates: - package-ecosystem: "uv" directory: "/" schedule: interval: "cron" cronjob: "0 0 * * 6" timezone: "Asia/Shanghai" target-branch: "develop" registries: - pip_mirror ================================================ FILE: .github/workflows/Close_Stale_Issues_and_PRs.yaml ================================================ name: "自动管理过时的问题和PR" on: schedule: - cron: "0 0 * * 6" 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)," ================================================ FILE: .github/workflows/Delete_untagged_images.yml ================================================ 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 ================================================ FILE: .github/workflows/Manually_build_executable_programs.yml ================================================ 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" --collect-all fastmcp --collect-all rich --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" --collect-all fastmcp --collect-all rich --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/ ================================================ FILE: .github/workflows/Manually_docker_image.yml ================================================ 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 ================================================ FILE: .github/workflows/Release_build_executable_program.yml ================================================ 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" --collect-all fastmcp --collect-all rich --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" --collect-all fastmcp --collect-all rich --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 }} ================================================ FILE: .github/workflows/Release_docker_image.yml ================================================ 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 ================================================ FILE: .gitignore ================================================ __pycache__/ *.pyc /.venv/ /.ruff_cache/ /.idea/ /.run/ /Volume/ !/.github/ ================================================ FILE: .python-version ================================================ 3.12 ================================================ FILE: Dockerfile ================================================ # ---- 阶段 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"] ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================
XHS-Downloader

XHS-Downloader

简体中文 | English

JoeanAmier%2FXHS-Downloader | Trendshift
GitHub GitHub forks GitHub Repo stars GitHub code size in bytes GitHub release (with filter)
Static Badge Static Badge Static Badge GitHub all releases

🔥 小红书链接提取/作品采集工具:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品链接、用户链接;采集小红书作品信息;提取小红书作品下载地址;下载小红书作品文件!

🔥 “小红书”、“XiaoHongShu”、“RedNote” 含义相同,本项目统称为 “小红书”

📑 项目功能

项目程序与用户脚本功能清单(点击展开)
    程序功能
  • ✅ 采集小红书作品信息
  • ✅ 提取小红书作品下载地址
  • ✅ 下载小红书作品文件
  • ✅ 下载小红书 livePhoto 文件
  • ✅ 自动跳过已下载的作品文件
  • ✅ 作品文件完整性处理机制
  • ✅ 自定义图文作品文件下载格式
  • ✅ 持久化储存作品信息至文件
  • ✅ 作品文件储存至单独文件夹
  • ✅ 后台监听剪贴板下载作品
  • ✅ 记录已下载作品 ID
  • ✅ 支持命令行下载作品文件
  • ☑️ 从浏览器读取 Cookie
  • ✅ 自定义文件名称格式
  • ✅ 支持 API 调用功能
  • ✅ 支持 MCP 调用功能
  • ✅ 支持文件断点续传下载
  • ✅ 智能识别作品文件类型
  • ✅ 支持设置作者备注
  • ✅ 自动更新作者昵称
    脚本功能
  • ✅ 下载小红书作品文件
  • ✅ 提取推荐页面作品链接
  • ✅ 提取账号发布作品链接
  • ✅ 提取账号收藏作品链接
  • ✅ 提取账号点赞作品链接
  • ✅ 提取账号专辑作品链接
  • ✅ 提取搜索结果作品链接
  • ✅ 提取搜索结果用户链接

📸 程序截图

前往 bilibili 观看演示前往 YouTube 观看演示



🔗 支持链接

  • https://www.xiaohongshu.com/explore/作品ID?xsec_token=XXX
  • https://www.xiaohongshu.com/discovery/item/作品ID?xsec_token=XXX
  • https://www.xiaohongshu.com/user/profile/作者ID/作品ID?xsec_token=XXX
  • https://xhslink.com/分享码

  • 支持单次输入多个作品链接,链接之间使用空格分隔;程序会自动提取有效链接,无需额外处理!

🪟 关于终端

⭐ 推荐使用 Windows 终端 (Windows 11 默认终端)运行程序以便获得最佳显示效果!

🥣 使用方法

如果仅需下载作品文件,建议选择 程序运行Docker 运行;如果有其他需求,建议选择 源码运行

⚠️ Cookie 配置为非强制项;如遇功能异常,建议配置或更新 Cookie 后再次尝试!

⚠️ 未设置 Cookie 时,视频作品只能下载低分辨率文件;建议配置 Cookie 以获取更高画质(无需登录账号)!

🖱 程序运行

⭐ Mac OS、Windows 10 及以上用户可前往 Releases 或者 Actions 下载程序压缩包,解压后打开程序文件夹,双击运行 main 即可使用。

⭐ 本项目包含自动构建可执行文件的 GitHub Actions,使用者可以随时使用 GitHub Actions 将最新源码构建为可执行文件!

⭐ 自动构建可执行文件教程请查阅本文档的 构建可执行文件指南 部分;如果需要更加详细的图文教程,请 查阅文章

注意:由于 Mac OS 平台的可执行文件 main 未经过代码签名,首次运行时会受到系统安全限制。请先在终端执行 xattr -cr 项目文件夹路径 命令移除安全标记,执行一次后即可正常运行。

若通过此方式使用程序,文件默认下载路径为:.\_internal\Volume\Download;配置文件路径为:.\_internal\Volume\settings.json

程序更新

方案一:下载并解压文件,将旧版本的 _internal\Volume 文件夹复制到新版本的 _internal 文件夹。

方案二:下载并解压文件(不要运行程序),复制全部文件,直接覆盖旧版本文件。

⌨️ 源码运行

  1. 安装 ≥3.12 版本的 Python 解释器
  2. 下载最新的源码或 Releases 发布的源码至本地
    1. 使用 pip 安装项目依赖
    2. 运行 python -m venv venv 命令创建虚拟环境(可选)
    3. 运行 .\venv\Scripts\activate.ps1 或者 venv\Scripts\activate 命令激活虚拟环境(可选)
    4. 运行 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt 命令安装程序所需模块
    5. 运行 python .\main.py 或者 python main.py 命令启动 XHS-Downloader
      使用 uv 安装项目依赖(推荐)
    1. 运行 uv sync --no-dev 命令同步环境依赖
    2. 运行 uv run main.py 命令启动 XHS-Downloader

⌨️ Docker 运行

  1. 获取镜像
    • 方式一:使用 Dockerfile 文件构建镜像
    • 方式二:使用 docker pull joeanamier/xhs-downloader 命令拉取镜像
    • 方式三:使用 docker pull ghcr.io/joeanamier/xhs-downloader 命令拉取镜像
  2. 创建容器
    • TUI 模式:docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称>
    • API 模式:docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称> python main.py api
    • MCP 模式:docker run --name 容器名称(可选) -p 主机端口号:5556 -v xhs_downloader_volume:/app/Volume -it <镜像名称> python main.py mcp

    • 注意:此处的 <镜像名称> 需与您在第一步中使用的镜像名称保持一致(joeanamier/xhs-downloaderghcr.io/joeanamier/xhs-downloader
  3. 运行容器
    • 启动容器:docker start -i 容器名称/容器 ID
    • 重启容器:docker restart -i 容器名称/容器 ID

Docker 运行项目时不支持 命令行调用模式,无法使用 读取剪贴板监听剪贴板 功能,可以正常粘贴内容,其他功能如有异常请反馈!

🛠 命令行模式

项目支持命令行运行模式,若想要下载图文作品的部分图片,可以使用此模式设置需要下载的图片序号!

注意:未设置 --index 参数时,支持传入多个作品链接,全部链接需要使用引号包围,链接之间使用空格分隔;已设置 --index 参数时,不支持传入多个作品链接,即使传入多个作品链接,程序仅处理首个作品链接!

bool 类型参数支持使用 truefalse10yesnoonoff(不区分大小写)来设置。

从浏览器读取 Cookie

该功能已失效,请参考 获取 Cookie 教程!

可以使用命令行实现 从浏览器读取 Cookie 并写入配置文件!

命令示例:python .\main.py --browser_cookie Chrome --update_settings

兼容性提醒:此功能依赖的第三方模块已长期未更新,可能无法正常支持最新浏览器版本。若功能出现异常,请尝试手动获取 Cookie!



🖥 服务器模式

服务器模式包含 API 模式和 MCP 模式!

API 模式

启动:运行命令:python .\main.py api

关闭:按下 Ctrl + C 关闭服务器

访问 http://127.0.0.1:5556/docs 或者 http://127.0.0.1:5556/redoc;你会看到自动生成的交互式 API 文档!

请求接口:/xhs/detail

请求方法:POST

请求格式:JSON

请求参数:

参数 类型 含义 默认值
url str 小红书作品链接,自动提取,不支持多链接;必需参数
download bool 是否下载作品文件;设置为 true 将会耗费更多时间;可选参数 false
index list[int] 下载指定序号的图片文件,仅对图文作品生效;download 参数设置为 false 时不生效;可选参数 null
cookie str 请求数据时使用的 Cookie;可选参数 配置文件 cookie 参数
proxy str 请求数据时使用的代理;可选参数 配置文件 proxy 参数
skip bool 是否跳过存在下载记录的作品;设置为 true 将不会返回存在下载记录的作品数据;可选参数 false

代码示例:

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())

MCP 模式

启动:运行命令:python .\main.py mcp

关闭:按下 Ctrl + C 关闭服务器

MCP 配置示例

[//]: # (

STDIO

)

Streamable HTTP

MCP URL:http://127.0.0.1:5556/mcp/

MCP配置示例

MCP 调用示例

MCP 功能及调用示例(点击展开)

获取小红书作品信息

MCP获取数据

下载小红书作品文件

下载图文作品时可以指定需要下载的图片序号;默认不返回作品信息,如需返回作品信息,请在对话时明确表述。

MCP下载文件
MCP下载文件

📜 其他说明

  • 由于作品链接携带日期信息,使用先前日期获取的作品链接可能会被风控,建议下载作品文件时使用最新获取的作品链接
  • Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie
  • 如果开启保存作品数据至文件功能,作品数据默认储存至 ./Volume/Download/ExploreData.db 文件
  • 程序下载记录数据储存至 ./Volume/ExploreID.db 文件
  • 为了避免请求频率过高对平台服务器造成影响,本项目内置请求延时机制

🕹 用户脚本

如果您的浏览器安装了 Tampermonkey 浏览器扩展程序,可以使用用户脚本体验项目功能!

用户脚本链接(右键单击复制链接):master 分支develop 分支


查看 Tampermonkey 用户脚本截图(点击展开)


提示:使用 XHS-Downloader 用户脚本批量提取作品链接,搭配 XHS-Downloader 程序可以实现批量下载作品文件!

修改用户脚本语言

切换语言

🌏 连接服务器

⭐ 本项目支持通过浏览器用户脚本与主程序联动,实现一键推送下载任务。

    功能说明:
  • 在项目程序的配置文件中,需要将 script_server 参数设置为 true
  • 保持项目程序在后台运行,它将作为服务器,接收用户脚本的指令(TUI、MCP 和 API 模式均支持)
  • 当您在浏览器中访问作品页面时,点击用户脚本菜单中的 推送下载任务 选项
  • 用户脚本会将下载任务发送给项目程序,由项目程序负责处理和下载文件

📜 脚本说明

  • 下载小红书作品文件时,脚本需要花费时间处理文件,请等待片刻,请勿多次点击下载按钮
  • 提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品
  • 提取推荐作品链接、搜索作品、用户链接时,脚本可以自动滚动指定次数加载更多内容,默认滚动次数:50 次
  • 自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动页面次数,修改后立即生效
  • 如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作
  • 支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载
  • 向服务器推送下载任务时,文件格式、名称规则等设置以服务器配置文件中的设置为准
  • 使用全局代理工具可能会导致脚本下载文件失败,如有异常,请尝试关闭代理工具,必要时向作者反馈
  • XHS-Downloader 用户脚本仅实现可见即可得的数据采集功能,无任何收费功能和破解功能

自动滚动页面功能代码已重构,该功能默认关闭!启用该功能可能会被小红书检测为自动化操作,从而导致账号受到风控或封禁风险!

💻 二次开发

如果有其他需求,可以根据 example.py 的注释提示进行代码调用或修改!

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,
                ],
            )
        )

📋 读取剪贴板

项目使用 pyperclip 实现读取剪贴板功能,该模块在不同的系统上会有差异。

在 Windows 上,不需要额外的模块。

在 Mac 上,该模块使用 pbcopy 和 pbpaste 命令,这些命令应该随操作系统一起提供。

在 Linux 上,该模块使用 xclip 或 xsel 命令,这些命令应该随操作系统一起提供。否则,请运行 "sudo apt-get install xclip" 或 "sudo apt-get install xsel"(注意:xsel 似乎并不总是有效)

在其他 Linux 系统上,你需要安装 qtpy 或 PyQT5 模块。

⚙️ 配置文件

项目根目录下的 ./Volume/settings.json 文件,首次运行自动生成,可以自定义程序运行参数;如果设置了无效的参数值,程序将会使用参数默认值!

如果您在程序界面修改配置时无法正常交互,可以直接编辑配置文件;如果您的计算机没有合适的程序编辑 JSON 文件,建议使用 在线工具 编辑配置文件内容,修改后需要重启软件才能生效。

参数 类型 含义 默认值
mapping_data str: str #作者别名映射表,格式:作者ID: 作者别名
work_path str 作品数据 / 文件保存根路径 项目根路径/Volume
folder_name str 作品文件储存文件夹名称 Download
name_format str 作品文件名称格式,字段之间使用空格分隔,支持字段:收藏数量评论数量分享数量点赞数量作品标签作品ID作品标题作品描述作品类型发布时间最后更新时间作者昵称作者ID 发布时间 作者昵称 作品标题
user_agent str 浏览器 User Agent 内置 Chrome User Agent
cookie str 小红书网页版 Cookie,无需登录,非必需参数!
proxy str 设置程序代理 null
timeout int 请求数据超时限制,单位:秒 10
chunk int 下载文件时,每次从服务器获取的数据块大小,单位:字节 2097152(2 MB)
max_retry int 请求数据失败时,重试的最大次数,单位:秒 5
record_data bool 是否保存作品数据至文件,保存格式:SQLite false
image_format str 图文作品文件下载格式,支持:AUTOPNGWEBPJPEGHEIC
部分作品没有 HEIC 格式的文件,此时下载的文件可能为 WEBP 格式!
设置为 AUTO 时表示动态格式,实际格式取决于服务器响应数据!
JPEG
image_download bool 图文、图集作品文件下载开关 true
video_download bool 视频作品文件下载开关 true
live_download bool 图文动图文件下载开关 false
video_preference str 视频作品文件下载偏好;含义:resolution:分辨率优先;bitrate:码率优先;size:文件大小优先 resolution
folder_mode bool 是否将每个作品的文件储存至单独的文件夹;文件夹名称与文件名称保持一致 false
download_record bool 是否记录下载成功的作品 ID,如果开启,程序将会自动跳过下载存在记录的作品 true
author_archive bool #是否将每个作者的作品储存至单独的文件夹;文件夹名称为 作者ID_作者昵称 false
write_mtime bool 是否将作品文件的 修改时间 属性修改为作品的发布时间 false
language str 设置程序语言,目前支持:zh_CNen_US zh_CN
script_server bool 是否开启用户脚本服务器,用于接收浏览器用户脚本的下载任务(TUI、MCP 和 API 模式生效) false

如果 author_archive 参数设置为 true,程序会把每个作者的作品储存至单独的文件夹;当作者的昵称发生变化时,程序会自动更新已下载作品文件名称中的作者昵称部分!

除此之外,你还可以通过设置 mapping_data 参数为某个作者设置别名;如果对某个作者设置了别名,程序会使用你设置的作者别名去替代作者昵称!


其他说明:user_agent参数获取示例;强烈建议根据实际浏览器信息进行设置!

🌐 Cookie

  1. 打开浏览器(可选无痕模式启动),访问 https://www.xiaohongshu.com/explore
  2. 登录小红书账号(可跳过)
  3. 按下 F12 打开开发人员工具
  4. 选择 网络 选项卡
  5. 勾选 保留日志
  6. 过滤 输入框输入 cookie-name:web_session
  7. 选择 Fetch/XHR 筛选器
  8. 点击小红书页面任意作品
  9. 网络 选项卡选择任意数据包(如果无数据包,重复步骤7)
  10. 全选复制 Cookie 写入程序或配置文件

🗳 下载记录

XHS-Downloader 会将下载过的作品 ID 储存至数据库,当重复下载相同的作品时,XHS-Downloader 会自动跳过该作品的文件下载(即使作品文件不存在),如果想要重新下载作品文件,请先删除数据库中对应的作品 ID,再使用 XHS-Downloader 下载作品文件!

该功能默认开启,如果关闭该功能,XHS-Downloader 会检查文件是否存在,若文件存在则跳过下载!

构建可执行文件指南

构建可执行文件指南(点击展开) 本指南将引导您通过 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 仓库的方式执行打包流程

⭐ Star 趋势

Star History Chart

♥️ 支持项目

如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star ⭐,感谢您的支持!

微信(WeChat) 支付宝(Alipay)
微信赞助二维码 支付宝赞助二维码

如果您愿意,可以考虑提供资助为 XHS-Downloader 提供额外的支持!

🌟 贡献指南

欢迎对本项目做出贡献!为了保持代码库的整洁、高效和易于维护,请仔细阅读以下指南,以确保您的贡献能够顺利被接受和整合。

  • 在开始开发前,请从 develop 分支拉取最新的代码,以此为基础进行修改;这有助于避免合并冲突并保证您的改动基于最新的项目状态。
  • 如果您的更改涉及多个不相关的功能或问题,请将它们分成多个独立的提交或拉取请求。
  • 每个拉取请求应尽可能专注于单一功能或修复,以便于代码审查和测试。
  • 遵循现有的代码风格;请确保您的代码与项目中已有的代码风格保持一致;建议使用 Ruff 工具保持代码格式规范。
  • 编写可读性强的代码;添加适当的注释帮助他人理解您的意图。
  • 每个提交都应该包含一个清晰、简洁的提交信息,以描述所做的更改。提交信息应遵循以下格式:<类型>: <简短描述>
  • 当您准备提交拉取请求时,请优先将它们提交到 develop 分支;这是为了给维护者一个缓冲区,在最终合并到 master 分支之前进行额外的测试和审查。
  • 建议在开发前或遇到疑问时与作者沟通,确保开发方向一致,避免重复劳动或无效提交。

参考资料:

✉️ 联系作者

说明:QQ 群聊仅限于讨论项目使用问题,严禁发布任何广告,严禁讨论任何账号交易、账号流量、流量变现、灰色产业等相关的内容!

作者的其他开源项目:

# 💰 项目赞助 ## DartNode [![Powered by DartNode](static/DartNode_AD.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") *** ## ZMTO ZMTO

ZMTO:一家专业的云基础设施提供商,以可靠的尖端技术与专业支持,提供高效的解决方案,并为符合条件的开源项目提供企业级VPS基础设施,支持开源生态系统的可持续发展与创新。

⚠️ 免责声明

  1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。
  2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。
  3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可,使用者需自行查阅并遵守相应协议,作者不对第三方组件的稳定性、安全性及合规性承担任何责任。
  4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。
  5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。
  6. 使用者不得使用本工具从事任何侵犯知识产权的行为,包括但不限于未经授权下载、传播受版权保护的内容,开发者不参与、不支持、不认可任何非法内容的获取或分发。
  7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规,确保处理行为合法正当;因违规操作导致的法律责任由使用者自行承担。
  8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。
  9. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。
  10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。
  11. 本项目不授予使用者任何专利许可;若使用本项目导致专利纠纷或侵权,使用者自行承担全部风险和责任。未经作者或权利人书面授权,不得使用本项目进行任何商业宣传、推广或再授权。
  12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利,并可能要求其销毁已获取的代码及衍生作品。
  13. 作者保留在不另行通知的情况下更新本声明的权利,使用者持续使用即视为接受修订后的条款。
在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。 # 💡 项目参考 * 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 ================================================ FILE: README_EN.md ================================================
XHS-Downloader

XHS-Downloader

简体中文 | English

JoeanAmier%2FXHS-Downloader | Trendshift
GitHub GitHub forks GitHub Repo stars GitHub code size in bytes GitHub release (with filter)
Static Badge Static Badge Static Badge GitHub all releases

🔥 RedNote Link Extraction/Content Collection Tool:Extract account-published, favorites, and liked notes links; extract search result notes links and user links; collect RedNote notes information; extract RedNote notes download addresses; download RedNote notes files!

🔥 "RedNote", "XiaoHongShu" and "小红书" have the same meaning, and this project is collectively referred to as "RedNote".

⭐ 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.

📑 Project Features

Program Features and User Script Features (Click to Expand)
    Program Features
  • ✅ Collect RedNote notes information
  • ✅ Extract RedNote notes download addresses
  • ✅ Download RedNote notes files
  • ✅ Download RedNote livePhoto files
  • ✅ Automatically skip already downloaded notes files
  • ✅ notes file integrity handling mechanism
  • ✅ Customizable image notes file download format
  • ✅ Persistently store notes information to files
  • ✅ Store notes files to a separate folder
  • ✅ Background clipboard monitoring for notes download
  • ✅ Record downloaded notes IDs
  • ✅ Support command line for downloading notes files
  • ☑️ Read cookies from browser
  • ✅ Customizable file name format
  • ✅ Support API call functionality
  • ✅ Support MCP call functionality
  • ✅ Support file breakpoint resume download
  • ✅ Intelligent recognition of notes file types
  • ✅ Supports author alias configuration
  • ✅ Automatic author nickname updates
    Script Features
  • ✅ Download RedNote notes files
  • ✅ Extract discovery page notes links
  • ✅ Extract account-published notes links
  • ✅ Extract account-favorited notes links
  • ✅ Extract account-liked notes links
  • ✅ Extract account-board notes links
  • ✅ Extract search result notes links
  • ✅ Extract search result user links

📸 Program Screenshots

Watch Demo on BilibiliWatch Demo on YouTube



🔗 Supported Links

  • https://www.xiaohongshu.com/explore/NoteID?xsec_token=XXX
  • https://www.xiaohongshu.com/discovery/item/NoteID?xsec_token=XXX
  • https://www.xiaohongshu.com/user/profile/AuthorID/NoteID?xsec_token=XXX
  • https://xhslink.com/ShareCode

  • Supports entering multiple notes links at once, separated by spaces; the program will automatically extract valid links without additional processing!

🪟 About the Terminal

⭐ It is recommended to use the Windows Terminal (default terminal for Windows 11) to run the program for the best display effect!

🥣 Usage

If you only need to download notes files, it is recommended to choose Program Run; if you have other needs, it is recommended to choose Source Code Run!

⚠️ Cookies are optional. If issues occur, please try configuring or updating them and retry!

⚠️ When Cookie is not set, video works can only be downloaded in low resolution; it is recommended to configure Cookie to obtain higher quality (no need to log in to the account)!

🖱 Program Run

⭐ Mac OS, Windows 10 and above users can go to Releases or Actions to download the program package, unzip it, open the program folder, and double-click to run main to use.

⭐ 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!

⭐ For the automatic building executable files tutorial, please refer to the Build of Executable File Guide section of this document. If you need a more detailed step-by-step tutorial with illustrations, please check out this article!

Note: Due to the macOS platform's executable file main not being code-signed, it will be restricted by system security measures on first run. Please execute the command xattr -cr project_folder_path in the terminal to remove the security flag, after which it can run normally.

If you use the program in this way, the default download path for files is: .\_internal\Volume\Download; the configuration file path is: .\_internal\Volume\settings.json

Update Methods

Method 1: Download and extract the files, then copy the old version of the _internal\Volume folder into the new version's _internal folder.

Method 2: Download and extract the files (do not run the program), then copy all files and directly overwrite the old version.

⌨️ Run from Source Code

  1. Install Python interpreter version ≥3.12
  2. Download the latest source code or the source code released in Releases to your local machine
    1. Install project dependencies using pip
    2. Run the command python -m venv venv to create a virtual environment (optional)
    3. Run the command .\venv\Scripts\activate.ps1 or venv\Scripts\activate to activate the virtual environment (optional)
    4. Run the command pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt to install the required modules for the program
    5. Run the command python .\main.py or python main.py to start XHS-Downloader
      Install project dependencies using uv (recommended)
    1. Run the command uv sync --no-dev to synchronize environment dependencies
    2. Run the command uv run main.py to start XHS-Downloader

⌨️ Docker Run

  1. Get Image
    • Method 1: Build the image using the Dockerfile
    • Method 2: Pull the image using the command docker pull joeanamier/xhs-downloader
    • Method 3: Pull the image using the command docker pull ghcr.io/joeanamier/xhs-downloader
  2. Create Container
    • TUI Mode: docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name>
    • API Mode: docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name> python main.py api
    • MCP Mode: docker run --name ContainerName(optional) -p HostPort:5556 -v xhs_downloader_volume:/app/Volume -it <image name> python main.py mcp

    • Note: The <image name> here must be consistent with the image name you used in the first step (joeanamier/xhs-downloader or ghcr.io/joeanamier/xhs-downloader)
  3. Run Container
    • Start Container: docker start -i ContainerName/ContainerID
    • Restart Container: docker restart -i ContainerName/ContainerID

When running the project via Docker, the command line call mode is not supported. The clipboard reading and clipboard monitoring functions are unavailable, but pasting content notes fine. Please provide feedback if other features are not functioning properly!

🛠 Command Line Mode

The project supports command line mode. If you want to download specific images from a text and image notes, you can use this mode to set the image sequence number you want to download!

Note: When the --index parameter is not set, multiple notes links can be passed in. All links must be enclosed in quotation marks and separated by spaces. When the --index parameter is set, multiple notes links are not supported. Even if multiple links are passed in, the program will only process the first link!

The bool type parameters support setting with true, false, 1, 0, yes, no, on or off (case insensitive).

Read Browser Cookies

This feature is no longer available. Please refer to the Obtain Cookie tutorial!

You can use the command line to read cookies from browser and write them to the configuration file!

Command example: python .\main.py --browser_cookie Chrome --update_settings

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!



🖥 Server Mode

Server modes include API mode and MCP mode!

API Mode

Start: Run the command: python .\main.py api

Stop: Press Ctrl + C to stop the server

Open http://127.0.0.1:5556/docs or http://127.0.0.1:5556/redoc; you will see automatically generated interactive API documentation!

Request endpoint: /xhs/detail

Request method: POST

Request format: JSON

Request parameters:

Parameter Type Description Default
url str RedNote notes link, auto-extraction, does not support multiple links; Required parameter None
download bool Whether to download the notes file; set to true will take more time; Optional parameter false
index list[int] Download specific image files by index, only effective for text and image notes; not effective when the download parameter is set to false; Optional parameter null
cookie str Cookie used when requesting data; Optional parameter Settings cookie Value
proxy str Proxy used when requesting data; Optional parameter Settings proxy Value
skip bool Whether to skip notes with download records; set to true will not return notes data with download records; Optional parameter false

Code example:

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())

MCP Mode

Start: Run the command: python .\main.py mcp

Stop: Press Ctrl + C to stop the server

MCP Configuration Example

[//]: # (

STDIO

)

Streamable HTTP

MCP URL:http://127.0.0.1:5556/mcp/

MCP Configuration Example

MCP Invocation Example

MCP Function and Call Example (Click to Expand)

Retrieve RedNote Notes Information

MCP Data Retrieval

Download RedNote Notes Files

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.

MCP File Download
MCP File Download

📜 Others

  • Due to the date information carried in the links of RedNote notes, using links obtained from previous dates may be subject to risk control. It is recommended to use the latest RedNote notes links when downloading RedNote work files
  • Windows system requires running programs as an administrator to read Chromium, Chrome, Edge browser cookies
  • If the function to save notes data to a file is enabled, the notes data will be stored by default in the ./Volume/Download/ExploreData.db file
  • The program's download records will be stored in the ./Volume/ExploreID.db file
  • To prevent high-frequency requests from impacting the platform's servers, this project includes a built-in request delay mechanism

🕹 User Script

If your browser has the Tampermonkey extension installed, you can use the userscript to try the project's features!

Userscript links (right-click to copy the link): master branch, develop branch


View Tampermonkey userscript screenshots (click to expand)


Note: Using the XHS-Downloader user script to batch extract notes links, in combination with the XHS-Downloader program, can achieve batch downloading of notes files!

Modify user script language

切换语言

🌐 Connect to Server

⭐ This project supports interaction with the main program through a browser userscript, enabling one-click push of download tasks.

    Function Description:
  • In the project program's configuration file, you need to set the script_server parameter to true
  • 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)
  • When you visit a post page in your browser, click the Push Download Task option in the userscript menu
  • The userscript will send the download task to the project program, which will handle and download the files

📜 Script Instructions

  • When downloading notes from RedNote, the script requires time to process the files. Please wait for a moment and do not click the download button multiple times.
  • When extracting links for posts, collects, likes, and board from an account, the script can automatically scroll the page until all notes are loaded.
  • When extracting recommended notes links, search notes, 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.
  • 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.
  • If the automatic page scroll feature is not enabled, users need to manually scroll the page to load more content before performing other actions.
  • Support packaging and downloading of work files; This feature is enabled by default, and notes from multiple files will be downloaded in compressed file format
  • When pushing download tasks to the server, the file format, name rules, etc. settings will be based on the server configuration file settings
  • 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.
  • XHS-Downloader userscript only implements the data collection functionality for visible content and does not include any paid or cracked features.

The automatic page scroll feature has been refactored and is turned off by default! Enabling this feature may be detected as automated behavior by RedNote, potentially resulting in account risk control or banning.

💻 Secondary Development

If you have other needs, you can perform code calls or modifications based on the comments in example.py!

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,
                ],
            )
        )

📋 Read Clipboard

The project uses pyperclip to implement clipboard reading functionality, which varies across different systems.

On Windows, no additional modules are needed.

On Mac, this module makes use of the pbcopy and pbpaste commands, which should come with the os.

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.)

Otherwise on Linux, you will need the qtpy or PyQT5 modules installed.

⚙️ Configuration File

The ./Volume/settings.json 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.

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 online tool. Remember to restart the software after making changes for them to take effect.

Parameter Type Description Default Value
mapping_data str: str #Author alias mapping data, format: author ID: author alias null
work_path str Root path for saving notes data/files Project root path/Volume
folder_name str Name of the folder for storing notes files Download
name_format str #Format of notes file name, separated by spaces between fields, supports fields: 收藏数量评论数量分享数量点赞数量作品标签作品ID作品标题作品描述作品类型发布时间最后更新时间作者昵称作者ID 发布时间 作者昵称 作品标题
user_agent str Browser User Agent Built-in Chrome User Agent
cookie str RedNote web version cookie, No login required, non essential parameters! None
proxy str Set program proxy null
timeout int Request data timeout limit, in seconds 10
chunk int Size of data chunk to fetch from the server each time when downloading files, in bytes 2097152(2 MB)
max_retry int Maximum number of retries when requesting data fails 5
record_data bool Whether to save notes data to a file, saved in SQLite format false
image_format str Download format for image notes files, supported: AUTOPNGWEBPJPEGHEIC
Some notes do not have files in HEIC format, and the downloaded files may be in WEBP format
When set toAUTO, it represents dynamic format, and the actual format depends on the server's response data
JPEG
image_download bool Switch for downloading image and atlas notes files true
video_download bool Switch for downloading video notes files true
live_download bool Switch for downloading animated image files false
video_preference str Video notes file download preference; Meaning: resolution: resolution priority; bitrate: bitrate priority; size: file size priority resolution
folder_mode bool Whether to store each notes files in a separate folder; the folder name matches the file name false
download_record bool Do record the ID of successfully downloaded notes? If enabled, the program will automatically skip downloading notes with records true
author_archive bool #Whether to save each author's notes into a separate folder; The folder name is authorID_nickname false
write_mtime bool Whether to modify the modified time attribute of the notes file to the publication time of the notes. false
language str Set program language. Currently supported: zh_CN, en_US zh_CN
script_server bool Whether to enable the user script server for receiving download tasks from the browser user script (effective in TUI, MCP, and API modes) false

name_format instructions (Currently only supports Chinese values) :

  • 收藏数量: Number of Collections
  • 评论数量: Number of Comments
  • 分享数量: Number of Shares
  • 点赞数量: Number of Likes
  • 作品标签: Notes Tags
  • 作品ID: Notes ID
  • 作品标题: Notes Title
  • 作品描述: Notes Description
  • 作品类型: Notes Type
  • 发布时间: Publish Time
  • 最后更新时间: Last Updated Time
  • 作者昵称: Author Nickname
  • 作者ID: Author ID

When author_archive is set to true, the program will store each author's notes in dedicated folders. If an author's nickname changes, the program automatically updates the nickname portion in existing downloaded filenames!

Additionally, you can configure author aliases through the mapping_data parameter. When an alias is set, the program will use your custom alias instead of the original nickname in filenames!


Additional Notes: The parameters user_agent examples are provided for reference; Strongly recommend setting according to actual browser information!

🌐 Cookie

  1. Open the browser (optional: start in incognito mode) and visit https://www.xiaohongshu.com/explore
  2. Log in to your RedNote account (can be skipped)
  3. Press F12 to open the developer tools
  4. Select the Network tab
  5. Check Preserve log
  6. In the Filter input box, enter cookie-name:web_session
  7. Select the Fetch/XHR filter
  8. Click on any piece of notes on the RedNote page
  9. In the Network tab, select any data packet (if no packets appear, repeat step 7)
  10. Copy and paste the entire Cookie into the program or configuration file

🗳 Download Records

XHS-Downloader will store the IDs of downloaded notes in a database. When downloading the same notes again, XHS-Downloader will automatically skip the file download (even if the notes file does not exist). If you want to re-download the notes file, please delete the corresponding notes ID from the database and then use XHS-Downloader to download the notes file again!

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!

Build of Executable File Guide

Build of Executable File Guide (Click to Expand) 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

⭐ Star History

Star History Chart

♥️ Support the Project

If XHS-Downloader has been helpful to you, please consider giving it a Star ⭐, Thank you for your support!

微信(WeChat) 支付宝(Alipay)
微信赞助二维码 支付宝赞助二维码

If you are willing, you may consider making a donation to provide additional support for XHS-Downloader!

🌟 Contribution Guidelines

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.

  • Before starting development, please pull the latest code from the develop 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.
  • If your changes involve multiple unrelated features or issues, please split them into several independent commits or pull requests.
  • Each pull request should focus on a single feature or fix as much as possible, to facilitate code review and testing.
  • 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.
  • Write code that is easy to read; add appropriate annotation to help others understand your intentions.
  • Each commit should include a clear and concise commit message describing the changes made. The commit message should follow this format: <type>: <short description>
  • When you are ready to submit a pull request, please prioritize submitting them to the develop branch; this provides maintainers with a buffer zone for additional testing and review before final merging into the master branch.
  • 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.

Reference materials:

✉️ Contact the Author

Other Open Source Projects by the Author:

# 💰 Project Sponsorship ## DartNode [![Powered by DartNode](static/DartNode_AD.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") *** ## ZMTO ZMTO

ZMTO: 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.

⚠️ Disclaimer

  1. 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.
  2. 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.
  3. 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.
  4. Users must strictly comply with the requirements of the GNU General Public License v3.0 when using this project and properly indicate that the code was used under the GNU General Public License v3.0.
  5. 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.
  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 copyright-protected content without authorization. The developers do not participate in, support, or endorse any unauthorized acquisition or distribution of illegal content.
  7. 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.
  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 responsible for any loss or damage arising from such usage.
  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 the XHS-Downloader project.
  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.
  11. 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.
  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 notes.
  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.
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. # 💡 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 ================================================ FILE: example.py ================================================ 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, ) as xhs: print( await xhs.extract( url, # download=True, ) ) if __name__ == "__main__": # run(example()) # run(example_api()) run(test()) ================================================ FILE: locale/README.md ================================================ # 命令参考 **运行命令前,确保已经安装了 `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. ================================================ FILE: locale/en_US/LC_MESSAGES/xhs.po ================================================ # 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 , 2024. # msgid "" msgstr "" "Project-Id-Version: XHS-Downloader 2.7\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-06 16:45+0800\n" "PO-Revision-Date: 2024-12-22 14:14+0800\n" "Last-Translator: \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:223 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过下载" msgstr "notes {0} has a download record, skip download" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:247 msgid "提取作品文件下载地址失败" msgstr "Failed to extract the download address for the RedNote notes files" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:280 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:328 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:743 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:930 msgid "提取小红书作品链接失败" msgstr "Failed to extract the links for RedNote notes" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:288 #, python-brace-format msgid "共 {0} 个小红书作品待处理..." msgstr "{0} notes from RedNote are awaiting processing..." #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:309 #, python-brace-format msgid "共处理 {0} 个作品,成功 {1} 个,失败 {2} 个,跳过 {3} 个" msgstr "Processed a total of {0} notes: {1} succeeded, {2} failed, {3} skipped" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:400 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过处理" msgstr "notes {0} has a download record, skip processing" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:404 #, python-brace-format msgid "开始处理作品:{0}" msgstr "Start processing the notes: {0}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:412 #, python-brace-format msgid "{0} 获取数据失败" msgstr "{0} failed to retrieve data" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:425 #, python-brace-format msgid "{0} 提取数据失败" msgstr "{0} failed to extract data" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:439 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:82 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "视频" msgstr "video" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:442 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:89 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:82 msgid "图文" msgstr "image" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:443 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:90 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "图集" msgstr "LivePhoto" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:447 #, python-brace-format msgid "未知的作品类型:{0}" msgstr "Unknown notes type: {0}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:505 #, python-brace-format msgid "作品处理完成:{0}" msgstr "notes processing completed: {0}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:611 msgid "" "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件," "如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!" msgstr "" "The program will automatically read and extract the link to RedNote notes " "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:712 msgid "跳转至项目 GitHub 仓库" msgstr "Jump to the project's GitHub repository" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:713 msgid "重定向至项目 GitHub 仓库主页" msgstr "Redirect to the project's GitHub repository homepage" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:721 msgid "获取作品数据及下载地址" msgstr "Fetch notes data and download links" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:937 msgid "获取小红书作品数据成功" msgstr "Successfully obtained data on RedNote notes" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:755 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:939 msgid "获取小红书作品数据失败" msgstr "Failed to obtain data on RedNote notes" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:825 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:870 msgid "小红书作品链接" msgstr "Link to RedNote notes" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:873 msgid "指定需要下载的图文作品序号" msgstr "Specify the serial number of the images notes to be downloaded" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:877 msgid "是否需要返回作品信息数据" msgstr "Whether to return notes information data" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:891 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:896 msgid "作品文件下载任务执行完毕" msgstr "notes file download task completed" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:901 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:906 msgid "作品文件下载任务未执行" msgstr "notes file download task not executed" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:131 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:182 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:192 #, python-brace-format msgid "{0} 文件已存在,跳过下载" msgstr "{0} already exists, skipping download" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:221 #, python-brace-format msgid "文件 {0} 缓存异常,重新下载" msgstr "File {0} cache exception, download again" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:248 #, python-brace-format msgid "文件 {0} 下载成功" msgstr "file {0} download successful" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:254 #, 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:332 #, 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:79 msgid "未知" msgstr "unknown" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:66 #, python-brace-format msgid "网络异常,{0} 请求失败: {1}" msgstr "Network error, {0} request failed: {1}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:129 msgid "小红书作品链接,多个链接使用空格分隔" msgstr "RedNote notes links, separate multiple links with spaces" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136 msgid "" "下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\"" msgstr "" "Download images files with specified serial numbers, only effective for " "images notes; Example of multiple serial numbers input: \"1 3 5 7\"" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141 msgid "作品数据/文件保存根路径" msgstr "Root path for saving notes data / files" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40 msgid "作品文件储存文件夹名称" msgstr "Name of the folder for storing notes files" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:143 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49 msgid "作品文件名称格式" msgstr "Format of notes file name" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:145 msgid "小红书网页版 Cookie,无需登录" msgstr "RedNote web version cookie, no need to log in" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:146 #: 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:147 #: 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:153 #: 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:156 #: 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:157 msgid "是否记录作品数据至文件" msgstr "Record notes data to file" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:162 msgid "图文作品文件下载格式,支持:PNG、WEBP、JPEG、HEIC、AUTO" msgstr "" "Image notes file download format, supporting: PNG、WEBP、JPEG、HEIC、AUTO" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:164 msgid "动态图片下载开关" msgstr "LivePhoto download switch" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165 #: 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:170 msgid "是否将每个作品的文件储存至单独的文件夹" msgstr "Whether to save each work's files into separate folders" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:176 msgid "是否将每个作者的作品储存至单独的文件夹" msgstr "Whether to save each author's notes into separate folders" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183 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:187 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:188 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:209 msgid "查看详细参数说明" msgstr "View detailed parameter descriptions" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:210 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:240 #, python-brace-format msgid "代理 {0} 测试成功" msgstr "Agent {0} tested successfully" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244 #, python-brace-format msgid "代理 {0} 测试超时" msgstr "Agent {0} test timeout" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:252 #, python-brace-format msgid "代理 {0} 测试失败:{1}" msgstr "Agent {0} test failed: {1}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:55 #, 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:81 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:101 msgid "文件夹" msgstr "folder" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:85 #, 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:105 #, 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:171 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:185 msgid "文件" msgstr "file" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:175 #, 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:193 #, 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:202 #, 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:211 #, 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:32 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:20 #: 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:231 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 notes" #: 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 notes 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:82 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:93 msgid "未输入任何小红书作品链接" msgstr "No RedNote notes links provided" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:122 msgid "下载小红书作品文件失败" msgstr "Failed to download the RedNote notes 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:21 msgid "关闭监听" msgstr "Close" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:33 msgid "已启动监听剪贴板模式" msgstr "Currently in monitoring clipboard mode" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:35 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 notes to be deleted" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:26 msgid "" "支持输入作品 ID 或包含作品 ID 的作品链接,多个链接或 ID 之间使用空格分隔" msgstr "" "Support input of notes ID or links containing notes 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 notes ID" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\record.py:46 msgid "删除下载记录成功" msgstr "Successfully deleted download record" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:30 msgid "作品数据 / 文件保存根路径" msgstr "Root path for saving notes 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 notes data" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:125 msgid "作品归档保存模式" msgstr "notes 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:168 msgid "脚本服务器开关" msgstr "Script server switch" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:176 msgid "图片下载格式" msgstr "Image download format" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:180 msgid "程序语言" msgstr "Program language" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:184 msgid "视频下载偏好" msgstr "Video preferences" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:213 msgid "保存配置" msgstr "Save settings" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:217 msgid "放弃更改" msgstr "Discard changes" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:227 msgid "小红书网页版 Cookie,无需登录,参数已设置" msgstr "" "RedNote web version cookie, no login required, parameters have been set" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:228 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:45 #, python-brace-format msgid "检测到新版本:{0}.{1}" msgstr "Detected new version: {0} {1}" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:53 msgid "当前版本为开发版, 可更新至正式版" msgstr "Detected a new official version" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:58 msgid "当前已是最新开发版" msgstr "You are already using the latest development version" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:63 msgid "当前已是最新正式版" msgstr "You are already using the latest official version" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:70 msgid "检测新版本失败" msgstr "Failed to check for a new version" ================================================ FILE: locale/generate_path.py ================================================ 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}") ================================================ FILE: locale/po_to_mo.py ================================================ 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())) ================================================ FILE: locale/xhs.pot ================================================ # 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 , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: XHS-Downloader 2.7\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-06 16:45+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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:223 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:247 msgid "提取作品文件下载地址失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:280 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:328 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:743 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:930 msgid "提取小红书作品链接失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:288 #, python-brace-format msgid "共 {0} 个小红书作品待处理..." msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:309 #, python-brace-format msgid "共处理 {0} 个作品,成功 {1} 个,失败 {2} 个,跳过 {3} 个" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:400 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过处理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:404 #, python-brace-format msgid "开始处理作品:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:412 #, python-brace-format msgid "{0} 获取数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:425 #, python-brace-format msgid "{0} 提取数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:439 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:82 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "视频" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:442 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:89 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:82 msgid "图文" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:443 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:90 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "图集" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:447 #, python-brace-format msgid "未知的作品类型:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:505 #, python-brace-format msgid "作品处理完成:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:611 msgid "" "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件," "如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:712 msgid "跳转至项目 GitHub 仓库" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:713 msgid "重定向至项目 GitHub 仓库主页" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:721 msgid "获取作品数据及下载地址" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:937 msgid "获取小红书作品数据成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:755 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:939 msgid "获取小红书作品数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:825 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:870 msgid "小红书作品链接" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:873 msgid "指定需要下载的图文作品序号" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:877 msgid "是否需要返回作品信息数据" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:891 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:896 msgid "作品文件下载任务执行完毕" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:901 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:906 msgid "作品文件下载任务未执行" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:131 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:182 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:192 #, python-brace-format msgid "{0} 文件已存在,跳过下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:221 #, python-brace-format msgid "文件 {0} 缓存异常,重新下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:248 #, python-brace-format msgid "文件 {0} 下载成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:254 #, python-brace-format msgid "网络异常,{0} 下载失败,错误信息: {1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:332 #, 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:79 msgid "未知" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:66 #, python-brace-format msgid "网络异常,{0} 请求失败: {1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:129 msgid "小红书作品链接,多个链接使用空格分隔" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136 msgid "" "下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\"" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141 msgid "作品数据/文件保存根路径" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40 msgid "作品文件储存文件夹名称" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:143 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49 msgid "作品文件名称格式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:145 msgid "小红书网页版 Cookie,无需登录" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:146 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:78 msgid "网络代理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:147 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:88 msgid "请求数据超时限制,单位:秒" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:153 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:98 msgid "下载文件时,每次从服务器获取的数据块大小,单位:字节" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:156 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:108 msgid "请求数据失败时,重试的最大次数" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:157 msgid "是否记录作品数据至文件" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:162 msgid "图文作品文件下载格式,支持:PNG、WEBP、JPEG、HEIC、AUTO" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:164 msgid "动态图片下载开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:149 msgid "作品下载记录开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:170 msgid "是否将每个作品的文件储存至单独的文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:176 msgid "是否将每个作者的作品储存至单独的文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183 msgid "是否将作品文件的修改时间属性修改为作品的发布时间" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:187 msgid "设置程序语言,目前支持:zh_CN、en_US" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:188 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:209 msgid "查看详细参数说明" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:210 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:240 #, python-brace-format msgid "代理 {0} 测试成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244 #, python-brace-format msgid "代理 {0} 测试超时" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:252 #, python-brace-format msgid "代理 {0} 测试失败:{1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:55 #, python-brace-format msgid "{old_folder} 文件夹不存在,跳过处理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:81 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:101 msgid "文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:85 #, python-brace-format msgid "文件夹 {old_folder} 已重命名为 {new_folder}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:105 #, python-brace-format msgid "文件夹 {old_} 重命名为 {new_}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:171 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:185 msgid "文件" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:175 #, python-brace-format msgid "文件 {old_file} 重命名为 {new_file}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:193 #, python-brace-format msgid "{type} {old}被占用,重命名失败: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:202 #, python-brace-format msgid "{type} {new}名称重复,重命名失败: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:211 #, python-brace-format msgid "处理{type} {old}时发生预期之外的错误: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\tools.py:32 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:20 #: 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:231 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:82 msgid "免责声明\n" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:93 msgid "未输入任何小红书作品链接" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:122 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:21 msgid "关闭监听" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:33 msgid "已启动监听剪贴板模式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:35 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:46 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:168 msgid "脚本服务器开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:176 msgid "图片下载格式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:180 msgid "程序语言" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:184 msgid "视频下载偏好" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:213 msgid "保存配置" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:217 msgid "放弃更改" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:227 msgid "小红书网页版 Cookie,无需登录,参数已设置" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:228 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:45 #, python-brace-format msgid "检测到新版本:{0}.{1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:53 msgid "当前版本为开发版, 可更新至正式版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:58 msgid "当前已是最新开发版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:63 msgid "当前已是最新正式版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:70 msgid "检测新版本失败" msgstr "" ================================================ FILE: locale/zh_CN/LC_MESSAGES/xhs.po ================================================ # 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 , 2024. # msgid "" msgstr "" "Project-Id-Version: XHS-Downloader 2.7\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-02-06 16:45+0800\n" "PO-Revision-Date: 2024-12-22 14:14+0800\n" "Last-Translator: \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:223 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:247 msgid "提取作品文件下载地址失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:280 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:328 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:743 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:930 msgid "提取小红书作品链接失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:288 #, python-brace-format msgid "共 {0} 个小红书作品待处理..." msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:309 #, python-brace-format msgid "共处理 {0} 个作品,成功 {1} 个,失败 {2} 个,跳过 {3} 个" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:400 #, python-brace-format msgid "作品 {0} 存在下载记录,跳过处理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:404 #, python-brace-format msgid "开始处理作品:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:412 #, python-brace-format msgid "{0} 获取数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:425 #, python-brace-format msgid "{0} 提取数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:439 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:82 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "视频" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:442 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:89 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:82 msgid "图文" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:443 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:90 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\explore.py:81 msgid "图集" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:447 #, python-brace-format msgid "未知的作品类型:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:505 #, python-brace-format msgid "作品处理完成:{0}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:611 msgid "" "程序会自动读取并提取剪贴板中的小红书作品链接,并自动下载链接对应的作品文件," "如需关闭,请点击关闭按钮,或者向剪贴板写入 “close” 文本!" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:712 msgid "跳转至项目 GitHub 仓库" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:713 msgid "重定向至项目 GitHub 仓库主页" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:721 msgid "获取作品数据及下载地址" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:753 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:937 msgid "获取小红书作品数据成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:755 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:939 msgid "获取小红书作品数据失败" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:825 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:870 msgid "小红书作品链接" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:873 msgid "指定需要下载的图文作品序号" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:877 msgid "是否需要返回作品信息数据" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:891 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:896 msgid "作品文件下载任务执行完毕" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:901 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\app.py:906 msgid "作品文件下载任务未执行" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:131 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:182 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:192 #, python-brace-format msgid "{0} 文件已存在,跳过下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:221 #, python-brace-format msgid "文件 {0} 缓存异常,重新下载" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:248 #, python-brace-format msgid "文件 {0} 下载成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:254 #, python-brace-format msgid "网络异常,{0} 下载失败,错误信息: {1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\download.py:332 #, 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:79 msgid "未知" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\application\request.py:66 #, python-brace-format msgid "网络异常,{0} 请求失败: {1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:129 msgid "小红书作品链接,多个链接使用空格分隔" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:136 msgid "" "下载指定序号的图片文件,仅对图文/图集作品生效;多个序号输入示例:\"1 3 5 7\"" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:141 msgid "作品数据/文件保存根路径" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:142 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:40 msgid "作品文件储存文件夹名称" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:143 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:49 msgid "作品文件名称格式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:145 msgid "小红书网页版 Cookie,无需登录" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:146 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:78 msgid "网络代理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:147 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:88 msgid "请求数据超时限制,单位:秒" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:153 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:98 msgid "下载文件时,每次从服务器获取的数据块大小,单位:字节" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:156 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:108 msgid "请求数据失败时,重试的最大次数" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:157 msgid "是否记录作品数据至文件" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:162 msgid "图文作品文件下载格式,支持:PNG、WEBP、JPEG、HEIC、AUTO" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:164 msgid "动态图片下载开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:165 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:149 msgid "作品下载记录开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:170 msgid "是否将每个作品的文件储存至单独的文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:176 msgid "是否将每个作者的作品储存至单独的文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:183 msgid "是否将作品文件的修改时间属性修改为作品的发布时间" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:187 msgid "设置程序语言,目前支持:zh_CN、en_US" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:188 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:209 msgid "查看详细参数说明" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\CLI\main.py:210 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:240 #, python-brace-format msgid "代理 {0} 测试成功" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:244 #, python-brace-format msgid "代理 {0} 测试超时" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\manager.py:252 #, python-brace-format msgid "代理 {0} 测试失败:{1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:55 #, python-brace-format msgid "{old_folder} 文件夹不存在,跳过处理" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:81 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:101 msgid "文件夹" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:85 #, python-brace-format msgid "文件夹 {old_folder} 已重命名为 {new_folder}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:105 #, python-brace-format msgid "文件夹 {old_} 重命名为 {new_}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:171 #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:185 msgid "文件" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:175 #, python-brace-format msgid "文件 {old_file} 重命名为 {new_file}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:193 #, python-brace-format msgid "{type} {old}被占用,重命名失败: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:202 #, python-brace-format msgid "{type} {new}名称重复,重命名失败: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\mapping.py:211 #, python-brace-format msgid "处理{type} {old}时发生预期之外的错误: {error}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\module\tools.py:32 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:20 #: 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:231 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:82 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:93 msgid "未输入任何小红书作品链接" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\index.py:122 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:21 msgid "关闭监听" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:33 msgid "已启动监听剪贴板模式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\monitor.py:35 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:46 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:168 msgid "脚本服务器开关" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:176 msgid "图片下载格式" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:180 msgid "程序语言" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:184 msgid "视频下载偏好" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:213 msgid "保存配置" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:217 msgid "放弃更改" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:227 msgid "小红书网页版 Cookie,无需登录,参数已设置" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\setting.py:228 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:45 #, python-brace-format msgid "检测到新版本:{0}.{1}" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:53 msgid "当前版本为开发版, 可更新至正式版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:58 msgid "当前已是最新开发版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:63 msgid "当前已是最新正式版" msgstr "" #: C:\Users\You\PycharmProjects\XHS-Downloader\source\TUI\update.py:70 msgid "检测新版本失败" msgstr "" ================================================ FILE: main.py ================================================ from asyncio import run from asyncio.exceptions import CancelledError from contextlib import suppress from sys import argv from source import Settings from source import XHS from source import XHSDownloader from source import cli async def app(): async with XHSDownloader() as xhs: await xhs.run_async() 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, ) 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, ) 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() ================================================ FILE: pyproject.toml ================================================ [project] name = "XHS-Downloader" version = "2.8" description = "小红书(XiaoHongShu、RedNote)链接提取/作品采集工具:提取账号发布、收藏、点赞、专辑作品链接;提取搜索结果作品、用户链接;采集小红书作品信息;提取小红书作品下载地址;下载小红书无水印作品文件" authors = [ { name = "JoeanAmier", email = "yonglelolu@foxmail.com" }, ] readme = "README.md" license = "GPL-3.0" requires-python = ">=3.12" dependencies = [ "aiofiles>=25.1.0", "aiosqlite>=0.22.1", "click>=8.3.1", "emoji>=2.15.0", "fastapi>=0.128.5", "fastmcp>=2.14.5", "httpx[http2,socks]>=0.28.1", "lxml>=6.0.2", "pyperclip>=1.11.0", "pyyaml>=6.0.3", "textual>=7.5.0", "uvicorn>=0.40.0", "websockets>=16.0", ] [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 = [ "nuitka>=4.0", "pycryptodome>=3.23.0", "pyinstaller>=6.17.0", "textual-dev>=1.7.0", ] ================================================ FILE: requirements.txt ================================================ # 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.22.1 # 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.135.3 # via xhs-downloader (pyproject.toml) fastmcp>=3.1.0 # via xhs-downloader (pyproject.toml) httpx[http2,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) textual==8.2.0 # via xhs-downloader (pyproject.toml) uvicorn==0.42.0 # via xhs-downloader (pyproject.toml) websockets==16.0 # via xhs-downloader (pyproject.toml) ================================================ FILE: source/CLI/__init__.py ================================================ from .main import cli __all__ = ["cli"] ================================================ FILE: source/CLI/main.py ================================================ 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): return function(ctx, param, value) if value else None 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 != None} @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、JPEG、HEIC、AUTO"), ), ("--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", "jpeg", "JPEG", "heic", "HEIC", "auto", "AUTO"] ), ) @option( "--live_download", "-ld", type=bool, ) @option( "--video_preference", "-vp", type=Choice(["resolution", "bitrate", "size"]), ) @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", ""]) ================================================ FILE: source/TUI/__init__.py ================================================ from .app import XHSDownloader __all__ = ["XHSDownloader"] ================================================ FILE: source/TUI/about.py ================================================ 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") ================================================ FILE: source/TUI/app.py ================================================ 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() ================================================ FILE: source/TUI/index.py ================================================ 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") ================================================ FILE: source/TUI/loading.py ================================================ 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", ) ================================================ FILE: source/TUI/monitor.py ================================================ 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() ================================================ FILE: source/TUI/progress.py ================================================ from textual.app import ComposeResult from textual.screen import Screen __all__ = ["Progress"] class Progress(Screen): def compose(self) -> ComposeResult: pass ================================================ FILE: source/TUI/record.py ================================================ 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() ================================================ FILE: source/TUI/setting.py ================================================ 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", ), 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", ), Select.from_values( ["resolution", "bitrate", "size"], value=self.data["video_preference"], allow_blank=False, id="video_preference", ), 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.lower(), "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, "video_preference": self.query_one("#video_preference").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() ================================================ FILE: source/TUI/update.py ================================================ 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 ================================================ FILE: source/__init__.py ================================================ from .CLI import cli from .TUI import XHSDownloader from .application import XHS from .module import Settings __all__ = [ "XHS", "XHSDownloader", "cli", "Settings", ] ================================================ FILE: source/application/__init__.py ================================================ from .app import XHS __all__ = ["XHS"] ================================================ FILE: source/application/app.py ================================================ 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="JPEG", image_download=True, video_download=True, live_download=False, video_preference="resolution", 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, **kwargs, ): switch_language(language) self.print = Print() self.manager = Manager( ROOT, work_path, folder_name, name_format, chunk, user_agent, cookie, # self.read_browser_cookie(read_cookie) or cookie, proxy, timeout, max_retry, record_data, image_format, image_download, video_download, live_download, video_preference, 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.deal_video_link( data, self.manager.video_preference, ) 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 not result: count.skip += 1 elif all(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 | { "作品链接": url, }, 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, ) ] ) 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, ) if not url: msg = _("提取小红书作品链接失败") else: if data := await self.__deal_extract( url[0], extract.download, extract.index, 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, ) if not url: msg = _("提取小红书作品链接失败") elif data := await self.__deal_extract( url[0], download, index, 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, ) ================================================ FILE: source/application/download.py ================================================ 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}") ================================================ FILE: source/application/explore.py ================================================ 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") or 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 _("图文") ================================================ FILE: source/application/image.py ================================================ 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) if not any( token_list := [ cls.__extract_image_token(Namespace.object_extract(i, "urlDefault")) for i in images ] ): token_list = [ cls.__extract_image_token(Namespace.object_extract(i, "url")) 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 ] ================================================ FILE: source/application/request.py ================================================ 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.blank_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, ) ================================================ FILE: source/application/user_posted.py ================================================ from xhshow import Xhshow from typing import TYPE_CHECKING from ..module import retry, sleep_time from httpx import get if TYPE_CHECKING: from ..module import Manager class UserPosted: encipher = Xhshow() def __init__( self, manager: "Manager", url: str, params: dict, cookies: str = None, proxy: str = None, ): self.url = url self.params = params self.headers = manager.blank_headers.copy() self.client = manager.request_client self.cookies = self.get_cookie(cookies) self.print = manager.print self.retry = manager.retry self.timeout = manager.timeout self.proxy = proxy def get_cookie(self, cookies: str = None) -> dict | str: if cookies: self.headers["cookie"] = cookies return cookies return dict(self.client.cookies) def run( self, verify=True, ): ... @retry async def get_data(self): headers = self.get_headers() if self.proxy: response = get( self.url, params=self.params, headers=headers, proxy=self.proxy, follow_redirects=True, verify=False, timeout=self.timeout, ) else: response = await self.client.get( self.url, params=self.params, headers=headers, ) await sleep_time() response.raise_for_status() return response.json() def get_headers(self): headers = self.encipher.sign_headers_get( uri=self.url, cookies=self.cookies, params=self.params, ) return headers | self.headers ================================================ FILE: source/application/video.py ================================================ from source.expansion import Namespace from .request import Html __all__ = ["Video"] class Video: VIDEO_LINK = ( "video", "consumer", "originVideoKey", ) @classmethod def deal_video_link( cls, data: Namespace, preference="resolution", ): return cls.generate_video_link(data) or cls.get_video_link(data, preference) @classmethod def generate_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 [] ) @classmethod def get_video_link( cls, data: Namespace, preference="resolution", ) -> list: if not (items := cls.get_video_items(data)): return [] match preference: case "resolution": items.sort(key=lambda x: x.height) case "bitrate": items.sort(key=lambda x: x.videoBitrate) case "size": items.sort(key=lambda x: x.size) case _: raise ValueError(f"Invalid video preference value: {preference}") return [b[0]] if (b := items[-1].backupUrls) else [items[-1].masterUrl] @staticmethod def get_video_items(data: Namespace) -> list: h264 = data.safe_extract("video.media.stream.h264", []) h265 = data.safe_extract("video.media.stream.h265", []) return [*h264, *h265] ================================================ FILE: source/expansion/__init__.py ================================================ # 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 ================================================ FILE: source/expansion/browser.py ================================================ 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 功能不支持当前平台!")) ================================================ FILE: source/expansion/cleaner.py ================================================ 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")) ================================================ FILE: source/expansion/converter.py ================================================ from typing import Union from re import compile from lxml.etree import HTML from yaml import safe_load __all__ = ["Converter"] class Converter: YAML_ILLEGAL = compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]") INITIAL_STATE = "//script/text()" PC_KEYS_LINK = ( "note", "noteDetailMap", "[-1]", "note", ) PHONE_KEYS_LINK = ( "noteData", "data", "noteData", ) 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) @classmethod def _convert_object(cls, text: str) -> dict: cleaned = cls.YAML_ILLEGAL.sub("", text.lstrip("window.__INITIAL_STATE__=")) return safe_load(cleaned) @classmethod def _filter_object(cls, data: dict) -> dict: return ( cls.deep_get(data, cls.PHONE_KEYS_LINK) or cls.deep_get(data, cls.PC_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() return next( ( script for script in scripts if script.startswith("window.__INITIAL_STATE__") ), "", ) ================================================ FILE: source/expansion/error.py ================================================ class CacheError(Exception): def __init__(self, message: str): super().__init__(message) self.message = message def __str__(self): return self.message ================================================ FILE: source/expansion/file_folder.py ================================================ 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() ================================================ FILE: source/expansion/namespace.py ================================================ 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)) ================================================ FILE: source/expansion/pyi_rth_beartype.py ================================================ """ 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() ================================================ FILE: source/expansion/truncate.py ================================================ 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}" ================================================ FILE: source/module/__init__.py ================================================ 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 ================================================ FILE: source/module/extend.py ================================================ __all__ = ["Account"] class Account: pass ================================================ FILE: source/module/manager.py ================================================ from pathlib import Path from re import compile, sub from shutil import move, rmtree from os import utime from http.cookies import SimpleCookie 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, video_preference: str, 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.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.blank_headers | { "referer": "https://www.xiaohongshu.com/", }, cookies=self.cookie_str_to_dict(cookie), timeout=timeout, verify=False, http2=True, 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.video_preference = self.check_video_preference(video_preference) 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 "jpeg" @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_, ) @staticmethod def check_video_preference(preference: str) -> str: if preference in {"resolution", "bitrate", "size"}: return preference return "resolution" 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) @staticmethod def cookie_str_to_dict(cookie_str: str) -> dict: cookie = SimpleCookie() cookie.load(cookie_str) return {key: morsel.value for key, morsel in cookie.items()} ================================================ FILE: source/module/mapping.py ================================================ 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 ================================================ FILE: source/module/model.py ================================================ 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 ================================================ FILE: source/module/recorder.py ================================================ 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()] ================================================ FILE: source/module/script.py ================================================ 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() ================================================ FILE: source/module/settings.py ================================================ 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, # 请求头 # "a_user_agent": USERAGENT, # 请求头 # "b_user_agent": USERAGENT, # 请求头 "cookie": "", # Cookie "proxy": None, # 代理设置 "timeout": 10, # 超时时间(秒) "chunk": 1024 * 1024 * 2, # 下载块大小(字节) "max_retry": 5, # 最大重试次数 "record_data": False, # 是否记录作品数据 "image_format": "JPEG", # 图文作品格式 "image_download": True, # 是否下载图文 "video_download": True, # 是否下载视频 "live_download": False, # 是否下载动图 "video_preference": "resolution", # 视频文件偏好 "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) ================================================ FILE: source/module/static.py ================================================ from pathlib import Path VERSION_MAJOR = 2 VERSION_MINOR = 8 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/143.0.0.0 " "Safari/537.36 Edg/143.0.0.0" ) 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__) ================================================ FILE: source/module/tools.py ================================================ from asyncio import sleep from random import lognormvariate from math import log 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, ) def get_wait_time( avg_delay: float | int = 6.0, sigma: float = 0.6, ) -> float: mu = log(avg_delay) - (sigma**2 / 2) return max(0.5, lognormvariate(mu, sigma)) async def sleep_time(): await sleep(get_wait_time()) ================================================ FILE: source/translation/__init__.py ================================================ from .translate import switch_language, _ ================================================ FILE: source/translation/translate.py ================================================ 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 ================================================ FILE: static/20250619.js ================================================ function L(h, b) { var C = F(); L = function (f, v) { f = f - 0x1aa; var t = C[f]; return t; } ; return L(h, b); } (function (h, b) { var C6 = { h: 0x210, b: 0x31f }; var Fn = L; var C = h(); while (!![]) { try { var f = parseInt(Fn(0x314)) / 0x1 + parseInt(Fn(0x23e)) / 0x2 + parseInt(Fn(0x220)) / 0x3 * (-parseInt(Fn(0x26e)) / 0x4) + -parseInt(Fn(0x319)) / 0x5 * (parseInt(Fn(0x305)) / 0x6) + -parseInt(Fn(C6.h)) / 0x7 + parseInt(Fn(0x2b0)) / 0x8 * (parseInt(Fn(0x1c2)) / 0x9) + -parseInt(Fn(C6.b)) / 0xa * (-parseInt(Fn(0x2ce)) / 0xb); if (f === b) { break; } else { C['push'](C['shift']()); } } catch (v) { C['push'](C['shift']()); } } }(F, 0xbce69)); function F() { var RC = ['JZLvj', 'oahov', 'gPDFl', 'DaRdU', 'eGCPD', 'pMsaX', 'vQQUk', 'SssvC', 'AMBCI', 'vSFPC', 'wSOnE', 'NIOdi', 'mXOEa', 'YAvpt', '3956hsoIXl', 'UPxpr', 'hEIFf', 'oNTxv', 'PTNqV', 'qPPwK', 'oXgav', 'XEyPA', 'sRVor', 'FsXNX', 'oRxVE', 'GrgCd', 'ALpNt', 'kEqSg', 'VHkPt', 'GlBWa', 'yAZPc', 'yOvzN', 'AWvMp', 'kRdje', 'zCPbZ', 'fIDlR', 'QmRpf', 'lGvVL', 'kgeUA', 'NcKvK', 'MRQGa', 'reduce', 'zXemr', 'xCAJf', 'xnNmv', 'bHZSF', 'pop', 'nwOLN', 'iZTGz', 'mjdDi', 'lOrOp', 'aSIKX', 'GTnDt', 'Vdmrg', 'vpgNZ', 'sNnEf', 'nMMxv', 'ikzvH', 'GKtmp', 'JiTri', 'pCEKQ', 'CGaSJ', 'JSON', 'XWoOH', 'eqyFf', 'quyBc', 'hfEPg', 'ijFaB', 'Zfoeu', 'MrzJy', '_sabo_95cb2', 'CScej', 'oBnDc', 'odVfd', 'XjCEE', 'kPfVf', 'SobNN', 'zzwfi', 'hXCqG', 'YqyTm', '8XPIkOX', 'rpzig', 'vfuFM', 'qyZqB', 'WwqmJ', 'WqSKX', 'grXOT', 'TnYHa', 'HIswS', 'TByEj', 'DBWWn', 'eOFNg', 'WzxBd', 'WHsCZ', 'SEoiV', 'svkqH', 'lbZOY', 'bind', 'OMccf', '4|1|2|0|5|9|6|3|8|7', 'epJNI', 'VLqYs', 'gSDNx', '_sabo_5b836', '_sabo_3088c', 'map', 'ZWORk', 'indexOf', 'hoXih', 'OChTg', '5235241KFSZcx', 'aMltr', 'nDlQf', 'PHGCq', 'wWSUD', 'odYLt', 'tXtzn', 'PGxhU', 'uEfBT', 'AfBYF', 'sVuBy', 'fRDpO', 'JbvFu', 'KdPNG', 'DinAD', 'jYhqS', 'zQsUh', 'aiVyO', 'GnKMQ', 'Lkyuv', 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 'bOaOU', 'goMdg', 'aLwwx', 'prototype', 'AryJI', 'tRAuT', 'eXpUz', 'atDry', 'charCodeAt', 'JBdED', 'KAEds', 'qMPgm', 'tGZGI', 'xRcPH', 'zNAiV', 'wBccl', 'APjhA', 'vfvwF', 'buGvd', 'qehkP', 'sioDi', 'Uiemt', 'dOPfp', 'vsGkJ', 'WbHfy', 'Horkt', 'jXFeA', 'OIQJh', 'IvpwU', 'FdoKy', 'orFnF', 'ZvPja', 'zHUzN', 'CZeXt', '722346TUWvEl', 'kweEL', 'PxhoI', 'RRNkR', 'ysQKL', 'YqfES', 'ibqQa', 'PUCdd', 'BWpPZ', 'hhbCq', 'fJlXY', 'OGgZX', 'fromCharCode', 'tRJfx', 'dBKVS', '890714JAyMWZ', 'FrWRh', 'ptgek', 'nlBkp', 'nzYNo', '45oABSvD', 'hxqUd', 'TwqJu', 'eMmdn', 'jHxlb', 'nHhGK', '10BSxYuf', 'HRfcg', 'gUbAP', 'FGzOP', 'tItQY', 'jTwuf', 'CFtvf', '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', 'rCmpM', 'OoaGy', 'MpRAu', 'deFSN', 'GKBtV', 'XDorZ', 'nVIbO', 'IdiaC', 'vWruB', 'OzdlV', 'CcUsi', 'join', 'dAAVb', 'djLSf', 'LjgZY', 'HLABO', 'lGKwN', 'PXTBo', 'zGaFk', 'FAcyL', 'sMIQH', 'HozPp', 'oJfRr', 'WTylz', 'UGTAB', 'qlpXf', 'JcTTq', 'mMONc', 'AKXKb', 'MmaEG', 'bZSXv', 'LJyjj', 'ckdoI', 'oeJmh', 'PPfJp', 'icctW', 'NNfMe', 'Mzwla', 'NJFpJ', 'KHzzB', 'OMgMD', 'ENgsj', 'ImoXf', 'kmutF', 'zUOBa', '_sabo_57ecb', 'xblso', 'GkjFS', 'BllIK', 'wCzTn', 'mTPIJ', 'cIovd', 'ZRZmq', 'hakVh', 'IWYXF', 'gFinC', 'vqEeQ', 'HeuYr', 'dJeju', 'zaGpL', 'bEIcG', 'split', 'hyZoA', 'ijCbw', 'YULRE', 'ZgtSl', 'GNxQH', 'Ufprf', 'SmgXY', 'uDAFK', 'hkMoL', '_sabo_c097b', '7229619GDXiCt', 'SOakV', '_sabo_e2c0a', 'dReHp', 'XNrHe', 'uLSjZ', 'lUoXu', 'ECpKE', 'lDXhj', 'MLjuz', 'isNaN', 'TNAaC', 'Error', 'YVUIV', 'GRZrF', 'Dqqnv', 'SROLQ', 'svANh', 'vyrbl', 'apply', 'zryFJ', 'WIitv', '_sabo_10b1e', 'NVYmF', 'ZavHO', 'Uint8Array', 'EFeRo', 'FqByk', 'QgjFq', 'Ofenh', 'snVSB', 'kcAaU', 'MGiPt', 'kSUUe', 'YIEGq', 'SvIPY', 'SKkEN', 'slice', '_sabo_6da36', 'QSewX', 'ziuYy', 'gnqzd', 'qfOel', 'hZcaT', 'CnNTH', 'XwdTh', 'KhWSm', 'ljzHB', 'KjZAP', 'blfSb', 'iEAsQ', 'oWFrJ', 'RGcqc', 'uMofr', 'CDGuO', 'bdltS', 'pRDMG', 'rKNGe', 'zJdLz', '_sabo_c724', 'sHzOh', 'watuf', 'AleBJ', '_sabo_d4818', 'rbpGg', 'IgyrF', 'qbJjB', 'encodeURI', 'hgTCW', 'ivsOQ', 'NEMiU', 'ciJml', 'RRAzt', 'EUTZE', 'byFQy', 'charAt', 'PlApi', 'afgyW', '966651pmXOPB', 'bApKG', 'GYyhS', 'JYyAi', 'yMgIH', 'ahuAk', 'CcYre', 'GstgQ', 'eDqxI', 'zCLPb', 'EhpkL', 'nAdsq', 'ShWNE', 'Efzgp', 'ngarZ', 'uLFqi', '4458qRqukd', 'VgmAq', 'UEaJS', 'cLzzq', 'prjDE', 'pisTm', 'vfeYb', 'lIxby', 'QOeak', 'wQhwE', 'ODGrw', 'cAlyE', 'vEcMv', 'ldnrr', 'jqmtI', 'DGWOU', 'length', 'MqBmP', 'Wxdrg', 'ZHwyQ', 'NTrbI', 'SeLDj', 'OoSWq', 'tVHze', 'TYkNO', 'yHiCc', 'KECwk', 'sqlMq', 'QImRg', 'YsXRR', '2590134kfnprK', 'VRBVo', 'XvVud', 'NLzhS', 'tBAUu', 'unshift', 'rtvbE', 'uQaTs', 'yrndN', 'QNaSf', 'uWAZn', 'awHpc', 'LLEzS', 'DiKZc', 'SIhNg', 'push', 'kHKev', 'fVHLl', 'ERaGW', 'splice', 'QcqJP', 'ucaVm', 'huAES', 'mhMSh', 'mNQYE', 'lAWcZ', 'dvFSE', 'GGXmC', 'YqUuj', 'umXpK', 'eTZYs', 'RlxEZ', 'BNqwn', 'MMTae']; F = function () { return RC; } ; return F(); } (function () { var Rb = { h: 0x227, b: 0x1f0, C: 0x223, f: 0x2a0, v: 0x334, t: 0x1bb, c: 0x1ed, W: 0x273, R: 0x250, S: 0x29f, E: 0x25f, i: 0x353, N: 0x316, g: 0x2fb, x: 0x300, r: 0x1da, m: 0x30a, e: 0x297, B: 0x1cc, y: 0x265 }; var Rh = { h: 0x224, b: 0x330, C: 0x1ab, f: 0x24a, v: 0x1fa, t: 0x265, c: 0x247, W: 0x249, R: 0x338, S: 0x1bc, E: 0x2c3 }; var vA = { h: 0x29a }; var vB = { h: 0x2c4 }; var vE = { h: 0x25a }; var fD = { h: 0x1ff }; var ft = { h: 0x304 }; var fb = { h: 0x350 }; var f7 = { h: 0x218 }; var f2 = { h: 0x31d }; var CO = { h: 0x350 }; var CI = { h: 0x2cf }; var FQ = L; var h = { 'aMltr': function (C, f, v, t, c) { return C(f, v, t, c); }, 'MmaEG': function (C, f) { return C + f; }, 'GKtmp': function (C, f, v) { return C(f, v); }, 'eDqxI': function (C, f) { return C < f; }, 'ENgsj': function (C, f) { return C === f; }, 'wWSUD': FQ(0x202), 'AryJI': function (C, f) { return C & f; }, 'prjDE': FQ(Rb.h), 'kgeUA': function (C, f) { return C << f; }, 'jHxlb': function (C, f) { return C & f; }, 'XDorZ': function (C, f) { return C | f; }, 'hgTCW': function (C, f) { return C & f; }, 'kRdje': function (C, f) { return C !== f; }, 'OzdlV': 'ZRZmq', 'vpgNZ': FQ(0x1d0), 'BNqwn': function (C, f) { return C >> f; }, 'wCzTn': FQ(Rb.b), 'vQQUk': function (C, f) { return C != f; }, 'cAlyE': function (C, f) { return C(f); }, 'rKKzw': FQ(0x31b), 'LLEzS': FQ(0x28a), 'mjdDi': FQ(0x24f), 'umXpK': FQ(Rb.C), 'jTwuf': function (C, f, v, t, c) { return C(f, v, t, c); }, 'CZeXt': function (C, f, v, t, c) { return C(f, v, t, c); }, 'xblso': FQ(Rb.f), 'qehkP': FQ(0x31e), 'pRDMG': FQ(0x267), 'AWvMp': function (C, f, v, t, c) { return C(f, v, t, c); }, 'bEIcG': function (C, f) { return C >>> f; }, 'nMMxv': FQ(0x27b), 'NLzhS': FQ(0x2e0), 'kcAaU': FQ(Rb.v), 'lUoXu': function (C, f) { return C == f; }, 'DaRdU': function (C, f) { return C !== f; }, 'NEMiU': 'DLZuo', 'TNZwf': 'bNhav', 'cIovd': function (C, f) { return C - f; }, 'VkNzq': FQ(Rb.t), 'poQoR': FQ(0x2a7), 'KHzzB': function (C, f) { return C * f; }, 'NTrbI': function (C, f) { return C - f; }, 'watuf': function (C, f) { return C ^ f; }, 'tBAUu': function (C, f, v) { return C(f, v); }, 'nAdsq': FQ(Rb.c), 'vfuFM': function (C, f) { return C in f; }, 'ikzvH': FQ(Rb.W), 'mTPIJ': FQ(0x207), 'AENql': function (C, f) { return C !== f; }, 'QOeak': function (C, f, v) { return C(f, v); }, 'QSewX': function (C, f) { return C - f; }, 'pMsaX': 'undefined', 'wurWF': function (C, f, v) { return C(f, v); }, 'eOFNg': function (C, f, v, t, c) { return C(f, v, t, c); }, 'oBnDc': function (C, f, v, t, c) { return C(f, v, t, c); }, 'cbnsj': function (C, f) { return C % f; }, 'qlpXf': FQ(0x1f7), 'CcYre': function (C, f, v) { return C(f, v); }, 'ySQYi': function (C, f, v) { return C(f, v); }, 'GTknR': FQ(Rb.R), 'dJeju': function (C, f, v) { return C(f, v); }, 'MrzJy': function (C, f, v) { return C(f, v); }, 'piLdu': 'SFzDm', 'qMPgm': FQ(Rb.S), 'zzwfi': function (C, f) { return C == f; }, 'EsoLm': FQ(0x31c), 'QNaSf': FQ(0x27f), 'awHpc': FQ(Rb.E), 'uCZeD': function (C, f) { return C === f; }, 'IvpwU': 'gNlYm', 'qCvbJ': FQ(0x32a), 'JYyAi': FQ(0x1f1), 'YpQLH': FQ(0x264), 'YqUuj': function (C, f, v) { return C(f, v); }, 'hXCqG': FQ(Rb.i), 'PXTBo': FQ(Rb.N), 'tItQY': function (C, f, v, t, c) { return C(f, v, t, c); }, 'GkjFS': function (C, f) { return C > f; }, 'jkiDJ': FQ(Rb.g), 'PPfJp': FQ(0x29d), 'epJNI': function (C, f) { return C !== f; }, 'YckUU': FQ(Rb.x), 'iEKgt': FQ(Rb.r), 'LTTNI': function (C) { return C(); }, 'gSDNx': function (C, f, v, t, c) { return C(f, v, t, c); }, 'dBKVS': function (C, f) { return C instanceof f; }, 'GNxQH': FQ(0x2b6), 'ucOWm': function (C, f, v) { return C(f, v); }, 'ucaVm': FQ(Rb.m), 'zHUzN': function (C, f) { return C(f); }, 'sVuBy': FQ(0x325), 'dAAVb': FQ(0x24b), 'sNnEf': FQ(0x326), 'FsXNX': 'window', 'GstgQ': FQ(0x1ce), 'PxhoI': FQ(0x1db), 'ZkQes': FQ(0x29e), 'lAWcZ': FQ(0x205) }; function b() { var RL = { h: 0x28d, b: 0x23a, C: 0x256, f: 0x2ea, v: 0x2d6, t: 0x21e }; var R9 = { h: 0x2a6 }; var WO = { h: 0x1d7 }; var WU = { h: 0x309 }; var WV = { h: 0x2f3, b: 0x32b, C: 0x343, f: 0x2a6 }; var Wk = { h: 0x2f7 }; var Wl = { h: 0x1cf }; var WD = { h: 0x284, b: 0x26a, C: 0x1fd, f: 0x209 }; var Wg = { h: 0x1b9 }; var WN = { h: 0x1d5, b: 0x1fd }; var WS = { h: 0x1fc }; var Wc = { h: 0x1b3, b: 0x274 }; var Wb = { h: 0x2d5 }; var Wh = { h: 0x1e7, b: 0x346 }; var W5 = { h: 0x1bf }; var W1 = { h: 0x21d }; var cI = { h: 0x2c7, b: 0x2c8, C: 0x1b8, f: 0x24d, v: 0x2e5, t: 0x2eb }; var cq = { h: 0x2c5, b: 0x1d3 }; var cU = { h: 0x230, b: 0x20e }; var cd = { h: 0x1d8 }; var ce = { h: 0x28e }; var cm = { h: 0x24d }; var cE = { h: 0x2fa, b: 0x2c8, C: 0x2e9 }; var cv = { h: 0x30c, b: 0x1d9, C: 0x1ae, f: 0x24d, v: 0x2e2, t: 0x2cb, c: 0x20d, W: 0x29c, R: 0x29c, S: 0x20f, E: 0x311, i: 0x33a, N: 0x24d, g: 0x332 }; var ta = { h: 0x341 }; var tD = { h: 0x287 }; var vQ = { h: 0x2cf }; var vl = { h: 0x2c6 }; var vy = { h: 0x218 }; var v9 = { h: 0x280 }; var fq = { h: 0x242 }; var fT = { h: 0x2a8 }; var fo = { h: 0x29a }; var fY = { h: 0x234 }; var fR = { h: 0x1b6 }; var fC = { h: 0x2cf }; var Cz = { h: 0x218 }; var Cs = { h: 0x344 }; var FK = FQ; var C = { 'nwOLN': function (R, S, E, i, N) { return R(S, E, i, N); }, 'kSUUe': function (R, S, E, i, N) { var FZ = L; return h[FZ(CI.h)](R, S, E, i, N); }, 'eTZYs': function (R, S) { var FV = L; return h[FV(Cs.h)](R, S); }, 'lGvVL': function (R, S, E) { return h['GKtmp'](R, S, E); }, 'SROLQ': function (R, S) { var FM = L; return h[FM(Cz.h)](R, S); }, 'fJlXY': function (R, S) { var FU = L; return h[FU(CO.h)](R, S); }, 'PUCdd': h[FK(0x2d2)], 'FGzOP': function (R, S) { return R >> S; }, 'afgyW': function (R, S) { var FT = FK; return h[FT(0x2e7)](R, S); }, 'kHKev': h[FK(Rh.h)], 'zGaFk': '2|0|4|3|1', 'RRNkR': function (R, S) { var FP = FK; return h[FP(0x286)](R, S); }, 'fAOHV': function (R, S) { var Fp = FK; return h[Fp(f2.h)](R, S); }, 'pCEKQ': function (R, S) { var Fq = FK; return h[Fq(0x32c)](R, S); }, 'sHzOh': function (R, S) { var FI = FK; return h[FI(0x2e7)](R, S); }, 'GGXmC': function (R, S) { var Fs = FK; return h[Fs(0x206)](R, S); }, 'NVYmF': function (R, S) { var Fw = FK; return h[Fw(0x281)](R, S); }, 'XwdTh': h[FK(Rh.b)], 'PHsGL': function (R, S) { var Fz = FK; return h[Fz(f7.h)](R, S); }, 'hxqUd': h[FK(0x296)], 'GrgCd': function (R, S) { return h['BNqwn'](R, S); }, 'kzPnv': function (R, S) { var FO = FK; return h[FO(0x32c)](R, S); }, 'FrWRh': function (R, S) { return R !== S; }, 'TByEj': h[FK(Rh.C)], 'FAcyL': function (R, S) { return h['vQQUk'](R, S); }, 'AleBJ': function (R, S) { var FJ = FK; return h[FJ(0x22b)](R, S); }, 'huAES': h['rKKzw'], 'DGWOU': function (R, S) { var L0 = FK; return h[L0(fb.h)](R, S); }, 'jXFeA': h[FK(Rh.f)], 'uLSjZ': h[FK(0x291)], 'zaGpL': function (R, S, E, i, N) { var L1 = FK; return h[L1(fC.h)](R, S, E, i, N); }, 'OChTg': function (R, S) { return R(S); }, 'zCLPb': h[FK(0x25b)], 'WHsCZ': function (R, S, E, i, N) { var L2 = FK; return h[L2(0x324)](R, S, E, i, N); }, 'quyBc': function (R, S, E, i, N) { var L3 = FK; return h[L3(ft.h)](R, S, E, i, N); }, 'DBWWn': h[FK(0x355)], 'rCmpM': h[FK(0x2f6)], 'HRfcg': function (R, S) { return h['ENgsj'](R, S); }, 'bdltS': FK(0x27d), 'wMDXH': h[FK(Rh.v)], 'WwqmJ': function (R, S, E, i, N) { return h['AWvMp'](R, S, E, i, N); }, 'BWpPZ': function (R, S) { var L4 = FK; return h[L4(fR.h)](R, S); }, 'snVSB': h[FK(0x298)], 'RlxEZ': h[FK(0x241)], 'SmgXY': function (R, S, E, i, N, g, x, r) { return R(S, E, i, N, g, x, r); }, 'zCPbZ': function (R, S, E) { var L5 = FK; return h[L5(0x29a)](R, S, E); }, 'UEaJS': h[FK(0x1e1)], 'YqyTm': function (R, S) { var L6 = FK; return h[L6(0x1c8)](R, S); }, 'aSIKX': function (R, S) { var L7 = FK; return h[L7(0x263)](R, S); }, 'xnNmv': FK(0x34b), 'QgjFq': h[FK(0x208)], 'ldnrr': h['TNZwf'], 'ZvPja': function (R, S, E, i, N, g, x, r) { return R(S, E, i, N, g, x, r); }, 'uLFqi': function (R, S) { var L8 = FK; return h[L8(0x1ad)](R, S); }, 'MRQGa': function (R, S) { return R === S; }, 'dfxVY': h['VkNzq'], 'MBgMf': h['poQoR'], 'hyZoA': function (R, S) { return R - S; }, 'LJyjj': function (R, S) { var L9 = FK; return h[L9(0x344)](R, S); }, 'aLwwx': function (R, S) { var LF = FK; return h[LF(0x34e)](R, S); }, 'WqSKX': function (R, S, E, i, N) { var LL = FK; return h[LL(0x2cf)](R, S, E, i, N); }, 'gPDFl': function (R, S) { var Lh = FK; return h[Lh(fY.h)](R, S); }, 'iZTGz': FK(0x1e4), 'TNAaC': function (R, S) { var Lb = FK; return h[Lb(fD.h)](R, S); }, 'odVfd': function (R, S, E) { var LC = FK; return h[LC(0x242)](R, S, E); }, 'yMgIH': function (R, S, E) { return R(S, E); }, 'PGxhU': FK(0x211), 'SvIPY': h[FK(0x21b)], 'AfBYF': function (R, S) { var Lf = FK; return h[Lf(0x2b2)](R, S); }, 'RGcqc': function (R) { return R(); }, 'zJdLz': function (R, S, E) { var Lv = FK; return h[Lv(fo.h)](R, S, E); }, 'oXgav': h[FK(0x299)], 'sMIQH': function (R, S, E, i, N) { return h['AWvMp'](R, S, E, i, N); }, 'gFinC': h[FK(0x1ac)], 'orFnF': function (R, S) { return R !== S; }, 'HlDqZ': function (R, S, E, i, N) { return R(S, E, i, N); }, 'rKNGe': function (R, S) { return R != S; }, 'gUbAP': function (R, S) { return h['AENql'](R, S); }, 'OMccf': FK(0x1e6), 'oeJmh': function (R, S) { return h['BNqwn'](R, S); }, 'WIitv': function (R, S, E) { var Lt = FK; return h[Lt(0x29a)](R, S, E); }, 'XNrHe': function (R, S, E) { var Lc = FK; return h[Lc(0x228)](R, S, E); }, 'lHsxx': function (R, S) { var LW = FK; return h[LW(0x1e9)](R, S); }, 'ckdoI': function (R, S) { var LR = FK; return h[LR(0x22b)](R, S); }, 'IgyrF': h[FK(Rh.t)], 'zryFJ': function (R, S, E) { var LS = FK; return h[LS(0x228)](R, S, E); }, 'SOakV': function (R, S, E) { return h['wurWF'](R, S, E); }, 'QcqJP': function (R, S, E, i, N) { return h['eOFNg'](R, S, E, i, N); }, 'IdiaC': function (R, S, E, i, N) { var LE = FK; return h[LE(fT.h)](R, S, E, i, N); }, 'sioDi': function (R, S, E) { return R(S, E); }, 'DHUPP': function (R, S) { return h['cbnsj'](R, S); }, 'NJFpJ': function (R, S, E) { var Li = FK; return h[Li(fq.h)](R, S, E); }, 'yHiCc': function (R, S, E) { return h['GKtmp'](R, S, E); }, 'uNwcj': h[FK(0x340)], 'ciJml': function (R, S, E) { var LN = FK; return h[LN(0x216)](R, S, E); }, 'NcKvK': function (R, S, E, i, N) { return R(S, E, i, N); }, 'YVUIV': function (R, S) { return R / S; }, 'qfltV': function (R, S) { return R - S; }, 'yzhnN': function (R, S, E) { return h['ySQYi'](R, S, E); }, 'sRVor': function (R, S) { return R === S; }, 'ODGrw': function (R, S, E) { return R(S, E); }, 'APjhA': function (R, S) { var Lg = FK; return h[Lg(0x263)](R, S); }, 'GKBtV': h['GTknR'], 'AKXKb': function (R, S, E) { var Lx = FK; return h[Lx(0x216)](R, S, E); }, 'FJREI': function (R, S, E, i, N) { return R(S, E, i, N); }, 'gnqzd': function (R, S, E) { var Lr = FK; return h[Lr(0x1b4)](R, S, E); }, 'atDry': function (R, S, E, i, N) { return R(S, E, i, N); }, 'ELHrH': function (R, S) { return R - S; }, 'vQqZP': function (R, S, E) { var Lm = FK; return h[Lm(0x2a5)](R, S, E); }, 'ocRxu': function (R, S, E, i, N) { var Le = FK; return h[Le(v9.h)](R, S, E, i, N); }, 'dvFSE': function (R, S) { return R !== S; }, 'ABTJb': h['piLdu'], 'MGiPt': h[FK(0x2ee)], 'gQkLB': function (R, S, E, i, N) { return R(S, E, i, N); }, 'ahuAk': function (R, S) { return R > S; }, 'AMBCI': function (R, S, E) { return R(S, E); }, 'RRAzt': function (R, S, E) { return h['ySQYi'](R, S, E); }, 'MqBmP': function (R, S) { var LB = FK; return h[LB(0x2ad)](R, S); }, 'tGZGI': function (R, S, E, i, N) { var Ly = FK; return h[Ly(0x2bb)](R, S, E, i, N); }, 'JZLvj': h['EsoLm'], 'vEcMv': h[FK(Rh.c)], 'fIDlR': function (R, S, E, i, N) { return h['CZeXt'](R, S, E, i, N); }, 'HAyBd': function (R, S, E) { return R(S, E); }, 'xCAJf': h[FK(Rh.W)], 'kmutF': function (R, S) { return h['uCZeD'](R, S); }, 'wQhwE': h[FK(0x2ff)], 'hVFPT': h['qCvbJ'], 'bHZSF': h[FK(0x213)], 'JmIJf': h['YpQLH'], 'tXtzn': function (R, S, E) { var LY = FK; return h[LY(0x29a)](R, S, E); }, 'HIswS': function (R, S) { var LD = FK; return h[LD(0x266)](R, S); }, 'vtWFF': function (R, S, E) { var Lj = FK; return h[Lj(vE.h)](R, S, E); }, 'WTylz': function (R, S) { return R - S; }, 'ImoXf': h[FK(0x2ae)], 'ljrRl': h[FK(Rh.R)], 'CcUsi': function (R, S, E, i, N) { var Ll = FK; return h[Ll(0x323)](R, S, E, i, N); }, 'hoXih': function (R, S) { var LA = FK; return h[LA(0x356)](R, S); }, 'Qkaka': h['jkiDJ'], 'Ufprf': function (R, S, E, i, N) { return h['CZeXt'](R, S, E, i, N); }, 'TcDxS': function (R, S, E) { var LG = FK; return h[LG(0x242)](R, S, E); }, 'Uiemt': h[FK(0x349)], 'tRJfx': function (R, S) { return R + S; }, 'jksMW': function (R, S, E) { return h['QOeak'](R, S, E); }, 'OIQJh': function (R, S) { var Lo = FK; return h[Lo(vB.h)](R, S); }, 'WAmru': function (R, S) { var Lk = FK; return h[Lk(vy.h)](R, S); }, 'Wxdrg': h['YckUU'], 'QImRg': h['iEKgt'], 'rpzig': function (R, S, E, i, N) { var La = FK; return h[La(0x2bb)](R, S, E, i, N); }, 'FjvPb': function (R) { return h['LTTNI'](R); }, 'EhpkL': function (R, S, E, i, N) { return h['aMltr'](R, S, E, i, N); }, 'ngarZ': function (R, S, E, i, N) { var Ld = FK; return h[Ld(vl.h)](R, S, E, i, N); }, 'JcTTq': function (R, S, E) { var LX = FK; return h[LX(vA.h)](R, S, E); }, 'KdPNG': function (R, S) { var LH = FK; return h[LH(0x313)](R, S); }, 'MLjuz': h[FK(Rh.S)], 'tVHze': function (R, S, E, i, N) { var Lu = FK; return h[Lu(0x2a8)](R, S, E, i, N); }, 'ybgJn': function (R, S, E) { return h['ucOWm'](R, S, E); }, 'bRryP': h[FK(0x253)], 'vSFPC': function (R, S) { var Ln = FK; return h[Ln(0x303)](R, S); }, 'KECwk': h[FK(0x2d8)], 'fSmTE': function (R, S, E, i, N) { return R(S, E, i, N); }, 'UPxpr': FK(Rh.E), 'aiVyO': function (R, S) { var LQ = FK; return h[LQ(0x2e7)](R, S); }, 'mMONc': function (R, S) { var LZ = FK; return h[LZ(0x25e)](R, S); }, 'mNQYE': h[FK(0x333)], 'goMdg': function (R, S, E, i, N) { return R(S, E, i, N); }, 'uEfBT': function (R, S, E, i, N) { var LV = FK; return h[LV(0x2bb)](R, S, E, i, N); }, 'kweEL': function (R, S, E, i, N) { var LM = FK; return h[LM(vQ.h)](R, S, E, i, N); } }; var f = 0x7fffffff , v = 0x1 , t = 0x0 , c = !!v , W = !!t; return function (R, S, E) { var RF = { h: 0x233 }; var R6 = { h: 0x1d4 }; var R5 = { h: 0x255, b: 0x29b, C: 0x2cb, f: 0x311, v: 0x20d, t: 0x238 }; var Wz = { h: 0x215, b: 0x20a }; var WZ = { h: 0x1af, b: 0x245 }; var WQ = { h: 0x30e }; var Wu = { h: 0x233 }; var WH = { h: 0x27a }; var WX = { h: 0x310 }; var Wa = { h: 0x34d, b: 0x22a }; var WB = { h: 0x2da }; var Wr = { h: 0x1d6, b: 0x2a6 }; var Wi = { h: 0x2c2 }; var WE = { h: 0x285 }; var WR = { h: 0x315, b: 0x1f2 }; var Wt = { h: 0x1fc }; var Wf = { h: 0x24d }; var WC = { h: 0x26d, b: 0x23b, C: 0x230 }; var W7 = { h: 0x32f, b: 0x1ec }; var W2 = { h: 0x262, b: 0x1fd }; var W0 = { h: 0x282 }; var cJ = { h: 0x230, b: 0x2aa, C: 0x2f2, f: 0x243, v: 0x2e6, t: 0x2c1 }; var cw = { h: 0x2d9 }; var cT = { h: 0x226, b: 0x293, C: 0x2c8, f: 0x302 }; var cu = { h: 0x23d }; var cX = { h: 0x2a1 }; var ck = { h: 0x23f, b: 0x1df }; var cD = { h: 0x22f, b: 0x219 }; var cy = { h: 0x1f3 }; var cx = { h: 0x2c7, b: 0x2c8 }; var cR = { h: 0x1fd, b: 0x2a6, C: 0x1c4 }; var cW = { h: 0x221 }; var cf = { h: 0x24d, b: 0x2d3, C: 0x1b7, f: 0x27c, v: 0x311, t: 0x235, c: 0x1dc, W: 0x1f8, R: 0x1fd, S: 0x2a6 }; var ch = { h: 0x308 }; var cL = { h: 0x29c }; var c5 = { h: 0x20f }; var tz = { h: 0x285 }; var tq = { h: 0x2e4 }; var tu = { h: 0x237 }; var vq = { h: 0x288 }; var LT = FK; var i = { 'eXpUz': function (p, q, I, s, w) { return p(q, I, s, w); }, 'GTnDt': function (p, q, I) { var LU = L; return C[LU(0x1d6)](p, q, I); }, 'FqByk': function (p, q, I, s, w) { return p(q, I, s, w); }, 'tRAuT': function (p, q) { var LK = L; return C[LK(0x301)](p, q); }, 'JNcQS': C[LT(0x260)], 'nzYNo': 'ByABT', 'vsGkJ': function (p, q, I) { return C['NJFpJ'](p, q, I); }, 'OoSWq': 'XrElB', 'XEyPA': C[LT(0x22c)], 'VLqYs': function (p, q, I, s, w) { var LP = LT; return C[LP(0x283)](p, q, I, s, w); }, 'kPfVf': function (p, q) { var Lp = LT; return C[Lp(0x200)](p, q); }, 'ECpKE': function (p, q, I) { return C['HAyBd'](p, q, I); }, 'ALpNt': function (p, q) { var Lq = LT; return C[Lq(vq.h)](p, q); }, 'blfSb': C[LT(0x28b)], 'hakVh': function (p, q, I, s, w) { var LI = LT; return C[LI(0x1b5)](p, q, I, s, w); }, 'BllIK': function (p, q) { var Ls = LT; return C[Ls(0x352)](p, q); }, 'jqmtI': C[LT(0x229)], 'VRBVo': C['hVFPT'], 'qbJjB': C[LT(RL.h)], 'OoaGy': function (p, q, I, s, w) { return p(q, I, s, w); }, 'dOPfp': function (p, q) { return p === q; }, 'YsXRR': C['JmIJf'], 'QmRpf': function (p, q, I) { var Lw = LT; return C[Lw(0x2d4)](p, q, I); }, 'uWAZn': function (p, q) { var Lz = LT; return C[Lz(0x2b8)](p, q); }, 'mXOEa': function (p, q, I) { return C['vtWFF'](p, q, I); }, 'sqlMq': function (p, q) { var LO = LT; return C[LO(0x33e)](p, q); }, 'hMwnk': function (p, q) { var LJ = LT; return C[LJ(0x2b8)](p, q); }, 'lbZOY': function (p, q) { return p === q; }, 'LjgZY': C[LT(0x351)], 'Vdmrg': C['ljrRl'], 'YqBZK': function (p, q, I, s, w) { return p(q, I, s, w); }, 'XvVud': function (p, q, I, s, w) { var h0 = LT; return C[h0(0x32e)](p, q, I, s, w); }, 'svANh': function (p, q, I, s, w) { var h1 = LT; return C[h1(0x331)](p, q, I, s, w); }, 'HLABO': function (p, q) { var h2 = LT; return C[h2(0x2cc)](p, q); }, 'fRDpO': function (p, q, I) { var h3 = LT; return C[h3(0x1fc)](p, q, I); }, 'ZWORk': function (p) { var h4 = LT; return C[h4(0x1f6)](p); }, 'CnNTH': function (p, q) { return p < q; }, 'XjCEE': 'vnGmT', 'wBccl': C['Qkaka'], 'ysQKL': function (p, q) { return p - q; }, 'SEoiV': function (p, q, I, s, w) { var h5 = LT; return C[h5(0x1bd)](p, q, I, s, w); }, 'Efzgp': function (p, q) { var h6 = LT; return C[h6(0x308)](p, q); }, 'ijFaB': function (p, q, I) { return p(q, I); }, 'PVQkv': function (p, q) { return p <= q; }, 'TnYHa': function (p, q, I, s, w) { return p(q, I, s, w); }, 'uDAFK': function (p, q, I) { return p(q, I); }, 'oahov': function (p, q, I) { return C['TcDxS'](p, q, I); }, 'ERHkq': function (p, q, I) { return p(q, I); }, 'hEIFf': C[LT(0x2f8)], 'YULRE': function (p, q) { var h7 = LT; return C[h7(0x312)](p, q); }, 'OGgZX': function (p, q, I) { return C['jksMW'](p, q, I); }, 'qfOel': function (p, q, I) { return p(q, I); }, 'oJfRr': function (p, q, I, s, w, z) { return p(q, I, s, w, z); }, 'HozPp': function (p) { var h8 = LT; return C[h8(0x1f6)](p); }, 'BvKJu': function (p, q) { var h9 = LT; return C[h9(0x2fe)](p, q); }, 'pisTm': LT(0x1b2), 'JbvFu': function (p) { return p(); }, 'PHGCq': function (p, q) { return C['WAmru'](p, q); }, 'oWFrJ': function (p) { return C['RGcqc'](p); }, 'oxiGH': function (p, q, I, s, w, z, O, J) { return p(q, I, s, w, z, O, J); }, 'DinAD': function (p, q) { var hF = LT; return C[hF(0x2cd)](p, q); }, 'YAvpt': C[LT(0x232)], 'yAZPc': C[LT(0x23c)], 'QnhQH': function (p, q, I, s, w) { var hL = LT; return C[hL(0x2b1)](p, q, I, s, w); }, 'hhbCq': function (p, q, I, s, w) { return C['WHsCZ'](p, q, I, s, w); }, 'ijCbw': function (p, q, I, s, w) { var hh = LT; return C[hh(tD.h)](p, q, I, s, w); }, 'rtvbE': function (p, q) { return p(q); }, 'icctW': function (p) { return C['FjvPb'](p); }, 'iEAsQ': function (p, q, I) { return p(q, I); }, 'wSOnE': function (p, q, I, s, w) { var hb = LT; return C[hb(0x21a)](p, q, I, s, w); }, 'IWYXF': function (p, q, I, s, w) { var hC = LT; return C[hC(0x21e)](p, q, I, s, w); }, 'Lkyuv': function (p, q, I) { var hf = LT; return C[hf(0x1eb)](p, q, I); }, 'twOao': function (p, q, I) { var hv = LT; return C[hv(ta.h)](p, q, I); }, 'QMMxd': function (p, q, I) { var ht = LT; return C[ht(0x209)](p, q, I); }, 'yrndN': function (p, q, I, s, w) { return p(q, I, s, w); }, 'ShWNE': function (p, q) { var hc = LT; return C[hc(0x2db)](p, q); }, 'SIhNg': C[LT(0x1cb)], 'dMFQI': function (p, q, I, s, w) { var hW = LT; return C[hW(tu.h)](p, q, I, s, w); }, 'KAEds': function (p, q, I) { return C['ybgJn'](p, q, I); }, 'byFQy': C['bRryP'], 'ZHwyQ': function (p, q) { var hR = LT; return C[hR(0x269)](p, q); }, 'Dqqnv': C[LT(RL.b)], 'uQaTs': function (p, q, I) { return C['gnqzd'](p, q, I); }, 'udbjY': function (p, q, I, s, w) { return C['fSmTE'](p, q, I, s, w); }, 'lGKwN': function (p, q) { return p >>> q; }, 'Mzwla': function (p, q, I) { return p(q, I); }, 'mhMSh': C[LT(0x26f)], 'RjPbU': function (p, q) { return p | q; }, 'JBdED': function (p, q) { var hS = LT; return C[hS(0x2df)](p, q); }, 'oNTxv': function (p, q) { var hE = LT; return C[hE(0x342)](p, q); }, 'qyZqB': function (p, q) { var hi = LT; return C[hi(0x352)](p, q); }, 'JiTri': C[LT(RL.C)], 'vyrbl': function (p, q, I, s, w) { var hN = LT; return C[hN(tq.h)](p, q, I, s, w); } }; var N = [] , g = [] , x = {} , r = [] , m = { '_sabo_c097b': R } , e = {} , B = t , y = []; var Y = function (I) { var he = LT; var w = { 'ziuYy': function (F9, FF, FL, Fh, Fb) { return C['nwOLN'](F9, FF, FL, Fh, Fb); }, 'wBqAO': function (F9, FF, FL, Fh, Fb) { return C['kSUUe'](F9, FF, FL, Fh, Fb); }, 'Horkt': function (F9, FF) { var hg = L; return C[hg(0x25c)](F9, FF); }, 'hfEPg': function (F9, FF, FL) { var hx = L; return C[hx(tz.h)](F9, FF, FL); }, 'dReHp': function (F9, FF, FL) { return F9(FF, FL); }, 'SobNN': function (F9, FF) { var hr = L; return C[hr(0x1d2)](F9, FF); }, 'jYhqS': function (F9, FF) { var hm = L; return C[hm(0x30f)](F9, FF); }, 'MpRAu': C[he(cv.h)], 'svkqH': function (F9, FF) { return F9 == FF; }, 'oRxVE': function (F9, FF) { var hB = he; return C[hB(0x322)](F9, FF); }, 'Zfoeu': function (F9, FF) { var hy = he; return C[hy(0x20f)](F9, FF); }, 'lDXhj': C[he(0x24e)], 'nDlQf': C[he(0x339)], 'VHkPt': function (F9, FF) { var hY = he; return C[hY(0x308)](F9, FF); }, 'odYLt': function (F9, FF) { var hD = he; return C[hD(c5.h)](F9, FF); }, 'PTNqV': function (F9, FF) { return F9 == FF; }, 'nVIbO': function (F9, FF) { return C['fAOHV'](F9, FF); }, 'qiPlA': function (F9, FF) { var hj = he; return C[hj(0x29c)](F9, FF); }, 'EFeRo': function (F9, FF) { var hl = he; return C[hl(0x1fe)](F9, FF); }, 'OMgMD': function (F9, FF) { return F9 >> FF; }, 'SeLDj': function (F9, FF) { var hA = he; return C[hA(cL.h)](F9, FF); }, 'CDGuO': function (F9, FF) { var hG = he; return C[hG(ch.h)](F9, FF); }, 'ZMIOq': function (F9, FF) { return F9 & FF; }, 'zNAiV': function (F9, FF) { var ho = he; return C[ho(0x259)](F9, FF); } }; if (C[he(cv.b)](C[he(0x1ef)], he(cv.C))) { c[he(cv.f)](w[he(0x1ea)](W, R, S, E, F6)); } else { if (!I) { return ''; } var z = function (FF) { var hk = he; var FL = [] , Fh = FF[hk(0x230)]; var Fb = 0x0; for (var Fb = 0x0; w[hk(0x2ac)](Fb, Fh); Fb++) { if (w[hk(0x2dd)](w['MpRAu'], w[hk(0x329)])) { var FC = FF[hk(0x2eb)](Fb); if (w[hk(0x2bf)](w[hk(0x278)](FC, 0x7) & 0xff, 0x0)) { FL[hk(cf.h)](FF['charAt'](Fb)); } else { if (w[hk(0x2a4)](w[hk(0x278)](FC, 0x5), 0xff) == 0x6) { if (w['jYhqS'](w[hk(0x1ca)], w[hk(0x1ca)])) { var Ff = w[hk(0x2d0)][hk(0x1b7)]('|'); var Fv = 0x0; while (!![]) { switch (Ff[Fv++]) { case '0': var Ft = w[hk(0x27c)](w[hk(0x2d3)](FC, 0x1f), 0x6); continue; case '1': FL[hk(0x24d)](String[hk(0x311)](FW)); continue; case '2': var Fc = FF[hk(0x2eb)](++Fb); continue; case '3': var FW = Ft | FR; continue; case '4': var FR = w[hk(cf.b)](Fc, 0x3f); continue; } break; } } else { w['wBqAO'](E, w[hk(0x2fc)](w[hk(0x2a2)](F6, N, F8), w[hk(0x1c5)](x, F3, F1)), FF, B, 0x0); return ++y; } } else { if (w[hk(0x272)](w[hk(0x32d)](w['oRxVE'](FC, 0x4), 0xff), 0xe)) { var FS = '1|2|0|5|4|3'[hk(cf.C)]('|'); var FE = 0x0; while (!![]) { switch (FS[FE++]) { case '0': var Ft = w['qiPlA'](w[hk(cf.f)](FC, 0x4), w['EFeRo'](w[hk(0x34f)](Fc, 0x2), 0xf)); continue; case '1': var Fc = FF['charCodeAt'](++Fb); continue; case '2': var Fi = FF['charCodeAt'](++Fb); continue; case '3': FL[hk(0x24d)](String[hk(cf.v)](FW)); continue; case '4': var FW = w[hk(cf.t)](w['VHkPt'](w[hk(cf.c)](Ft, 0xff), 0x8), FR); continue; case '5': var FR = w['SeLDj'](w[hk(cf.W)](w['ZMIOq'](Fc, 0x3), 0x6), w[hk(0x2f1)](Fi, 0x3f)); continue; } break; } } } } } else { return FW[hk(0x201)] ? Fh[hk(cf.R)][FR[hk(cf.S)]] : W[hk(0x1c4)]; } } return FL[hk(0x332)](''); }; var O = he(cv.v)[he(0x1b7)](''); var J = I['length']; var F0 = 0x0; var F1 = []; while (C['PHsGL'](F0, J)) { if ('GRZrF' === C[he(0x31a)]) { var F2 = O[he(cv.t)](I[he(cv.c)](F0++)); var F3 = O['indexOf'](I['charAt'](F0++)); var F4 = O[he(cv.t)](I[he(0x20d)](F0++)); var F5 = O[he(0x2cb)](I[he(0x20d)](F0++)); var F6 = C[he(cv.W)](F2 << 0x2, C[he(0x322)](F3, 0x4)); var F7 = C[he(cv.R)](C['RRNkR'](C[he(cv.S)](F3, 0xf), 0x4), C[he(0x279)](F4, 0x2)); var F8 = C['kzPnv'](C[he(0x259)](F4, 0x3) << 0x6, F5); F1[he(0x24d)](String[he(cv.E)](F6)); if (F4 != 0x40) { if (C[he(0x315)](C[he(0x2b9)], C[he(0x2b9)])) { v[t] = c; return ++W; } else { F1[he(0x24d)](String['fromCharCode'](F7)); } } if (C[he(cv.i)](F5, 0x40)) { F1[he(cv.N)](String[he(0x311)](F8)); } } else { return b; } } return C['AleBJ'](z, F1[he(cv.g)]('')); } }; var D = function (p, q, I, s) { var ha = LT; var w = { 'VgmAq': function (z, O, J) { return z(O, J); }, 'WzxBd': function (z, O, J, F0, F1) { return z(O, J, F0, F1); } }; if (C['NVYmF'](C[ha(0x254)], C[ha(0x254)])) { var O = w[ha(cW.h)](E, i, N) , J = g(x, r); w[ha(0x2bc)](m, J++, e, B, 0x0); O['_sabo_c724'][O[ha(0x2a6)]] = J; return ++y; } else { return { '_sabo_e2c0a': p, '_sabo_c724': q, '_sabo_95cb2': I, '_sabo_d4818': s }; } }; var j = function (p) { var hd = LT; return p['_sabo_d4818'] ? p[hd(cR.h)][p[hd(cR.b)]] : p[hd(cR.C)]; }; var l = function (p, q) { return q['hasOwnProperty'](p) ? c : W; }; var A = function (p, q) { var hX = LT; if (i[hX(0x294)](l, p, q)) { return i[hX(0x1dd)](D, t, q, p, v); } var I; if (q[hX(0x1e8)]) { I = A(p, q[hX(0x1e8)]); if (I) { if (i['tRAuT'](i['JNcQS'], i[hX(0x318)])) { return I; } else { return ''; } } } if (q[hX(0x2c8)]) { I = i[hX(cE.h)](A, p, q[hX(cE.b)]); if (I) { if (i[hX(0x236)] === i[hX(0x275)]) { var z = R(S, E) , O = {}; i[hX(cE.C)](i, i[hX(0x294)](N, z, O), g, x, 0x0); return ++r; } else { return I; } } } return W; }; var G = function (p) { var hH = LT; var q = i['GTnDt'](A, p, x); if (q) { return q; } return i[hH(0x2c5)](D, t, x, p, v); }; var o = function () { var hQ = LT; var p = { 'buGvd': function (q, I, s, w, z) { var hu = L; return C[hu(0x1e3)](q, I, s, w, z); }, 'lOrOp': function (q, I) { var hn = L; return C[hn(0x308)](q, I); } }; if (C['DGWOU'](C[hQ(0x2fd)], C[hQ(0x1c7)])) { p[hQ(0x2f5)](E, p[hQ(0x292)](i(N, g), x(r, m)), e, B, 0x0); return ++y; } else { N = x[hQ(cx.h)] ? x['_sabo_5b836'] : r; x = x[hQ(cx.b)] ? x[hQ(0x2c8)] : x; B--; } }; var k = function (p) { x = { '_sabo_3088c': x, '_sabo_6da36': p, '_sabo_5b836': N }; N = []; B++; }; var a = function () { var hZ = LT; y[hZ(cm.h)](C[hZ(0x1b5)](D, B, t, t, t)); }; var d = function () { var hV = LT; return C[hV(0x2cd)](j, y[hV(ce.h)]()); }; var X = function (p, q) { return e[p] = q; }; var H = function (p) { var hM = LT; if (i[hM(0x27a)](i[hM(0x1f3)], i[hM(cy.h)])) { return e[p]; } else { return i[hM(0x2ab)](v, i[hM(0x1c9)](t, c, W)); } }; var u = [C[LT(RL.f)](D, t, t, t, t), C[LT(RL.v)](D, t, t, t, t), C[LT(RL.t)](D, t, t, t, t), C[LT(0x2a1)](D, t, t, t, t), C[LT(0x306)](D, t, t, t, t)]; var n = [E, function p(q) { return u[q]; } , function (q) { var hU = LT; if (C[hU(cD.h)]('cLzzq', C[hU(cD.b)])) { return C[hU(0x2bd)](D, t, m[hU(0x1d8)], q, v); } else { return f(v, t); } } , function (q) { return G(q); } , function (q) { var hK = LT; return i[hK(0x2c5)](D, t, R, S['d'][q], v); } , function (q) { var hP = LT; var I = { 'Ofenh': function (s, w, z, O, J) { var hT = L; return i[hT(0x1af)](s, w, z, O, J); }, 'AuyZZ': function (s, w, z) { return s(w, z); }, 'NIOdi': function (s, w, z) { return s(w, z); } }; if (i['BllIK'](i[hP(0x22e)], i[hP(ck.h)])) { I[hP(ck.b)](E, I['AuyZZ'](i, N, g) * I[hP(0x26b)](x, r, m), e, B, 0x0); return ++y; } else { return D(m[hP(0x1c1)], t, t, t); } } , function (q) { return D(t, S['d'], q, v); } , function (q) { var hp = LT; return C[hp(0x2a1)](D, m[hp(cd.h)], E, E, t); } , function (q) { var hq = LT; if (C['DGWOU'](C[hq(0x2ba)], C[hq(0x327)])) { return ++b; } else { return C[hq(cX.h)](D, t, e, q, t); } } ]; var Q = function (q, I) { var hI = LT; if (i[hI(0x27a)](i[hI(0x204)], 'ljzHB')) { return n[q] ? n[q](I) : i[hI(0x328)](D, t, t, t, t); } else { b['push']([]); } }; var Z = function (q, I) { var hs = LT; if (i[hs(0x2f9)](i['YsXRR'], i[hs(cu.h)])) { return i['kPfVf'](j, i['QmRpf'](Q, q, I)); } else { c = { '_sabo_3088c': W, '_sabo_6da36': R, '_sabo_5b836': S }; E = []; i++; } }; var V = function (q, I, s, w) { var hw = LT; if (C[hw(0x320)](C[hw(0x1f9)], C['wMDXH'])) { f['push'](v[0x0]); return ++t; } else { u[t] = C[hw(0x2b4)](D, q, I, s, w); } }; var M = function (q) { var hO = LT; var I = { 'xAbsm': function (O, J, F0, F1, F2) { return O(J, F0, F1, F2); }, 'hkMoL': function (O, J) { var hz = L; return C[hz(0x30d)](O, J); }, 'zMwEL': function (O, J, F0) { return O(J, F0); }, 'PlApi': function (O, J, F0) { return O(J, F0); } }; if (C[hO(0x315)](C[hO(0x1e0)], C[hO(0x25d)])) { var s = t; while (C['SROLQ'](s, q[hO(cU.h)])) { var w = q[s]; var z = P[w[t]]; s = C[hO(0x1be)](z, w[0x1], w[0x2], w[0x3], w[0x4], s, T, q); } } else { I['xAbsm'](E, I[hO(0x1c0)](I['zMwEL'](i, N, g), I[hO(cU.b)](x, r, m)), e, B, 0x0); return ++y; } }; var U = function (q, I, s, w) { var b0 = LT; var z = { 'boEzQ': function (F2, F3, F4) { var hJ = L; return C[hJ(0x282)](F2, F3, F4); } }; if (C['FrWRh'](C[b0(0x222)], b0(cT.h))) { var O = j(q); var J = j(I); if (C[b0(0x2af)](O, 0x7fffffff)) { if (C[b0(cT.b)](C[b0(0x28c)], C[b0(0x1de)])) { return s; } else { c = z['boEzQ'](W, R, S[b0(cT.C)]); if (E) { return N; } } } while (C['SROLQ'](O, J)) { if ('AsWrn' !== C[b0(0x22d)]) { var F0 = w[O]; var F1 = P[F0[t]]; O = C[b0(cT.f)](F1, F0[0x1], F0[0x2], F0[0x3], F0[0x4], O, T, w); } else { return b; } } return O; } else { var F5 = i[b0(0x2fa)](W, R, S); E(delete F5[b0(0x1fd)][F5['_sabo_95cb2']], i, N, 0x0); return ++g; } }; var K = function (q, I) { var b3 = LT; var s = { 'dsPCH': function (O, J) { var b1 = L; return i[b1(0x248)](O, J); }, 'GYyhS': function (O, J, F0) { var b2 = L; return i[b2(0x26c)](O, J, F0); } }; var w = N[b3(0x251)](i['sqlMq'](N[b3(0x230)], 0x6), 0x6); var z = i['hMwnk'](w[0x4][b3(0x1c4)], 0x7fffffff); try { if (i['lbZOY'](i[b3(0x335)], i[b3(0x295)])) { E(s['dsPCH'](s[b3(0x212)](i, N, g), x(r, m)), e, B, 0x0); return ++y; } else { q = U(w[0x0], w[0x1], q, I); } } catch (J) { u[0x2] = i['YqBZK'](D, J, t, t, t); q = i['XvVud'](U, w[0x2], w[0x3], q, I); u[0x2] = i[b3(cq.h)](D, t, t, t, t); } finally { q = i[b3(cq.b)](U, w[0x4], w[0x5], q, I); } return i[b3(0x336)](w[0x5]['_sabo_e2c0a'], q) ? w[0x5]['_sabo_e2c0a'] : q; }; var T = C[LT(0x347)](Y, S['b'])[LT(0x1b7)]('')[LT(0x289)](function (q, I) { var b4 = LT; if (!q[b4(0x230)] || C['YqyTm'](q[C[b4(0x21f)](q[b4(0x230)], v)][b4(0x230)], 0x5)) { if (C[b4(0x288)](C['dfxVY'], C['MBgMf'])) { S = E['_sabo_5b836'] ? i[b4(cI.h)] : N; g = x[b4(0x2c8)] ? r[b4(cI.b)] : m; e--; } else { q[b4(0x24d)]([]); } } q[C[b4(cI.C)](q[b4(0x230)], v)][b4(cI.f)](C[b4(0x346)](C[b4(cI.v)](-v, 0x1), I[b4(cI.t)]())); return q; }, []); var P = [function (q, I, s, w, z, O, J) { var b8 = LT; var F0 = { 'ibqQa': function (F5, F6, F7, F8, F9) { var b5 = L; return i[b5(0x240)](F5, F6, F7, F8, F9); }, 'KWsvb': function (F5, F6, F7) { var b6 = L; return i[b6(cw.h)](F5, F6, F7); }, 'DqXoc': function (F5) { var b7 = L; return i[b7(0x2ca)](F5); }, 'nlBkp': function (F5, F6) { return F5 < F6; } }; var F1 = Z(q, I); if (i[b8(0x1ee)](N[b8(cJ.h)], F1)) { if (i[b8(cJ.b)] !== i[b8(cJ.C)]) { return ++z; } else { F0[b8(0x30b)](E, F0['KWsvb'](i, N, g), x, r, 0x0); var F6 = F0['DqXoc'](m); while (F0[b8(0x317)](F6, e)) { Y(); } return y; } } var F2 = N[b8(0x251)](i[b8(0x309)](N[b8(0x230)], F1), F1)['map'](j) , F3 = N[b8(0x28e)]() , F4 = j(F3); F2[b8(cJ.f)](null); i[b8(0x2be)](V, new (Function[b8(cJ.v)][b8(cJ.t)][b8(0x1d5)](F4, F2))(), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var b9 = LT; C['WqSKX'](V, Z(q, I) & C[b9(W0.h)](Z, s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bF = LT; V(i[bF(W1.h)](i[bF(0x284)](Z, q, I), i['ijFaB'](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bL = LT; var F0 = C['lGvVL'](Q, q, I) , F1 = C[bL(W2.h)](C[bL(0x282)](Z, q, I), 0x1); F0[bL(W2.b)][F0['_sabo_95cb2']] = F1; V(F1, E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { throw N['pop'](); } , function (q, I, s, w, z, O, J) { var bh = LT; i[bh(0x2c5)](V, i['PVQkv'](i['QmRpf'](Z, q, I), i[bh(0x1c9)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bb = LT; i[bb(0x2b7)](V, i['vsGkJ'](Z, q, I) | i[bb(W5.h)](Z, s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { u[0x4] = g['pop'](); return ++z; } , function (q, I, s, w, z, O, J) { var bC = LT; if (i[bC(0x270)] === bC(W7.h)) { i[bC(0x2c5)](E, i[bC(0x261)](i, N, g) > i['ERHkq'](x, r, m), e, B, 0x0); return ++y; } else { i[bC(0x2be)](V, i[bC(0x1ba)](i[bC(0x310)](Z, q, I), i[bC(W7.b)](Z, s, w)), E, E, 0x0); return ++z; } } , function (q, I, s, w, z, O, J) { var bf = LT; if (i['BvKJu'](i[bf(0x225)], 'jgodB')) { V(i[bf(0x2a3)](Z, q, I), E, E, 0x0); var F0 = i['JbvFu'](d); while (i[bf(0x2d1)](F0, B)) { i[bf(0x1f5)](o); } return Infinity; } else { W(); i[bf(0x33d)](R, S, E, i, 0x0, 0x0); i['HozPp'](N); return g; } } , function (q, I, s, w, z, O, J) { var bv = LT; if (C['MRQGa'](C['iZTGz'], C[bv(0x290)])) { V(C[bv(0x1cd)](Z(q, I), C[bv(0x2a9)](Z, s, w)), E, E, 0x0); return ++z; } else { var F1 = R[S]; var F2 = E[F1[i]]; N = i['oxiGH'](F2, F1[0x1], F1[0x2], F1[0x3], F1[0x4], g, F1, r); } } , function (q, I, s, w, z, O, J) { return ++z; } , function (q, I, s, w, z, O, J) { var WL = { h: 0x2dc }; var bt = LT; var F0 = T[bt(Wh.h)](C[bt(0x285)](Z, q, I), C[bt(Wh.b)](C[bt(0x214)](Z, s, w), 0x1)) , F1 = x; C[bt(0x2b5)](V, function () { var bc = bt; m = { '_sabo_c097b': this || R, '_sabo_57ecb': m, '_sabo_10b1e': arguments, '_sabo_6da36': F1 }; M(F0); m = m[bc(0x354)]; return i[bc(WL.h)](j, u[0x0]); }, E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bW = LT; if (C[bW(0x22f)](C[bW(Wb.h)], C[bW(0x1e5)])) { return f[v] = t; } else { C[bW(0x2b4)](V, C[bW(0x2d7)](C[bW(0x282)](Z, q, I), Z(s, w)), E, E, 0x0); return ++z; } } , function (q, I, s, w, z, O, J) { var bR = LT; if (i[bR(0x2e8)](i[bR(WC.h)], i[bR(0x27e)])) { i['QnhQH'](V, ~i['GTnDt'](Z, q, I), E, E, 0x0); return ++z; } else { v[0x4] = t[i[bR(WC.b)](c[bR(WC.C)], 0x1)]; return ++W; } } , , function (q, I, s, w, z, O, J) { var bS = LT; g[bS(Wf.h)](u[0x0]); return ++z; } , function (q, I, s, w, z, O, J) { var bE = LT; C[bE(0x1f6)](a); C[bE(0x200)](k, m[bE(0x1e8)]); return ++z; } , function (q, I, s, w, z, O, J) { var bi = LT; return C[bi(Wt.h)](Z, q, I); } , function (q, I, s, w, z, O, J) { var bN = LT; if (bN(Wc.h) === C[bN(Wc.b)]) { i[bN(0x30e)](E, i[bN(0x310)](i, N, g) && x(r, m), e, B, 0x0); return ++y; } else { var F0 = C[bN(0x282)](Q, q, I) , F1 = Z(q, I); V(F1++, E, E, 0x0); F0[bN(0x1fd)][F0[bN(0x2a6)]] = F1; return ++z; } } , function (q, I, s, w, z, O, J) { var bx = LT; var F0 = { 'KjZAP': function (F1, F2, F3, F4, F5) { var bg = L; return C[bg(0x33b)](F1, F2, F3, F4, F5); } }; if (C[bx(WR.h)](C[bx(0x1b1)], bx(0x207))) { B[0x2] = F0[bx(0x1f2)](y, Y, D, j, l); A = F0[bx(WR.b)](G, o[0x2], k[0x3], a, d); X[0x2] = F0[bx(0x1f2)](H, u, n, Q, Z); } else { C[bx(0x28f)](V, C[bx(0x301)](C[bx(0x285)](Z, q, I), Z(s, w)), E, E, 0x0); return ++z; } } , function (q, I, s, w, z, O, J) { var br = LT; V(typeof C[br(WS.h)](Z, q, I), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bm = LT; C['HlDqZ'](V, C[bm(0x1fb)](C[bm(0x285)](Z, q, I), C[bm(WE.h)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var be = LT; if (C[be(0x321)](C['OMccf'], C[be(Wi.h)])) { throw b['pop'](); } else { V(C[be(0x348)](C['WIitv'](Z, q, I), C[be(0x1d7)](Z, s, w)), E, E, 0x0); return ++z; } } , function (q, I, s, w, z, O, J) { var bB = LT; var F0 = C[bB(0x1c6)](Z, q, I); if (C[bB(0x1d2)](N['length'], F0)) { return ++z; } var F1 = N[bB(0x251)](C['lHsxx'](N[bB(0x230)], F0), F0)[bB(0x2c9)](j) , F2 = N['pop']() , F3 = C['ckdoI'](j, F2); V(F3[bB(WN.h)](typeof F2['_sabo_c724'] == C[bB(0x203)] ? R : F2[bB(WN.b)], F1), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var by = LT; i[by(Wg.h)](V, 0x0, i[by(0x244)](j, Q(q, I)), Z(s, w), 0x1); return ++z; } , function (q, I, s, w, z, O, J) { var bY = LT; i[bY(0x34a)](o); return ++z; } , function (q, I, s, w, z, O, J) { var bD = LT; var F0 = C[bD(Wr.h)](Q, q, I) , F1 = C[bD(0x25c)](C[bD(0x1c3)](Z, q, I), 0x1); F0[bD(0x1fd)][F0[bD(Wr.b)]] = F1; C[bD(0x252)](V, F1, E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bj = LT; N[bj(0x24d)](u[0x0]); return ++z; } , function (q, I, s, w, z, O, J) { var bl = LT; C['IdiaC'](V, C['aLwwx'](C[bl(0x2f7)](Z, q, I), C[bl(0x1d6)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bA = LT; i[bA(0x33c)](o); i[bA(0x33d)](V, E, E, E, 0x0, 0x0); i[bA(WB.h)](d); return Infinity; } , function (q, I, s, w, z, O, J) { var bG = LT; V(C['DHUPP'](C[bG(0x34d)](Z, q, I), C[bG(0x239)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { return ++z; } , function (q, I, s, w, z, O, J) { var bo = LT; if (C[bo(0x30f)](C['uNwcj'], bo(0x2de))) { var F1 = i[bo(0x1f4)](E, i, N) , F2 = i[bo(WD.h)](g, x, r); i[bo(WD.b)](m, F2--, e, B, 0x0); F1[bo(WD.C)][F1['_sabo_95cb2']] = F2; return ++y; } else { return C[bo(WD.f)](K, z, J); } } , function (q, I, s, w, z, O, J) { x[I] = undefined; return ++z; } , function (q, I, s, w, z, O, J) { var bk = LT; C[bk(0x287)](V, C[bk(Wl.h)](C[bk(0x1d6)](Z, q, I), Z(s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var ba = LT; V(C['qfltV'](C[ba(0x1c3)](Z, q, I), C[ba(0x285)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bd = LT; i[bd(0x1b0)](V, {}, E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bX = LT; i[bX(0x2be)](V, !i[bX(0x2e1)](Z, q, I), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bH = LT; var F0 = C[bH(Wk.h)](Z, q, I) , F1 = {}; C['kSUUe'](V, C['yzhnN'](X, F0, F1), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bu = LT; V(C[bu(0x276)](C[bu(Wa.h)](Z, q, I), C[bu(Wa.b)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { V(i['twOao'](Z, q, I) >= i['QMMxd'](Z, s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bn = LT; i[bn(0x246)](V, i[bn(0x21c)](Z(q, I), i[bn(WX.h)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bQ = LT; if (i[bQ(WH.h)](i[bQ(0x24c)], 'grXOT')) { i['dMFQI'](V, -i[bQ(0x2ed)](Z, q, I), E, E, 0x0); return ++z; } else { return C[f]; } } , function (q, I, s, w, z, O, J) { var bZ = LT; if (i[bZ(0x1aa)](i[bZ(0x20c)], i[bZ(0x20c)])) { return !i[bZ(Wu.h)](j, u[0x0]) ? i['mXOEa'](Z, q, I) : ++z; } else { return b; } } , function (q, I, s, w, z, O, J) { var bV = LT; if (i[bV(0x2c0)]('CFtvf', i[bV(0x1d1)])) { u[0x3] = i[bV(0x1af)](D, N[bV(0x230)], 0x0, 0x0, 0x0); return ++z; } else { debugger; return ++b; } } , function (q, I, s, w, z, O, J) { var bM = LT; i[bM(WQ.h)](V, i[bM(0x1c9)](Z, q, I), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bU = LT; i[bU(WZ.h)](V, Z(q, I) && i[bU(WZ.b)](Z, s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bK = LT; if (C[bK(WV.h)](C[bK(WV.b)], bK(0x250))) { return ++b; } else { var F0 = C[bK(WV.C)](Q, q, I); C['FJREI'](V, delete F0['_sabo_c724'][F0[bK(WV.f)]], E, E, 0x0); return ++z; } } , function (q, I, s, w, z, O, J) { var bT = LT; var F0 = C[bT(0x1eb)](Z, q, I); C[bT(0x2ea)](V, N['splice'](C['ELHrH'](N[bT(0x230)], F0), F0)[bT(0x2c9)](j), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bP = LT; u[0x4] = g[i[bP(WU.h)](g[bP(0x230)], 0x1)]; return ++z; } , function (q, I, s, w, z, O, J) { var bp = LT; i['udbjY'](V, i[bp(0x337)](i[bp(0x34c)](Z, q, I), Z(s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bq = LT; var F0 = C['yMgIH'](Q, q, I) , F1 = C['vQqZP'](Z, q, I); C['ocRxu'](V, F1--, E, E, 0x0); F0['_sabo_c724'][F0[bq(0x2a6)]] = F1; return ++z; } , function (q, I, s, w, z, O, J) { return ++z; } , function (q, I, s, w, z, O, J) { var bI = LT; if (C[bI(0x258)](C['ABTJb'], C[bI(0x1e2)])) { u[0x1] = N[bI(0x28e)](); return ++z; } else { return i[bI(0x2d9)](f, v, t); } } , function (q, I, s, w, z, O, J) { u[0x0] = N[C['gPDFl'](N['length'], 0x1)]; return ++z; } , function (q, I, s, w, z, O, J) { return f; } , function (q, I, s, w, z, O, J) { var bs = LT; V(i[bs(0x1bf)](Z, q, I) || Z(s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bw = LT; V(+C[bw(0x343)](Z, q, I), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bz = LT; C['gQkLB'](V, C[bz(Wz.h)](C[bz(0x268)](Z, q, I), C[bz(Wz.b)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bO = LT; V(C[bO(0x231)](C['RRAzt'](Z, q, I), C[bO(WO.h)](Z, s, w)), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var bJ = LT; var F0 = { 'xRcPH': i[bJ(R5.h)], 'TYkNO': function (F1, F2) { return i['RjPbU'](F1, F2); }, 'bZSXv': function (F1, F2) { return F1 << F2; }, 'bOaOU': function (F1, F2) { return F1 << F2; }, 'vfvwF': function (F1, F2) { var C0 = bJ; return i[C0(0x2ec)](F1, F2); }, 'EUTZE': function (F1, F2) { return F1 != F2; }, 'UGTAB': function (F1, F2) { var C1 = bJ; return i[C1(0x271)](F1, F2); } }; if (i[bJ(0x2b3)](bJ(0x24b), i[bJ(R5.b)])) { return i['kPfVf'](j, u[0x0]) ? i[bJ(0x284)](Z, q, I) : ++z; } else { var F2 = F0[bJ(0x2f0)][bJ(0x1b7)]('|'); var F3 = 0x0; while (!![]) { switch (F2[F3++]) { case '0': var F4 = k[bJ(R5.C)](a[bJ(0x20d)](d++)); continue; case '1': var F5 = D[bJ(0x2cb)](j['charAt'](l++)); continue; case '2': var F6 = A['indexOf'](G[bJ(0x20d)](F4++)); continue; case '3': X[bJ(0x24d)](H[bJ(R5.f)](F8)); continue; case '4': var F7 = B[bJ(0x2cb)](y[bJ(R5.v)](Y++)); continue; case '5': var F8 = F0['TYkNO'](F0[bJ(0x345)](F7, 0x2), F5 >> 0x4); continue; case '6': var F9 = F0[bJ(R5.t)](F0[bJ(0x2e3)](F0['vfvwF'](F6, 0x3), 0x6), F4); continue; case '7': if (F0['EUTZE'](F4, 0x40)) { U['push'](K['fromCharCode'](F9)); } continue; case '8': if (F0[bJ(0x20b)](F6, 0x40)) { V[bJ(0x24d)](M[bJ(0x311)](FF)); } continue; case '9': var FF = F0[bJ(0x2e3)](F0[bJ(0x2f4)](F5, 0xf), 0x4) | F0[bJ(0x33f)](F6, 0x2); continue; } break; } } } , function (q, I, s, w, z, O, J) { var C2 = LT; var F0 = Z(q, I); i[C2(R6.h)](V, i[C2(0x244)](H, F0), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { debugger; return ++z; } , function (q, I, s, w, z, O, J) { var C3 = LT; i['dMFQI'](V, i[C3(0x2ed)](Z, q, I) < Z(s, w), E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var C4 = LT; var F0 = Q(q, I) , F1 = Z(s, w); C[C4(0x2ef)](V, F0[C4(0x1fd)][F0[C4(R9.h)]] = F1, E, E, 0x0); return ++z; } , function (q, I, s, w, z, O, J) { var C5 = LT; i[C5(RF.h)](k, null); return ++z; } ]; return C[LT(0x2cd)](M, T); } ; } ;b()(window, { 'b': h[FQ(Rb.e)], 'd': ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '$', '_', '[', ']', 0x4f, 0x6e2, 0x0, 0x6e3, 0x722, 0x723, 0x869, 0x953, 0xa2c, 0xa2d, 0xa39, 0xa3a, 0xa76, 0xa77, 0x14d5, 0x18bf, 0x1c17, 0x1c18, 0x1c90, 0x1c91, 0x1d08, 0x1d09, 0x1da2, 0x1da3, 0x1e27, 0x1e28, 0x1ed7, 0x1ed8, 0x1f86, 0x1f87, 0x1fe8, 0x1fe9, 0x2098, 0x2099, 0x20fe, 0x20ff, 0x21d5, 0x21d6, 0x221d, 0x221e, 0x228f, 0x2290, 0x22fd, 0x22fe, 0x23b2, 0x23b3, 0x2404, 0x2405, 0x2441, 0x2442, 0x248b, 0x248c, 0x2511, 0x2512, 0x2a3d, 0x2a3e, 0x2a7b, 0x2a7c, 0x2a9d, 0x2a9e, 0x2adb, 0x3287, 0x331f, 0x3320, 0x33ef, h[FQ(0x277)], 0x1, '', 0x2, ![], 0x341f, 0x3487, 0x3488, 0x34f0, 0x34f1, 0x35ae, null, 0x40, 0x9, 0x86a, 0x952, '+', '/', '=', 0x135, 0x7c, 0x93, 0xaa, 0xd1, 0x3, 0x4, 0xf, 0x6, 0x3f, FQ(Rb.B), 0xf4, 0xff, 0x6f, 0xd7, 0x43, 0x4b, 0x80, 0x64, 0xd3, 0x7f, 0x67, 0x800, 0x94, 0xc0, 0xc, 0xe0, 0x17, 0x1d, 0x59, 0x56, 0x37, 0x55, 0x7fffffff, 0x9c, 0x95, h[FQ(0x217)], '\x20', ':', h[FQ(0x307)], 0xac, 0xb6, '-', 0x15, 0x39, 0x2c, 0x2f, 0x35, 0x19, !![], 0x14d6, 0x14e1, 0x14e2, 0x1539, 0x153a, 0x1573, 0x1574, 0x1771, 0x1772, 0x17c4, 0x17c5, 0x17cc, 0x63, 0x77, 0x7b, 0xf2, 0x6b, 0xc5, 0x30, 0x2b, 0xfe, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0xa4, 0x72, 0xb7, 0xfd, 0x26, 0x36, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x5, 0x9a, 0x7, 0x12, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x83, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x84, 0x53, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xfb, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xda, 0x21, 0x10, 0xf3, 0xd2, 0xcd, 0x13, 0xec, 0x5f, 0x97, 0x44, 0xc4, 0xa7, 0x7e, 0x3d, 0x5d, 0x73, 0x60, 0x81, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0xb, 0xdb, 0x32, 0x3a, 0xa, 0x49, 0x24, 0x5c, 0xc2, 0x62, 0x91, 0xe4, 0x79, 0xe7, 0xc8, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0xea, 0x65, 0x7a, 0xae, 0x8, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0xf6, 0xe, 0x61, 0x57, 0xb9, 0x86, 0xc1, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0xd, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0xb0, 0x54, 0xbb, 0x16, 0x100, 0xc66363a5, 0xf87c7c84, 0xee777799, 0xf67b7b8d, 0xfff2f20d, 0xd66b6bbd, 0xde6f6fb1, 0x91c5c554, 0x60303050, 0x2010103, 0xce6767a9, 0x562b2b7d, 0xe7fefe19, 0xb5d7d762, 0x4dababe6, 0xec76769a, 0x8fcaca45, 0x1f82829d, 0x89c9c940, 0xfa7d7d87, 0xeffafa15, 0xb25959eb, 0x8e4747c9, 0xfbf0f00b, 0x41adadec, 0xb3d4d467, 0x5fa2a2fd, 0x45afafea, 0x239c9cbf, 0x53a4a4f7, 0xe4727296, 0x9bc0c05b, 0x75b7b7c2, 0xe1fdfd1c, 0x3d9393ae, 0x4c26266a, 0x6c36365a, 0x7e3f3f41, 0xf5f7f702, 0x83cccc4f, 0x6834345c, 0x51a5a5f4, 0xd1e5e534, 0xf9f1f108, 0xe2717193, 0xabd8d873, 0x62313153, 0x2a15153f, 0x804040c, 0x95c7c752, 0x46232365, 0x9dc3c35e, 0x30181828, 0x379696a1, 0xa05050f, 0x2f9a9ab5, 0xe070709, 0x24121236, 0x1b80809b, 0xdfe2e23d, 0xcdebeb26, 0x4e272769, 0x7fb2b2cd, 0xea75759f, 0x1209091b, 0x1d83839e, 0x582c2c74, 0x341a1a2e, 0x361b1b2d, 0xdc6e6eb2, 0xb45a5aee, 0x5ba0a0fb, 0xa45252f6, 0x763b3b4d, 0xb7d6d661, 0x7db3b3ce, 0x5229297b, 0xdde3e33e, 0x5e2f2f71, 0x13848497, 0xa65353f5, 0xb9d1d168, 0xc1eded2c, 0x40202060, 0xe3fcfc1f, 0x79b1b1c8, 0xb65b5bed, 0xd46a6abe, 0x8dcbcb46, 0x67bebed9, 0x7239394b, 0x944a4ade, 0x984c4cd4, 0xb05858e8, 0x85cfcf4a, 0xbbd0d06b, 0xc5efef2a, 0x4faaaae5, 0xedfbfb16, 0x864343c5, 0x9a4d4dd7, 0x66333355, 0x11858594, 0x8a4545cf, 0xe9f9f910, 0x4020206, 0xfe7f7f81, 0xa05050f0, 0x783c3c44, 0x259f9fba, 0x4ba8a8e3, 0xa25151f3, 0x5da3a3fe, 0x804040c0, 0x58f8f8a, 0x3f9292ad, 0x219d9dbc, 0x70383848, 0xf1f5f504, 0x63bcbcdf, 0x77b6b6c1, 0xafdada75, 0x42212163, 0x20101030, 0xe5ffff1a, 0xfdf3f30e, 0xbfd2d26d, 0x81cdcd4c, 0x180c0c14, 0x26131335, 0xc3ecec2f, 0xbe5f5fe1, 0x359797a2, 0x884444cc, 0x2e171739, 0x93c4c457, 0x55a7a7f2, 0xfc7e7e82, 0x7a3d3d47, 0xc86464ac, 0xba5d5de7, 0x3219192b, 0xe6737395, 0xc06060a0, 0x19818198, 0x9e4f4fd1, 0xa3dcdc7f, 0x44222266, 0x542a2a7e, 0x3b9090ab, 0xb888883, 0x8c4646ca, 0xc7eeee29, 0x6bb8b8d3, 0x2814143c, 0xa7dede79, 0xbc5e5ee2, 0x160b0b1d, 0xaddbdb76, 0xdbe0e03b, 0x64323256, 0x743a3a4e, 0x140a0a1e, 0x924949db, 0xc06060a, 0x4824246c, 0xb85c5ce4, 0x9fc2c25d, 0xbdd3d36e, 0x43acacef, 0xc46262a6, 0x399191a8, 0x319595a4, 0xd3e4e437, 0xf279798b, 0xd5e7e732, 0x8bc8c843, 0x6e373759, 0xda6d6db7, 0x18d8d8c, 0xb1d5d564, 0x9c4e4ed2, 0x49a9a9e0, 0xd86c6cb4, 0xac5656fa, 0xf3f4f407, 0xcfeaea25, 0xca6565af, 0xf47a7a8e, 0x47aeaee9, 0x10080818, 0x6fbabad5, 0xf0787888, 0x4a25256f, 0x5c2e2e72, 0x381c1c24, 0x57a6a6f1, 0x73b4b4c7, 0x97c6c651, 0xcbe8e823, 0xa1dddd7c, 0xe874749c, 0x3e1f1f21, 0x964b4bdd, 0x61bdbddc, 0xd8b8b86, 0xf8a8a85, 0xe0707090, 0x7c3e3e42, 0x71b5b5c4, 0xcc6666aa, 0x904848d8, 0x6030305, 0xf7f6f601, 0x1c0e0e12, 0xc26161a3, 0x6a35355f, 0xae5757f9, 0x69b9b9d0, 0x17868691, 0x99c1c158, 0x3a1d1d27, 0x279e9eb9, 0xd9e1e138, 0xebf8f813, 0x2b9898b3, 0x22111133, 0xd26969bb, 0xa9d9d970, 0x78e8e89, 0x339494a7, 0x2d9b9bb6, 0x3c1e1e22, 0x15878792, 0xc9e9e920, 0x87cece49, 0xaa5555ff, 0x50282878, 0xa5dfdf7a, 0x38c8c8f, 0x59a1a1f8, 0x9898980, 0x1a0d0d17, 0x65bfbfda, 0xd7e6e631, 0x844242c6, 0xd06868b8, 0x824141c3, 0x299999b0, 0x5a2d2d77, 0x1e0f0f11, 0x7bb0b0cb, 0xa85454fc, 0x6dbbbbd6, 0x2c16163a, 0xa5c66363, 0x84f87c7c, 0x99ee7777, 0x8df67b7b, 0xdfff2f2, 0xbdd66b6b, 0xb1de6f6f, 0x5491c5c5, 0x50603030, 0x3020101, 0xa9ce6767, 0x7d562b2b, 0x19e7fefe, 0x62b5d7d7, 0xe64dabab, 0x9aec7676, 0x458fcaca, 0x9d1f8282, 0x4089c9c9, 0x87fa7d7d, 0x15effafa, 0xebb25959, 0xc98e4747, 0xbfbf0f0, 0xec41adad, 0x67b3d4d4, 0xfd5fa2a2, 0xea45afaf, 0xbf239c9c, 0xf753a4a4, 0x96e47272, 0x5b9bc0c0, 0xc275b7b7, 0x1ce1fdfd, 0xae3d9393, 0x6a4c2626, 0x5a6c3636, 0x417e3f3f, 0x2f5f7f7, 0x4f83cccc, 0x5c683434, 0xf451a5a5, 0x34d1e5e5, 0x8f9f1f1, 0x93e27171, 0x73abd8d8, 0x53623131, 0x3f2a1515, 0xc080404, 0x5295c7c7, 0x65462323, 0x5e9dc3c3, 0x28301818, 0xa1379696, 0xf0a0505, 0xb52f9a9a, 0x90e0707, 0x36241212, 0x9b1b8080, 0x3ddfe2e2, 0x26cdebeb, 0x694e2727, 0xcd7fb2b2, 0x9fea7575, 0x1b120909, 0x9e1d8383, 0x74582c2c, 0x2e341a1a, 0x2d361b1b, 0xb2dc6e6e, 0xeeb45a5a, 0xfb5ba0a0, 0xf6a45252, 0x4d763b3b, 0x61b7d6d6, 0xce7db3b3, 0x7b522929, 0x3edde3e3, 0x715e2f2f, 0x97138484, 0xf5a65353, 0x68b9d1d1, 0x2cc1eded, 0x60402020, 0x1fe3fcfc, 0xc879b1b1, 0xedb65b5b, 0xbed46a6a, 0x468dcbcb, 0xd967bebe, 0x4b723939, 0xde944a4a, 0xd4984c4c, 0xe8b05858, 0x4a85cfcf, 0x6bbbd0d0, 0x2ac5efef, 0xe54faaaa, 0x16edfbfb, 0xc5864343, 0xd79a4d4d, 0x55663333, 0x94118585, 0xcf8a4545, 0x10e9f9f9, 0x6040202, 0x81fe7f7f, 0xf0a05050, 0x44783c3c, 0xba259f9f, 0xe34ba8a8, 0xf3a25151, 0xfe5da3a3, 0xc0804040, 0x8a058f8f, 0xad3f9292, 0xbc219d9d, 0x48703838, 0x4f1f5f5, 0xdf63bcbc, 0xc177b6b6, 0x75afdada, 0x63422121, 0x30201010, 0x1ae5ffff, 0xefdf3f3, 0x6dbfd2d2, 0x4c81cdcd, 0x14180c0c, 0x35261313, 0x2fc3ecec, 0xe1be5f5f, 0xa2359797, 0xcc884444, 0x392e1717, 0x5793c4c4, 0xf255a7a7, 0x82fc7e7e, 0x477a3d3d, 0xacc86464, 0xe7ba5d5d, 0x2b321919, 0x95e67373, 0xa0c06060, 0x98198181, 0xd19e4f4f, 0x7fa3dcdc, 0x66442222, 0x7e542a2a, 0xab3b9090, 0x830b8888, 0xca8c4646, 0x29c7eeee, 0xd36bb8b8, 0x3c281414, 0x79a7dede, 0xe2bc5e5e, 0x1d160b0b, 0x76addbdb, 0x3bdbe0e0, 0x56643232, 0x4e743a3a, 0x1e140a0a, 0xdb924949, 0xa0c0606, 0x6c482424, 0xe4b85c5c, 0x5d9fc2c2, 0x6ebdd3d3, 0xef43acac, 0xa6c46262, 0xa8399191, 0xa4319595, 0x37d3e4e4, 0x8bf27979, 0x32d5e7e7, 0x438bc8c8, 0x596e3737, 0xb7da6d6d, 0x8c018d8d, 0x64b1d5d5, 0xd29c4e4e, 0xe049a9a9, 0xb4d86c6c, 0xfaac5656, 0x7f3f4f4, 0x25cfeaea, 0xafca6565, 0x8ef47a7a, 0xe947aeae, 0x18100808, 0xd56fbaba, 0x88f07878, 0x6f4a2525, 0x725c2e2e, 0x24381c1c, 0xf157a6a6, 0xc773b4b4, 0x5197c6c6, 0x23cbe8e8, 0x7ca1dddd, 0x9ce87474, 0x213e1f1f, 0xdd964b4b, 0xdc61bdbd, 0x860d8b8b, 0x850f8a8a, 0x90e07070, 0x427c3e3e, 0xc471b5b5, 0xaacc6666, 0xd8904848, 0x5060303, 0x1f7f6f6, 0x121c0e0e, 0xa3c26161, 0x5f6a3535, 0xf9ae5757, 0xd069b9b9, 0x91178686, 0x5899c1c1, 0x273a1d1d, 0xb9279e9e, 0x38d9e1e1, 0x13ebf8f8, 0xb32b9898, 0x33221111, 0xbbd26969, 0x70a9d9d9, 0x89078e8e, 0xa7339494, 0xb62d9b9b, 0x223c1e1e, 0x92158787, 0x20c9e9e9, 0x4987cece, 0xffaa5555, 0x78502828, 0x7aa5dfdf, 0x8f038c8c, 0xf859a1a1, 0x80098989, 0x171a0d0d, 0xda65bfbf, 0x31d7e6e6, 0xc6844242, 0xb8d06868, 0xc3824141, 0xb0299999, 0x775a2d2d, 0x111e0f0f, 0xcb7bb0b0, 0xfca85454, 0xd66dbbbb, 0x3a2c1616, 0x63a5c663, 0x7c84f87c, 0x7799ee77, 0x7b8df67b, 0xf20dfff2, 0x6bbdd66b, 0x6fb1de6f, 0xc55491c5, 0x30506030, 0x1030201, 0x67a9ce67, 0x2b7d562b, 0xfe19e7fe, 0xd762b5d7, 0xabe64dab, 0x769aec76, 0xca458fca, 0x829d1f82, 0xc94089c9, 0x7d87fa7d, 0xfa15effa, 0x59ebb259, 0x47c98e47, 0xf00bfbf0, 0xadec41ad, 0xd467b3d4, 0xa2fd5fa2, 0xafea45af, 0x9cbf239c, 0xa4f753a4, 0x7296e472, 0xc05b9bc0, 0xb7c275b7, 0xfd1ce1fd, 0x93ae3d93, 0x266a4c26, 0x365a6c36, 0x3f417e3f, 0xf702f5f7, 0xcc4f83cc, 0x345c6834, 0xa5f451a5, 0xe534d1e5, 0xf108f9f1, 0x7193e271, 0xd873abd8, 0x31536231, 0x153f2a15, 0x40c0804, 0xc75295c7, 0x23654623, 0xc35e9dc3, 0x18283018, 0x96a13796, 0x50f0a05, 0x9ab52f9a, 0x7090e07, 0x12362412, 0x809b1b80, 0xe23ddfe2, 0xeb26cdeb, 0x27694e27, 0xb2cd7fb2, 0x759fea75, 0x91b1209, 0x839e1d83, 0x2c74582c, 0x1a2e341a, 0x1b2d361b, 0x6eb2dc6e, 0x5aeeb45a, 0xa0fb5ba0, 0x52f6a452, 0x3b4d763b, 0xd661b7d6, 0xb3ce7db3, 0x297b5229, 0xe33edde3, 0x2f715e2f, 0x84971384, 0x53f5a653, 0xd168b9d1, 0xed2cc1ed, 0x20604020, 0xfc1fe3fc, 0xb1c879b1, 0x5bedb65b, 0x6abed46a, 0xcb468dcb, 0xbed967be, 0x394b7239, 0x4ade944a, 0x4cd4984c, 0x58e8b058, 0xcf4a85cf, 0xd06bbbd0, 0xef2ac5ef, 0xaae54faa, 0xfb16edfb, 0x43c58643, 0x4dd79a4d, 0x33556633, 0x85941185, 0x45cf8a45, 0xf910e9f9, 0x2060402, 0x7f81fe7f, 0x50f0a050, 0x3c44783c, 0x9fba259f, 0xa8e34ba8, 0x51f3a251, 0xa3fe5da3, 0x40c08040, 0x8f8a058f, 0x92ad3f92, 0x9dbc219d, 0x38487038, 0xf504f1f5, 0xbcdf63bc, 0xb6c177b6, 0xda75afda, 0x21634221, 0x10302010, 0xff1ae5ff, 0xf30efdf3, 0xd26dbfd2, 0xcd4c81cd, 0xc14180c, 0x13352613, 0xec2fc3ec, 0x5fe1be5f, 0x97a23597, 0x44cc8844, 0x17392e17, 0xc45793c4, 0xa7f255a7, 0x7e82fc7e, 0x3d477a3d, 0x64acc864, 0x5de7ba5d, 0x192b3219, 0x7395e673, 0x60a0c060, 0x81981981, 0x4fd19e4f, 0xdc7fa3dc, 0x22664422, 0x2a7e542a, 0x90ab3b90, 0x88830b88, 0x46ca8c46, 0xee29c7ee, 0xb8d36bb8, 0x143c2814, 0xde79a7de, 0x5ee2bc5e, 0xb1d160b, 0xdb76addb, 0xe03bdbe0, 0x32566432, 0x3a4e743a, 0xa1e140a, 0x49db9249, 0x60a0c06, 0x246c4824, 0x5ce4b85c, 0xc25d9fc2, 0xd36ebdd3, 0xacef43ac, 0x62a6c462, 0x91a83991, 0x95a43195, 0xe437d3e4, 0x798bf279, 0xe732d5e7, 0xc8438bc8, 0x37596e37, 0x6db7da6d, 0x8d8c018d, 0xd564b1d5, 0x4ed29c4e, 0xa9e049a9, 0x6cb4d86c, 0x56faac56, 0xf407f3f4, 0xea25cfea, 0x65afca65, 0x7a8ef47a, 0xaee947ae, 0x8181008, 0xbad56fba, 0x7888f078, 0x256f4a25, 0x2e725c2e, 0x1c24381c, 0xa6f157a6, 0xb4c773b4, 0xc65197c6, 0xe823cbe8, 0xdd7ca1dd, 0x749ce874, 0x1f213e1f, 0x4bdd964b, 0xbddc61bd, 0x8b860d8b, 0x8a850f8a, 0x7090e070, 0x3e427c3e, 0xb5c471b5, 0x66aacc66, 0x48d89048, 0x3050603, 0xf601f7f6, 0xe121c0e, 0x61a3c261, 0x355f6a35, 0x57f9ae57, 0xb9d069b9, 0x86911786, 0xc15899c1, 0x1d273a1d, 0x9eb9279e, 0xe138d9e1, 0xf813ebf8, 0x98b32b98, 0x11332211, 0x69bbd269, 0xd970a9d9, 0x8e89078e, 0x94a73394, 0x9bb62d9b, 0x1e223c1e, 0x87921587, 0xe920c9e9, 0xce4987ce, 0x55ffaa55, 0x28785028, 0xdf7aa5df, 0x8c8f038c, 0xa1f859a1, 0x89800989, 0xd171a0d, 0xbfda65bf, 0xe631d7e6, 0x42c68442, 0x68b8d068, 0x41c38241, 0x99b02999, 0x2d775a2d, 0xf111e0f, 0xb0cb7bb0, 0x54fca854, 0xbbd66dbb, 0x163a2c16, 0x6363a5c6, 0x7c7c84f8, 0x777799ee, 0x7b7b8df6, 0xf2f20dff, 0x6b6bbdd6, 0x6f6fb1de, 0xc5c55491, 0x30305060, 0x1010302, 0x6767a9ce, 0x2b2b7d56, 0xfefe19e7, 0xd7d762b5, 0xababe64d, 0x76769aec, 0xcaca458f, 0x82829d1f, 0xc9c94089, 0x7d7d87fa, 0xfafa15ef, 0x5959ebb2, 0x4747c98e, 0xf0f00bfb, 0xadadec41, 0xd4d467b3, 0xa2a2fd5f, 0xafafea45, 0x9c9cbf23, 0xa4a4f753, 0x727296e4, 0xc0c05b9b, 0xb7b7c275, 0xfdfd1ce1, 0x9393ae3d, 0x26266a4c, 0x36365a6c, 0x3f3f417e, 0xf7f702f5, 0xcccc4f83, 0x34345c68, 0xa5a5f451, 0xe5e534d1, 0xf1f108f9, 0x717193e2, 0xd8d873ab, 0x31315362, 0x15153f2a, 0x4040c08, 0xc7c75295, 0x23236546, 0xc3c35e9d, 0x18182830, 0x9696a137, 0x5050f0a, 0x9a9ab52f, 0x707090e, 0x12123624, 0x80809b1b, 0xe2e23ddf, 0xebeb26cd, 0x2727694e, 0xb2b2cd7f, 0x75759fea, 0x9091b12, 0x83839e1d, 0x2c2c7458, 0x1a1a2e34, 0x1b1b2d36, 0x6e6eb2dc, 0x5a5aeeb4, 0xa0a0fb5b, 0x5252f6a4, 0x3b3b4d76, 0xd6d661b7, 0xb3b3ce7d, 0x29297b52, 0xe3e33edd, 0x2f2f715e, 0x84849713, 0x5353f5a6, 0xd1d168b9, 0xeded2cc1, 0x20206040, 0xfcfc1fe3, 0xb1b1c879, 0x5b5bedb6, 0x6a6abed4, 0xcbcb468d, 0xbebed967, 0x39394b72, 0x4a4ade94, 0x4c4cd498, 0x5858e8b0, 0xcfcf4a85, 0xd0d06bbb, 0xefef2ac5, 0xaaaae54f, 0xfbfb16ed, 0x4343c586, 0x4d4dd79a, 0x33335566, 0x85859411, 0x4545cf8a, 0xf9f910e9, 0x2020604, 0x7f7f81fe, 0x5050f0a0, 0x3c3c4478, 0x9f9fba25, 0xa8a8e34b, 0x5151f3a2, 0xa3a3fe5d, 0x4040c080, 0x8f8f8a05, 0x9292ad3f, 0x9d9dbc21, 0x38384870, 0xf5f504f1, 0xbcbcdf63, 0xb6b6c177, 0xdada75af, 0x21216342, 0x10103020, 0xffff1ae5, 0xf3f30efd, 0xd2d26dbf, 0xcdcd4c81, 0xc0c1418, 0x13133526, 0xecec2fc3, 0x5f5fe1be, 0x9797a235, 0x4444cc88, 0x1717392e, 0xc4c45793, 0xa7a7f255, 0x7e7e82fc, 0x3d3d477a, 0x6464acc8, 0x5d5de7ba, 0x19192b32, 0x737395e6, 0x6060a0c0, 0x81819819, 0x4f4fd19e, 0xdcdc7fa3, 0x22226644, 0x2a2a7e54, 0x9090ab3b, 0x8888830b, 0x4646ca8c, 0xeeee29c7, 0xb8b8d36b, 0x14143c28, 0xdede79a7, 0x5e5ee2bc, 0xb0b1d16, 0xdbdb76ad, 0xe0e03bdb, 0x32325664, 0x3a3a4e74, 0xa0a1e14, 0x4949db92, 0x6060a0c, 0x24246c48, 0x5c5ce4b8, 0xc2c25d9f, 0xd3d36ebd, 0xacacef43, 0x6262a6c4, 0x9191a839, 0x9595a431, 0xe4e437d3, 0x79798bf2, 0xe7e732d5, 0xc8c8438b, 0x3737596e, 0x6d6db7da, 0x8d8d8c01, 0xd5d564b1, 0x4e4ed29c, 0xa9a9e049, 0x6c6cb4d8, 0x5656faac, 0xf4f407f3, 0xeaea25cf, 0x6565afca, 0x7a7a8ef4, 0xaeaee947, 0x8081810, 0xbabad56f, 0x787888f0, 0x25256f4a, 0x2e2e725c, 0x1c1c2438, 0xa6a6f157, 0xb4b4c773, 0xc6c65197, 0xe8e823cb, 0xdddd7ca1, 0x74749ce8, 0x1f1f213e, 0x4b4bdd96, 0xbdbddc61, 0x8b8b860d, 0x8a8a850f, 0x707090e0, 0x3e3e427c, 0xb5b5c471, 0x6666aacc, 0x4848d890, 0x3030506, 0xf6f601f7, 0xe0e121c, 0x6161a3c2, 0x35355f6a, 0x5757f9ae, 0xb9b9d069, 0x86869117, 0xc1c15899, 0x1d1d273a, 0x9e9eb927, 0xe1e138d9, 0xf8f813eb, 0x9898b32b, 0x11113322, 0x6969bbd2, 0xd9d970a9, 0x8e8e8907, 0x9494a733, 0x9b9bb62d, 0x1e1e223c, 0x87879215, 0xe9e920c9, 0xcece4987, 0x5555ffaa, 0x28287850, 0xdfdf7aa5, 0x8c8c8f03, 0xa1a1f859, 0x89898009, 0xd0d171a, 0xbfbfda65, 0xe6e631d7, 0x4242c684, 0x6868b8d0, 0x4141c382, 0x9999b029, 0x2d2d775a, 0xf0f111e, 0xb0b0cb7b, 0x5454fca8, 0xbbbbd66d, 0x16163a2c, 0x1886, 0x18be, 0x5c7389a, 0x7a15a75b, 0x63a31db5, 0x18ef9a44, 0x37f40bae, 0x569067cf, 0x60f11eff, 0x57c12f89, 0x4de1acf5, 0x1b71cb3a, 0x7b80d5c5, 0x2c41fa4c, 0x7c2d582f, 0x5d64804d, 0x18b06e2c, 0x2ddce040, 0x39e517e, 0x565c35fb, 0x2fdfab32, 0x5c413cd5, 0x5fdf6dab, 0x9835852, 0x265cf364, 0x4c5c4621, 0x7d818408, 0x21a0e53e, 0x5201db5e, 0x67be1727, 0x51bfeedd, 0x583f0105, 0x288bc1d1, 0x1e21f31e, 0x2b35f452, 0x3f811624, 0x79342f0c, 0x1d340dac, 0x3601f9fa, 0x980efda, 0x70b4c0d6, 0x3dbe72fe, 0xf2fff90, 0x75da9be3, 0x76fe3e37, 0x35337a7c, 0x2e42b142, 0x55c26485, 0x79839ec9, 0x32a45bae, 0x53c037cf, 0x66a14eff, 0x51917f89, 0x4e890381, 0x1d493450, 0x7be87ab1, 0x2a79053a, 0xea4b784, 0x13ed83cc, 0x6805f97d, 0x427cfc45, 0x1614d9b0, 0x5f95a64, 0x6dfca31f, 0x2f805f5a, 0x34249845, 0x31ddc227, 0x5c21613a, 0x73a13e64, 0x76bfe2e0, 0x29608f4b, 0x20e3d71b, 0x6bf2479, 0x4895834c, 0x3514074c, 0x14b4e272, 0x46b53930, 0x68873e04, 0x7f6ee5c0, 0x4864e91f, 0x79d1c1b9, 0x4dff68b2, 0x32918d72, 0x7af5646d, 0x324a5d6, 0x3e89548f, 0x17e9dbc4, 0x370a0cdf, 0x31b528a8, 0x13c, 0x126, 0x19f, 0x192, 0x187, 0x189, 0x148, 0x140, 0x1fa, 0x1ad, 0x17cd, 0x1885, 0x335, 0x334, 0x357, 0x15c, 0x158, 0x143, 0x144, 0x157, '|', 0x125, 0x1f6, ';', 0x1ce, 0x288, '{', '}', h['ZkQes'], '.', '\x22', '\x27', ',', 0x190, '*', 0.02, '!', 0x526, 0x525, 0x528, 0x114, 0x132, 0x15a, 0x180, 0x1a5, 0x1d2, 0x1fb, 0x222, 0x247, 0x26b, 0x292, 0x2b5, 0x2db, 0x301, 0x31c, 0x331, 0x35e, 0x364, 0x391, 0x3bf, 0x3ea, '\x5c', 0x406, 0x42b, 0x403, 0x487, 0x460, 0x468, 0x47b, 0x483, 0x441, 0x497, 0x4cd, 0x4f4, 0x51f, 0x2adc, 0x2afd, 0x2afe, 0x2b0c, 0x2b0d, 0x2b3c, 0x2b3d, 0x2b63, 0x2b64, 0x2b8a, 0x2b8b, 0x2bad, 0x2bae, 0x2bd1, 0x2bd2, 0x3144, 0x3145, 0x3180, 0x3181, 0x31e2, 0x31e3, 0x31fe, 0x31ff, 0x325f, 0x3260, 0x3276, 0x3277, 0x3286, 0xffff, 0x67452301, 0x10325477, 0x67452302, 0x10325476, 0x567, 0x28955b88, 0x173848aa, 0x242070db, 0x3e423112, 0xa83f051, 0x4787c62a, 0x57cfb9ed, 0x2b96aff, 0x698098d8, 0x74bb0851, 0xa44f, 0x76a32842, 0x6b901122, 0x2678e6d, 0x5986bc72, 0x49b40821, 0x9e1da9e, 0x3fbf4cc0, 0x265e5a51, 0x16493856, 0x29d0efa3, 0x2441453, 0x275e197f, 0x182c0438, 0x21e1cde6, 0x3cc8f82a, 0xb2af279, 0x455a14ed, 0x561c16fb, 0x3105c08, 0x676f02d9, 0x72d5b376, 0x5c6be, 0x788e097f, 0x6d9d6122, 0x21ac7f4, 0x5b4115bc, 0x4bdecfa9, 0x944b4a0, 0x41404390, 0x289b7ec6, 0x155ed806, 0x2b10cf7b, 0x4881d05, 0x262b2fc7, 0x1924661b, 0x1fa27cf8, 0x3b53a99b, 0xbd6ddbc, 0x432aff97, 0x546bdc59, 0x36c5fc7, 0x655b59c3, 0x70f3336e, 0x100b83, 0x7a7ba22f, 0x6fa87e4f, 0x1d31920, 0x5cfebcec, 0x4e0811a1, 0x8ac817e, 0x42c50dcb, 0x2ad7d2bb, 0x14792c6f, h[FQ(Rb.y)], 0x33f0, 0x33fb, 0x33fc, 0x3416, 0x3417, 0x341e, h[FQ(0x257)]] }); }()); ================================================ FILE: static/Release_Notes.md ================================================ **项目更新内容:** 1. 修复 CLI 模式 KeyError 异常 2. 修复控制字符导致程序报错的问题 3. 重构项目内置请求延时机制 ***** **用户脚本更新内容:** **版本号:2.3.1** ================================================ FILE: static/XHS-Downloader.js ================================================ // ==UserScript== // @name XHS-Downloader // @namespace xhs_downloader // @homepage https://github.com/JoeanAmier/XHS-Downloader // @version 2.3.1 // @tag 小红书 // @tag RedNote // @tag XiaoHongShu // @description 提取小红书作品/用户链接,下载小红书图文/视频作品文件 // @description:en Extract RedNote works/user links, Download images/videos files // @author JoeanAmier // @match http*://www.xiaohongshu.com/explore* // @match http*://www.xiaohongshu.com/discovery/item/* // @match http*://www.xiaohongshu.com/user/profile/* // @match http*://www.xiaohongshu.com/search_result* // @match http*://www.xiaohongshu.com/board/* // @icon64 data:image/png;base64,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 // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @grant GM_setClipboard // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @license GNU General Public License v3.0 // @run-at document-end // @updateURL https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js // @downloadURL https://raw.githubusercontent.com/JoeanAmier/XHS-Downloader/master/static/XHS-Downloader.js // @supportURL https://github.com/JoeanAmier/XHS-Downloader/issues // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // ==/UserScript== (function () { 'use strict'; const i18n = { 'CN': { instructionsText: `功能清单: 1. 下载小红书作品文件 2. 提取推荐页面作品链接 3. 提取账号发布作品链接 4. 提取账号收藏作品链接 5. 提取账号专辑作品链接 6. 提取账号点赞作品链接 7. 提取搜索结果作品链接 8. 提取搜索结果用户链接 注意事项: 1. 下载小红书作品文件时,脚本需要花费时间处理文件,请等待片刻,请勿多次点击下载按钮 2. 提取账号发布、收藏、点赞、专辑作品链接时,脚本可以自动滚动页面直至加载全部作品 3. 提取推荐作品链接、搜索作品、用户链接时,脚本可以自动滚动指定次数加载更多内容,默认滚动次数:50 次 4. 自动滚动页面功能默认关闭;用户可以自由开启,并修改滚动页面次数,修改后立即生效 5. 如果未开启自动滚动页面功能,用户需要手动滚动页面以便加载更多内容后再进行其他操作 6. 支持作品文件打包下载;该功能默认开启,多个文件的作品将会以压缩包格式下载 7. 向服务器推送下载任务时,文件格式、名称规则等设置以服务器配置文件中的设置为准 8. 使用全局代理工具可能会导致脚本下载文件失败,如有异常,请尝试关闭代理工具,必要时向作者反馈 9. XHS-Downloader 用户脚本仅实现可见即可得的数据采集功能,无任何收费功能和破解功能 项目开源地址:https://github.com/JoeanAmier/XHS-Downloader `, disclaimerText: `1. 使用者对本项目的使用由使用者自行决定,并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。 2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性,但不保证代码完全没有错误或缺陷。 3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可,使用者需自行查阅并遵守相应协议,作者不对第三方组件的稳定性、安全性及合规性承担任何责任。 4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求,并在适当的地方注明使用了 GNU General Public License v3.0 的代码。 5. 使用者在使用本项目的代码和功能时,必须自行研究相关法律法规,并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险,均由使用者自行承担。 6. 使用者不得使用本工具从事任何侵犯知识产权的行为,包括但不限于未经授权下载、传播受版权保护的内容,开发者不参与、不支持、不认可任何非法内容的获取或分发。 7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规,确保处理行为合法正当;因违规操作导致的法律责任由使用者自行承担。 8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来,或要求其对使用者使用本项目所产生的任何损失或损害负责。 9. 本项目的作者不会提供 XHS-Downloader 项目的付费版本,也不会提供与 XHS-Downloader 项目相关的任何商业服务。 10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关,原创作者不承担与二次开发行为或其结果相关的任何责任,使用者应自行对因二次开发可能带来的各种情况负全部责任。 11. 本项目不授予使用者任何专利许可;若使用本项目导致专利纠纷或侵权,使用者自行承担全部风险和责任。未经作者或权利人书面授权,不得使用本项目进行任何商业宣传、推广或再授权。 12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利,并可能要求其销毁已获取的代码及衍生作品。 13. 作者保留在不另行通知的情况下更新本声明的权利,使用者持续使用即视为接受修订后的条款。 在使用本项目的代码和功能之前,请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意,请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能,则视为您已完全理解并接受上述免责声明,并自愿承担使用本项目的一切风险和后果。 `, readmeTitle: 'XHS-Downloader 脚本说明', disclaimerTitle: 'XHS-Downloader 免责声明', disclaimerConfirm: '我已知晓', readmeMenuTitle: "阅读脚本说明和免责声明", aboutText: `项目开源协议:GNU General Public License v3.0 项目开源地址:https://github.com/JoeanAmier/XHS-Downloader 如果 XHS-Downloader 对您有帮助,请考虑为它点个 Star ⭐,感谢您的支持! ✨ 作者的其他开源项目: DouK-Downloader(抖音、DouYin、TikTok):https://github.com/JoeanAmier/TikTokDownloader KS-Downloader(快手、KuaiShou):https://github.com/JoeanAmier/KS-Downloader 项目 Discord 社区:https://discord.com/invite/ZYtmgKud9Y `, aboutTitle: '关于 XHS-Downloader', errorTitle: '发生异常', errorText: (text) => `${text}请向作者反馈!\n项目开源地址:https://github.com/JoeanAmier/XHS-Downloader`, imageExtractError: "解析图文作品数据发生异常!", downloadLinkError: "处理下载链接发生异常!", downloadTips: "正在下载文件,请稍等...", downloadError: "下载作品文件发生异常!", extractError: "读取作品数据发生异常!", linkExtractSuccess: '作品/用户链接已复制到剪贴板!', linkExtractError: "未提取到任何作品/用户链接!", videoDownloadError: "下载视频作品文件发生异常!", imageDownloadError: "下载图文作品文件发生异常!", signInPrompt: "提取作品链接失败!受平台限制,未登录状态下无法通过账号主页浏览作品详情!请登录后重试!", jsZipError: "XHS-Downloader 用户脚本依赖库 JSZip 加载失败,作品文件打包下载功能无法使用,请尝试刷新网页或者向作者反馈!", tipsTitle: '脚本提示', confirmButton: '确认', closeButton: '关闭', autoScrollLabel: '自动滚动页面', autoScrollDesc: '启用后,页面将根据规则自动滚动以便加载更多内容', filePackLabel: '文件打包下载', filePackDesc: '启用后,多个文件的作品将会以压缩包格式下载', scrollCountLabel: '自动滚动次数', scrollCountDesc: '自动滚动页面的次数(仅在启用自动滚动页面时可用)', linkCheckboxSwitchLabel: '链接提取选择模式', linkCheckboxSwitchDesc: '关闭后,提取作品链接时无需确认直接提取全部链接', imageCheckboxSwitchLabel: '图片下载选择模式', imageCheckboxSwitchDesc: '关闭后,下载图文作品时无需确认直接下载全部文件', keepMenuVisibleLabel: '菜单保持显示', keepMenuVisibleDesc: '启用后,功能菜单无需鼠标悬停始终保持显示', scriptServerURLLabel: 'WebSocket 服务器地址', scriptServerURLDesc: '用户脚本连接的 WebSocket 服务器', scriptServerSwitchLabel: '连接服务器', scriptServerSwitchDesc: '启用后,可以把作品下载任务推送至服务器', imageDownloadFormatLabel: '图片下载格式', imageDownloadFormatDesc: '图文作品文件下载格式', saveSettingsButton: '保存设置', cancelSettingsButton: '放弃修改', selectAllButton: '全部选中', deselectAllButton: '全部取消', startDownloadButton: '开始下载', closeDownloadButton: '取消下载', imageSelectionTip: '请至少选择一张图片!', itemsExtractTip: '请选择需要提取的项目', itemsExtractConfirm: '提取链接', itemsExtractCancel: '放弃', extractRecommendLinksText: '提取推荐作品链接', extractRecommendLinksDescription: '提取推荐页面的作品链接至剪贴板', downloadNoteFilesText: '下载作品文件', downloadNoteFilesDescription: '下载当前作品文件', pushDownloadTaskText: '推送下载任务', pushDownloadTaskDescription: '向服务器发送下载请求', extractPublishedLinksText: '提取发布作品链接', extractPublishedLinksDescription: '提取账号发布作品链接至剪贴板', extractLikedLinksText: '提取点赞作品链接', extractLikedLinksDescription: '提取账号点赞作品链接至剪贴板', extractSavedLinksText: '提取收藏作品链接', extractSavedLinksDescription: '提取账号收藏作品链接至剪贴板', extractSearchNoteLinksText: '提取作品链接', extractSearchNoteLinksDescription: '提取搜索结果的作品链接至剪贴板', extractSearchUsersLinksText: '提取用户链接', extractSearchUsersLinksDescription: '提取搜索结果的用户链接至剪贴板', extractAlbumNotesLinksText: '提取专辑作品链接', extractAlbumNotesLinksDescription: '提取当前专辑的作品链接至剪贴板', modifyScriptSettingsText: '修改用户脚本设置', modifyScriptSettingsDescription: '修改用户脚本设置', aboutXHSText: '关于 XHS-Downloader', aboutXHSDescription: '查看 XHS-Downloader 更多信息', imageCheckboxTitle: '请选中需要下载的图片', scriptServerError: '脚本服务器连接出错,请检查网络连接或脚本服务器状态是否正常!', pushTaskError: '脚本服务器未连接,请检查网络连接或脚本服务器状态是否正常!', pushTaskSuccess: "已向服务器发送下载请求", settingsTitle: '用户脚本设置', }, 'EN': { instructionsText: `Features: 1. Download RedNote note files 2. Extract note links from the Recommendation page 3. Extract note links from an account's Published tab 4. Extract note links from an account's Collections tab 5. Extract note links from an account's Albums 6. Extract note links from an account's Liked tab 7. Extract note links from search results 8. Extract user links from search results Notes: 1. When downloading note files, the script needs time to process. Please wait a moment and do not click the download button repeatedly. 2. When extracting links from Published, Collections, Liked, or Albums, the script can automatically scroll the page until all notes are loaded. 3. When extracting Recommendation, Search Notes, or User links, the script can automatically scroll a specified number of times. Default: 50 times. 4. Auto-scroll is disabled by default; users can enable it and modify the scroll count. Changes take effect immediately. 5. If auto-scroll is disabled, users must manually scroll the page to load more content before performing extractions. 6. Supports batch downloading (ZIP format); this feature is enabled by default. Notes with multiple files will be downloaded as a compressed package. 7. When pushing tasks to a server, settings such as file format and naming rules are determined by the server's configuration file. 8. Using global proxy tools may cause download failures. If issues occur, try disabling the proxy and provide feedback to the author if necessary. 9. The XHS-Downloader userscript only provides "what you see is what you get" data collection; it contains no paid features or decryption/cracking functions. Open Source: https://github.com/JoeanAmier/XHS-Downloader `, disclaimerText: `1. The use of this project is at the user's own discretion and risk. The author is not responsible for any loss, liability, or risk arising from its use. 2. The code and functions provided are based on existing knowledge and technology. While efforts are made to ensure correctness and security, the author does not guarantee that the code is entirely error-free. 3. All third-party libraries, plugins, or services relied upon by this project follow their own original open-source or commercial licenses. Users must consult and comply with those agreements. 4. Users must strictly adhere to the GNU General Public License v3.0 requirements and credit the use of GPL v3.0 code where appropriate. 5. Users must research relevant laws and regulations to ensure their use of this project is legal and compliant. Any legal liability arising from violations is borne solely by the user. 6. Users must not use this tool for any acts that infringe on intellectual property rights, including but not limited to unauthorized downloading or distribution of copyrighted content. 7. This project assumes no responsibility for the compliance of data collection, storage, or transmission activities performed by the user. 8. Under no circumstances shall the author or contributors be held liable for any damages or losses related to the user's actions. 9. The author will not provide a paid version of XHS-Downloader, nor any commercial services related to the project. 10. Any secondary development, modification, or compilation of this program is unrelated to the original author. The user is solely responsible for any consequences of such actions. 11. This project does not grant any patent licenses. The user assumes all risks regarding patent disputes. Commercial promotion or sub-licensing without written authorization is prohibited. 12. The author reserves the right to terminate service to any user violating this disclaimer and may request the destruction of obtained code. 13. The author reserves the right to update this disclaimer without notice. Continued use constitutes acceptance of the revised terms. Before using this project, please carefully consider and accept the above disclaimer. If you have any doubts or disagree, do not use the code or functions. Use of the project implies full understanding and acceptance of these terms. `, readmeTitle: 'XHS-Downloader Instructions', disclaimerTitle: 'XHS-Downloader Disclaimer', disclaimerConfirm: 'I acknowledge', readmeMenuTitle: "Read Instructions and Disclaimer", aboutText: `License: GNU General Public License v3.0 GitHub: https://github.com/JoeanAmier/XHS-Downloader If XHS-Downloader helps you, please consider giving it a Star ⭐. Thanks for your support! ✨ Other Projects by the Author: DouK-Downloader (DouYin, TikTok): https://github.com/JoeanAmier/TikTokDownloader KS-Downloader (KuaiShou): https://github.com/JoeanAmier/KS-Downloader Discord Community: https://discord.com/invite/ZYtmgKud9Y `, aboutTitle: 'About XHS-Downloader', errorTitle: 'Exception Occurred', errorText: (text) => `${text} Please report this to the author!\nGitHub: https://github.com/JoeanAmier/XHS-Downloader`, imageExtractError: "Error parsing image note data!", downloadLinkError: "Error processing download links!", downloadTips: "Downloading file, please wait...", downloadError: "Error downloading note files!", extractError: "Error reading note data!", linkExtractSuccess: 'Note/User links copied to clipboard!', linkExtractError: "No Note/User links extracted!", videoDownloadError: "Error downloading video note!", imageDownloadError: "Error downloading image note!", signInPrompt: "Failed to extract links! Due to platform restrictions, note details cannot be viewed via account pages without logging in. Please log in and try again!", jsZipError: "JSZip library failed to load. ZIP packaging is unavailable. Please refresh or contact the author!", tipsTitle: 'Script Tips', confirmButton: 'Confirm', closeButton: 'Close', autoScrollLabel: 'Auto-scroll Page', autoScrollDesc: 'When enabled, the page will automatically scroll to load more content', filePackLabel: 'Package Files for Download', filePackDesc: 'When enabled, notes with multiple files will be downloaded as a ZIP archive', scrollCountLabel: 'Auto-scroll Count', scrollCountDesc: 'Number of times to scroll (only active when Auto-scroll is enabled)', linkCheckboxSwitchLabel: 'Link Extraction Selection Mode', linkCheckboxSwitchDesc: 'If disabled, all links will be extracted immediately without confirmation', imageCheckboxSwitchLabel: 'Image Download Selection Mode', imageCheckboxSwitchDesc: 'If disabled, all images will be downloaded immediately without confirmation', keepMenuVisibleLabel: 'Keep Menu Visible', keepMenuVisibleDesc: 'When enabled, the menu stays visible without needing a mouse hover', scriptServerURLLabel: 'WebSocket Server URL', scriptServerURLDesc: 'The WebSocket server address the script connects to', scriptServerSwitchLabel: 'Connect to Server', scriptServerSwitchDesc: 'When enabled, download tasks can be pushed to the server', imageDownloadFormatLabel: 'Image Download Format', imageDownloadFormatDesc: 'Preferred file format for downloading images', saveSettingsButton: 'Save Settings', cancelSettingsButton: 'Discard Changes', selectAllButton: 'Select All', deselectAllButton: 'Deselect All', startDownloadButton: 'Start Download', closeDownloadButton: 'Cancel Download', imageSelectionTip: 'Please select at least one image!', itemsExtractTip: 'Please select items to extract', itemsExtractConfirm: 'Extract Links', itemsExtractCancel: 'Cancel', extractRecommendLinksText: 'Extract Recommended Note Links', extractRecommendLinksDescription: '', downloadNoteFilesText: 'Download Note Files', downloadNoteFilesDescription: '', pushDownloadTaskText: 'Push Download Task', pushDownloadTaskDescription: 'Send download request to the server', extractPublishedLinksText: 'Extract Published Note Links', extractPublishedLinksDescription: '', extractLikedLinksText: 'Extract Liked Note Links', extractLikedLinksDescription: '', extractSavedLinksText: 'Extract Collected Note Links', extractSavedLinksDescription: '', extractSearchNoteLinksText: 'Extract Note Links', extractSearchNoteLinksDescription: 'Extract note links from search results', extractSearchUsersLinksText: 'Extract User Links', extractSearchUsersLinksDescription: 'Extract user links from search results', extractAlbumNotesLinksText: 'Extract Album Note Links', extractAlbumNotesLinksDescription: 'Extract note links from the current album', modifyScriptSettingsText: 'Modify Script Settings', modifyScriptSettingsDescription: '', aboutXHSText: 'About XHS-Downloader', aboutXHSDescription: '', imageCheckboxTitle: 'Please select images to download', scriptServerError: 'Server connection error. Please check your network or server status!', pushTaskError: 'Server not connected. Please check your network or server status!', pushTaskSuccess: "Download request sent to server successfully", settingsTitle: 'Script Settings', }, }; let lang = GM_getValue("language", undefined); if (!lang) { lang = navigator.language.toLowerCase().includes('zh') ? 'CN' : 'EN'; GM_setValue("language", lang); } let t = i18n[lang]; const iconBase64 = "data:image/png;base64,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"; const defaultsWebSocketURL = "ws://127.0.0.1:5558"; let config = { disclaimer: GM_getValue("disclaimer", false), packageDownloadFiles: GM_getValue("packageDownloadFiles", true), autoScrollSwitch: GM_getValue("autoScrollSwitch", false), maxScrollCount: GM_getValue("maxScrollCount", 50), keepMenuVisible: GM_getValue("keepMenuVisible", false), linkCheckboxSwitch: GM_getValue("linkCheckboxSwitch", true), imageCheckboxSwitch: GM_getValue("imageCheckboxSwitch", true), imageDownloadFormat: GM_getValue("imageDownloadFormat", "jpeg"), scriptServerURL: GM_getValue("scriptServerURL", defaultsWebSocketURL), scriptServerSwitch: GM_getValue("scriptServerSwitch", false), fileNameFormat: undefined, icon: { type: 'image', // 可选: image/svg/font image: { url: iconBase64, // 图片URL或Base64 size: 64, // 图标尺寸 borderRadius: '50%' // 形状(50%为圆形) }, }, // 位置配置 position: { bottom: '6rem', left: '1rem' }, // 动画配置 animation: { duration: 0.25, // 动画时长(s) easing: 'cubic-bezier(0.4, 0, 0.2, 1)' } }; const readme = async () => { await showTextModal({ title: t.readmeTitle, text: t.instructionsText, mode: 'info', closeText: t.closeButton }); if (!config.disclaimer) { showTextModal({ title: t.disclaimerTitle, text: t.disclaimerText, mode: 'confirm', confirmText: t.disclaimerConfirm, closeText: t.closeButton }).then(answer => { GM_setValue("disclaimer", answer); config.disclaimer = answer; }); } }; if (!config.disclaimer) { readme().then(); } console.info("用户接受 XHS-Downloader 免责声明", config.disclaimer) GM_registerMenuCommand(t.readmeMenuTitle, function () { readme().then(); }); GM_registerMenuCommand("切换语言/Switch language", function () { lang = lang === "CN" ? "EN" : "CN"; GM_setValue("language", lang); t = i18n[lang]; }); const updatePackageDownloadFiles = (value) => { config.packageDownloadFiles = value; GM_setValue("packageDownloadFiles", config.packageDownloadFiles); }; const updateAutoScrollSwitch = (value) => { config.autoScrollSwitch = value; GM_setValue("autoScrollSwitch", config.autoScrollSwitch); }; const updateMaxScrollCount = (value) => { config.maxScrollCount = parseInt(value) || 50; GM_setValue("maxScrollCount", config.maxScrollCount); }; const updateKeepMenuVisible = (value) => { config.keepMenuVisible = value; GM_setValue("keepMenuVisible", config.keepMenuVisible); if (config.keepMenuVisible) { showMenu(); } else { hideMenu(); } }; const updateLinkCheckboxSwitch = (value) => { config.linkCheckboxSwitch = value; GM_setValue("linkCheckboxSwitch", config.linkCheckboxSwitch); } const updateImageCheckboxSwitch = (value) => { config.imageCheckboxSwitch = value; GM_setValue("imageCheckboxSwitch", config.imageCheckboxSwitch); } const updateScriptServerURL = (value) => { config.scriptServerURL = value; GM_setValue("scriptServerURL", config.scriptServerURL); } const updateScriptServerSwitch = (value) => { webSocket.disconnect(); if (value) { webSocket.url = config.scriptServerURL; webSocket.connect(); } config.scriptServerSwitch = value; GM_setValue("scriptServerSwitch", config.scriptServerSwitch); } const updateImageDownloadFormat = (value) => { config.imageDownloadFormat = value.toLowerCase(); GM_setValue("imageDownloadFormat", config.imageDownloadFormat); } const updateFileNameFormat = (value) => { config.fileNameFormat = value; GM_setValue("fileNameFormat", config.fileNameFormat); }; const about = () => { showTextModal({ title: t.aboutTitle, text: t.aboutText, mode: 'info', closeText: t.closeButton }).then(); } const abnormal = (text) => { showTextModal({ title: t.errorTitle, text: t.errorText(text), mode: 'info', closeText: t.closeButton }).then(); }; const runTips = (text) => { showTextModal({ title: t.tipsTitle, text: text, mode: 'info', closeText: t.closeButton }).then(); } const generateVideoUrl = note => { try { const key = note.video?.consumer?.originVideoKey; if (key) return [`https://sns-video-bd.xhscdn.com/${key}`]; const video = note.video.media.stream.h265; return [video[video.length - 1].masterUrl]; } catch (error) { console.error("Error deal video URL:", error); return []; } }; const generateImageUrl = note => { let images = note.imageList; const regex = /http:\/\/sns-webpic-qc\.xhscdn.com\/\d+\/[0-9a-z]+\/(\S+)!/; let urls = []; try { images.forEach((item) => { const url = item.urlDefault || item.url; let match = url.match(regex); if (match && match[1]) { urls.push( `https://ci.xiaohongshu.com/${match[1]}?imageView2/format/${GM_getValue( "imageDownloadFormat", "jpeg" )}`); } }) return urls } catch (error) { console.error("Error generating image URLs:", error); return []; } }; const extractImageWebpUrls = (note, urls,) => { try { let items = [] let {imageList} = note; if (urls.length !== imageList.length) { console.error("图片数量不一致!") return [] } for (const [index, item] of imageList.entries()) { const url = item.urlDefault || item.url; if (url) { items.push({ webp: url, index: index + 1, url: urls[index], }) } else { console.error("提取图片预览链接失败", item) break } } return items; } catch (error) { console.error("Error occurred in generating image object:", error); return [] } }; const download = async (urls, note, server = false,) => { const name = extractName(); if (server) { let data = {data: note, index: null,}; if (note.type === "normal") { let items = extractImageWebpUrls(note, urls); if (items.length === 0) { console.error("解析图文作品数据失败", note) abnormal(t.imageExtractError) } else if (urls.length > 1 && config.imageCheckboxSwitch) { data.index = await showImageSelectionModal(items, name, server,); } } webSocket.send(JSON.stringify(data)); } else { console.debug(`文件名称 ${name}`); if (note.type === "video") { showToast(t.downloadTips); await downloadVideo(urls[0], name); } else { let items = extractImageWebpUrls(note, urls); if (items.length === 0) { console.error("解析图文作品数据失败", note) abnormal(t.imageExtractError) } else if (urls.length > 1 && config.imageCheckboxSwitch) { await showImageSelectionModal(items, name,); } else { showToast(t.downloadTips); await downloadImage(items, name); } } } }; const exploreDeal = async (note, server = false,) => { try { let links; if (note.type === "normal") { links = generateImageUrl(note); } else { links = generateVideoUrl(note); } if (links.length > 0) { // console.debug("下载链接", links); await download(links, note, server,); } else { abnormal(t.downloadLinkError) } } catch (error) { console.error("Error in exploreDeal function:", error); abnormal(t.downloadError); } }; const extractNoteInfo = () => { const data = unsafeWindow.__INITIAL_STATE__?.noteData?.data?.noteData; if (data) return data; const regex = /\/explore\/([^?]+)/; const match = currentUrl.match(regex); if (match) { return unsafeWindow.__INITIAL_STATE__.note.noteDetailMap[match[1]].note; } else { console.error("从链接提取作品 ID 失败", currentUrl,); } }; const extractDownloadLinks = async (server = false) => { if (currentUrl.includes("https://www.xiaohongshu.com/explore/") || currentUrl.includes( "https://www.xiaohongshu.com/discovery/item/")) { let note = extractNoteInfo(); if (note) { await exploreDeal(note, server,); } else { abnormal(t.extractError); } } }; const triggerDownload = (name, blob) => { // 创建 Blob 对象的 URL const blobUrl = URL.createObjectURL(blob); // 创建一个临时链接元素 const tempLink = document.createElement("a"); tempLink.href = blobUrl; tempLink.download = name; // 将链接添加到 DOM 并模拟点击 document.body.appendChild(tempLink); // 避免某些浏览器安全限制 tempLink.click(); // 清理临时链接元素 document.body.removeChild(tempLink); // 从 DOM 中移除临时链接 URL.revokeObjectURL(blobUrl); // 释放 URL // console.debug(`文件已成功下载: ${name}`); } const downloadFile = async (link, name, trigger = true, retries = 5) => { for (let attempt = 1; attempt <= retries; attempt++) { try { // 使用 fetch 获取文件数据 const response = await fetch(link, { "headers": { "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", "accept-language": "zh-SG,zh;q=0.9", }, "method": "GET", }); // 检查响应状态码 if (!response.ok) { console.error(`下载失败,状态码: ${response.status},URL: ${link},尝试次数: ${attempt}`); continue; // 继续下一次尝试 } const blob = await response.blob(); if (trigger) { triggerDownload(name, blob); return true; } else { return blob; } } catch (error) { console.error(`下载失败 (${name}),错误信息:`, error, `尝试次数: ${attempt}`); if (attempt === retries) { return false; // 如果达到最大重试次数,返回失败 } } } return false; // 如果所有尝试都失败,返回失败 }; const downloadFiles = async (items, name,) => { const downloadResults = []; // 用于存储下载结果 const downloadPromises = items.map(async (item) => { let fileName; if (item.index) { fileName = `${name}_${item.index}.${GM_getValue("imageDownloadFormat", "jpeg")}`; // 根据索引生成文件名 } else { fileName = `${name}.${GM_getValue("imageDownloadFormat", "jpeg")}`; } const result = await downloadFile(item.url, fileName, false); // 调用单个文件下载方法 if (result) { downloadResults.push({name: fileName, file: result}); return true; // 成功 } else { return false; // 失败 } }); // 等待所有下载操作完成 const results = await Promise.all(downloadPromises); if (results.every(result => result === true)) { try { const zip = new JSZip(); downloadResults.forEach((item) => { zip.file(item.name, item.file); }); const content = await zip.generateAsync({type: "blob", compression: "STORE"}); triggerDownload(`${name}.zip`, content,) return true; } catch (error) { console.error('生成 ZIP 文件或保存失败,错误信息:', error); return false; } } else { return false; } }; const truncateString = (str, maxLength) => { if (str.length > maxLength) { const halfLength = Math.floor(maxLength / 2) - 1; // 减去 1 留出省略号的空间 return str.slice(0, halfLength) + '...' + str.slice(-halfLength); } return str; }; const extractName = () => { let name = document.title.replace(/ - 小红书$/, "") .replace(/[^\u4e00-\u9fa5a-zA-Z0-9 ~!@#$%&()_\-+=\[\];"',.!()【】:“”,。《》?]/g, ""); name = truncateString(name, 64,); let match = currentUrl.match(/\/([0-9a-z]+?)\?/); let id = match ? match[1] : null; return name === "" ? id : name }; const downloadVideo = async (url, name) => { if (!await downloadFile(url, `${name}.mp4`)) { abnormal(t.videoDownloadError); } }; const downloadImage = async (items, name) => { let success; if (!config.packageDownloadFiles && items.length > 1) { let result = []; for (let item of items) { result.push(await downloadFile( item.url, `${name}_${item.index}.${GM_getValue("imageDownloadFormat", "jpeg")}` )); } success = result.every(item => item === true); } else if (items.length === 1) { success = await downloadFile(items[0].url, `${name}.${GM_getValue("imageDownloadFormat", "jpeg")}`); } else { success = await downloadFiles(items, name,); } if (!success) { abnormal(t.imageDownloadError); } }; const window_scrollBy = (x, y,) => { window.scrollBy(x, y,); } // 随机整数生成函数 const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; // 判断是否需要暂停,模拟用户的停顿行为 const shouldPause = () => Math.random() < 0.2; // 20%几率停顿 // 执行一次增量滚动 const scrollOnce = () => { const scrollDistanceMin = 100; // 最小滚动距离 const scrollDistanceMax = 300; // 最大滚动距离 const scrollDistance = getRandomInt(scrollDistanceMin, scrollDistanceMax); window_scrollBy(0, scrollDistance); // 增量滚动 }; // 检查是否已经滚动到底部 const isAtBottom = () => { const docHeight = document.documentElement.scrollHeight; const winHeight = window.innerHeight; const scrollPos = window.scrollY; return (docHeight - winHeight - scrollPos <= 10); // 如果距离底部小于10px,认为滚动到底部 }; // 自动滚动主函数 const scrollScreen = (callback, endless = false, scrollCount = 0,) => { const timeoutMin = 250; // 最小滚动间隔 const timeoutMax = 500; // 最大滚动间隔 const scrollInterval = setInterval(() => { if (shouldPause()) { // 停顿,模拟用户的休息 clearInterval(scrollInterval); setTimeout(() => { scrollScreen(callback, endless, scrollCount,); // 重新启动滚动 }, getRandomInt(timeoutMin, timeoutMax,)); // 随机停顿时间 } else if (endless) { // 无限滚动至底部模式 if (!isAtBottom()) { scrollOnce(); // 执行一次滚动 } else { // 到达底部,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 } } else if (scrollCount < config.maxScrollCount && !isAtBottom()) { scrollOnce(); // 执行一次滚动 scrollCount++; } else { // 如果到达底部或滚动次数已满,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 } }, getRandomInt(timeoutMin, timeoutMax)); // 随机滚动间隔 }; const scrollScreenEvent = (callback, endless = false) => { if (config.autoScrollSwitch) { scrollScreen(callback, endless,); } else { callback(); } }; const extractNotesInfo = order => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[order]; return notesRawValue.filter(item => item?.noteCard).map( item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]); }; const extractBoardInfo = () => { // 定义正则表达式来匹配 URL 中的 ID const regex = /\/board\/([a-z0-9]+)\?/; // 使用 exec 方法执行正则表达式 const match = regex.exec(currentUrl); // 检查是否有匹配 if (match) { // 提取 ID const id = match[1]; // match[0] 是整个匹配的字符串,match[1] 是第一个括号内的匹配 const notesRawValue = unsafeWindow.__INITIAL_STATE__.board.boardFeedsMap._rawValue[id].notes; return notesRawValue.map( item => [item.noteId, item.xsecToken, item.cover.urlDefault, item.user.nickName, item.displayTitle,]); } else { console.error("从链接提取专辑 ID 失败", currentUrl,); return []; } }; const extractFeedInfo = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.feed.feeds._rawValue; return notesRawValue.filter(item => item?.noteCard).map( item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]); }; const extractSearchNotes = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.feeds._rawValue; return notesRawValue.filter(item => item?.noteCard).map( item => [item.id, item.xsecToken, item.noteCard.cover.urlDefault, item.noteCard.user.nickName, item.noteCard.displayTitle,]); } const extractSearchUsers = () => { const notesRawValue = unsafeWindow.__INITIAL_STATE__.search.userLists._rawValue; return notesRawValue.map(item => item.id); } const generateNoteUrls = data => data.map( ([id, token,]) => `https://www.xiaohongshu.com/discovery/item/${id}?source=webshare&xhsshare=pc_web&xsec_token=${token}&xsec_source=pc_share`) .join(" "); const generateUserUrls = data => data.map(id => `https://www.xiaohongshu.com/user/profile/${id}`).join(" "); const invalidDetection = data => data.every(([first]) => Boolean(first)); const extractAllLinks = (callback, order) => { scrollScreenEvent(() => { let data; if (order >= 0 && order <= 2) { data = extractNotesInfo(order); if (!invalidDetection(data)) { runTips(t.signInPrompt); return; } } else if (order === 3) { data = extractSearchNotes(); } else if (order === 4) { data = extractSearchUsers(); } else if (order === -1) { data = extractFeedInfo() } else if (order === 5) { data = extractBoardInfo() } else { data = [] } if (data.length === 0) { callback(""); return; } let urlsString; if (order === 4) { urlsString = generateUserUrls(data); callback(urlsString); } else if (config.linkCheckboxSwitch) { showListSelectionModal(data.map(([id, token, cover, author, title,]) => ({ id: id, token: token, image: cover, author: author, title: title, })),).then((selected) => { if (selected.length > 0) { urlsString = generateNoteUrls(selected.map(item => [item.id, item.token])); callback(urlsString); } }); } else { urlsString = generateNoteUrls(data.map(item => item.slice(0, 2))) callback(urlsString); } }, [0, 1, 2, 5].includes(order)) }; const extractAllLinksEvent = (order = 0) => { extractAllLinks(urlsString => { if (urlsString) { GM_setClipboard(urlsString, "text", () => { showToast(t.linkExtractSuccess); }); } else { showToast(t.linkExtractError) } }, order); }; if (typeof JSZip === 'undefined') { runTips(t.jsZipError); } let style = document.createElement('style'); style.textContent = ` /* 通用 Overlay(三个弹窗共用) */ #SettingsOverlay, #imageSelectionOverlay, #textGenericOverlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.32); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s; } /* Settings 容器,仅本块特有尺寸 */ .optimized-scroll-modal { background: white; border-radius: 16px; width: 380px; /* 缩小窗口宽度 */ max-width: 95vw; max-height: 95vh; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; display: flex; flex-direction: column; } /* 通用头部/内容/底部/按钮(三个弹窗共用) */ .modal-header { padding: 1rem; border-bottom: 1px solid #eee; text-align: center; } .modal-header span { font-size: 1.25rem; font-weight: 500; color: #212121; } .modal-body { flex: 1; padding: 1rem; overflow-y: auto; } .modal-footer { padding: 1rem; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 12px; } .primary-btn { background: #2196F3; color: white; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } .secondary-btn { background: #f0f0f0; color: #666; padding: 8px 24px; border-radius: 24px; cursor: pointer; transition: all 0.2s; } /* Settings 专用的设置项样式(保持不变) */ .setting-item { margin: 0.5rem 0; padding: 10px; border-radius: 8px; transition: background 0.2s; } .setting-item:hover { background: #f0f0f0; } .setting-item label { display: flex; justify-content: space-between; align-items: center; width: 100%; } .setting-item label span { font-size: 1rem; font-weight: 500; color: #333; } .toggle-switch { position: relative; width: 40px; height: 20px; } .toggle-switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background: #ccc; transition: 0.4s; border-radius: 34px; } .slider:before { content: ""; position: absolute; height: 16px; width: 16px; left: 2px; bottom: 2px; background: white; border-radius: 50%; transition: 0.4s; } input:checked + .slider { background: #2196F3; } input:checked + .slider:before { transform: translateX(20px); } .number-input { display: flex; align-items: center; border: 1px solid #ddd; border-radius: 8px; overflow: hidden; margin: 6px 0; } .number-input input { width: 60px; text-align: center; border: none; } .number-button { padding: 4px 8px; background: #f0f0f0; border: none; cursor: pointer; transition: all 0.2s; } .text-input { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9rem; margin-top: 8px; transition: border-color 0.2s; } .text-input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 4px rgba(33, 150, 243, 0.3); } .select-input { width: 100px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.9rem; margin-top: 8px; background: #fff; transition: border-color 0.2s, box-shadow 0.2s; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M3 4.5L6 7.5L9 4.5H3Z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; padding-right: 32px; } .select-input:focus { outline: none; border-color: #2196F3; box-shadow: 0 0 4px rgba(33, 150, 243, 0.3); } .select-input:disabled { background-color: #f5f5f5; color: #999; cursor: not-allowed; } .setting-description { font-size: 0.875rem; color: #757575; margin-top: 4px; line-height: 1.4; text-align: left; } /* 通用动画:统一定义一次 */ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } @keyframes scaleUp { from { transform: scale(0.98); } to { transform: scale(1); } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } `; document.head.appendChild(style); // 覆盖修改:文本弹窗样式 (() => { if (!document.getElementById('textModalStyle')) { const style = document.createElement('style'); style.id = 'textModalStyle'; style.textContent = ` /* 仅文本弹窗容器特有的尺寸与外观 */ .text-generic-modal { background: #fff; border-radius: 16px; width: 80%; max-width: 700px; max-height: 80vh; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; display: flex; flex-direction: column; } /* 仅该弹窗使用的文本内容样式 */ .text-content { white-space: pre-wrap; word-break: break-word; color: #1e272e; line-height: 1.6; font-size: 0.95rem; user-select: text; } `; document.head.appendChild(style); } })(); /** * 显示文本弹窗(手动调用) * @param {Object} opts * @param {string} opts.title 标题 * @param {string} opts.text 文本内容 * @param {'confirm'|'info'} opts.mode 模式:confirm=确认+关闭;info=仅关闭 * @param {string} [opts.confirmText='确认'] 确认按钮文案(仅 confirm 模式生效) * @param {string} [opts.closeText='关闭'] 关闭按钮文案 * @returns {Promise} confirm 返回 true;关闭/点遮罩返回 false */ function showTextModal(opts) { const { title = t.tipsTitle, text = '', mode = 'info', confirmText = t.confirmButton, closeText = t.closeButton, } = opts || {}; if (document.getElementById('textGenericOverlay')) { return Promise.resolve(false); } return new Promise((resolve) => { const overlay = document.createElement('div'); overlay.id = 'textGenericOverlay'; const modal = document.createElement('div'); modal.className = 'text-generic-modal'; const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = `${title}`; const body = document.createElement('div'); body.className = 'modal-body'; const content = document.createElement('div'); content.className = 'text-content'; content.textContent = text ?? ''; body.appendChild(content); const footer = document.createElement('div'); footer.className = 'modal-footer'; if (mode === 'confirm') { const okBtn = document.createElement('button'); okBtn.className = 'primary-btn'; okBtn.textContent = confirmText; okBtn.addEventListener('click', () => close(true)); footer.appendChild(okBtn); } const closeBtn = document.createElement('button'); closeBtn.className = 'secondary-btn'; closeBtn.textContent = closeText; closeBtn.addEventListener('click', () => close(false)); footer.appendChild(closeBtn); modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); function close(result) { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => { overlay.remove(); resolve(result); }, 200); } overlay.addEventListener('click', (e) => { if (e.target === overlay) close(false); }); }); } // 创建开关项 const createSwitchItem = ({label, description, checked, disabled = false}) => { const item = document.createElement('div'); item.className = 'setting-item'; item.style.opacity = disabled ? 0.6 : 1; item.innerHTML = `
${description}
`; return item; }; // 创建数值输入项 const createNumberInput = ({label, description, value, min, max, disabled}) => { const item = document.createElement('div'); item.className = 'setting-item'; const numberInput = document.createElement('div'); numberInput.className = 'number-input'; numberInput.style.opacity = disabled ? 0.6 : 1; numberInput.innerHTML = ` `; item.innerHTML = `
${description}
`; // 绑定数值按钮事件 const container = item.querySelector('.number-input'); container.querySelectorAll('.number-button').forEach(btn => { btn.addEventListener('click', () => { const input = container.querySelector('input'); if (input.disabled) { return; } let val = parseInt(input.value); if (btn.dataset.action === 'increment') { val = Math.min(val + 1, max); } else { val = Math.max(val - 1, min); } input.value = val; }); }); return item; }; // 创建文本输入项 const createTextInput = ({label, description, placeholder, value, disabled = false}) => { const item = document.createElement('div'); item.className = 'setting-item'; item.style.opacity = disabled ? 0.6 : 1; item.innerHTML = `
${label}
${description}
`; return item; }; // 创建下拉框项 const createSelectItem = ({label, description, options, value, disabled = false}) => { const item = document.createElement('div'); item.className = 'setting-item'; item.style.opacity = disabled ? 0.6 : 1; // 生成选项HTML const optionsHtml = options.map( option => ``).join(''); item.innerHTML = `
${description}
`; return item; }; // 关闭弹窗函数 const closeSettingsModal = () => { const overlay = document.getElementById('SettingsOverlay'); if (overlay) { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => overlay.remove(), 200); } }; /* ==================== 弹窗逻辑 ==================== */ const showSettings = () => { if (document.getElementById('SettingsOverlay')) { return; } // 创建覆盖层 const overlay = document.createElement('div'); overlay.id = 'SettingsOverlay'; // 创建弹窗 const modal = document.createElement('div'); modal.className = 'optimized-scroll-modal'; // 创建头部 const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = ` ${t.settingsTitle} `; // 创建内容区域 const body = document.createElement('div'); body.className = 'modal-body'; // 自动滚动开关 const autoScroll = createSwitchItem({ label: t.autoScrollLabel, description: t.autoScrollDesc, checked: GM_getValue("autoScrollSwitch", false), }); // 文件打包开关 const filePack = createSwitchItem({ label: t.filePackLabel, description: t.filePackDesc, checked: GM_getValue("packageDownloadFiles", true), }); // 滚动次数设置 const scrollCount = createNumberInput({ label: t.scrollCountLabel, description: t.scrollCountDesc, value: GM_getValue("maxScrollCount", 50), min: 10, max: 9999, disabled: !GM_getValue("autoScrollSwitch", false), }); const linkCheckboxSwitch = createSwitchItem({ label: t.linkCheckboxSwitchLabel, description: t.linkCheckboxSwitchDesc, checked: GM_getValue("linkCheckboxSwitch", true), }); const imageCheckboxSwitch = createSwitchItem({ label: t.imageCheckboxSwitchLabel, description: t.imageCheckboxSwitchDesc, checked: GM_getValue("imageCheckboxSwitch", true), }); const keepMenuVisible = createSwitchItem({ label: t.keepMenuVisibleLabel, description: t.keepMenuVisibleDesc, checked: GM_getValue("keepMenuVisible", false), }); const scriptServerURL = createTextInput({ label: t.scriptServerURLLabel, description: t.scriptServerURLDesc, placeholder: defaultsWebSocketURL, value: GM_getValue("scriptServerURL", defaultsWebSocketURL), disabled: !GM_getValue("scriptServerSwitch", false), }); const scriptServerSwitch = createSwitchItem({ label: t.scriptServerSwitchLabel, description: t.scriptServerSwitchDesc, checked: GM_getValue("scriptServerSwitch", false), }); const imageDownloadFormat = createSelectItem({ label: t.imageDownloadFormatLabel, description: t.imageDownloadFormatDesc, options: ["PNG", "WEBP", "JPEG", "HEIC"], value: GM_getValue("imageDownloadFormat", "jpeg") .toUpperCase(), }); // const nameFormat = createTextInput({ // label: '文件名称格式', // description: '设置文件的名称格式(例如:{date}-{title})。', // placeholder: '{date}-{title}', // value: GM_getValue("fileNameFormat",) // }); // 绑定自动滚动开关控制次数输入 autoScroll.querySelector('input').addEventListener('change', (e) => { scrollCount.querySelector('input').disabled = !e.target.checked; scrollCount.querySelector('.number-input').style.opacity = e.target.checked ? 1 : 0.6; }); scriptServerSwitch.querySelector('input').addEventListener('change', (e) => { scriptServerURL.querySelector('input').disabled = !e.target.checked; scriptServerURL.querySelector('.text-input').style.opacity = e.target.checked ? 1 : 0.6; }); // 组合内容 body.appendChild(filePack); body.appendChild(autoScroll); body.appendChild(scrollCount); body.appendChild(linkCheckboxSwitch); body.appendChild(imageCheckboxSwitch); body.appendChild(imageDownloadFormat); body.appendChild(keepMenuVisible); body.appendChild(scriptServerURL); body.appendChild(scriptServerSwitch); // 创建底部按钮 const footer = document.createElement('div'); footer.className = 'modal-footer'; const saveBtn = document.createElement('button'); saveBtn.className = 'primary-btn'; saveBtn.textContent = t.saveSettingsButton; const cancelBtn = document.createElement('button'); cancelBtn.className = 'secondary-btn'; cancelBtn.textContent = t.cancelSettingsButton; footer.appendChild(saveBtn); footer.appendChild(cancelBtn); // 组装弹窗 modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); // 保存事件 saveBtn.addEventListener('click', () => { updateAutoScrollSwitch(autoScroll.querySelector('input').checked); updatePackageDownloadFiles(filePack.querySelector('input').checked); updateKeepMenuVisible(keepMenuVisible.querySelector('input').checked); updateLinkCheckboxSwitch(linkCheckboxSwitch.querySelector('input').checked); updateImageCheckboxSwitch(imageCheckboxSwitch.querySelector('input').checked); updateMaxScrollCount(parseInt(scrollCount.querySelector('input').value) || 50) updateScriptServerURL(scriptServerURL.querySelector('.text-input').value.trim() || defaultsWebSocketURL); updateScriptServerSwitch(scriptServerSwitch.querySelector('input').checked); updateImageDownloadFormat(imageDownloadFormat.querySelector('select').value.trim() || "jpeg"); // updateFileNameFormat(nameFormat.querySelector('.text-input').value.trim() || null); closeSettingsModal(); }); // 关闭事件 cancelBtn.addEventListener('click', closeSettingsModal); overlay.addEventListener('click', (e) => e.target === overlay && closeSettingsModal()); }; /* ==================== 样式定义 ==================== */ style = document.createElement('style'); style.textContent = ` /* 图片选择弹窗:仅容器尺寸与自身网格等特有样式 */ .image-selection-modal { background: white; border-radius: 16px; width: 80%; max-width: 900px; max-height: 90vh; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; display: flex; flex-direction: column; } /* 图片网格等仅此弹窗拥有的样式 */ .image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 12px; } .image-item { position: relative; border-radius: 8px; overflow: hidden; cursor: pointer; transition: all 0.2s; border: 2px solid transparent; aspect-ratio: 3 / 4; background: conic-gradient(#eee 25%, transparent 0 50%, #eee 0 75%, transparent 0) 0 0/16px 16px #fafafa; } .image-item img { width: 100%; height: 100%; object-fit: contain; object-position: center; border-radius: 6px; display: block; } .image-item.selected { border-color: #2196F3; } .image-checkbox { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; opacity: 0; } .image-checkbox + label { position: absolute; top: 8px; right: 8px; width: 20px; height: 20px; background: white; border: 1px solid #ccc; border-radius: 50%; cursor: pointer; display: flex; justify-content: center; align-items: center; transition: all 0.2s; } .image-checkbox:checked + label { background: #2196F3; border-color: #2196F3; } .image-checkbox:checked + label::after { content: "✓"; color: white; font-size: 12px; } `; document.head.appendChild(style); // 关闭弹窗函数 const closeImagesModal = () => { const overlay = document.getElementById('imageSelectionOverlay'); if (overlay) { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => overlay.remove(), 200); } }; /* ==================== 弹窗逻辑 ==================== */ const showImageSelectionModal = (imageUrls, name, server = false,) => { return new Promise((resolve,) => { if (document.getElementById('imageSelectionOverlay')) { return; } // 创建覆盖层 const overlay = document.createElement('div'); overlay.id = 'imageSelectionOverlay'; // 创建弹窗 const modal = document.createElement('div'); modal.className = 'image-selection-modal'; // 创建头部 const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = ` ${t.imageCheckboxTitle} `; // 创建内容区域 const body = document.createElement('div'); body.className = 'modal-body'; // 创建图片网格 const imageGrid = document.createElement('div'); imageGrid.className = 'image-grid'; // 动态生成图片项 imageUrls.forEach((image) => { const item = document.createElement('div'); item.className = 'image-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'image-checkbox'; checkbox.id = `image-checkbox-${image.index}`; checkbox.checked = true; const label = document.createElement('label'); label.htmlFor = `image-checkbox-${image.index}`; const img = document.createElement('img'); img.src = image.webp; img.index = image.index; img.url = image.url; img.alt = `图片_${image.index}`; item.appendChild(checkbox); item.appendChild(label); item.appendChild(img); // 绑定点击事件 item.addEventListener('click', (e) => { if (e.target.tagName !== 'INPUT') { checkbox.checked = !checkbox.checked; item.classList.toggle('selected', checkbox.checked); } }); imageGrid.appendChild(item); }); body.appendChild(imageGrid); // 创建底部按钮 const footer = document.createElement('div'); footer.className = 'modal-footer'; // 新增:全选 / 全不选 const selectAllBtn = document.createElement('button'); selectAllBtn.className = 'secondary-btn'; selectAllBtn.textContent = t.selectAllButton; const selectNoneBtn = document.createElement('button'); selectNoneBtn.className = 'secondary-btn'; selectNoneBtn.textContent = t.deselectAllButton; const confirmBtn = document.createElement('button'); confirmBtn.className = 'primary-btn'; confirmBtn.textContent = t.startDownloadButton; const closeBtn = document.createElement('button'); closeBtn.className = 'secondary-btn'; closeBtn.textContent = t.closeDownloadButton; footer.appendChild(selectAllBtn); footer.appendChild(selectNoneBtn); footer.appendChild(confirmBtn); footer.appendChild(closeBtn); // 组装弹窗 modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); // 确认事件 confirmBtn.addEventListener('click', async () => { const selectedImages = Array.from(document.querySelectorAll('.image-checkbox:checked')) .map((checkbox) => { let item = checkbox.parentElement.querySelector('img'); return { index: item.index, url: item.url, } }); if (selectedImages.length === 0) { showToast(t.imageSelectionTip); return; } closeImagesModal(); if (server) { resolve(selectedImages.map(item => item.index)); } else { showToast(t.downloadTips); await downloadImage(selectedImages, name) } }); // 关闭事件 closeBtn.addEventListener('click', closeImagesModal); overlay.addEventListener('click', (e) => e.target === overlay && closeImagesModal()); const setAllChecked = (checked) => { const items = imageGrid.querySelectorAll('.image-item'); items.forEach((item) => { const box = item.querySelector('.image-checkbox'); if (!box || box.disabled) return; box.checked = checked; item.classList.toggle('selected', checked); }); }; // 全选 / 全不选 selectAllBtn.addEventListener('click', () => setAllChecked(true)); selectNoneBtn.addEventListener('click', () => setAllChecked(false)); }) }; (() => { if (!document.getElementById('listSelectionStyle')) { const style = document.createElement('style'); style.id = 'listSelectionStyle'; style.textContent = ` /* 列表弹窗容器,仅定义差异尺寸,其他沿用通用 modal 样式 */ .list-selection-modal { background: #fff; border-radius: 16px; width: 80%; max-width: 800px; max-height: 80vh; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; animation: scaleUp 0.3s; display: flex; flex-direction: column; } /* 列表容器 */ .list-container { display: flex; flex-direction: column; gap: 10px; } /* 列表项 */ .list-item { display: grid; grid-template-columns: 32px 0px 64px 1fr; /* 序号、复选框、缩略图、文本区 */ align-items: center; gap: 8px; padding: 8px; border: 1px solid #eee; border-radius: 10px; transition: background 0.15s, border-color 0.15s, box-shadow 0.15s; cursor: pointer; } .list-item:hover { background: #fafafa; border-color: #e6e6e6; } .list-item.selected { border-color: #2196F3; box-shadow: 0 0 0 4px rgba(33,150,243,0.12) inset; } /* 序号样式 */ .list-number { text-align: center; } /* 复选框样式(使用原生复选框以保证可访问性与简单性) */ .list-checkbox { width: 18px; height: 18px; margin: 0 0 0 2px; cursor: pointer; } /* 缩略图 */ .list-thumb { width: 64px; height: 64px; object-fit: cover; border-radius: 8px; user-select: none; pointer-events: none; /* 点击行切换选择,避免图片拦截点击 */ background: #f2f2f2; } /* 文本区 */ .list-texts { display: flex; flex-direction: column; min-width: 0; /* 允许文本正确换行截断 */ } .list-author { font-size: 0.95rem; color: #212121; font-weight: 500; line-height: 1.4; word-break: break-word; } .list-title { margin-top: 4px; font-size: 0.85rem; color: #757575; line-height: 1.4; word-break: break-word; } `; document.head.appendChild(style); } })(); /** * 显示列表选择弹窗 * @param {Array<{id:any, image:string, author:string, title:string}>} list * @param {Object} [options] * @param {string} [options.title='请选择'] 弹窗标题 * @param {string} [options.confirmText='确认'] 确认按钮文本 * @param {string} [options.cancelText='取消'] 取消按钮文本 * @param {boolean} [options.prechecked=true] 是否默认勾选全部 * @returns {Promise} 点击确认返回选中项数组;取消/关闭返回 null */ function showListSelectionModal(list, options = {}) { const { title = t.itemsExtractTip, confirmText = t.itemsExtractConfirm, cancelText = t.itemsExtractCancel, prechecked = true, } = options; if (document.getElementById('listSelectionOverlay')) return Promise.resolve(null); return new Promise((resolve) => { // 覆盖层 const overlay = document.createElement('div'); overlay.id = 'listSelectionOverlay'; overlay.style.cssText = ` position: fixed; inset: 0; background: rgba(0,0,0,0.32); backdrop-filter: blur(4px); display: flex; justify-content: center; align-items: center; z-index: 10000; animation: fadeIn 0.3s; `; // 弹窗 const modal = document.createElement('div'); modal.className = 'list-selection-modal'; // 头部 const header = document.createElement('div'); header.className = 'modal-header'; header.innerHTML = `${title}`; // 内容 const body = document.createElement('div'); body.className = 'modal-body'; const container = document.createElement('div'); container.className = 'list-container'; // id -> item 映射 const map = new Map(); list.forEach((item, index) => { const row = document.createElement('div'); row.className = 'list-item'; row.dataset.key = item.id; // 添加序号 const number = document.createElement('div'); number.className = 'list-number'; number.textContent = (index + 1).toString(); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.className = 'list-checkbox'; checkbox.checked = prechecked; const img = document.createElement('img'); img.className = 'list-thumb'; img.src = item.image || ''; img.alt = 'thumb'; const texts = document.createElement('div'); texts.className = 'list-texts'; const author = document.createElement('div'); author.className = 'list-author'; author.textContent = item.author ?? ''; const title = document.createElement('div'); title.className = 'list-title'; title.textContent = item.title ?? ''; texts.appendChild(author); texts.appendChild(title); row.appendChild(number); row.appendChild(checkbox); row.appendChild(img); row.appendChild(texts); if (checkbox.checked) row.classList.add('selected'); row.addEventListener('click', (e) => { if ((e.target instanceof HTMLElement) && e.target.classList.contains('list-checkbox')) return; checkbox.checked = !checkbox.checked; row.classList.toggle('selected', checkbox.checked); }); checkbox.addEventListener('change', () => { row.classList.toggle('selected', checkbox.checked); }); map.set(row.dataset.key, item); container.appendChild(row); }); body.appendChild(container); // 底部按钮 const footer = document.createElement('div'); footer.className = 'modal-footer'; // 新增:全选 / 全不选 const selectAllBtn = document.createElement('button'); selectAllBtn.className = 'secondary-btn'; selectAllBtn.textContent = t.selectAllButton; const selectNoneBtn = document.createElement('button'); selectNoneBtn.className = 'secondary-btn'; selectNoneBtn.textContent = t.deselectAllButton; // 右侧操作:确认 / 取消 const confirmBtn = document.createElement('button'); confirmBtn.className = 'primary-btn'; confirmBtn.textContent = confirmText; const cancelBtn = document.createElement('button'); cancelBtn.className = 'secondary-btn'; cancelBtn.textContent = cancelText; // 将按钮加入 footer(顺序:全选、全不选、确认、取消) footer.appendChild(selectAllBtn); footer.appendChild(selectNoneBtn); footer.appendChild(confirmBtn); footer.appendChild(cancelBtn); // 组装 modal.appendChild(header); modal.appendChild(body); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); // 辅助:批量设置选择状态 const setAllChecked = (checked) => { container.querySelectorAll('.list-item').forEach((row) => { const box = row.querySelector('.list-checkbox'); if (box) { box.checked = checked; row.classList.toggle('selected', checked); } }); }; // 关闭 const close = (result) => { overlay.style.animation = 'fadeOut 0.2s'; setTimeout(() => { overlay.remove(); resolve(result); }, 200); }; // 事件绑定 selectAllBtn.addEventListener('click', () => setAllChecked(true)); selectNoneBtn.addEventListener('click', () => setAllChecked(false)); cancelBtn.addEventListener('click', () => close(null)); overlay.addEventListener('click', (e) => { if (e.target === overlay) close(null); }); confirmBtn.addEventListener('click', () => { const selected = []; container.querySelectorAll('.list-item').forEach((row) => { const checkbox = row.querySelector('.list-checkbox'); if (checkbox && checkbox.checked) { const {key} = row.dataset; if (map.has(key)) selected.push(map.get(key)); } }); close(selected); }); }); } // 创建主图标 const createIcon = () => { const icon = document.createElement('div'); icon.style = ` position: fixed; bottom: ${config.position.bottom}; left: ${config.position.left}; width: ${config.icon[config.icon.type].size}px; height: ${config.icon[config.icon.type].size}px; background: white; border-radius: ${config.icon.image.borderRadius || '8px'}; cursor: pointer; z-index: 9999; box-shadow: 0 3px 5px rgba(0,0,0,0.12), 0 3px 5px rgba(0,0,0,0.24); display: flex; align-items: center; justify-content: center; transition: all ${config.animation.duration}s ${config.animation.easing}; `; icon.style.backgroundImage = `url(${config.icon.image.url})`; icon.style.backgroundSize = 'cover'; return icon; }; // 创建菜单容器 const menu = document.createElement('div'); menu.style = ` position: fixed; bottom: calc(${config.position.bottom} + ${config.icon[config.icon.type].size}px + 1rem); left: ${config.position.left}; width: 255px; max-width: calc(100vw - 4rem); background: white; border-radius: 16px; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); overflow: hidden; display: none; z-index: 9998; transform-origin: bottom left; transition: all ${config.animation.duration}s ${config.animation.easing}; opacity: 0; transform: translateY(10px) scaleY(0.95); will-change: transform, opacity; `; // 创建菜单内容容器 const menuContent = document.createElement('div'); menuContent.style = ` max-height: 400px; overflow-y: auto; overscroll-behavior: contain; `; menu.appendChild(menuContent); // 初始化样式 style = document.createElement('style'); style.textContent = ` :root { --primary: #2196F3; --surface: #ffffff; --on-surface: #212121; --ripple-color: rgba(33, 150, 243, 0.15); --border-radius: 12px; } .menu-item { display: flex; padding: 1rem 1.5rem; cursor: pointer; position: relative; transition: all 0.2s ease; align-items: center; } .menu-item:hover { background: var(--ripple-color); } .menu-item:not(:last-child) { border-bottom: 1px solid #eee; } .icon-container { margin-right: 1rem; display: flex; align-items: center; } .material-icons { font-size: 24px; color: var(--primary); } .content { flex: 1; } .menuTitle { font-size: 0.95rem; color: var(--on-surface); font-weight: 500; margin-bottom: 2px; } .subtitle { font-size: 0.75rem; color: #757575; line-height: 1.4; } .menu-enter { animation: slideIn ${config.animation.duration}s ${config.animation.easing}; } .menu-exit { animation: slideOut ${config.animation.duration}s ${config.animation.easing}; } @keyframes slideIn { from { opacity: 0; transform: translateY(10px) scaleY(0.95); } to { opacity: 1; transform: translateY(0) scaleY(1); } } @keyframes slideOut { from { opacity: 1; transform: translateY(0) scaleY(1); } to { opacity: 0; transform: translateY(10px) scaleY(0.95); } } .ripple { position: absolute; border-radius: 50%; transform: scale(0); animation: ripple 0.6s linear; background: var(--ripple-color); pointer-events: none; } @keyframes ripple { to { transform: scale(2); opacity: 0; } } /* 滚动条样式 */ ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 10px; } ::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 10px; } ::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } `; document.head.appendChild(style); // 创建并显示一个自动消失的消息弹窗(Toast) function showToast(message, duration = 5000) { const toast = document.createElement('div'); toast.textContent = message; // 基础样式(可按需调整) Object.assign(toast.style, { position: 'fixed', left: '50%', bottom: '10rem', transform: 'translateX(-50%)', maxWidth: '80vw', padding: '10px 16px', background: 'rgba(128, 128, 128, 0.6)', color: '#fff', fontSize: '16px', lineHeight: '1.4', borderRadius: '8px', boxShadow: '0 6px 16px rgba(0,0,0,0.25)', zIndex: '99999', opacity: '0', transition: 'opacity 200ms ease', pointerEvents: 'none', whiteSpace: 'pre-wrap', textAlign: 'center', }); document.body.appendChild(toast); // 触发淡入 requestAnimationFrame(() => { toast.style.opacity = '1'; }); // 指定时间后淡出并移除 const hide = () => { toast.style.opacity = '0'; const remove = () => { toast.removeEventListener('transitionend', remove); if (toast.parentNode) toast.parentNode.removeChild(toast); }; toast.addEventListener('transitionend', remove); }; setTimeout(hide, duration); } // 涟漪效果 const createRipple = e => { const target = e.currentTarget; const rect = target.getBoundingClientRect(); const size = Math.max(rect.width, rect.height); const x = e.clientX - rect.left - size / 2; const y = e.clientY - rect.top - size / 2; const ripple = document.createElement('span'); ripple.className = 'ripple'; ripple.style.width = ripple.style.height = `${size}px`; ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; target.appendChild(ripple); setTimeout(() => ripple.remove(), 600); }; // 隐藏菜单 let hideTimeout; const hideMenu = () => { if (config.keepMenuVisible) { return; } hideTimeout = setTimeout(() => { menu.classList.remove('menu-enter'); menu.classList.add('menu-exit'); menu.style.opacity = 0; menu.style.transform = 'translateY(10px) scaleY(0.95)'; setTimeout(() => { menu.style.display = 'none'; menu.classList.remove('menu-exit'); isMenuVisible = false; }, config.animation.duration * 1000); }, 100); }; let currentUrl; // 动态生成菜单内容 const updateMenuContent = () => { menuContent.innerHTML = ''; // 根据URL生成不同菜单项 currentUrl = window.location.href; const menuItems = []; if (!config.disclaimer) { menuItems.push({ text: 'README', icon: ' 📄 ', action: readme, description: t.readmeMenuTitle },); } else if (currentUrl === "https://www.xiaohongshu.com/explore" || currentUrl.includes( "https://www.xiaohongshu.com/explore?")) { menuItems.push({ text: t.extractRecommendLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(-1), description: t.extractRecommendLinksDescription },); } else if (currentUrl.includes("https://www.xiaohongshu.com/explore/") || currentUrl.includes( "https://www.xiaohongshu.com/discovery/item/")) { menuItems.push({ text: t.downloadNoteFilesText, icon: ' 📦 ', action: () => extractDownloadLinks(false), description: t.downloadNoteFilesDescription },); if (config.scriptServerSwitch) { menuItems.push({ text: t.pushDownloadTaskText, icon: ' 🌏 ', action: () => extractDownloadLinks(true), description: t.pushDownloadTaskDescription }); } } else if (currentUrl.includes("https://www.xiaohongshu.com/user/profile/")) { menuItems.push({ text: t.extractPublishedLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(0), description: t.extractPublishedLinksDescription }, { text: t.extractLikedLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(2), description: t.extractLikedLinksDescription }, { text: t.extractSavedLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(1), description: t.extractSavedLinksDescription },); } else if (currentUrl.includes("https://www.xiaohongshu.com/search_result")) { menuItems.push({ text: t.extractSearchNoteLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(3), description: t.extractSearchNoteLinksDescription }, { text: t.extractSearchUsersLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(4), description: t.extractSearchUsersLinksDescription },); } else if (currentUrl.includes("https://www.xiaohongshu.com/board/")) { menuItems.push({ text: t.extractAlbumNotesLinksText, icon: ' ⛓ ', action: () => extractAllLinksEvent(5), description: t.extractAlbumNotesLinksDescription },); } // 常用功能 menuItems.push({ separator: true }, { text: t.modifyScriptSettingsText, icon: ' ⚙️ ', action: showSettings, description: t.modifyScriptSettingsDescription }, { text: t.aboutXHSText, icon: ' 📒 ', action: about, description: t.aboutXHSDescription }); // 创建菜单项 menuItems.forEach(item => { if (item.separator) { const divider = document.createElement('div'); divider.style = ` height: 8px; background: #f5f5f5; `; menuContent.appendChild(divider); return; } const btn = document.createElement('div'); btn.className = 'menu-item'; btn.innerHTML = `
${item.icon}
${item.description}
`; btn.addEventListener('click', (e) => { e.stopPropagation(); item.action(); hideMenu(); }); btn.addEventListener('mousedown', createRipple); menuContent.appendChild(btn); }); }; // URL监测相关 let lastUrl = window.location.href; let isMenuVisible = false; // 显示菜单 const showMenu = () => { clearTimeout(hideTimeout); menu.style.display = 'block'; void menu.offsetHeight; // 触发重绘 menu.classList.add('menu-enter'); menu.style.opacity = 1; menu.style.transform = 'translateY(0) scaleY(1)'; updateMenuContent(); isMenuVisible = true; }; // 事件监听 const icon = createIcon(); icon.addEventListener('mouseenter', showMenu); icon.addEventListener('mouseleave', hideMenu); menu.addEventListener('mouseenter', () => clearTimeout(hideTimeout)); menu.addEventListener('mouseleave', hideMenu); // URL变化监听 const setupUrlListener = () => { const observeUrl = () => { if (window.location.href !== lastUrl) { lastUrl = window.location.href; if (isMenuVisible) { updateMenuContent(); } } requestAnimationFrame(observeUrl); }; observeUrl(); }; // 添加到页面 document.body.appendChild(icon); document.body.appendChild(menu); document.head.appendChild(style); setupUrlListener(); if (config.keepMenuVisible) { showMenu(); } class WebSocketManager { constructor(url) { this.url = url; this.ws = null; } onOpen() { } onMessage(message) { } onClose(event) { } onError(error) { console.error('Script Server WebSocket error:', error); showToast(t.scriptServerError,); } get isConnected() { return this.ws && this.ws.readyState === WebSocket.OPEN; } connect() { if (this.ws && this.ws.readyState !== WebSocket.CLOSED) { return; } try { this.ws = new WebSocket(this.url); this.ws.onopen = (event) => this.onOpen(event); this.ws.onmessage = (event) => this.onMessage(event); this.ws.onclose = (event) => { this.ws = null; this.onClose(event); }; this.ws.onerror = (event) => { this.ws = null; this.onError(event); }; } catch (error) { this.onError(error); } } disconnect() { if (this.isConnected) { this.ws.close(); } } send(data) { if (this.isConnected) { this.ws.send(data); showToast(t.pushTaskSuccess); } else { showToast(t.pushTaskError,); } } } const webSocket = new WebSocketManager(config.scriptServerURL,); if (config.scriptServerSwitch) { webSocket.connect(); } })(); ================================================ FILE: static/XHS-Downloader.tcss ================================================ 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; } ================================================ FILE: static/自动滚动页面.js ================================================ // ==UserScript== // @name 自动滚动页面 // @namespace http://tampermonkey.net/ // @version 0.1 // @description 模拟自然滚动,测试页面滚动效果 // @author ChatGPT, JoeanAmier // @match *://*/* // 匹配所有页面,也可以指定具体网站 // @grant none // @run-at document-end // 在页面加载完毕后运行脚本 // ==/UserScript== (function () { 'use strict'; // 配置滚动模式 const scrollMode = 'limited'; // 'none'、'endless' 或 'limited' const maxScrollCount = 10; // 最大滚动次数(如果模式是 'limited') // 随机整数生成函数 const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; // 判断是否需要暂停,模拟用户的停顿行为 const shouldPause = () => Math.random() < 0.2; // 20%几率停顿 // 执行一次增量滚动 const scrollOnce = () => { const scrollDistanceMin = 50; // 最小滚动距离 const scrollDistanceMax = 200; // 最大滚动距离 const scrollDistance = getRandomInt(scrollDistanceMin, scrollDistanceMax); console.log(`滚动距离: ${scrollDistance}px`); // 日志输出滚动距离 window.scrollBy(0, scrollDistance); // 增量滚动 }; // 检查是否已经滚动到底部 const isAtBottom = () => { const docHeight = document.documentElement.scrollHeight; const winHeight = window.innerHeight; const scrollPos = window.scrollY; return (docHeight - winHeight - scrollPos <= 10); // 如果距离底部小于10px,认为滚动到底部 }; // 自动滚动主函数 const scrollScreen = (callback, scrollCount = 0,) => { const timeoutMin = 100; // 最小滚动间隔 const timeoutMax = 300; // 最大滚动间隔 console.log('开始滚动...'); const scrollInterval = setInterval(() => { if (shouldPause()) { // 停顿,模拟用户的休息 clearInterval(scrollInterval); setTimeout(() => { scrollScreen(callback, scrollCount,); // 重新启动滚动 }, getRandomInt(500, 1500)); // 随机停顿时间 } else if (scrollMode === 'endless') { // 无限滚动至底部模式 if (!isAtBottom()) { scrollOnce(); // 执行一次滚动 } else { // 到达底部,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 console.log('已经到达页面底部,停止滚动'); } } else if (scrollMode === 'limited') { // 滚动指定次数模式 if (scrollCount < maxScrollCount && !isAtBottom()) { scrollOnce(); // 执行一次滚动 scrollCount++; } else { // 如果到达底部或滚动次数已满,停止滚动 clearInterval(scrollInterval); callback(); // 调用回调函数 console.log(`已经滚动${scrollCount}次,停止滚动`); } } else if (scrollMode === 'none') { // 关闭滚动功能 clearInterval(scrollInterval); console.log('自动滚动已关闭'); } }, getRandomInt(timeoutMin, timeoutMax)); // 随机滚动间隔 }; // 等待页面完全加载后执行滚动 window.addEventListener('load', () => { console.log('页面加载完成'); // 检查页面是否足够长,以便滚动 if (document.body.scrollHeight > window.innerHeight && scrollMode !== 'none') { // 执行自动滚动 scrollScreen(() => { console.log('滚动完成'); }); } else { console.log('页面没有足够的内容进行滚动,或者自动滚动已关闭'); } }); })();