[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: 报告项目问题\ntitle: '[功能异常] '\nlabels: ''\nassignees: JoeanAmier\n\n---\n\n**问题描述**\n\n清晰简洁地描述该错误是什么。\n\nA clear and concise description of what the bug is.\n\n**重现步骤**\n\n重现该问题的步骤：\n\nSteps to reproduce the behavior:\n\n1. ...\n2. ...\n3. ...\n\n**预期结果**\n\n清晰简洁地描述您预期会发生的情况。\n\nA clear and concise description of what you expected to happen.\n\n**补充信息**\n\n在此添加有关该问题的任何其他上下文信息，例如：操作系统、运行方式、配置文件、错误截图、运行日志等。\n\n请注意：提供配置文件时，请删除 Cookie 内容，避免敏感数据泄露！\n\nAdd any other contextual information about the issue here, such as operating system, runtime mode, configuration files,\nerror screenshots, runtime logs, etc.\n\nPlease note: When providing configuration files, please delete cookie content to avoid sensitive data leakage!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: 功能优化建议\ntitle: '[优化建议] '\nlabels: ''\nassignees: JoeanAmier\n\n---\n\n**功能请求**\n\n清晰简洁地描述问题是什么。例如：当 [...] 时，我总是感到沮丧。\n\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**描述您希望的解决方案**\n\n清晰简洁地描述您希望发生的情况。\n\nA clear and concise description of what you want to happen.\n\n**描述您考虑过的替代方案**\n\n清晰简洁地描述您考虑过的任何替代解决方案或功能。\n\nA clear and concise description of any alternative solutions or features you've considered.\n\n**补充信息**\n\n在此添加有关功能请求的任何其他上下文或截图。\n\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nregistries:\n  pip_mirror:\n    type: python-index\n    url: https://mirrors.ustc.edu.cn/pypi/simple\nupdates:\n  - package-ecosystem: \"uv\"\n    directory: \"/\"\n    schedule:\n      interval: \"cron\"\n      cronjob: \"0 0 * * 6\"\n      timezone: \"Asia/Shanghai\"\n    target-branch: \"develop\"\n    registries:\n      - pip_mirror\n"
  },
  {
    "path": ".github/workflows/Close_Stale_Issues_and_PRs.yaml",
    "content": "name: \"自动管理过时的问题和PR\"\non:\n  schedule:\n    - cron: \"0 0 * * 6\"\n  workflow_dispatch:\n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v9\n        with:\n          stale-issue-message: |\n            ⚠️ 此 Issue 已超过一定时间未活动，如果没有进一步更新，将在 14 天后关闭。\n            ⚠️ This issue has been inactive for a certain period of time. If there are no further updates, it will be closed in 14 days.\n          close-issue-message: |\n            🔒 由于长时间未响应，此 Issue 已被自动关闭。如有需要，请重新打开或提交新 issue。\n            🔒 Due to prolonged inactivity, this issue has been automatically closed. If needed, please reopen it or submit a new issue.\n          stale-pr-message: |\n            ⚠️ 此 PR 已超过一定时间未更新，请更新，否则将在 14 天后关闭。\n            ⚠️ This PR has not been updated for a certain period of time. Please update it, otherwise it will be closed in 14 days.\n          close-pr-message: |\n            🔒 此 PR 已因无更新而自动关闭。如仍需合并，请重新打开或提交新 PR。\n            🔒 This PR has been automatically closed due to inactivity. If you still wish to merge it, please reopen it or submit a new PR.\n\n          days-before-stale: 28\n          days-before-close: 14\n\n          ascending: true\n\n          stale-issue-label: \"未跟进问题(Stale)\"\n          close-issue-label: \"自动关闭(Close)\"\n          stale-pr-label: \"未跟进问题(Stale)\"\n          close-pr-label: \"自动关闭(Close)\"\n          exempt-issue-labels: \"功能异常(bug),文档补充(docs),功能优化(enhancement),适合新手(good first issue),\"\n          exempt-pr-labels: \"功能异常(bug),文档补充(docs),功能优化(enhancement),适合新手(good first issue),\"\n"
  },
  {
    "path": ".github/workflows/Delete_untagged_images.yml",
    "content": "name: 删除 GHCR Untagged 镜像\non:\n  schedule:\n    - cron: \"0 0 15 * *\"\n  release:\n    types: [ published ]\n  workflow_dispatch:\n\njobs:\n  delete-untagged:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Delete all containers from package without tags\n        uses: Chizkiyahu/delete-untagged-ghcr-action@v6\n        with:\n          token: ${{ secrets.PAT_TOKEN }}\n          repository_owner: ${{ github.repository_owner }}\n          repository: ${{ github.repository }}\n          package_name: \"tiktok-downloader\"\n          untagged_only: true\n          owner_type: user\n"
  },
  {
    "path": ".github/workflows/Manually_build_executable_programs.yml",
    "content": "name: 构建可执行文件\n\non:\n  workflow_dispatch:\n\njobs:\n  build:\n    name: 构建于 ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ windows-latest, windows-11-arm, macos-15-intel, macos-latest ]\n\n    steps:\n      - name: 签出存储库\n        uses: actions/checkout@v4\n\n      - name: 设置 Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: 安装依赖项\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n          pip install pyinstaller\n\n      - name: 构建 Win 可执行文件\n        if: runner.os == 'Windows'\n        run: |\n          echo \"DATE=$(Get-Date -Format 'yyyyMMdd')\" >> $env:GITHUB_ENV\n          pyinstaller --icon=./static/images/DouK-Downloader.ico --add-data \"static:static\" --add-data \"locale:locale\" --collect-all rich main.py\n        shell: pwsh\n\n      - name: 构建 Mac 可执行文件\n        if: runner.os == 'macOS'\n        run: |\n          echo \"DATE=$(date +'%Y%m%d')\" >> $GITHUB_ENV\n          pyinstaller --icon=./static/images/DouK-Downloader.icns --add-data \"static:static\" --add-data \"locale:locale\" --collect-all rich main.py\n\n      - name: 上传文件\n        uses: actions/upload-artifact@v4\n        with:\n          name: DouK-Downloader_${{ runner.os }}_${{ runner.arch }}_${{ env.DATE }}\n          path: dist/main/\n"
  },
  {
    "path": ".github/workflows/Manually_docker_image.yml",
    "content": "name: 构建并发布 Docker 镜像\n\non:\n  workflow_dispatch:\n    inputs:\n      is_beta:\n        type: boolean\n        required: true\n        description: \"开发版\"\n        default: true\n      custom_version:\n        type: string\n        required: false\n        description: \"版本号\"\n        default: \"\"\n\npermissions:\n  contents: read\n  packages: write\n  attestations: write\n  id-token: write\n\nenv:\n  REGISTRY: ghcr.io\n  DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/tiktok-downloader\n  GHCR_REPO: ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/tiktok-downloader\n\njobs:\n  publish-docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: 拉取源码\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: 获取最新的发布标签\n        id: get-latest-release\n        run: |\n          if [ -z \"${{ github.event.inputs.custom_version }}\" ]; then\n            LATEST_TAG=$(curl -s \\\n              -H \"Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}\" \\\n              https://api.github.com/repos/${{ github.repository }}/releases/latest \\\n              | jq -r '.tag_name')\n          else\n            LATEST_TAG=${{ github.event.inputs.custom_version }}\n          fi\n          if [ -z \"$LATEST_TAG\" ]; then\n            exit 1\n          fi\n          echo \"LATEST_TAG=$LATEST_TAG\" >> $GITHUB_ENV\n\n      - name: 设置 QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: 设置 Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: 生成标签\n        id: generate-tags\n        run: |\n          if [ \"${{ inputs.is_beta }}\" == \"true\" ]; then\n            LATEST_TAG=\"${LATEST_TAG%.*}.$(( ${LATEST_TAG##*.} + 1 ))\"\n            echo \"LATEST_TAG=$LATEST_TAG\" >> $GITHUB_ENV\n            TAGS=\"${{ env.DOCKER_REPO }}:${LATEST_TAG}-dev,${{ env.GHCR_REPO }}:${LATEST_TAG}-dev\"\n          else\n            TAGS=\"${{ env.DOCKER_REPO }}:${LATEST_TAG},${{ env.DOCKER_REPO }}:latest,${{ env.GHCR_REPO }}:${LATEST_TAG},${{ env.GHCR_REPO }}:latest\"\n          fi\n          echo \"TAGS=$TAGS\" >> $GITHUB_ENV\n\n      - name: 登录到 Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: 登录到 GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: 构建和推送 Docker 镜像到 Docker Hub 和 GHCR\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          push: true\n          tags: ${{ env.TAGS }}\n          provenance: false\n          sbom: false\n"
  },
  {
    "path": ".github/workflows/Release_build_executable_program.yml",
    "content": "name: 自动构建并发布可执行文件\n\non:\n  release:\n    types: [ published ]\n\npermissions:\n  contents: write\n  discussions: write\n\njobs:\n  build:\n    name: 构建于 ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ windows-latest, windows-11-arm, macos-15-intel, macos-latest ]\n\n    steps:\n      - name: 签出存储库\n        uses: actions/checkout@v4\n\n      - name: 设置 Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: 安装依赖项\n        run: |\n          python -m pip install --upgrade pip\n          pip install -r requirements.txt\n          pip install pyinstaller\n\n      - name: 构建 Win 可执行文件\n        if: runner.os == 'Windows'\n        run: |\n          pyinstaller --icon=./static/images/DouK-Downloader.ico --add-data \"static:static\" --add-data \"locale:locale\" --collect-all rich main.py\n        shell: pwsh\n\n      - name: 构建 Mac 可执行文件\n        if: runner.os == 'macOS'\n        run: |\n          pyinstaller --icon=./static/images/DouK-Downloader.icns --add-data \"static:static\" --add-data \"locale:locale\" --collect-all rich main.py\n\n      - name: 创建压缩包\n        run: |\n          7z a \"DouK-Downloader_V${{ github.event.release.tag_name }}_${{ runner.os }}_${{ runner.arch }}.zip\" ./dist/main/*\n        shell: bash\n\n      - name: 上传文件到 release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            ./DouK-Downloader_V*.zip\n          name: DouK-Downloader V${{ github.event.release.tag_name }}\n          body_path: ./docs/Release_Notes.md\n          draft: ${{ github.event.release.draft }}\n          prerelease: ${{ github.event.release.prerelease }}\n"
  },
  {
    "path": ".github/workflows/Release_docker_image.yml",
    "content": "name: 自动构建并发布 Docker 镜像\n\non:\n  release:\n    types: [ published ]\n\npermissions:\n  contents: read\n  packages: write\n  attestations: write\n  id-token: write\n\nenv:\n  REGISTRY: ghcr.io\n  DOCKER_REPO: ${{ secrets.DOCKERHUB_USERNAME }}/tiktok-downloader\n  GHCR_REPO: ghcr.io/${{ secrets.DOCKERHUB_USERNAME }}/tiktok-downloader\n\njobs:\n  publish-docker:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: 拉取源码\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: 设置 QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: 设置 Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: 登录到 Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: 登录到 GitHub Container Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: 构建和推送 Docker 镜像到 Docker Hub 和 GHCR\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          platforms: linux/amd64,linux/arm64,linux/arm/v7\n          push: true\n          tags: |\n            ${{ env.DOCKER_REPO }}:${{ github.event.release.tag_name }}\n            ${{ env.DOCKER_REPO }}:latest\n            ${{ env.GHCR_REPO }}:${{ github.event.release.tag_name }}\n            ${{ env.GHCR_REPO }}:latest\n          provenance: false\n          sbom: false\n"
  },
  {
    "path": ".gitignore",
    "content": "__pycache__/\n*.pyc\n/.venv/\n/.ruff_cache/\n/.idea/\n/.run/\n/Volume/\n!/.github/\n"
  },
  {
    "path": ".python-version",
    "content": "3.12\n"
  },
  {
    "path": "Dockerfile",
    "content": "# ---- 阶段 1: 构建器 (Builder) ----\n# 使用一个功能完整的镜像，它包含编译工具或可以轻松安装它们\nFROM python:3.12-bullseye as builder\n\n# 安装编译 uvloop 和 httptools 所需的系统依赖 (C编译器等)\nRUN apt-get update && apt-get install -y --no-install-recommends \\\n    build-essential \\\n    && rm -rf /var/lib/apt/lists/*\n\n# 设置工作目录\nWORKDIR /app\n\n# 复制需求文件\nCOPY requirements.txt .\n\n# 在这个具备编译环境的阶段安装所有 Python 依赖\n# 安装到一个独立的目录 /install 中，以便后续复制\nRUN pip install --no-cache-dir --prefix=\"/install\" -r requirements.txt\n\n# ---- 阶段 2: 最终镜像 (Final Image) ----\n# 使用轻量级 slim 镜像作为最终的运行环境\nFROM python:3.12-slim\n\n# 设置工作目录\nWORKDIR /app\n\n# 添加元数据标签\nLABEL name=\"DouK-Downloader\" authors=\"JoeanAmier\" repository=\"https://github.com/JoeanAmier/TikTokDownloader\"\n\n# 从构建器阶段，将已经安装好的依赖包复制到最终镜像的系统路径中\nCOPY --from=builder /install /usr/local\n\n# 复制你的应用程序代码和相关文件\nCOPY src /app/src\nCOPY locale /app/locale\nCOPY static /app/static\nCOPY license /app/license\nCOPY main.py /app/main.py\n\n# 暴露端口\nEXPOSE 5555\n\n# 创建挂载点\nVOLUME /app/Volume\n\n# 设置容器启动命令\nCMD [\"python\", \"main.py\"]\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img src=\"./static/images/DouK-Downloader.png\" alt=\"DouK-Downloader\" height=\"256\" width=\"256\"><br>\n<h1>DouK-Downloader</h1>\n<p>简体中文 | <a href=\"README_EN.md\">English</a></p>\n<a href=\"https://trendshift.io/repositories/6222\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/6222\" alt=\"\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n<br>\n<img alt=\"GitHub\" src=\"https://img.shields.io/github/license/JoeanAmier/TikTokDownloader?style=flat-square\">\n<img alt=\"GitHub forks\" src=\"https://img.shields.io/github/forks/JoeanAmier/TikTokDownloader?style=flat-square&color=55efc4\">\n<img alt=\"GitHub Repo stars\" src=\"https://img.shields.io/github/stars/JoeanAmier/TikTokDownloader?style=flat-square&color=fda7df\">\n<img alt=\"GitHub code size in bytes\" src=\"https://img.shields.io/github/languages/code-size/JoeanAmier/TikTokDownloader?style=flat-square&color=a29bfe\">\n<br>\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Python-3.12-b8e994?style=flat-square&logo=python&labelColor=3dc1d3\">\n<img alt=\"GitHub release (with filter)\" src=\"https://img.shields.io/github/v/release/JoeanAmier/TikTokDownloader?style=flat-square&color=48dbfb\">\n<img src=\"https://img.shields.io/badge/Sourcery-enabled-884898?style=flat-square&color=1890ff\" alt=\"\">\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Docker-badc58?style=flat-square&logo=docker\">\n<img alt=\"GitHub all releases\" src=\"https://img.shields.io/github/downloads/JoeanAmier/TikTokDownloader/total?style=flat-square&color=ffdd59\">\n</div>\n<br>\n<p>🔥 <b>TikTok 发布/喜欢/合辑/直播/视频/图集/音乐；抖音发布/喜欢/收藏/收藏夹/视频/图集/实况/直播/音乐/合集/评论/账号/搜索/热榜数据采集工具：</b>完全开源，基于 HTTPX 模块实现的免费数据采集和文件下载工具；批量下载抖音账号发布、喜欢、收藏、收藏夹作品；批量下载 TikTok 账号发布、喜欢作品；下载抖音链接或 TikTok 链接作品；获取抖音直播拉流地址；下载抖音直播视频；获取 TikTok 直播拉流地址；下载 TikTok 直播视频；采集抖音作品评论数据；批量下载抖音合集作品；批量下载 TikTok 合辑作品；采集抖音账号详细数据；采集抖音用户 / 作品 / 直播搜索结果；采集抖音热榜数据。</p>\n<p>⭐ 本项目历史名称：<code>TikTokDownloader</code></p>\n<p>📣 本项目将于未来进行代码结构重构，目标是让代码更加稳健，并具备更好的可维护性与扩展性；如果你对项目设计、实现方式或优化思路有想法，欢迎提出建议或参与讨论！</p>\n<hr>\n\n# 📝 项目功能\n\n<details>\n<summary>功能列表（点击展开）</summary>\n<ul>\n<li>✅ 下载抖音视频/图集</li>\n<li>✅ 下载抖音实况/动图</li>\n<li>✅ 下载最高画质视频文件</li>\n<li>✅ 下载 TikTok 视频原画</li>\n<li>✅ 下载 TikTok 视频/图集</li>\n<li>✅ 下载抖音账号发布/喜欢/收藏/收藏夹作品</li>\n<li>✅ 下载 TikTok 账号发布/喜欢作品</li>\n<li>✅ 采集抖音 / TikTok 详细数据</li>\n<li>✅ 批量下载链接作品</li>\n<li>✅ 多账号批量下载作品</li>\n<li>✅ 自动跳过已下载的文件</li>\n<li>✅ 持久化保存采集数据</li>\n<li>✅ 支持 CSV/XLSX/SQLite 格式保存数据</li>\n<li>✅ 下载动态/静态封面图</li>\n<li>✅ 获取抖音直播拉流地址</li>\n<li>✅ 获取 TikTok 直播拉流地址</li>\n<li>✅ 调用 ffmpeg 下载直播</li>\n<li>✅ Web UI 交互界面</li>\n<li>✅ 采集抖音作品评论数据</li>\n<li>✅ 下载抖音合集作品</li>\n<li>✅ 下载 TikTok 合辑作品</li>\n<li>✅ 记录点赞收藏等统计数据</li>\n<li>✅ 筛选作品发布时间</li>\n<li>✅ 支持账号作品增量下载</li>\n<li>✅ 支持使用代理采集数据</li>\n<li>✅ 支持局域网远程访问</li>\n<li>✅ 采集抖音账号详细数据</li>\n<li>✅ 作品统计数据更新</li>\n<li>✅ 支持自定义账号/合集标识</li>\n<li>✅ 自动更新账号昵称/标识</li>\n<li>✅ 部署至私有服务器</li>\n<li>✅ 部署至公开服务器</li>\n<li>✅ 采集抖音搜索数据</li>\n<li>✅ 采集抖音热榜数据</li>\n<li>✅ 记录已下载作品 ID</li>\n<li>☑️ <del>扫码登陆获取 Cookie</del></li>\n<li>✅ 从浏览器读取 Cookie</li>\n<li>✅ 支持 Web API 调用</li>\n<li>✅ 支持多线程下载作品</li>\n<li>✅ 文件完整性处理机制</li>\n<li>✅ 自定义规则筛选作品</li>\n<li>✅ 按文件夹归档保存作品文件</li>\n<li>✅ 自定义设置文件大小上限</li>\n<li>✅ 支持文件断点续传下载</li>\n<li>✅ 监听剪贴板链接下载作品</li>\n</ul>\n</details>\n\n# 💻 程序截图\n\n<p><a href=\"https://www.bilibili.com/video/BV1d7eAzTEFs/\">前往 bilibili 观看演示</a>；<a href=\"https://youtu.be/yMU-RWl55hg\">前往 YouTube 观看演示</a></p>\n\n## 终端交互模式\n\n<p>建议通过配置文件管理账号，更多介绍请查阅 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation\">文档</a></p>\n\n![终端模式截图](docs/screenshot/终端交互模式截图CN1.png)\n*****\n![终端模式截图](docs/screenshot/终端交互模式截图CN2.png)\n*****\n![终端模式截图](docs/screenshot/终端交互模式截图CN3.png)\n\n## Web UI 交互模式\n\n> **项目代码已重构，该模式代码尚未更新，未来开发完成重新开放！**\n\n## Web API 接口模式\n\n![WebAPI模式截图](docs/screenshot/WebAPI模式截图CN1.png)\n*****\n![WebAPI模式截图](docs/screenshot/WebAPI模式截图CN2.png)\n\n> **启动该模式后，访问 `http://127.0.0.1:5555/docs` 或者 `http://127.0.0.1:5555/redoc` 可以查阅自动生成的文档！**\n\n### API 调用示例代码\n\n```python\nfrom httpx import post\nfrom rich import print\n\n\ndef demo():\n    headers = {\"token\": \"\"}\n    data = {\n        \"detail_id\": \"0123456789\",\n        \"pages\": 2,\n    }\n    api = \"http://127.0.0.1:5555/douyin/comment\"\n    response = post(api, json=data, headers=headers)\n    print(response.json())\n\n\ndemo()\n```\n\n# 📋 项目说明\n\n## 快速入门\n\n<p>⭐ Mac OS、Windows 10 及以上用户可前往 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> 或者 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/actions\">Actions</a> 下载已编译的程序，开箱即用！</p>\n<p>⭐ 本项目包含自动构建可执行文件的 GitHub Actions，使用者可以随时使用 GitHub Actions 将最新源码构建为可执行文件！</p>\n<p>⭐ 自动构建可执行文件教程请查阅本文档的 <code>构建可执行文件指南</code> 部分；如果需要更加详细的图文教程，请 <a href=\"https://mp.weixin.qq.com/s/TorfoZKkf4-x8IBNLImNuw\">查阅文章</a>！</p>\n<p><strong>注意：由于 Mac OS 平台的可执行文件 <code>main</code> 未经过代码签名，首次运行时会受到系统安全限制。请先在终端执行 <code>xattr -cr 项目文件夹路径</code> 命令移除安全标记，执行一次后即可正常运行。</strong></p>\n<hr>\n<ol>\n<li><b>运行可执行文件</b> 或者 <b>配置环境运行</b>（二选一）\n<ol><b>运行可执行文件</b>\n<li>下载 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> 或者 Actions 构建的可执行文件压缩包</li>\n<li>解压后打开程序文件夹，双击运行 <code>main</code></li>\n</ol>\n<ol><b>配置环境运行</b>\n\n[//]: # (<li>安装不低于 <code>3.12</code> 版本的 <a href=\"https://www.python.org/\">Python</a> 解释器</li>)\n<li>安装 <code>3.12</code> 版本的 <a href=\"https://www.python.org/\">Python</a> 解释器</li>\n<li>下载最新的源码或 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> 发布的源码至本地</li>\n<ol><b>使用 pip 安装项目依赖</b>\n<li>运行 <code>python -m venv venv</code> 命令创建虚拟环境（可选）</li>\n<li>运行 <code>.\\venv\\Scripts\\activate.ps1</code> 或者 <code>venv\\Scripts\\activate</code> 命令激活虚拟环境（可选）</li>\n<li>运行 <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> 命令安装程序所需模块</li>\n<li>运行 <code>python .\\main.py</code> 或者 <code>python main.py</code> 命令启动 DouK-Downloader</li>\n</ol>\n<ol><b>使用 uv 安装项目依赖（推荐）</b>\n<li>运行 <code>uv sync --no-dev</code> 命令同步环境依赖</li>\n<li>运行 <code>uv run main.py</code> 命令启动 DouK-Downloader</li>\n</ol>\n</ol>\n</li>\n<li>阅读 DouK-Downloader 的免责声明，根据提示输入内容</li>\n<li>将 Cookie 信息写入配置文件\n<ol><b>从剪贴板读取 Cookie（推荐）</b>\n<li>参考 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie 提取教程</a>，复制所需 Cookie 至剪贴板</li>\n<li>选择 <code>从剪贴板读取 Cookie</code> 选项，程序会自动读取剪贴板的 Cookie 并写入配置文件</li>\n</ol>\n<ol><b>从浏览器读取 Cookie</b>\n<li>选择 <code>从浏览器读取 Cookie</code> 选项，按照提示输入浏览器类型或序号</li>\n</ol>\n<ol><b><del>扫码登录获取 Cookie</del>（失效）</b>\n<li><del>选择 <code>扫码登录获取 Cookie</code> 选项，程序会显示登录二维码图片，并使用默认应用打开图片</del></li>\n<li><del>使用抖音 APP 扫描二维码并登录账号</del></li>\n<li><del>按照提示操作，程序会自动将 Cookie 写入配置文件</del></li>\n</ol>\n</li>\n<li>返回程序界面，依次选择 <code>终端交互模式</code> -> <code>批量下载链接作品(通用)</code> -> <code>手动输入待采集的作品链接</code></li>\n<li>输入抖音作品链接即可下载作品文件（TikTok 平台需要更多初始设置，详见文档）</li>\n<li>更多详细说明请查看 <b><a href=\"https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation\">项目文档</a></b></li>\n</ol>\n<p>⭐ 推荐使用 <a href=\"https://learn.microsoft.com/zh-cn/windows/terminal/install\">Windows 终端</a>（Windows 11 自带默认终端）</p>\n\n### Docker 容器\n\n<ol>\n<li>获取镜像</li>\n<ul>\n<li>方式一：使用 <code>Dockerfile</code> 文件构建镜像</li>\n<li>方式二：使用 <code>docker pull joeanamier/tiktok-downloader</code> 命令拉取镜像</li>\n<li>方式三：使用 <code>docker pull ghcr.io/joeanamier/tiktok-downloader</code> 命令拉取镜像</li>\n</ul>\n<li>创建容器：<code>docker run --name 容器名称(可选) -p 主机端口号:5555 -v tiktok_downloader_volume:/app/Volume -it &lt;镜像名称&gt;</code>\n</li>\n<br><b>注意：</b>此处的 <code>&lt;镜像名称&gt;</code> 需与您在第一步中使用的镜像名称保持一致（例如 <code>joeanamier/tiktok-downloader</code> 或 <code>ghcr.io/joeanamier/tiktok-downloader</code>）\n<li>运行容器\n<ul>\n<li>启动容器：<code>docker start -i 容器名称/容器 ID</code></li>\n<li>重启容器：<code>docker restart -i 容器名称/容器 ID</code></li>\n</ul>\n</li>\n</ol>\n<p>Docker 容器无法直接访问宿主机的文件系统，部分功能不可用，例如：<code>从浏览器读取 Cookie</code>；其他功能如有异常请反馈！</p>\n<hr>\n\n## 关于 Cookie\n\n[点击查看 Cookie 获取教程](https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md)\n\n> * Cookie 仅需在失效后重新写入配置文件，并非每次运行程序都要写入配置文件！\n>\n> * Cookie 会影响下载的视频文件分辨率，如果无法下载最高分辨率的视频文件，请尝试更新 Cookie！\n>\n> * 程序获取数据失败时，可以尝试更新 Cookie 或者使用已登录的 Cookie！\n\n<hr>\n\n## 其他说明\n\n<ul>\n<li>程序提示用户输入时，直接回车代表返回上级菜单，输入 <code>Q</code> 或 <code>q</code> 代表结束运行</li>\n<li>由于获取账号喜欢作品和收藏作品数据仅返回喜欢 / 收藏作品的发布日期，不返回操作日期，因此程序需要获取全部喜欢 / 收藏作品数据再进行日期筛选；如果作品数量较多，可能会花费较长的时间；可通过 <code>max_pages</code> 参数控制请求次数</li>\n<li>获取私密账号的发布作品数据需要登录后的 Cookie，且登录的账号需要关注该私密账号</li>\n<li>批量下载账号作品或合集作品时，如果对应的昵称或标识发生变化，程序会自动更新已下载作品文件名称中的昵称和标识</li>\n<li>程序下载文件时会先将文件下载至临时文件夹，下载完成后再移动至储存文件夹；程序运行结束时会清空临时文件夹</li>\n<li><code>批量下载收藏作品模式</code> 目前仅支持下载当前已登录 Cookie 对应账号的收藏作品，暂不支持多账号</li>\n<li>如果想要程序使用代理请求数据，必须在 <code>settings.json</code> 设置 <code>proxy</code> 参数，否则程序不会使用代理</li>\n<li>如果您的计算机没有合适的程序编辑 JSON 文件，建议使用 <a href=\"https://www.toolhelper.cn/JSON/JSONFormat\">在线工具</a> 编辑配置文件内容，修改后需要重启软件才能生效。</li>\n<li>当程序请求用户输入内容或链接时，请注意避免输入的内容或链接包含换行符，这可能会导致预期之外的问题</li>\n<li>本项目不会支持付费作品下载，请勿反馈任何关于付费作品下载的问题</li>\n<li>Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie</li>\n<li>本项目并未针对程序多开的情况进行优化，如需程序多开，请复制整个项目的文件夹，避免出现预期之外的问题</li>\n<li>程序运行过程中，如需终止程序或 <code>ffmpeg</code>，请按下 <code>Ctrl + C</code> 终止运行，不要直接点击终端窗口的关闭按钮</li>\n</ul>\n<h2>构建可执行文件指南</h2>\n<details>\n<summary><b>构建可执行文件指南（点击展开）</b></summary>\n\n本指南将引导您通过 Fork 本仓库并执行 GitHub Actions 自动完成基于最新源码的程序构建和打包！\n\n---\n\n### 使用步骤\n\n#### 1. Fork 本仓库\n\n1. 点击项目仓库右上角的 **Fork** 按钮，将本仓库 Fork 到您的个人 GitHub 账户中\n2. 您的 Fork 仓库地址将类似于：`https://github.com/your-username/this-repo`\n\n---\n\n#### 2. 启用 GitHub Actions\n\n1. 前往您 Fork 的仓库页面\n2. 点击顶部的 **Settings** 选项卡\n3. 点击右侧的 **Actions** 选项卡\n4. 点击 **General** 选项\n5. 在 **Actions permissions** 下，选择 **Allow all actions and reusable workflows** 选项，点击 **Save** 按钮\n\n---\n\n#### 3. 手动触发打包流程\n\n1. 在您 Fork 的仓库中，点击顶部的 **Actions** 选项卡\n2. 找到名为 **构建可执行文件** 的工作流\n3. 点击右侧的 **Run workflow** 按钮：\n    - 选择 **master** 或者 **develop** 分支\n    - 点击 **Run workflow**\n\n---\n\n#### 4. 查看打包进度\n\n1. 在 **Actions** 页面中，您可以看到触发的工作流运行记录\n2. 点击运行记录，查看详细的日志以了解打包进度和状态\n\n---\n\n#### 5. 下载打包结果\n\n1. 打包完成后，进入对应的运行记录页面\n2. 在页面底部的 **Artifacts** 部分，您将看到打包的结果文件\n3. 点击下载并保存到本地，即可获得打包好的程序\n\n---\n\n### 注意事项\n\n1. **资源使用**：\n    - Actions 的运行环境由 GitHub 免费提供，普通用户每月有一定的免费使用额度（2000 分钟）\n\n2. **代码修改**：\n    - 您可以自由修改 Fork 仓库中的代码以定制程序打包流程\n    - 修改后重新触发打包流程，您将得到自定义的构建版本\n\n3. **与主仓库保持同步**：\n    - 如果主仓库更新了代码或工作流，建议您定期同步 Fork 仓库以获取最新功能和修复\n\n---\n\n### Actions 常见问题\n\n#### Q1: 为什么我无法触发工作流？\n\nA: 请确认您已按照步骤 **启用 Actions**，否则 GitHub 会禁止运行工作流\n\n#### Q2: 打包流程失败怎么办？\n\nA:\n\n- 检查运行日志，了解失败原因\n- 确保代码没有语法错误或依赖问题\n- 如果问题仍未解决，可以在本仓库的 [Issues 页面](https://github.com/JoeanAmier/TikTokDownloader/issues) 提出问题\n\n#### Q3: 我可以直接使用主仓库的 Actions 吗？\n\nA: 由于权限限制，您无法直接触发主仓库的 Actions。请通过 Fork 仓库的方式执行打包流程\n\n</details>\n\n## 程序更新\n\n<p><strong>方案一：</strong>下载并解压文件，将旧版本的 <code>_internal\\Volume</code> 文件夹复制到新版本的 <code>_internal</code> 文件夹。</p>\n<p><strong>方案二：</strong>下载并解压文件（不要运行程序），复制全部文件，直接覆盖旧版本文件。</p>\n\n# ⚠️ 免责声明\n\n<ol>\n<li>使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。</li>\n<li>本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。</li>\n<li>本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责任。</li>\n<li>使用者在使用本项目时必须严格遵守 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU\n    General Public License v3.0</a> 的要求，并在适当的地方注明使用了 <a\n        href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU General Public License\n    v3.0</a> 的代码。\n</li>\n<li>使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。</li>\n<li>使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分发。</li>\n<li>本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使用者自行承担。</li>\n<li>使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。</li>\n<li>本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-Downloader 项目相关的任何商业服务。</li>\n<li>基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的各种情况负全部责任。</li>\n<li>本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣传、推广或再授权。</li>\n<li>作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁已获取的代码及衍生作品。</li>\n<li>作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修订后的条款。</li>\n</ol>\n<b>在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险和后果。</b>\n<h1>🌟 贡献指南</h1>\n<p><strong>欢迎对本项目做出贡献！为了保持代码库的整洁、高效和易于维护，请仔细阅读以下指南，以确保您的贡献能够顺利被接受和整合。</strong></p>\n<ul>\n<li>在开始开发前，请从 <code>develop</code> 分支拉取最新的代码，以此为基础进行修改；这有助于避免合并冲突并保证您的改动基于最新的项目状态。</li>\n<li>如果您的更改涉及多个不相关的功能或问题，请将它们分成多个独立的提交或拉取请求。</li>\n<li>每个拉取请求应尽可能专注于单一功能或修复，以便于代码审查和测试。</li>\n<li>遵循现有的代码风格；请确保您的代码与项目中已有的代码风格保持一致；建议使用 Ruff 工具保持代码格式规范。</li>\n<li>编写可读性强的代码；添加适当的注释帮助他人理解您的意图。</li>\n<li>每个提交都应该包含一个清晰、简洁的提交信息，以描述所做的更改。提交信息应遵循以下格式：<code>&lt;类型&gt;: &lt;简短描述&gt;</code></li>\n<li>当您准备提交拉取请求时，请优先将它们提交到 <code>develop</code> 分支；这是为了给维护者一个缓冲区，在最终合并到 <code>master</code>\n分支之前进行额外的测试和审查。</li>\n<li>建议在开发前或遇到疑问时与作者沟通，确保开发方向一致，避免重复劳动或无效提交。</li>\n</ul>\n<p><strong>参考资料：</strong></p>\n<ul>\n<li><a href=\"https://www.contributor-covenant.org/zh-cn/version/2/1/code_of_conduct/\">贡献者公约</a></li>\n<li><a href=\"https://opensource.guide/zh-hans/how-to-contribute/\">如何为开源做贡献</a></li>\n</ul>\n\n# ♥️ 支持项目\n\n<p>如果 <b>DouK-Downloader</b> 对您有帮助，请考虑为它点个 <b>Star</b> ⭐，感谢您的支持！</p>\n<table>\n<thead>\n<tr>\n<th align=\"center\">微信(WeChat)</th>\n<th align=\"center\">支付宝(Alipay)</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\"><img src=\"./docs/微信赞助二维码.png\" alt=\"微信赞助二维码\" height=\"200\" width=\"200\"></td>\n<td align=\"center\"><img src=\"./docs/支付宝赞助二维码.png\" alt=\"支付宝赞助二维码\" height=\"200\" width=\"200\"></td>\n</tr>\n</tbody>\n</table>\n<p>如果您愿意，可以考虑提供资助为 <b>DouK-Downloader</b> 提供额外的支持！</p>\n\n# 💰 项目赞助\n\n## DartNode\n\n[![Powered by DartNode](docs/AD/DartNode_AD.png)](https://dartnode.com \"Powered by DartNode - Free VPS for Open Source\")\n\n***\n\n## ZMTO\n\n<p><a href=\"https://www.zmto.com/\"><img src=\"https://console.zmto.com/templates/2019/dist/images/logo_dark.svg\" alt=\"ZMTO\"></a></p>\n<p><a href=\"https://www.zmto.com/\">ZMTO</a>：一家专业的云基础设施提供商，以可靠的尖端技术与专业支持，提供高效的解决方案，并为符合条件的开源项目提供企业级VPS基础设施，支持开源生态系统的可持续发展与创新。</p>\n\n***\n\n## TikHub\n\n<p><a href=\"https://tikhub.io/?utm_source=github&utm_medium=readme&utm_campaign=tiktok_downloader&ref=github_joeanamier_tiktokdownloader\"><img src=\"docs/AD/TIKHUB_AD.jpg\" alt=\"TIKHUB\" width=\"458\" height=\"319\"></a></p>\n<p><a href=\"https://tikhub.io/?utm_source=github&utm_medium=readme&utm_campaign=tiktok_downloader&ref=github_joeanamier_tiktokdownloader\">TikHub API</a> 提供超过 700 个端点，可用于从 14+ 个社交媒体平台获取与分析数据 —— 包括视频、用户、评论、商店、商品与趋势等，一站式完成所有数据访问与分析。</p>\n<p>使用 <strong>邀请码</strong>：<code>ZrdH8McC</code> 注册并充值即可获得 <code>$2</code> 额度。</p>\n\n# ✉️ 联系作者\n\n<ul>\n<li>作者邮箱：yonglelolu@foxmail.com</li>\n<li>作者微信: Downloader_Tools</li>\n<li>微信公众号: Downloader Tools</li>\n<li><b>Discord 社区</b>: <a href=\"https://discord.com/invite/ZYtmgKud9Y\">点击加入社区</a></li>\n<li>QQ 群聊(用于项目交流与摸鱼闲聊): <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/QQ%E7%BE%A4%E8%81%8A%E4%BA%8C%E7%BB%B4%E7%A0%81.png\">扫码加入群聊</a></li>\n</ul>\n<p>✨ <b>作者的其他开源项目：</b></p>\n<ul>\n<li><b>XHS-Downloader（小红书、XiaoHongShu、RedNote）</b>：<a href=\"https://github.com/JoeanAmier/XHS-Downloader\">https://github.com/JoeanAmier/XHS-Downloader</a></li>\n<li><b>KS-Downloader（快手、KuaiShou）</b>：<a href=\"https://github.com/JoeanAmier/KS-Downloader\">https://github.com/JoeanAmier/KS-Downloader</a></li>\n</ul>\n<h1>⭐ Star 趋势</h1>\n<p>\n<img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=JoeanAmier/TikTokDownloader&amp;type=Timeline\"/>\n</p>\n\n# 💡 项目参考\n\n* https://github.com/Johnserf-Seed/f2\n* https://github.com/Evil0ctal/Douyin_TikTok_Download_API\n* https://github.com/justbeluga/tiktok-web-reverse-engineering\n* https://github.com/ihmily/DouyinLiveRecorder\n* https://github.com/encode/httpx/\n* https://github.com/Textualize/rich\n* https://github.com/omnilib/aiosqlite\n* https://github.com/Tinche/aiofiles\n* https://github.com/pyinstaller/pyinstaller\n* https://foss.heptapod.net/openpyxl/openpyxl\n* https://github.com/carpedm20/emoji/\n* https://github.com/lxml/lxml\n* https://ffmpeg.org/ffmpeg-all.html\n"
  },
  {
    "path": "README_EN.md",
    "content": "<div align=\"center\">\n<img src=\"./static/images/DouK-Downloader.png\" alt=\"DouK-Downloader\" height=\"256\" width=\"256\"><br>\n<h1>DouK-Downloader</h1>\n<p><a href=\"README.md\">简体中文</a> | English</p>\n<a href=\"https://trendshift.io/repositories/6222\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/6222\" alt=\"\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n<br>\n<img alt=\"GitHub\" src=\"https://img.shields.io/github/license/JoeanAmier/TikTokDownloader?style=flat-square\">\n<img alt=\"GitHub forks\" src=\"https://img.shields.io/github/forks/JoeanAmier/TikTokDownloader?style=flat-square&color=55efc4\">\n<img alt=\"GitHub Repo stars\" src=\"https://img.shields.io/github/stars/JoeanAmier/TikTokDownloader?style=flat-square&color=fda7df\">\n<img alt=\"GitHub code size in bytes\" src=\"https://img.shields.io/github/languages/code-size/JoeanAmier/TikTokDownloader?style=flat-square&color=a29bfe\">\n<br>\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Python-3.12-b8e994?style=flat-square&logo=python&labelColor=3dc1d3\">\n<img alt=\"GitHub release (with filter)\" src=\"https://img.shields.io/github/v/release/JoeanAmier/TikTokDownloader?style=flat-square&color=48dbfb\">\n<img src=\"https://img.shields.io/badge/Sourcery-enabled-884898?style=flat-square&color=1890ff\" alt=\"\">\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Docker-badc58?style=flat-square&logo=docker\">\n<img alt=\"GitHub all releases\" src=\"https://img.shields.io/github/downloads/JoeanAmier/TikTokDownloader/total?style=flat-square&color=ffdd59\">\n</div>\n<br>\n<p>🔥 <b>TikTok Posts/Liked/Mix/Live/Video/Image/Music; DouYin Posts/Liked/Favorites/Collections/Video/Image/LivePhoto/Live/Music/Mix/Comments/Account/Search/Hot Board Data Acquisition Tools:</b> Fully open-source, free data collection and file download tool based on HTTPX module implementation; batch download of DouYin account posts works, liked works, favorites works and collections works; batch download of TikTok account posts works and liked works; download of DouYin linked or TikTok linked works; obtain DouYin live stream push addresses; download DouYin live stream video; obtain TikTok live stream push addresses; download TikTok live stream video; collect DouYin works comments data; batch download of DouYin Mix works; batch download of TikTok Mix works; collect detailed data of DouYin accounts; collect DouYin user/works/live search results; collect DouYin Hot Board data.</p>\n<p>⭐ Previous project names: <code>TikTokDownloader</code></p>\n<p>📣 This project will undergo code structure refactoring in the future, with the goal of making the code more robust and providing better maintainability and extensibility. If you have any thoughts on project design, implementation methods, or optimization ideas, you are welcome to make suggestions or participate in discussions!</p>\n<p>⭐ Due to the author's limited energy, I was unable to update the English document in a timely manner, and the content may have become outdated, partial translation is machine translation, the translation result may be incorrect, Suggest referring to Chinese documentation. If you want to contribute to translation, we warmly welcome you.</p>\n<hr>\n\n# 📝 Project Features\n\n<details>\n<summary>Function List (Click to Expand)</summary>\n<ul>\n<li>✅ Download DouYin video/image</li>\n<li>✅ Download DouYin live photo</li>\n<li>✅ Download the highest quality video file</li>\n<li>✅ Download TikTok video source files</li>\n<li>✅ Download TikTok video/image</li>\n<li>✅ Download of DouYin account posts/liked/favorites works</li>\n<li>✅ Download of TikTok account posts/liked works</li>\n<li>✅ Collect detailed data from DouYin/TikTok</li>\n<li>✅ Batch download of linked works</li>\n<li>✅ Batch download of works from multiple accounts</li>\n<li>✅ Automatically skip already downloaded files</li>\n<li>✅ Persistently save collected data</li>\n<li>✅ Support CSV/XLSX/SQLite format for saving data</li>\n<li>✅ Download dynamic/static cover images</li>\n<li>✅ Obtain DouYin live stream push addresses</li>\n<li>✅ Obtain TikTok live stream push addresses</li>\n<li>✅ Use ffmpeg to download live video</li>\n<li>✅ Web UI interaction interface</li>\n<li>✅ Collect comments data from DouYin works</li>\n<li>✅ Batch download of DouYin Mix works</li>\n<li>✅ Batch download of TikTok Mix works</li>\n<li>✅ Record statistics such as likes and favorites</li>\n<li>✅ Filter works based on publication time</li>\n<li>✅ Support incremental downloading of account works</li>\n<li>✅ Support data Collections using proxies</li>\n<li>✅ Support remote access via LAN</li>\n<li>✅ Collect detailed data from DouYin accounts</li>\n<li>✅ Update statistics of works</li>\n<li>✅ Support custom account/mix mark</li>\n<li>✅ Automatically update account nickname/mark</li>\n<li>✅ Deploy to private servers</li>\n<li>✅ Deploy to public servers</li>\n<li>✅ Collect DouYin search data</li>\n<li>✅ Collect DouYin hot board data</li>\n<li>✅ Record IDs of already downloaded works</li>\n<li>☑️ <del>Scan QR code to log in and obtain Cookies</del></li>\n<li>✅ Obtain Cookies from browsers</li>\n<li>✅ Support Web API calls</li>\n<li>✅ Support multithreaded downloading of works</li>\n<li>✅ File integrity processing mechanism</li>\n<li>✅ Custom rules for filtering works</li>\n<li>✅ Archive and save works files by folder</li>\n<li>✅ Customize file size limit</li>\n<li>✅ Support resume downloading of files from breakpoints</li>\n<li>✅ Monitor clipboard links to download works</li>\n</ul>\n</details>\n\n# 💻 Program Screenshot\n\n<p><a href=\"https://www.bilibili.com/video/BV1d7eAzTEFs/\">Watch Demo on Bilibili</a>; <a href=\"https://youtu.be/yMU-RWl55hg\">Watch Demo on YouTube</a></p>\n\n## Terminal interaction mode\n\n<p>It is recommended to manage accounts through configuration files. For more information, please refer to the <a href=\"https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation\">documentation</a></p>\n\n![终端模式截图](docs/screenshot/终端交互模式截图EN1.png)\n*****\n![终端模式截图](docs/screenshot/终端交互模式截图EN2.png)\n*****\n![终端模式截图](docs/screenshot/终端交互模式截图EN3.png)\n\n## Web UI interaction mode\n\n> **The project code has been refactored; the code for this mode has not yet been updated. It will be reopened after\nfuture development is completed!**\n\n## Web API mode\n\n![WebAPI模式截图](docs/screenshot/WebAPI模式截图EN1.png)\n*****\n![WebAPI模式截图](docs/screenshot/WebAPI模式截图EN2.png)\n\n> **After starting this mode, Open http://127.0.0.1:5555/docs or http://127.0.0.1:5555/redoc to access the automatically\ngenerated documentation!**\n\n### API call example code\n\n```python\nfrom httpx import post\nfrom rich import print\n\n\ndef demo():\n    headers = {\"token\": \"\"}\n    data = {\n        \"detail_id\": \"0123456789\",\n        \"pages\": 2,\n    }\n    api = \"http://127.0.0.1:5555/douyin/comment\"\n    response = post(api, json=data, headers=headers)\n    print(response.json())\n\n\ndemo()\n```\n\n# 📋 Project Instructions\n\n## Quick Start\n\n<p>⭐ Mac OS and Windows 10 and above users can go to <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> or <a href=\"https://github.com/JoeanAmier/TikTokDownloader/actions\">Actions</a> to download the compiled program, ready to use!</p>\n<p>⭐ This project includes GitHub Actions for automatic building executable files. Users can use GitHub Actions to build the latest source code into executable files at any time!</p>\n<p>⭐ For the automatic building executable files tutorial, please refer to the <code>Build of Executable File Guide</code> section of this document. If you need a more detailed step-by-step tutorial with illustrations, please <a href=\"https://mp.weixin.qq.com/s/TorfoZKkf4-x8IBNLImNuw\">check out this article</a>!</p>\n<p><strong>Note: Due to the macOS platform's executable file <code>main</code> not being code-signed, it will be restricted by system security measures on first run. Please execute the command <code>xattr -cr project_folder_path</code> in the terminal to remove the security flag, after which it can run normally.</strong></p>\n<hr>\n<ol>\n<li><b>Run the executable file</b> or <b>configure the environment to run</b> (choose one of the two)\n<ol><b>Run the executable file</b>\n<li>Download the executable file compressed file built by <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> or Actions.</li>\n<li>After extracting, open the program folder and double-click to run <code>main</code>.</li>\n</ol>\n<ol><b>Configure the environment to run</b>\n\n[//]: # (<li>Install Python interpreter version not lower than <code>3.12</code></li>)\n<li>Install the <a href=\"https://www.python.org/\">Python</a> interpreter version <code>3.12</code></li>\n<li>Download the latest source code or the source code released in <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> to your local machine</li>\n<ol><b>Install project dependencies using pip</b>\n<li>Run the command <code>python -m venv venv</code> to create a virtual environment (optional)</li>\n<li>Run the command <code>.\\venv\\Scripts\\activate.ps1</code> or <code>venv\\Scripts\\activate</code> to activate the virtual environment (optional)</li>\n<li>Run the command <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> to install the required modules for the program</li>\n<li>Run the command <code>python .\\main.py</code> or <code>python main.py</code> to start DouK-Downloader</li>\n</ol>\n<ol><b>Install project dependencies using uv (recommended)</b>\n<li>Run the command <code>uv sync --no-dev</code> to synchronize environment dependencies</li>\n<li>Run the command <code>uv run main.py</code> to start DouK-Downloader</li>\n</ol>\n</ol>\n</li>\n<li>Read the disclaimer of DouK-Downloader and enter content according to the prompt.</li>\n<li>Write Cookie Information into Configuration File \n<ol><b>Read Cookie from Clipboard(Recommended)</b>\n<li>Refer to the <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie Extraction Tutorial</a>, copy the required Cookie to the clipboard</li>\n<li>Select the <code>Read Cookie from Clipboard</code> option, the program will automatically read the Cookie from the clipboard and write it into the configuration file</li>\n</ol>\n<ol><b>Read Cookie from Browser</b>\n<li>Select the <code>Read Cookie from Browser</code> option, then follow the prompts to input the browser type or its corresponding number</li>\n</ol>\n<ol><b><del>Obtain Cookie via QR Code Login</del> (No longer valid)</b>\n<li><del>Select the <code>Obtain Cookie via QR Code Login</code> option, the program will display a login QR code image and open it with the default application</del></li>\n<li><del>Use the TikTok app to scan the QR code and log in</del></li>\n<li><del>Follow the prompts, the program will automatically write the Cookie into the configuration file</del></li>\n</ol>\n</li>\n<li>Return to the program interface, sequentially select <code>Terminal interactive mode</code> -> <code>Batch download link works (general)</code> -> <code>Manually enter the link of the works to be collected</code>.</li>\n<li>Input the DouYin works link to download the works file (the TikTok platform requires more initial setup, please refer to the documentation for details).</li>\n<li>For more detailed instructions, please see <b><a href=\"https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation\">Project Documentation</a></b>.</li>\n</ol>\n<p>⭐ It is recommended to use <a href=\"https://learn.microsoft.com/zh-cn/windows/terminal/install\">Windows Terminal</a> (the default terminal that comes with Windows 11).</p>\n\n### Docker Container\n\n<ol>\n<li>Get the image</li>\n<ul>\n<li>Method 1: Build the image using the <code>Dockerfile</code>.</li>\n<li>Method 2: Pull the image using the command <code>docker pull joeanamier/tiktok-downloader</code>.</li>\n<li>Method 3: Pull the image using the command <code>docker pull ghcr.io/joeanamier/tiktok-downloader</code>.</li>\n</ul>\n<li>Create the container: <code>docker run --name ContainerName(optional) -p HostPort:5555 -v tiktok_downloader_volume:/app/Volume -it &lt;image name&gt;</code>.</li>\n<br><b>Note:</b> The <code>&lt;image name&gt;</code> here must be consistent with the image name you used in the first step (<code>joeanamier/tiktok-downloader</code> or <code>ghcr.io/joeanamier/tiktok-downloader</code>)\n<li>Run the container\n<ul>\n<li>Start the container: <code>docker start -i container name/container ID</code>.</li>\n<li>Restart the container: <code>docker restart -i container name/container ID</code>.</li>\n</ul>\n</li>\n</ol>\n<p>Docker containers cannot directly access the host machine's file system, and some features may be unavailable, for example: <code>Get Cookie from Browser</code>; if there are any other issues, please report!</p>\n<hr>\n\n## About Cookie\n\n[Click to view Cookie tutorial](https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md)\n\n> * Cookie only needs to be re-written to the configuration file after it expires, and not every time the program is\n    run.\n>\n> * The Cookie can affect the resolution of the video files downloaded from the DouYin platform. If you are unable to\n    download high-resolution video files, please try updating the Cookie!\n>\n> * When the program fails to obtain data, you can try updating the Cookie or using a Cookie that is already logged in!\n\n<hr>\n\n## Other Instructions\n\n<ul>\n<li>When the program prompts the user for input, pressing Enter directly will return to the previous menu, and inputting <code>Q</code> or <code>q</code> will end the program's execution.</li>\n<li>Since fetching data for liked and favorites works of an account only returns the publication dates of those works, not the dates of the actions (liking or favouring), the program needs to retrieve all liked and favorites works data before performing date filtering. If there are a large number of works, this may take a considerable amount of time. The number of requests can be controlled via the <code>max_pages</code> parameter.</li>\n<li>To obtain data for posts made by a private account, a logged-in Cookie is required, and the logged-in account must follow the private account.</li>\n<li>When batch downloading account posts works or mix works, if the corresponding nickname or mark parameter changes, the program will automatically update the nickname and mark parameter in the file names of the downloaded works.</li>\n<li>When downloading files, the program first downloads them to a temporary folder and then moves them to the storage folder upon completion. The temporary folder will be emptied when the program ends.</li>\n<li>The <code>Batch Download Favorites Works Mode</code> currently only supports downloading Favorites works for the account corresponding to the currently logged-in Cookie and does not support multiple accounts.</li>\n<li>If you want the program to use a proxy to request data, you must set the <code>proxy</code> parameter in <code>settings.json</code>; otherwise, the program will not use a proxy.</li>\n<li>If your computer does not have a suitable program for editing JSON files, we recommend using the <a href=\"https://www.toolhelper.cn/JSON/JSONFormat\">Online Tool</a> to edit the configuration file content, after modification, the software needs to be restarted to take effect.</li>\n<li>When the program prompts the user to input content or links, please be careful to avoid including newline characters, as this may cause unexpected issues.</li>\n<li>This project does not support downloading paid works. Please do not report any issues related to downloading paid works.</li>\n<li>On Windows systems, the program needs to be run as an administrator to read Cookies from Chromium, Chrome, and Edge browsers.</li>\n<li>This project has not been optimized for running multiple instances of the program. If you need to run multiple instances, please copy the entire project folder to avoid unexpected issues.</li>\n<li>During program execution, if you need to terminate the program or <code>ffmpeg</code>, please press <code>Ctrl + C</code> to stop the process. Do not click the close button on the terminal window directly.</li>\n</ul>\n<h2>Build of Executable File Guide</h2>\n<details>\n<summary>Build of Executable File Guide (Click to Expand)</summary>\n\nThis guide will walk you through forking this repository and executing GitHub Actions to automatically build and package\nthe program based on the latest source code!\n\n---\n\n### Steps to Use\n\n#### 1. Fork the Repository\n\n1. Click the **Fork** button at the top right of the project repository to fork it to your personal GitHub account\n2. Your forked repository address will look like this: `https://github.com/your-username/this-repo`\n\n---\n\n#### 2. Enable GitHub Actions\n\n1. Go to the page of your forked repository\n2. Click the **Settings** tab at the top\n3. Click the **Actions** tab on the right\n4. Click the **General** option\n5. Under **Actions permissions**, select **Allow all actions and reusable workflows** and click the **Save** button\n\n---\n\n#### 3. Manually Trigger the Build Process\n\n1. In your forked repository, click the **Actions** tab at the top\n2. Find the workflow named **构建可执行文件**\n3. Click the **Run workflow** button on the right:\n    - Select the **master** or **develop** branch\n    - Click **Run workflow**\n\n---\n\n#### 4. Check the Build Progress\n\n1. On the **Actions** page, you can see the execution records of the triggered workflow\n2. Click on the run record to view detailed logs to check the build progress and status\n\n---\n\n#### 5. Download the Build Result\n\n1. Once the build is complete, go to the corresponding run record page\n2. In the **Artifacts** section at the bottom of the page, you will see the built result file\n3. Click to download and save it to your local machine to get the built program\n\n---\n\n### Notes\n\n1. **Resource Usage**:\n    - GitHub provides free build environments for Actions, with a monthly usage limit (2000 minutes) for free-tier\n      users\n\n2. **Code Modifications**:\n    - You are free to modify the code in your forked repository to customize the build process\n    - After making changes, you can trigger the build process again to get your customized version\n\n3. **Stay in Sync with the Main Repository**:\n    - If the main repository is updated with new code or workflows, it is recommended that you periodically sync your\n      forked repository to get the latest features and fixes\n\n---\n\n### Frequently Asked Questions\n\n#### Q1: Why can't I trigger the workflow?\n\nA: Please ensure that you have followed the steps to **Enable Actions**. Otherwise, GitHub will prevent the workflow\nfrom running\n\n#### Q2: What should I do if the build process fails?\n\nA:\n\n- Check the run logs to understand the cause of the failure\n- Ensure there are no syntax errors or dependency issues in the code\n- If the problem persists, please open an issue on\n  the [Issues page](https://github.com/JoeanAmier/TikTokDownloader/issues)\n\n#### Q3: Can I directly use the Actions from the main repository?\n\nA: Due to permission restrictions, you cannot directly trigger Actions from the main repository. Please use the forked\nrepository to execute the build process\n\n</details>\n\n## Program Update\n\n<p><strong>Method 1:</strong> Download and extract the files, then copy the old version of the <code>_internal\\Volume</code> folder into the new version's <code>_internal</code> folder.</p>\n<p><strong>Method 2:</strong> Download and extract the files (do not run the program), then copy all files and directly overwrite the old version.</p>\n\n# ⚠️ Disclaimer\n\n<ol>\n<li>The user's use of this project is entirely at their own discretion and responsibility. The author assumes no liability for any losses, claims, or risks arising from the user's use of this project.</li>\n<li>The code and functionalities provided by the author of this project are based on current knowledge and technological developments. The author strives to ensure the correctness and security of the code according to existing technical capabilities but does not guarantee that the code is entirely free of errors or defects.</li>\n<li>All third-party libraries, plugins, or services relied upon by this project follow their respective open-source or commercial licenses. Users must review and comply with those license agreements. The author assumes no responsibility for the stability, security, or compliance of third-party components.</li>\n<li>Users must strictly comply with the requirements of the <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU General Public License v3.0</a> when using this project and properly indicate that the code was used under the <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU General Public License v3.0</a>.</li>\n<li>When using the code and features of this project, users must independently research relevant laws and regulations and ensure their actions are legal and compliant. Any legal liabilities or risks arising from violations of laws and regulations shall be borne solely by the user.</li>\n<li>Users must not use this tool to engage in any activities that infringe intellectual property rights, including but not limited to downloading or distributing copyright-protected content without authorization. The developers do not participate in, support, or endorse any unauthorized acquisition or distribution of illegal content.</li>\n<li>This project assumes no responsibility for the compliance of any data processing activities (including collection, storage, and transmission) conducted by users. Users must comply with relevant laws and regulations and ensure that their processing activities are lawful and proper. Legal liabilities resulting from non-compliant operations shall be borne by the user.</li>\n<li>Under no circumstances may users associate the author, contributors, or other related parties of this project with their usage of the project, nor may they hold these parties responsible for any loss or damage arising from such usage.</li>\n<li>The author of this project will not provide a paid version of the DouK-Downloader project, nor will they offer any commercial services related to the DouK-Downloader project.</li>\n<li>Any secondary development, modification, or compilation based on this project is unrelated to the original author. The original author assumes no liability for any consequences resulting from such secondary development. Users bear full responsibility for all outcomes arising from such modifications.</li>\n<li>This project grants no patent licenses; if the use of this project leads to patent disputes or infringement, the user bears all associated risks and responsibilities. Without written authorization from the author or rights holder, users may not use this project for any commercial promotion, marketing, or re-licensing.</li>\n<li>The author reserves the right to terminate service to any user who violates this disclaimer at any time and may require them to destroy all obtained code and derivative works.</li>\n<li>The author reserves the right to update this disclaimer at any time without prior notice. Continued use of the project constitutes acceptance of the revised terms.</li>\n</ol>\n<b>Before using the code and functionalities of this project, please carefully consider and accept the above disclaimer. If you have any questions or disagree with the statement, please do not use the code and functionalities of this project. If you use the code and functionalities of this project, it is considered that you fully understand and accept the above disclaimer, and willingly assume all risks and consequences associated with the use of this project.</b>\n<h1>🌟 Contribution Guidelines</h1>\n<p><strong>Welcome to contributing to this project! To keep the codebase clean, efficient, and easy to maintain, please read the following guidelines carefully to ensure that your contributions can be accepted and integrated smoothly.</strong></p>\n<ul>\n<li>Before starting development, please pull the latest code from the <code>develop</code> branch as the basis for your modifications; this helps avoid merge conflicts and ensures your changes are based on the latest state of the project.</li>\n<li>If your changes involve multiple unrelated features or issues, please split them into several independent commits or pull requests.</li>\n<li>Each pull request should focus on a single feature or fix as much as possible, to facilitate code review and testing.</li>\n<li>Follow the existing coding style; make sure your code is consistent with the style already present in the project; please use the Ruff tool to maintain code formatting standards.</li>\n<li>Write code that is easy to read; add appropriate annotation to help others understand your intentions.</li>\n<li>Each commit should include a clear and concise commit message describing the changes made. The commit message should follow this format: <code>&lt;type&gt;: &lt;short description&gt;</code></li>\n<li>When you are ready to submit a pull request, please prioritize submitting them to the <code>develop</code> branch; this provides maintainers with a buffer zone for additional testing and review before final merging into the <code>master</code> branch.</li>\n<li>It is recommended to communicate with the author before starting development or when encountering questions to ensure alignment in direction and avoid redundant efforts or unnecessary commits.</li>\n</ul>\n<p><strong>Reference materials:</strong></p>\n<ul>\n<li><a href=\"https://www.contributor-covenant.org/version/2/1/code_of_conduct/\">Contributor Covenant</a></li>\n<li><a href=\"https://opensource.guide/how-to-contribute/\">How to Contribute to Open Source</a></li>\n</ul>\n\n# ♥️ Support the Project\n\n<p>If <b>DouK-Downloader</b> has been helpful to you, please consider giving it a <b>Star</b> ⭐. Your support is greatly appreciated!</p>\n<table>\n<thead>\n<tr>\n<th align=\"center\">微信(WeChat)</th>\n<th align=\"center\">支付宝(Alipay)</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\"><img src=\"./docs/微信赞助二维码.png\" alt=\"微信赞助二维码\" height=\"200\" width=\"200\"></td>\n<td align=\"center\"><img src=\"./docs/支付宝赞助二维码.png\" alt=\"支付宝赞助二维码\" height=\"200\" width=\"200\"></td>\n</tr>\n</tbody>\n</table>\n<p>If you're willing, consider making a contribution to provide additional support for <b>DouK-Downloader</b>!</p>\n\n# 💰 Project Sponsorship\n\n## DartNode\n\n[![Powered by DartNode](docs/AD/DartNode_AD.png)](https://dartnode.com \"Powered by DartNode - Free VPS for Open Source\")\n\n***\n\n## ZMTO\n\n<p><a href=\"https://www.zmto.com/\"><img src=\"https://console.zmto.com/templates/2019/dist/images/logo_dark.svg\" alt=\"ZMTO\"></a></p>\n<p><a href=\"https://www.zmto.com/\">ZMTO</a>: A professional cloud infrastructure provider offering sophisticated solutions with reliable technology and expert support. We also empower qualified open source initiatives with enterprise-grade VPS infrastructure, driving sustainable development and innovation in the open source ecosystem. </p>\n\n***\n\n## TikHub\n\n<p><a href=\"https://tikhub.io/?utm_source=github&utm_medium=readme&utm_campaign=tiktok_downloader&ref=github_joeanamier_tiktokdownloader\"><img src=\"docs/AD/TIKHUB_AD.jpg\" alt=\"TIKHUB\" width=\"458\" height=\"319\"></a></p>\n<p><a href=\"https://tikhub.io/?utm_source=github&utm_medium=readme&utm_campaign=tiktok_downloader&ref=github_joeanamier_tiktokdownloader\">TikHub API</a> offers over 700 endpoints to retrieve and analyze data from 14+ social media platforms—including videos, users, comments, stores, products, trends, and more—enabling one-stop access and analysis of all your data.</p>\n<p>Use <strong>invitation code</strong>: <code>ZrdH8McC</code> to register and recharge to get <code>$2</code> credit.</p>\n\n# ✉️ Contact the Author\n\n<ul>\n<li>Author's Email: yonglelolu@foxmail.com</li>\n<li>Author's WeChat: Downloader_Tools</li>\n<li>Official WeChat Account: Downloader Tools</li>\n<li><b>Discord Community</b>: <a href=\"https://discord.com/invite/ZYtmgKud9Y\">Click to join the community</a></li>\n</ul>\n<p>✨ <b>The author's other open-source projects:</b></p>\n<ul>\n<li><b>XHS-Downloader（小红书、XiaoHongShu、RedNote）</b>：<a href=\"https://github.com/JoeanAmier/XHS-Downloader\">https://github.com/JoeanAmier/XHS-Downloader</a></li>\n<li><b>KS-Downloader（快手、KuaiShou）</b>：<a href=\"https://github.com/JoeanAmier/KS-Downloader\">https://github.com/JoeanAmier/KS-Downloader</a></li>\n</ul>\n<h1>⭐ Star History</h1>\n<p>\n<img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=JoeanAmier/TikTokDownloader&amp;type=Timeline\"/>\n</p>\n\n# 💡 Project References\n\n* https://github.com/Johnserf-Seed/f2\n* https://github.com/Evil0ctal/Douyin_TikTok_Download_API\n* https://github.com/justbeluga/tiktok-web-reverse-engineering\n* https://github.com/ihmily/DouyinLiveRecorder\n* https://github.com/encode/httpx/\n* https://github.com/Textualize/rich\n* https://github.com/omnilib/aiosqlite\n* https://github.com/Tinche/aiofiles\n* https://github.com/pyinstaller/pyinstaller\n* https://foss.heptapod.net/openpyxl/openpyxl\n* https://github.com/carpedm20/emoji/\n* https://github.com/lxml/lxml\n* https://ffmpeg.org/ffmpeg-all.html\n"
  },
  {
    "path": "docs/Cookie获取教程.md",
    "content": "# Cookie 获取教程\n\n本教程仅演示部分能够获取所需 `Cookie` 的方法，仍有其他方法能够获取所需 `Cookie`；本教程使用的浏览器为 `Microsoft Edge`\n，部分浏览器的开发人员工具可能不支持中文语言。\n\n**方法一\\(推荐\\)：**\n\n1. 打开浏览器\\(可选无痕模式启动\\)，访问`https://www.douyin.com/`\n2. 登录抖音账号\\(可跳过\\)\n3. 按 `F12` 打开开发人员工具\n4. 选择 `网络` 选项卡\n5. 勾选 `保留日志`\n6. 在 `筛选器` 输入框输入 `cookie-name:odin_tt`\n7. 点击加载任意一个作品的评论区\n8. 在开发人员工具窗口选择任意一个数据包\\(如果无数据包，重复步骤7\\)\n9. 全选并复制 `Cookie` 的值\n10. 运行 `main.py` ，根据提示写入 `Cookie`\n\n**截图示例：**\n\n<img src=\"screenshot/Cookie获取教程1.png\" alt=\"开发人员工具\">\n\n**方法二\\(不适用本项目\\)：**\n\n1. 打开浏览器\\(可选无痕模式启动\\)，访问`https://www.douyin.com/`\n2. 登录抖音账号\\(可跳过\\)\n3. 按 `F12` 打开开发人员工具\n4. 选择 `控制台` 选项卡\n5. 输入 `document.cookie` 后回车确认\n6. 检查 `Cookie` 是否包含 `passport_csrf_token` 和 `odin_tt` 字段\n7. 如果未包含所需字段，尝试刷新网页或者点击加载任意一个作品的评论区，回到步骤5\n8. 全选并复制 `Cookie` 的值\n9. 运行 `main.py` ，根据提示写入 `Cookie`\n\n**截图示例：**\n\n<img src=\"screenshot/Cookie获取教程2.png\" alt=\"开发人员工具\">\n\n# device_id 参数\n\n`device_id` 参数获取方法与 Cookie 类似。\n\n<img src=\"screenshot/device_id获取示例图.png\" alt=\"开发人员工具\">\n"
  },
  {
    "path": "docs/DouK-Downloader文档.md",
    "content": "<div align=\"center\">\n<img src=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/static/images/DouK-Downloader.png\" alt=\"DouK-Downloader\" height=\"256\" width=\"256\"><br>\n<h1>DouK-Downloader 项目文档</h1>\n<a href=\"https://trendshift.io/repositories/6222\" target=\"_blank\"><img src=\"https://trendshift.io/api/badge/repositories/6222\" alt=\"\" style=\"width: 250px; height: 55px;\" width=\"250\" height=\"55\"/></a>\n<br>\n<img alt=\"GitHub\" src=\"https://img.shields.io/github/license/JoeanAmier/TikTokDownloader?style=flat-square\">\n<img alt=\"GitHub forks\" src=\"https://img.shields.io/github/forks/JoeanAmier/TikTokDownloader?style=flat-square&color=55efc4\">\n<img alt=\"GitHub Repo stars\" src=\"https://img.shields.io/github/stars/JoeanAmier/TikTokDownloader?style=flat-square&color=fda7df\">\n<img alt=\"GitHub code size in bytes\" src=\"https://img.shields.io/github/languages/code-size/JoeanAmier/TikTokDownloader?style=flat-square&color=a29bfe\">\n<br>\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Python-3.12-b8e994?style=flat-square&logo=python&labelColor=3dc1d3\">\n<img alt=\"GitHub release (with filter)\" src=\"https://img.shields.io/github/v/release/JoeanAmier/TikTokDownloader?style=flat-square&color=48dbfb\">\n<img src=\"https://img.shields.io/badge/Sourcery-enabled-884898?style=flat-square&color=1890ff\" alt=\"\">\n<img alt=\"Static Badge\" src=\"https://img.shields.io/badge/Docker-badc58?style=flat-square&logo=docker\">\n<img alt=\"GitHub all releases\" src=\"https://img.shields.io/github/downloads/JoeanAmier/TikTokDownloader/total?style=flat-square&color=ffdd59\">\n</div>\n<br>\n<p>🔥 <b>TikTok 发布/喜欢/合辑/直播/视频/图集/音乐；抖音发布/喜欢/收藏/收藏夹/视频/图集/实况/直播/音乐/合集/评论/账号/搜索/热榜数据采集工具：</b>完全开源，基于 HTTPX 模块实现的免费数据采集和文件下载工具；批量下载抖音账号发布、喜欢、收藏、收藏夹作品；批量下载 TikTok 账号发布、喜欢作品；下载抖音链接或 TikTok 链接作品；获取抖音直播拉流地址；下载抖音直播视频；获取 TikTok 直播拉流地址；下载 TikTok 直播视频；采集抖音作品评论数据；批量下载抖音合集作品；批量下载 TikTok 合辑作品；采集抖音账号详细数据；采集抖音用户 / 作品 / 直播搜索结果；采集抖音热榜数据。</p>\n<p>⭐ <b>项目版本：<code>5.8 Beta</code>；文档更新日期：<code>2026/2/28</code></b></p>\n<p>⭐ <b>项目文档正在完善，如果发现任何错误或描述模糊之处，请告知作者以便改进！本项目历史名称：<code>TikTokDownloader</code></b></p>\n<p>⭐ Due to the author’s limited time and energy, the complete English documentation for this project is not yet available. If you wish to read the full documentation, we recommend using AI translation tools to assist your understanding. If you would like to contribute to the translation, your help is warmly welcomed.</p>\n<hr>\n<h1>快速入门</h1>\n<p>⭐ 本项目包含手动构建可执行文件的 GitHub Actions，使用者可以随时使用 GitHub Actions 将最新源码构建为可执行文件！</p>\n<p>⭐ 自动构建可执行文件教程请查阅本文档的 <code>构建可执行文件指南</code> 部分；如果需要更加详细的图文教程，请 <a href=\"https://mp.weixin.qq.com/s/TorfoZKkf4-x8IBNLImNuw\">查阅文章</a>！</p>\n<p><strong>注意：由于 Mac OS 平台的可执行文件 <code>main</code> 未经过代码签名，首次运行时会受到系统安全限制。请先在终端执行 <code>xattr -cr main.app</code> 命令移除安全标记，执行一次后即可正常运行。</strong></p>\n<ol>\n<li><b>运行可执行文件</b> 或者 <b>配置环境运行</b>\n<ol><b>运行可执行文件</b>\n<li>下载 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> 或者 Actions 构建的可执行文件压缩包</li>\n<li>解压后打开程序文件夹，双击运行 <code>main</code></li>\n</ol>\n<ol><b>配置环境运行</b>\n\n[//]: # (<li>安装不低于 <code>3.12</code> 版本的 <a href=\"https://www.python.org/\">Python</a> 解释器</li>)\n<li>安装 <code>3.12</code> 版本的 <a href=\"https://www.python.org/\">Python</a> 解释器</li>\n<li>下载最新的源码或 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\">Releases</a> 发布的源码至本地</li>\n<li>运行 <code>python -m venv venv</code> 命令创建虚拟环境（可选）</li>\n<li>运行 <code>.\\venv\\Scripts\\activate.ps1</code> 或者 <code>venv\\Scripts\\activate</code> 命令激活虚拟环境（可选）</li>\n<li>运行 <code>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</code> 命令安装程序所需模块</li>\n<li>运行 <code>python .\\main.py</code> 或者 <code>python main.py</code> 命令启动 DouK-Downloader</li>\n</ol>\n</li>\n<li>阅读 DouK-Downloader 的免责声明，根据提示输入内容</li>\n<li>将 Cookie 信息写入配置文件\n<ol><b>从剪贴板读取 Cookie（推荐）</b>\n<li>参考 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie 提取教程</a>，复制所需 Cookie 至剪贴板</li>\n<li>选择 <code>从剪贴板读取 Cookie</code> 选项，程序会自动读取剪贴板的 Cookie 并写入配置文件</li>\n</ol>\n<ol><b>从浏览器读取 Cookie</b>\n<li>选择 <code>从浏览器读取 Cookie</code> 选项，按照提示输入浏览器类型或序号</li>\n</ol>\n<ol><b><del>扫码登录获取 Cookie</del>（失效）</b>\n<li><del>选择 <code>扫码登录获取 Cookie</code> 选项，程序会显示登录二维码图片，并使用默认应用打开图片</del></li>\n<li><del>使用抖音 APP 扫描二维码并登录账号</del></li>\n<li><del>按照提示操作，程序会自动将 Cookie 写入配置文件</del></li>\n</ol>\n</li>\n<li>返回程序界面，依次选择 <code>终端交互模式</code> -> <code>批量下载链接作品(抖音)</code> -> <code>手动输入待采集的作品链接</code></li>\n<li>输入抖音作品链接即可下载作品文件</li>\n</ol>\n<p><b>TikTok 平台功能需要额外设置配置文件 <code>browser_info_tiktok</code> 的 <code>device_id</code> 参数，否则 TikTok 平台功能可能无法正常使用！参数获取方式与 Cookie 类似，详见 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie 获取教程</a></b></p>\n<h2>Docker 容器</h2>\n<ol>\n<li>获取镜像</li>\n<ul>\n<li>方式一：使用 <code>Dockerfile</code> 文件构建镜像</li>\n<li>方式二：使用 <code>docker pull joeanamier/tiktok-downloader</code> 命令拉取镜像</li>\n<li>方式三：使用 <code>docker pull ghcr.io/joeanamier/tiktok-downloader</code> 命令拉取镜像</li>\n</ul>\n<li>创建容器：<code>docker run --name 容器名称(可选) -p 主机端口号:5555 -v tiktok_downloader_volume:/app/Volume -it &lt;镜像名称&gt;</code>\n</li>\n<br><b>注意：</b>此处的 <code>&lt;镜像名称&gt;</code> 需与您在第一步中使用的镜像名称保持一致（例如 <code>joeanamier/tiktok-downloader</code> 或 <code>ghcr.io/joeanamier/tiktok-downloader</code>）\n<li>运行容器\n<ul>\n<li>启动容器：<code>docker start -i 容器名称/容器 ID</code></li>\n<li>重启容器：<code>docker restart -i 容器名称/容器 ID</code></li>\n</ul>\n</li>\n</ol>\n<p>Docker 容器无法直接访问宿主机的文件系统，部分功能不可用，例如：<code>从浏览器读取 Cookie</code>；其他功能如有异常请反馈！</p>\n<h1>Cookie 说明</h1>\n<p><a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">点击查看 Cookie 获取教程</a>；无效或失效的 Cookie 会导致程序获取数据失败！</p>\n<ul>\n<li>Cookie 仅需在失效后重新写入配置文件，并非每次运行程序都要写入配置文件！</li>\n<li><p>Cookie 会影响下载的视频文件分辨率，如果无法下载最高分辨率的视频文件，请尝试更新 Cookie！</li>\n<li>程序获取数据失败时，可以尝试更新 Cookie 或者使用已登录的 Cookie！</li>\n</ul>\n<h1>入门说明</h1>\n<h2>关于终端</h2>\n<p>⭐ 推荐使用 <a href=\"https://learn.microsoft.com/zh-cn/windows/terminal/install\">Windows 终端</a>（Windows 11 自带默认终端）运行程序以便获得最佳彩色交互显示效果！</p>\n<h2>链接类型</h2>\n<ul>\n<li>完整链接：使用浏览器打开抖音或 TikTok 链接时，地址栏所显示的 URL 地址。</li>\n<li>分享链接：点击 APP 或网页版的分享按钮得到的 URL 地址，抖音平台以 <code>https://v.</code> 开头，掺杂中文和其他字符；TikTok\n平台以 <code>https://vm.</code> 或 <code>https://vt.</code> 开头，不掺杂其他字符；使用时<b>不需要</b>手动去除中文和其他字符，程序会自动提取 URL 链接。</li>\n</ul>\n<h2>数据储存</h2>\n<ul>\n<li>项目支持使用 <code>CSV</code>、<code>XLSX</code>、<code>SQLite</code> 格式文件储存采集数据。</li>\n<li>配置文件 <code>settings.json</code> 的 <code>storage_format</code> 参数可设置数据储存格式类型，如果不设置该参数，程序不会储存任何数据至文件。</li>\n<li><code>采集作品评论数据</code>、<code>采集账号详细数据</code>、<code>采集搜索结果数据</code>、<code>采集抖音热榜数据</code> 模式必须设置 <code>storage_format</code> 参数才能正常使用。</li>\n<li>程序所有数据均储存至配置文件 <code>root</code> 参数路径下的 <code>Data</code> 文件夹。</li>\n</ul>\n<h2>文本文档</h2>\n<p>项目部分功能支持从文本文档（TXT）读取链接，如需使用，请在计算机任意路径创建一个空白文本文档，然后编辑文件内容，每行输入单个链接，编辑完成后保存文件。</p>\n<p>文本文档编码：UTF-8</p>\n<h3>文本文档内容示例</h3>\n\n```text\nhttps://www.douyin.com/user/abcd?vid=123456789\nhttps://www.douyin.com/search/key?modal_id=123456789\nhttps://www.douyin.com/video/123456789\nhttps://www.douyin.com/note/123456789\n```\n\n<h2>直播下载</h2>\n<p><code>获取直播拉流地址</code> 功能需要调用 <code>ffmpeg</code> 下载直播文件；程序会优先调用系统环境的 <code>ffmpeg</code>，其次调用 <code>ffmpeg</code> 参数指定的 <code>ffmpeg</code>，如果 <code>ffmpeg</code> 不可用，程序将不支持直播下载！</p>\n<p>建议前往 <a href=\"https://ffmpeg.org/download.html\">官方网站</a> 或者 <a href=\"https://github.com/BtbN/FFmpeg-Builds\">FFmpeg-Builds</a> 获取 <code>ffmpeg</code> 程序！</p>\n<p>项目开发时所用的 FFmpeg 版本信息如下，不同版本的 FFmpeg 可能会有差异；若功能异常，请向作者反馈！</p>\n<pre>\nffmpeg version n7.1.1-6-g48c0f071d4-20250405 Copyright (c) 2000-2025 the FFmpeg developers\nbuilt with gcc 14.2.0 (crosstool-NG 1.27.0.18_7458341)\n</pre>\n<h2>功能汇总</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">程序功能</th>\n<th align=\"center\">功能类型</th>\n</tr>\n</thead>\n<tbody><tr>\n<td align=\"center\">批量下载账号作品（发布、喜欢）</td>\n<td align=\"center\">文件下载, 数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">批量下载链接作品</td>\n<td align=\"center\">文件下载, 数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">获取直播拉流地址</td>\n<td align=\"center\">文件下载, 数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">采集作品评论数据</td>\n<td align=\"center\">数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">批量下载合集作品</td>\n<td align=\"center\">文件下载, 数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">采集账号详细数据</td>\n<td align=\"center\">数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">采集搜索结果数据</td>\n<td align=\"center\">数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">采集抖音热榜数据</td>\n<td align=\"center\">数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">批量下载收藏作品</td>\n<td align=\"center\">文件下载，数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">批量下载收藏夹作品</td>\n<td align=\"center\">文件下载，数据采集</td>\n</tr>\n<tr>\n<td align=\"center\">批量下载收藏音乐作品</td>\n<td align=\"center\">文件下载，数据采集</td>\n</tr>\n</tbody></table>\n<h2>关闭平台功能</h2>\n<p>本项目支持抖音平台和 TikTok 平台的数据采集和文件下载功能，平台功能默认开启，如果不需要使用平台的任何功能，可以编辑配置文件关闭平台功能。</p>\n<p>本项目内置参数更新机制，程序会周期性更新抖音与 TikTok 请求的部分参数，以保持参数的有效性（或许没有效果？），该功能无法防止参数失效，参数失效后需要重新写入 Cookie；关闭平台功能后，对应平台的参数更新功能将会禁用！</p>\n<h1>配置文件</h1>\n<p>配置文件：项目根目录下的 <code>./Volume/settings.json</code> 文件，可以自定义设置程序部分运行参数。</p>\n<p>若无特殊需求，大部分配置参数无需修改，直接使用默认值即可。</p>\n<p><b><code>cookie</code>、<code>cookie_tiktok</code> 与 <code>device_id</code>参数为必需参数，必须设置该参数才能正常使用程序</b>；其余参数可以根据实际需求进行修改！</p>\n<p>如果您的计算机没有合适的程序编辑 JSON 文件，建议使用 <a href=\"https://www.toolhelper.cn/JSON/JSONFormat\">在线工具</a> 编辑配置文件内容，修改后需要重启软件才能生效。</p>\n<p>注意: 手动修改 <code>settings.json</code> 后需要重新运行程序才会生效！</p>\n<h2>参数说明</h2>\n<table>\n<thead>\n<tr>\n<th align=\"center\">参数</th>\n<th align=\"center\">类型</th>\n<th align=\"center\">说明</th>\n<th align=\"center\">默认</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td align=\"center\"><i>mark</i></td>\n<td align=\"center\">str</td>\n<td align=\"center\"><a href=\"#mark\"><sup>1</sup></a>账号/合集标识，用于区分账号/合集；<strong>属于 accounts_urls、mix_urls 和 owner_url 子参数</strong></td>\n<td align=\"center\">账号昵称/合集标题</td>\n</tr>\n<tr>\n<td align=\"center\"><i>url</i></td>\n<td align=\"center\">str</td>\n<td align=\"center\">账号主页/合集作品链接；<strong>属于 accounts_urls、mix_urls 和 owner_url 子参数</strong></td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\"><i>tab</i></td>\n<td align=\"center\">str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>2</sup></a>主页标签，<code>post</code> 代表发布作品、<code>favorite</code> 代表喜欢作品；<strong>属于 accounts_urls 子参数</strong></td>\n<td align=\"center\">发布作品</td>\n</tr>\n<tr>\n<td align=\"center\"><i>earliest</i></td>\n<td align=\"center\">str | float | int</td>\n<td align=\"center\">作品最早发布日期，格式：<code>2023/1/1</code>、<code>整数</code>、<code>浮点数</code>；设置为数值代表基于 <code>latest</code>参数的前 XX 天，<strong>属于 accounts_urls 子参数</strong></td>\n<td align=\"center\">不限制</td>\n</tr>\n<tr>\n<td align=\"center\"><i>latest</i></td>\n<td align=\"center\">str | float | int</td>\n<td align=\"center\">作品最晚发布日期，格式：<code>2023/1/1</code>、<code>整数</code>、<code>浮点数</code>；设置为数值代表基于当天的前 XX 天，<strong>属于 accounts_urls 子参数</strong></td>\n<td align=\"center\">不限制</td>\n</tr>\n<tr>\n<td align=\"center\"><i>enable</i></td>\n<td align=\"center\">bool</td>\n<td align=\"center\">参数对象是否启用，设置为 <code>false</code> 时程序会跳过处理；<strong>属于 accounts_urls 和 mix_urls 子参数</strong></td>\n<td align=\"center\">启用</td>\n</tr>\n<tr>\n<td align=\"center\">accounts_urls[mark, url, tab, earliest, latest, enable]</td>\n<td align=\"center\">list[dict[str, str, str, Any, str, bool]]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>抖音平台：账号标识，账号链接，主页标签，最早发布日期，最晚发布日期，是否启用；作为 <code>批量下载账号作品</code> 模式选项，支持多账号，以字典格式包含六个参数</td>\n<td align=\"center\">无</td>\n<tr>\n<td align=\"center\">mix_urls[mark, url, enable]</td>\n<td align=\"center\">list[dict[str, str, bool]]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>抖音平台：合集标识，合集链接或作品链接，是否启用；作为 <code>批量下载合集作品</code> 模式选项，支持多合集，以字典格式包含三个参数</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">owner_url[mark, url]</td>\n<td align=\"center\">dict[str, str]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>抖音平台：当前登录 Cookie 的账号标识，账号主页链接；<code>批量下载收藏作品</code> 模式下用于获取账号信息，以字典格式包含两个参数</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">accounts_urls_tiktok[mark, url, tab, earliest, latest, enable]</td>\n<td align=\"center\">list[dict[str, str, str, Any, str, bool]]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>TikTok 平台；参数规则与 <code>accounts_urls</code> 一致</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">mix_urls_tiktok[mark, url, enable]</td>\n<td align=\"center\">list[dict[str, str, bool]]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>TikTok 平台；参数规则与 <code>mix_urls</code> 一致</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">owner_url_tiktok[mark, url](未生效)</td>\n<td align=\"center\">dict[str, str]</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>TikTok 平台；参数规则与 <code>owner_url</code> 一致</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">root</td>\n<td align=\"center\">str</td>\n<td align=\"center\">作品文件和数据记录保存路径；建议使用绝对路径</td>\n<td align=\"center\">项目根路径/Volume</td>\n</tr>\n<tr>\n<td align=\"center\">folder_name</td>\n<td align=\"center\">str</td>\n<td align=\"center\">批量下载链接作品时，保存文件夹的名称</td>\n<td align=\"center\">Download</td>\n</tr>\n<tr>\n<td align=\"center\">name_format</td>\n<td align=\"center\">str</td>\n<td align=\"center\">文件保存时的命名规则，值之间使用空格分隔，支持：<code>id</code>：作品 ID；<code>desc</code>：作品描述；<code>create_time</code>：发布时间；<code>nickname</code>：账号昵称；<code>mark</code>：账号标识；<code>uid</code>：账号 ID；<code>type</code>：作品类型</td>\n<td align=\"center\">发布时间-作品类型-账号昵称-描述</td>\n</tr>\n<tr>\n<td align=\"center\">desc_length</td>\n<td align=\"center\">int</td>\n<td align=\"center\">作品文件名中描述字段的最大字符数；超过限制的描述字段将折叠处理</td>\n<td align=\"center\">64</td>\n</tr>\n<tr>\n<td align=\"center\">name_length</td>\n<td align=\"center\">int</td>\n<td align=\"center\">作品文件名称的最大字符数；超过限制的文件名称将折叠处理</td>\n<td align=\"center\">128</td>\n</tr>\n<tr>\n<td align=\"center\">date_format</td>\n<td align=\"center\">str</td>\n<td align=\"center\">日期时间格式；<a href=\"https://docs.python.org/zh-cn/3/library/time.html?highlight=strftime#time.strftime\">点击查看设置规则</a></td>\n<td align=\"center\">年-月-日 时:分:秒</td>\n</tr>\n<tr>\n<td align=\"center\">split</td>\n<td align=\"center\">str</td>\n<td align=\"center\">文件命名的分隔符</td>\n<td align=\"center\">-</td>\n</tr>\n<tr>\n<td align=\"center\">folder_mode</td>\n<td align=\"center\">bool</td>\n<td align=\"center\">是否将每个作品的文件储存至单独的文件夹，文件夹名称格式与 <code>name_format</code> 参数一致</td>\n<td align=\"center\">false</td>\n</tr>\n<tr>\n<td align=\"center\">music</td>\n<td align=\"center\">bool</td>\n<td align=\"center\">是否下载作品音乐</td>\n<td align=\"center\">false</td>\n</tr>\n<tr>\n<td align=\"center\">truncate</td>\n<td align=\"center\">int</td>\n<td align=\"center\">文件下载进度条中描述字符串的最大长度，该参数用于调整显示效果</td>\n<td align=\"center\">64</td>\n</tr>\n<tr>\n<td align=\"center\">storage_format</td>\n<td align=\"center\">str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>采集数据持久化储存格式，支持：<code>csv</code>、<code>xlsx</code>、<code>sql</code>(SQLite)</td>\n<td align=\"center\">不保存</td>\n</tr>\n<tr>\n<td align=\"center\">cookie</td>\n<td align=\"center\">dict | str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>4</sup></a>抖音网页版 Cookie, 必需参数; 建议通过程序写入配置文件，亦可手动编辑</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">cookie_tiktok</td>\n<td align=\"center\">dict | str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>4</sup></a>TikTok 网页版 Cookie, 必需参数; 建议通过程序写入配置文件，亦可手动编辑</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">dynamic_cover</td>\n<td align=\"center\">bool</td>\n<td align=\"center\">是否下载视频作品动态封面图</td>\n<td align=\"center\">false</td>\n</tr>\n<tr>\n<td align=\"center\">static_cover</td>\n<td align=\"center\">bool</td>\n<td align=\"center\">是否下载视频作品静态封面图</td>\n<td align=\"center\">false</td>\n</tr>\n<tr>\n<td align=\"center\">proxy</td>\n<td align=\"center\">str</td>\n<td align=\"center\">抖音请求代理地址</td>\n<td align=\"center\">不使用代理</td>\n</tr>\n<tr>\n<td align=\"center\">proxy_tiktok</td>\n<td align=\"center\">str</td>\n<td align=\"center\">TikTok 请求代理地址</td>\n<td align=\"center\">不使用代理</td>\n</tr>\n<tr>\n<td align=\"center\"><a href=\"#twc\">twc_tiktok</a></td>\n<td align=\"center\">str</td>\n<td align=\"center\">TikTok Cookie 的 ttwid 值，一般情况下无需设置</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">download</td>\n<td align=\"center\">bool</td>\n<td align=\"center\">是否开启项目的下载功能，如果关闭，程序将不会下载任何文件</td>\n<td align=\"center\">true</td>\n</tr>\n<tr>\n<td align=\"center\">max_size</td>\n<td align=\"center\">int</td>\n<td align=\"center\">作品文件大小限制，单位字节，超出大小限制的作品文件将会跳过下载</td>\n<td align=\"center\">无限制</td>\n</tr>\n<tr>\n<td align=\"center\">chunk</td>\n<td align=\"center\">int</td>\n<td align=\"center\">每次从服务器接收的数据块大小，单位字节</td>\n<td align=\"center\">2097152(2 MB)</td>\n</tr>\n<tr>\n<td align=\"center\">timeout</td>\n<td align=\"center\">int</td>\n<td align=\"center\">请求数据的超时限制，单位秒</td>\n<td align=\"center\">10</td>\n</tr>\n<tr>\n<td align=\"center\">max_retry</td>\n<td align=\"center\">int</td>\n<td align=\"center\">发送请求获取数据发生异常时重试的最大次数，设置为 <code>0</code> 代表关闭重试</td>\n<td align=\"center\">10</td>\n</tr>\n<tr>\n<td align=\"center\">max_pages</td>\n<td align=\"center\">int</td>\n<td align=\"center\">批量下载账号喜欢作品、收藏作品或者采集作品评论数据时，请求数据的最大次数（不包括异常重试）</td>\n<td align=\"center\">不限制</td>\n</tr>\n<tr>\n<td align=\"center\">run_command</td>\n<td align=\"center\">str</td>\n<td align=\"center\">设置程序启动执行的默认命令，相当于模拟用户输入序号或内容（多个序号或内容之间使用空格分隔）</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">ffmpeg</td>\n<td align=\"center\">str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a><code>ffmpeg.exe</code> 路径，下载直播时使用，如果系统环境存在 <code>ffmpeg</code> 或者不想使用 <code>ffmpeg</code>，无需设置该参数</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">live_qualities</td>\n<td align=\"center\">str</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>3</sup></a>下载直播时的默认清晰度，支持设置为清晰度或者序号；当设置了该参数时，获取直播拉流地址将会直接下载指定清晰度的直播文件，不再提示输入清晰度；参数示例：<code>FULL_HD1</code>、<code>HD1</code>、<code>1</code>、<code>2</code> 等</td>\n<td align=\"center\">无</td>\n</tr>\n<tr>\n<td align=\"center\">douyin_platform</td>\n<td align=\"center\">bool</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>5</sup></a>是否启用抖音平台功能</td>\n<td align=\"center\">true</td>\n</tr>\n<tr>\n<td align=\"center\">tiktok_platform</td>\n<td align=\"center\">bool</td>\n<td align=\"center\"><a href=\"#supplement\"><sup>5</sup></a>是否启用 TikTok 平台功能</td>\n<td align=\"center\">true</td>\n</tr>\n<tr>\n<td align=\"center\">browser_info</td>\n<td align=\"center\">dict</td>\n<td align=\"center\">抖音平台浏览器信息，一般情况下无需修改</td>\n<td align=\"center\">内置参数</td>\n</tr>\n<tr>\n<td align=\"center\">browser_info_tiktok</td>\n<td align=\"center\">dict</td>\n<td align=\"center\">TikTok 平台浏览器信息，一般情况下仅需修改 <code>device_id</code> 参数，获取方式查阅 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie 获取教程</a></td>\n<td align=\"center\">内置参数</td>\n</tr>\n</tbody>\n</table>\n<div id=\"supplement\">\n<p><strong>补充说明：</strong></p>\n<ol>\n<li><a href=\"#mark\">详见标识参数说明</a></li>\n<li>设置为 <code>favorite</code> 时，需要确保账号喜欢作品公开可见，或者配置对应账号的登录 Cookie</li>\n<li>该参数仅在部分模式和功能中生效，如果不需要使用相应的模式和功能，无需设置该参数</li>\n<li>必须设置平台的 Cookie 才能使用该平台的数据采集和文件下载功能</li>\n<li>如果不需要使用该平台的任何功能，可以将该参数设置为 <code>false</code></li>\n</ol>\n</div>\n<h2>配置示例</h2>\n\n```json\n{\n  \"accounts_urls\": [\n    {\n      \"mark\": \"账号A\",\n      \"url\": \"https://www.douyin.com/user/aaa\",\n      \"tab\": \"post\",\n      \"earliest\": \"2024/3/1\",\n      \"latest\": \"2024/7/1\",\n      \"enable\": true\n    },\n    {\n      \"mark\": \"账号B\",\n      \"url\": \"https://v.douyin.com/bbb\",\n      \"tab\": \"favorite\",\n      \"earliest\": 30,\n      \"latest\": \"\",\n      \"enable\": false\n    }\n  ],\n  \"accounts_urls_tiktok\": \"参数规则与 accounts_urls 一致\",\n  \"mix_urls\": [\n    {\n      \"mark\": \"\",\n      \"url\": \"https://v.douyin.com/ccc\",\n      \"enable\": true\n    },\n    {\n      \"mark\": \"合集B\",\n      \"url\": \"https://www.douyin.com/video/123\",\n      \"enable\": false\n    }\n  ],\n  \"mix_urls_tiktok\": \"参数规则与 mix_urls 一致\",\n  \"owner_url\": {\n    \"mark\": \"已登录 Cookie 的账号标识，可以设置为空字符串\",\n    \"url\": \"已登录 Cookie 的账号主页链接\"\n  },\n  \"owner_url_tiktok\": \"参数规则与 owner_url 一致\",\n  \"root\": \"C:\\\\DouK-Downloader\",\n  \"folder_name\": \"SOLO\",\n  \"name_format\": \"create_time uid id\",\n  \"desc_length\": 64,\n  \"name_length\": 128,\n  \"date_format\": \"%Y-%m-%d\",\n  \"split\": \" \",\n  \"folder_mode\": false,\n  \"music\": false,\n  \"truncate\": 32,\n  \"storage_format\": \"xlsx\",\n  \"cookie\": {\n    \"key-1\": \"value-1\",\n    \"key-2\": \"value-2\",\n    \"key-3\": \"value-3\"\n  },\n  \"cookie_tiktok\": \"参数规则与 cookie 一致\",\n  \"dynamic_cover\": false,\n  \"static_cover\": false,\n  \"proxy\": \"http://127.0.0.1:9999\",\n  \"proxy_tiktok\": \"参数规则与 proxy 一致\",\n  \"twc_tiktok\": \"\",\n  \"download\": true,\n  \"max_size\": 104857600,\n  \"chunk\": 10485760,\n  \"timeout\": 5,\n  \"max_retry\": 10,\n  \"max_pages\": 2,\n  \"run_command\": \"6 2 1\",\n  \"ffmpeg\": \"C:\\\\DouK-Downloader\\\\ffmpeg.exe\",\n  \"live_qualities\": \"1\",\n  \"douyin_platform\": true,\n  \"tiktok_platform\": true,\n  \"browser_info\": {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n    \"pc_libra_divert\": \"Windows\",\n    \"browser_language\": \"zh-SG\",\n    \"browser_platform\": \"Win32\",\n    \"browser_name\": \"Chrome\",\n    \"browser_version\": \"139.0.0.0\",\n    \"engine_name\": \"Blink\",\n    \"engine_version\": \"139.0.0.0\",\n    \"os_name\": \"Windows\",\n    \"os_version\": \"10\",\n    \"webid\": \"\"\n  },\n  \"browser_info_tiktok\": {\n    \"User-Agent\": \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n    \"app_language\": \"zh-Hans\",\n    \"browser_language\": \"zh-SG\",\n    \"browser_name\": \"Mozilla\",\n    \"browser_platform\": \"Win32\",\n    \"browser_version\": \"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n    \"language\": \"zh-Hans\",\n    \"os\": \"windows\",\n    \"priority_region\": \"CN\",\n    \"region\": \"US\",\n    \"tz_name\": \"Asia/Shanghai\",\n    \"webcast_language\": \"zh-Hans\",\n    \"device_id\": \"0123456789\"\n  }\n}\n```\n\n<h2>参数详解</h2>\n<h3>下载喜欢作品</h3>\n\n```json\n{\n  \"accounts_urls\": [\n    {\n      \"mark\": \"\",\n      \"url\": \"https://www.douyin.com/user/aaa\",\n      \"tab\": \"favorite\",\n      \"earliest\": \"\",\n      \"latest\": \"\",\n      \"enable\": true\n    },\n    {\n      \"mark\": \"\",\n      \"url\": \"https://v.douyin.com/bbb\",\n      \"tab\": \"post\",\n      \"earliest\": \"\",\n      \"latest\": \"\",\n      \"enable\": true\n    },\n    {\n      \"mark\": \"\",\n      \"url\": \"https://www.douyin.com/user/ccc\",\n      \"tab\": \"favorite\",\n      \"earliest\": \"\",\n      \"latest\": \"\",\n      \"enable\": false\n    }\n  ]\n}\n```\n\n<p>将待下载的账号信息写入配置文件，每个账号对应一个对象/字典，<code>tab</code> 参数设置为 <code>favorite</code> 代表批量下载喜欢作品，支持多账号；<code>accounts_urls_tiktok</code>参数规则一致。</p>\n<p>下载账号喜欢作品需要确保账号喜欢作品公开可见，或者配置对应账号的登录 Cookie！</p>\n<p><b>下载账号喜欢作品需要使用已登录的 Cookie，否则可能无法获取正确的账号信息！</b></p>\n<h3>发布日期限制</h3>\n\n```json\n{\n  \"accounts_urls\": [\n    {\n      \"mark\": \"账号A\",\n      \"url\": \"https://v.douyin.com/aaa\",\n      \"tab\": \"post\",\n      \"earliest\": \"2023/12/1\",\n      \"latest\": \"\",\n      \"enable\": true\n    },\n    {\n      \"mark\": \"\",\n      \"url\": \"https://v.douyin.com/bbb\",\n      \"tab\": \"post\",\n      \"earliest\": 30,\n      \"latest\": \"2024/12/1\",\n      \"enable\": true\n    }\n  ]\n}\n```\n\n<p>如果已经采集某账号的全部发布作品，建议设置 <code>earliest</code> 和 <code>latest</code> 参数以减少后续采集请求次数，提高程序运行效率；<code>accounts_urls_tiktok</code>参数规则一致。</p>\n<p>示例：将 <code>earliest</code> 参数设置为 <code>2023/12/1</code>，程序获取账号发布作品数据时，不会获取早于 <code>2023/12/1</code> 的作品数据。</p>\n<p>示例：将 <code>earliest</code> 参数设置为 <code>30</code>，<code>latest</code> 参数设置为 <code>2024/12/1</code>，程序获取账号发布作品数据时，仅获取 2024 年 12 月 1 日当天及之前 30 天内发布的作品数据。</p>\n<p>示例：将 <code>earliest</code> 参数设置为 <code>15</code>，<code>latest</code> 参数设置为 <code>5</code>，程序获取账号发布作品数据时，仅获取前 5 天 ~ 前 20 天之间发布的作品数据。</p>\n<h3>文件储存路径</h3>\n\n```json\n{\n  \"root\": \"C:\\\\DouK-Downloader\",\n  \"folder_name\": \"SOLO\"\n}\n```\n\n<p>程序会将账号作品和合集作品的文件 和 记录的数据储存至 <code>C:\\DouK-Downloader</code> 文件夹内，链接作品的文件会储存至 <code>C:\\DouK-Downloader\\SOLO</code> 文件夹内。</p>\n<h3>文件名称格式</h3>\n\n```json\n{\n  \"name_format\": \"create_time uid id\",\n  \"split\": \" @ \"\n}\n```\n\n<p>作品文件名称格式为: <code>发布时间 @ 作者UID @ 作品ID</code></p>\n<ul>\n<li>如果作品没有描述，文件名称的描述内容将替换为作品 ID。</li>\n<li>批量下载链接作品时，如果在 <code>name_format</code> 参数中设置了 <code>mark</code> 字段，程序会自动替换为 <code>nickname</code> 字段。</li>\n</ul>\n<h3>日期时间格式</h3>\n\n```json\n{\n  \"date_format\": \"%Y-%m-%d\"\n}\n```\n\n<p>发布时间格式为：XXXX年-XX月-XX日，详细设置规则可以 <a href=\"https://docs.python.org/zh-cn/3/library/time.html?highlight=strftime#time.strftime\">查看文档</a></p>\n<h3>数据储存格式</h3>\n\n```json\n{\n  \"storage_format\": \"xlsx\"\n}\n```\n\n<p>使用 <code>XLSX</code> 格式储存程序采集数据。</p>\n<h3>文件大小限制</h3>\n\n```json\n{\n  \"max_size\": 104857600\n}\n```\n\n<p>作品文件大小限制为 104857600 字节(100 MB)，超过该大小的作品文件会自动跳过下载；直播文件不受限制。</p>\n<h3>文件分块下载</h3>\n\n```json\n{\n  \"chunk\": 10485760\n}\n```\n\n<p>下载文件时每次从服务器接收 10485760 字节 (10 MB)的数据块。</p>\n<ul>\n<li>影响下载速度：较大的 chunk 会增加每次下载的数据量，从而提高下载速度。相反，较小的 chunk 会降低每次下载的数据量，可能导致下载速度稍慢。</li>\n<li>影响内存占用：较大的 chunk 会一次性加载更多的数据到内存中，可能导致内存占用增加。相反，较小的 chunk 会减少每次加载的数据量，从而降低内存占用。</li>\n</ul>\n<h3>请求次数限制</h3>\n\n```json\n{\n  \"max_pages\": 2\n}\n```\n\n<p>下载账号喜欢作品、收藏作品以及采集作品评论数据时，仅获取前 <code>2</code> 页数据；用于解决下载账号喜欢作品、收藏作品需要获取全部数据的问题，以及作品评论数据数量过多的采集问题。</p>\n<p>不影响下载账号发布作品，如需控制账号发布作品数据获取次数，请使用 <code>earliest</code> 和 <code>latest</code> 参数实现。</p>\n<h3>默认执行命令</h3>\n\n```json\n{\n  \"run_command\": \"6 1 1 Q\"\n}\n```\n\n<p>上述命令表示运行程序自动依次执行 <code>终端交互模式</code> -> <code>批量下载账号作品(抖音)</code> -> <code>使用 accounts_urls 参数的账号链接(推荐)</code> -> <code>退出程序</code></p>\n<p>该参数可以实现设置默认启动模式、运行功能后自动退出、自动读取浏览器 Cookie 等高级自动化功能！</p>\n<ul>其他示例：\n<li><code>6 2</code>：代表依次执行 <code>终端交互模式</code> -> <code>批量下载账号作品(抖音)</code></li>\n<li><code>8</code>：代表执行<code>Web API 模式</code></li>\n<li><code>2 7</code>：代表依次执行<code>从浏览器读取 Cookie (抖音)</code> -> <code>Edge</code></li>\n</ul>\n<h3>程序代理设置</h3>\n\n```json\n{\n  \"proxy\": \"http://127.0.0.1:9999\"\n}\n```\n\n<p>程序获取网络数据时使用 <code>http://127.0.0.1:9999</code> 作为代理；程序会自动验证代理是否可用，如果代理不可用，则 <code>proxy</code> 参数不生效。</p>\n<p>如果您的电脑使用了代理工具且未修改默认端口，可以尝试以下设置：</p>\n<ul>\n<li>Clash: <code>http://127.0.0.1:7890</code></li>\n<li>v2rayN: <code>http://127.0.0.1:10809</code></li>\n</ul>\n<h1>高级配置</h1>\n<p>如果想要进一步修改程序功能，可以编辑 <code>src/custom</code> 文件夹内容（不适用于可执行文件），按照注释指引和实际需求进行自定义修改。</p>\n<b>部分可自定义设置的功能：</b>\n<ul>\n<li>设置作品文件下载的最大线程数量</li>\n<li>设置非法字符替换规则</li>\n<li>设置服务器模式主机及端口</li>\n<li>设置平台参数更新间隔</li>\n<li>设置彩色交互提示颜色</li>\n<li>设置请求数据延时间隔</li>\n<li>设置自定义作品筛选规则</li>\n<li>设置分批获取数据策略</li>\n<li>设置服务器模式参数验证</li>\n</ul>\n<h1>功能介绍</h1>\n<h2>从剪贴板读取 Cookie</h2>\n<p>参考 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6%95%99%E7%A8%8B.md\">Cookie 提取教程</a>，手动从浏览器复制所需 Cookie 内容至剪贴板，再按照程序提示操作；程序会自动读取剪贴板的内容并将有效的 Cookie 写入配置文件。</p>\n<p>成功写入配置文件后，程序会提示当前 Cookie 登录状态！</p>\n<h2>从浏览器读取 Cookie</h2>\n<p>自动读取本地浏览器的 Cookie 数据，并提取所需 Cookie 写入配置文件。</p>\n<p>成功写入配置文件后，程序会提示当前 Cookie 登录状态！</p>\n<p>Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie！</p>\n<p><strong>兼容性提醒：此功能依赖的第三方模块已长期未更新，可能无法正常支持最新浏览器版本。若功能出现异常，请尝试手动获取 Cookie！</strong></p>\n<h2><del>扫码登录获取 Cookie</del></h2>\n<p><del>程序自动获取抖音登录二维码，随后会在终端输出二维码，并使用系统默认图片浏览器打开二维码图片，使用者通过抖音 APP 扫码并登录账号，操作后关闭二维码图片窗口，程序会自动检查登录结果并将登录后的 Cookie 写入配置文件。</del></p>\n<p><b>注意：</b>扫码登录可能会导致抖音账号被风控，该功能仅限学习研究，未来可能禁用或移除该功能！</p>\n<h2>终端交互模式</h2>\n<p>功能最全面的模式，支持全部功能。</p>\n<h3>批量下载账号作品(抖音)</h3>\n<ol>\n<li>使用 <code>settings.json</code> 的 <code>accounts_urls</code> 参数中的账号链接。</li>\n<li>手动输入待采集的账号链接；此选项仅支持批量下载账号发布页作品，暂不支持参数设置。</li>\n<li>输入文本文档路径，读取文件包含的账号链接；此选项仅支持批量下载账号发布页作品，暂不支持参数设置。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/user/账号ID</code></li>\n<li><code>https://www.douyin.com/user/账号ID?modal_id=作品ID</code></li>\n</ul>\n<p><del>如果需要大批量采集账号作品，建议启用 <code>src/custom/function.py</code> 文件的 <code>suspend</code> 方法。</del>（默认启用）</p>\n<p><b>下载账号喜欢作品时需要使用已登录的 Cookie，否则程序可能无法正常获取账号消息！</b></p>\n<p>如果当前账号昵称或账号标识不是有效的文件夹名称时，程序会自动替换为账号 ID。</p>\n<p>每个账号的作品会下载至 <code>root</code> 参数路径下的账号文件夹，账号文件夹格式为 <code>UID123456789_mark_类型</code> 或者 <code>UID123456789_账号昵称_类型</code></p>\n<h3>批量下载链接作品(抖音)</h3>\n<ol>\n<li>手动输入待采集的作品链接。</li>\n<li>输入文本文档路径，读取文件包含的作品链接。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/note/作品ID</code></li>\n<li><code>https://www.douyin.com/video/作品ID</code></li>\n<li><code>https://www.douyin.com/discover?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/user/账号ID?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/search/关键词?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/channel/分区ID?modal_id=作品ID</code></li>\n</ul>\n<p>作品会下载至 <code>root</code> 参数和 <code>folder_name</code> 参数拼接成的文件夹。</p>\n<h3>获取直播拉流地址(抖音)</h3>\n<p>输入直播链接，不支持已结束的直播。</p>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://live.douyin.com/直播ID</code></li>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/follow?webRid=直播ID</code></li>\n</ul>\n<p>下载说明：</p>\n<ul>\n<li>程序会询问用户是否下载直播视频，支持同时下载多个直播视频。</li>\n<li>程序调用 <code>ffmpeg</code> 下载直播时，关闭 DouK-Downloader 不会影响直播下载。</li>\n<li><del>程序调用内置下载器下载直播时，需要保持 DouK-Downloader 运行直到直播结束。</del></li>\n<li>程序询问是否下载直播时，输入直播清晰度或者对应序号即可下载，例如：下载最高清晰度输入 <code>FULL_HD1</code> 或者 <code>1</code> 均可。</li>\n<li><del>程序调用内置下载器下载的直播文件，视频时长会显示为直播总时长，实际视频内容从下载时间开始，靠后部分的片段无法播放。</del></li>\n<li>直播视频会下载至 <code>root</code> 参数路径下的 <code>Live</code> 文件夹。</li>\n<li>按下 <code>Ctrl + C</code> 终止程序或 <code>ffmpeg</code> 并不会导致已下载文件丢失或损坏，但无法继续下载。</li>\n</ul>\n<h3>采集作品评论数据(抖音)</h3>\n<p><strong>评论回复采集功能暂不开放！</strong></p>\n<ol>\n<li>手动输入待采集的作品链接。</li>\n<li>输入文本文档路径，读取文件包含的作品链接。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/note/作品ID</code></li>\n<li><code>https://www.douyin.com/video/作品ID</code></li>\n<li><code>https://www.douyin.com/discover?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/user/账号ID?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/search/关键词?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/channel/分区ID?modal_id=作品ID</code></li>\n</ul>\n<p>支持采集<del>评论回复</del>、评论表情、评论图片；必须设置 <code>storage_format</code> 参数才能正常使用。</p>\n<p>储存名称格式：<code>作品123456789_评论数据</code></p>\n<h3>批量下载合集作品(抖音)</h3>\n<ol>\n<li>使用 <code>settings.json</code> 的 <code>mix_urls</code> 参数中的合集链接或作品链接。</li>\n<li>获取当前登录 Cookie 的收藏合集信息，并由使用者选择需要下载的合集；该选项暂不支持设置合集标识。</li>\n<li>输入合集链接，或者属于合集的任意一个作品链接；该选项暂不支持设置合集标识。</li>\n<li>输入文本文档路径，读取文件包含的作品链接或合集链接；该选项暂不支持设置合集标识。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/note/作品ID</code></li>\n<li><code>https://www.douyin.com/video/作品ID</code></li>\n<li><code>https://www.douyin.com/discover?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/user/账号ID?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/search/关键词?modal_id=作品ID</code></li>\n<li><code>https://www.douyin.com/collection/合集ID</code></li>\n<li><code>https://www.douyin.com/channel/分区ID?modal_id=作品ID</code></li>\n</ul>\n<p><del>如果需要大批量采集合集作品，建议启用 <code>src/custom/function.py</code> 文件的 <code>suspend</code> 方法。</del>（默认启用）</p>\n<p>如果当前合集标题或合集标识不是有效的文件夹名称时，程序会自动替换为合集 ID。</p>\n<p>每个合集的作品会下载至 <code>root</code> 参数路径下的合集文件夹，合集文件夹格式为 <code>MIX123456789_mark_合集作品</code> 或者 <code>MIX123456789_合集标题_合集作品</code></p>\n<h3>采集账号详细数据(抖音)</h3>\n<ol>\n<li>使用 <code>settings.json</code> 的 <code>accounts_urls</code> 参数中的账号链接。</li>\n<li>手动输入待采集的账号链接。</li>\n<li>输入文本文档路径，读取文件包含的账号链接。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://v.douyin.com/分享码/</code></li>\n<li><code>https://www.douyin.com/user/账号ID</code></li>\n<li><code>https://www.douyin.com/user/账号ID?modal_id=作品ID</code></li>\n</ul>\n<p>重复获取相同账号数据时会储存为新的数据行，不会覆盖原有数据；必须设置 <code>storage_format</code> 参数才能正常使用。</p>\n<h3>采集搜索结果数据(抖音)</h3>\n<h4>搜索条件规则</h4>\n<ul>\n<li>\n<strong>综合搜索参数顺序：</strong><code>关键词</code>;<code>总页数</code>;<code>排序依据</code>;<code>发布时间</code>;<code>视频时长</code>;<code>搜索范围</code>;<code>内容格式</code>\n</li>\n<li>\n<strong>视频搜索参数顺序：</strong><code>关键词</code>;<code>总页数</code>;<code>排序依据</code>;<code>发布时间</code>;<code>视频时长</code>;<code>搜索范围</code>\n</li>\n<li>\n<strong>用户搜索参数顺序：</strong><code>关键词</code>;<code>总页数</code>;<code>粉丝数量</code>;<code>用户类型</code>\n</li>\n<li>\n<strong>直播搜索参数顺序：</strong><code>关键词</code>;<code>总页数</code>\n</li>\n</ul>\n<h4>参数含义</h4>\n<ul>\n<li>排序依据：<code>0</code>：综合排序；<code>1</code>：最多点赞；<code>2</code>：最新发布</li>\n<li>发布时间：<code>0</code>：不限；<code>1</code>：一天内；<code>7</code>：一周内；<code>180</code>：半年内</li>\n<li>视频时长：<code>0</code>：不限；<code>1</code>：一分钟以内；<code>2</code>：一到五分钟；<code>3</code>：五分钟以上</li>\n<li>搜索范围：<code>0</code>：不限；<code>1</code>：最近看过；<code>2</code>：还未看过；<code>3</code>：关注的人</li>\n<li>内容形式：<code>0</code>：不限；<code>1</code>：视频；<code>2</code>：图文</li>\n<li>粉丝数量：<code>0</code>：不限；<code>1</code>：1000以下；<code>2</code>：1000-1W；<code>3</code>：1W-10W；<code>4</code>：10W-100W；<code>5</code>：100W以上</li>\n<li>用户类型：<code>0</code>：不限；<code>1</code>：普通用户；<code>2</code>：企业认证；<code>3</code>：个人认证</li>\n</ul>\n<p><strong>参数之间使用两个空格分隔；除了搜索关键词以外的参数均只支持输入数值；未输入的参数均视为 <code>0</code></strong></p>\n<p>程序采集的搜索结果数据会储存至文件；暂不支持直接下载搜索结果作品；必须设置 <code>storage_format</code> 参数才能正常使用。</p>\n<h4>参数输入示例</h4>\n<h5>综合搜索/视频搜索</h5>\n<p><strong>输入：</strong><code>猫咪</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">排序依据</th>\n<th align=\"center\">发布时间</th>\n<th align=\"center\">视频时长</th>\n<th align=\"center\">搜索范围</th>\n<th align=\"center\">内容形式</th>\n</tr>\n<tr>\n<td align=\"center\">猫咪</td>\n<td align=\"center\">1</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n</tr>\n</table>\n<hr>\n<p><strong>输入：</strong><code>猫咪&nbsp;&nbsp;2&nbsp;&nbsp;2&nbsp;&nbsp;7&nbsp;&nbsp;0&nbsp;&nbsp;1</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">排序依据</th>\n<th align=\"center\">发布时间</th>\n<th align=\"center\">视频时长</th>\n<th align=\"center\">搜索范围</th>\n<th align=\"center\">内容形式</th>\n</tr>\n<tr>\n<td align=\"center\">猫咪</td>\n<td align=\"center\">2</td>\n<td align=\"center\">最新发布</td>\n<td align=\"center\">一周内</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">最近看过</td>\n<td align=\"center\">不限</td>\n</tr>\n</table>\n<hr>\n<p><strong>输入：</strong><code>猫咪&nbsp;&nbsp;10&nbsp;&nbsp;0&nbsp;&nbsp;0&nbsp;&nbsp;0&nbsp;&nbsp;3</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">排序依据</th>\n<th align=\"center\">发布时间</th>\n<th align=\"center\">视频时长</th>\n<th align=\"center\">搜索范围</th>\n<th align=\"center\">内容形式</th>\n</tr>\n<tr>\n<td align=\"center\">猫咪</td>\n<td align=\"center\">10</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">关注的人</td>\n<td align=\"center\">不限</td>\n</tr>\n</table>\n<hr>\n<p><strong>输入：</strong><code>猫咪&nbsp;白&nbsp;&nbsp;5&nbsp;&nbsp;0&nbsp;&nbsp;180</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">排序依据</th>\n<th align=\"center\">发布时间</th>\n<th align=\"center\">视频时长</th>\n<th align=\"center\">搜索范围</th>\n<th align=\"center\">内容形式</th>\n</tr>\n<tr>\n<td align=\"center\">猫咪 白</td>\n<td align=\"center\">5</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">半年内</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n</tr>\n</table>\n<h5>用户搜索</h5>\n<p><strong>输入：</strong><code>小姐姐&nbsp;&nbsp;10&nbsp;&nbsp;0&nbsp;&nbsp;0</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">粉丝数量</th>\n<th align=\"center\">用户类型</th>\n</tr>\n<tr>\n<td align=\"center\">小姐姐</td>\n<td align=\"center\">10</td>\n<td align=\"center\">不限</td>\n<td align=\"center\">不限</td>\n</tr>\n</table>\n<hr>\n<p><strong>输入：</strong><code>小姐姐&nbsp;&nbsp;5&nbsp;&nbsp;4&nbsp;&nbsp;3</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n<th align=\"center\">粉丝数量</th>\n<th align=\"center\">用户类型</th>\n</tr>\n<tr>\n<td align=\"center\">小姐姐</td>\n<td align=\"center\">5</td>\n<td align=\"center\">10W-100W</td>\n<td align=\"center\">个人认证</td>\n</tr>\n</table>\n<h5>直播搜索</h5>\n<p><strong>输入：</strong><code>跳舞&nbsp;&nbsp;10</code></p>\n<table>\n<tr>\n<th align=\"center\" rowspan=\"2\">含义</th>\n<th align=\"center\">关键词</th>\n<th align=\"center\">总页数</th>\n</tr>\n<tr>\n<td align=\"center\">跳舞</td>\n<td align=\"center\">10</td>\n</tr>\n</table>\n<h3>采集抖音热榜数据(抖音)</h3>\n<p>无需输入任何内容；采集 <code>抖音热榜</code>、<code>娱乐榜</code>、<code>社会榜</code>、<code>挑战榜</code> 数据并储存至文件；必须设置 <code>storage_format</code> 参数才能正常使用。</p>\n<p>储存名称格式：<code>热榜数据_采集时间_热榜名称</code></p>\n<h3>批量下载话题作品(抖音)</h3>\n<p>暂不支持！</p>\n<h3>批量下载收藏作品(抖音)</h3>\n<p>无需输入任何内容；需要在配置文件写入已登录的 Cookie，并在 <code>owner_url</code> 参数填入对应的账号主页链接和账号标识（可选参数）；目前仅支持采集当前 Cookie 对应账号的收藏作品。</p>\n<p>文件夹格式为 <code>UID123456789_mark_收藏作品</code> 或者 <code>UID123456789_账号昵称_收藏作品</code></p>\n<h3>批量下载收藏夹作品(抖音)</h3>\n<p>无需输入任何内容；需要在配置文件写入已登录的 Cookie，程序会自动获取当前 Cookie 账号的收藏夹数据并展示，根据程序提示输入收藏夹序号下载对应收藏夹作品文件，输入 <code>ALL</code> 下载全部收藏夹作品。</p>\n<p>文件夹格式为 <code>CID123456789_收藏夹名称_收藏作品</code></p>\n<h3>批量下载账号作品(TikTok)</h3>\n<ol>\n<li>使用 <code>settings.json</code> 的 <code>accounts_urls_tiktok</code> 参数中的账号链接。</li>\n<li>手动输入待采集的账号链接；此选项仅支持批量下载账号发布页作品，暂不支持参数设置。</li>\n<li>输入文本文档路径，读取文件包含的账号链接；此选项仅支持批量下载账号发布页作品，暂不支持参数设置。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://www.tiktok.com/@TikTok号</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/video/作品ID</code></li>\n</ul>\n<p><del>如果需要大批量采集账号作品，建议启用 <code>src/custom/function.py</code> 文件的 <code>suspend</code> 方法。</del>（默认启用）</p>\n<p>如果当前账号昵称或账号标识不是有效的文件夹名称时，程序会自动替换为账号 ID。</p>\n<p>每个账号的作品会下载至 <code>root</code> 参数路径下的账号文件夹，账号文件夹格式为 <code>UID123456789_mark_类型</code> 或者 <code>UID123456789_账号昵称_类型</code></p>\n<h3>批量下载链接作品(TikTok)</h3>\n<ol>\n<li>手动输入待采集的作品链接。</li>\n<li>输入文本文档路径，读取文件包含的作品链接。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://vm.tiktok.com/分享码/</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/video/作品ID</code></li>\n</ul>\n<p>作品会下载至 <code>root</code> 参数和 <code>folder_name</code> 参数拼接成的文件夹。</p>\n<h3>批量下载合集作品(TikTok)</h3>\n<ol>\n<li>使用 <code>settings.json</code> 的 <code>mix_urls_tiktok</code> 参数中的合集链接。</li>\n<li>输入合集链接；该选项暂不支持设置合集标识。</li>\n<li>输入文本文档路径，读取文件包含的合集链接；该选项暂不支持设置合集标识。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://vt.tiktok.com/分享码/</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/playlist/合辑信息</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/collection/合辑信息</code></li>\n</ul>\n<p><del>如果需要大批量采集合集作品，建议启用 <code>src/custom/function.py</code> 文件的 <code>suspend</code> 方法。</del>（默认启用）</p>\n<p>如果当前合集标题或合集标识不是有效的文件夹名称时，程序会自动替换为合集 ID。</p>\n<p>每个合集的作品会下载至 <code>root</code> 参数路径下的合集文件夹，合集文件夹格式为 <code>MIX123456789_mark_合集作品</code> 或者 <code>MIX123456789_合集标题_合集作品</code></p>\n<h3>获取直播拉流地址(TikTok)</h3>\n<p>输入直播链接，不支持已结束的直播。</p>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://vt.tiktok.com/分享码/</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/live</code></li>\n</ul>\n<p>下载说明：</p>\n<ul>\n<li>程序会询问用户是否下载直播视频，支持同时下载多个直播视频。</li>\n<li>程序调用 <code>ffmpeg</code> 下载直播时，关闭 DouK-Downloader 不会影响直播下载。</li>\n<li><del>程序调用内置下载器下载直播时，需要保持 DouK-Downloader 运行直到直播结束。</del></li>\n<li>程序询问是否下载直播时，输入直播清晰度或者对应序号即可下载，例如：下载最高清晰度输入 <code>FULL_HD1</code> 或者 <code>1</code> 均可。</li>\n<li><del>程序调用内置下载器下载的直播文件，视频时长会显示为直播总时长，实际视频内容从下载时间开始，靠后部分的片段无法播放。</del></li>\n<li>直播视频会下载至 <code>root</code> 参数路径下的 <code>Live</code> 文件夹。</li>\n<li>按下 <code>Ctrl + C</code> 终止程序或 <code>ffmpeg</code> 并不会导致已下载文件丢失或损坏，但无法继续下载。</li>\n</ul>\n<h3><del>批量下载视频原画(TikTok)</del></h3>\n<p><strong>注意：本功能为实验性功能，依赖第三方 API 服务，可能不稳定或存在限制！</strong></p>\n<ol>\n<li>手动输入待采集的作品链接。</li>\n<li>输入文本文档路径，读取文件包含的作品链接。</li>\n</ol>\n<p>支持链接格式：</p>\n<ul>\n<li><code>https://vm.tiktok.com/分享码/</code></li>\n<li><code>https://www.tiktok.com/@TikTok号/video/作品ID</code></li>\n</ul>\n<p>作品会下载至 <code>root</code> 参数和 <code>folder_name</code> 参数拼接成的文件夹。</p>\n<h2>后台监听模式</h2>\n<h3>剪贴板监听下载</h3>\n<p>程序会自动检测并提取剪贴板中的抖音和 TikTok 作品链接，并自动下载作品文件；如需关闭，请按下 Ctrl+C，或将剪贴板内容设置为“close”以停止监听！</p>\n<h2>Web API 接口模式</h2>\n<p>启动服务器，提供 API 调用功能；支持局域网远程访问，可以部署至私有服务器或者公开服务器，远程部署建议设置参数验证，防止恶意请求！</p>\n<p>默认禁用局域网访问，如需开启，请修改 <code>src/custom/static.py</code> 文件的 <code>SERVER_HOST</code> 变量。</p>\n<p><strong>启动该模式后，访问 <code>http://127.0.0.1:5555/docs</code> 或者 <code>http://127.0.0.1:5555/redoc</code> 可以查阅自动生成的文档！</strong></p>\n<h3>API 调用示例代码</h3>\n<pre>\nfrom httpx import post\nfrom rich import print\n\n\ndef demo():\nheaders = {\"token\": \"\"}\ndata = {\n\"detail_id\": \"0123456789\",\n\"pages\": 2,\n}\napi = \"http://127.0.0.1:5555/douyin/comment\"\nresponse = post(api, json=data, headers=headers)\nprint(response.json())\n\ndemo()\n</pre>\n<h2>Web UI 交互模式</h2>\n<p><b>项目代码已重构，该模式代码尚未更新，未来开发完成重新开放！</b></p>\n<h2>启用/禁用作品下载记录</h2>\n<ul>\n<li>启用该功能：程序会记录下载成功的作品 ID，如果对作品文件进行移动、重命名或者删除操作，程序不会重复下载该作品，如果想要重新下载该作品，需要删除记录数据中对应的作品 ID。</li>\n<li>禁用该功能：程序会在下载文件前检测文件是否存在，如果文件存在会自动跳过下载该作品，如果对作品文件进行移动、重命名或者删除操作，程序将会重新下载该作品。</li>\n</ul>\n<p>数据路径: <code>./Volume/DouK-Downloader.db</code> 的 <code>download_data</code> 数据表。</p>\n<h2>删除指定下载记录</h2>\n<p>输入作品 ID 或者作品完整链接（多个作品之间使用空格分隔，支持混合输入），删除作品下载记录中对应的数据，如果输入 <code>all</code>，代表清空作品下载记录数据！</p>\n<h2>启用/禁用运行日志记录</h2>\n<p>是否将程序运行日志记录保存到文件，默认关闭，日志文件保存路径：<code>./Volume/Log</code></p>\n<p>如果在使用过程中发现程序 Bug，可以及时告知作者，并附上日志文件，日志记录有助于作者分析 Bug 原因和修复 Bug。</p>\n<h2>检查程序版本更新</h2>\n<p>程序会向 <code>https://github.com/JoeanAmier/TikTokDownloader/releases/latest</code>\n发送请求获取最新 <code>Releases</code> 版本号，并提示是否存在新版本。</p>\n<p>如果检查新版本失败，可能是访问 GitHub 超时，并非功能异常；如果存在新版本会提示新版本的 <code>URL</code> 地址，不会自动下载更新。</p>\n<h1>其他功能说明</h1>\n<h2>单次输入多个链接</h2>\n<p><code>批量下载账号作品</code>、<code>批量下载链接作品</code>、<code>获取直播拉流地址</code>、<code>采集作品评论数据</code>、<code>批量下载合集作品</code>、<code>采集账号详细数据</code>\n功能支持单次输入多个链接，实现批量下载 / 提取功能；支持完整链接与分享链接混合输入；输入多个链接时，需要使用空格分隔；无需对复制的链接进行额外处理，程序会自动提取输入文本中的有效链接。</p>\n<h2 id=\"mark\">账号/合集标识说明</h2>\n<h3>标识设置规则</h3>\n<ul>\n<li><code>name_format</code> 参数中没有使用 <code>nickname</code> 时，<code>mark</code> 设置没有限制。</li>\n<li><code>name_format</code> 参数中使用了 <code>nickname</code> 时，<code>mark</code> 与 <code>nickname</code> 不能设置为包含关系的字符串。</li>\n</ul>\n<p><strong>标识示例：</strong></p>\n<ul>\n<li>✔️ <code>nickname</code>：ABC，<code>mark</code>：DEF</li>\n<li>✔️ <code>nickname</code>：ABC，<code>mark</code>：BCD</li>\n<li>❌ <code>nickname</code>：ABC，<code>mark</code>：AB</li>\n<li>❌ <code>nickname</code>：BC，<code>mark</code>：ABC</li>\n</ul>\n<h3>账号标识说明</h3>\n<ul>\n<li>账号标识 <code>mark</code> 参数相当于账号备注，便于用户识别账号作品文件夹，避免账号昵称修改导致无法识别已下载作品问题。</li>\n<li><code>批量下载账号作品</code> 模式下，如果设置了 <code>mark</code> 参数，下载的作品将会保存至 <code>UID123456789_mark_发布作品</code>\n或 <code>UID123456789_mark_喜欢作品</code> 文件夹内。</li>\n<li><code>批量下载账号作品</code> 模式下，如果 <code>mark</code>\n参数设置为空字符串，程序将会使用账号昵称作为账号标识，下载的作品将会保存至 <code>UID123456789_账号昵称_发布作品</code>\n或 <code>UID123456789_账号昵称_喜欢作品</code> 文件夹内。</li>\n</ul>\n<h3>合集标识说明</h3>\n<p>与账号标识作用一致。</p>\n<h3>如何修改标识</h3>\n<p><strong>修改账号标识:</strong> 修改 <code>accounts_urls</code> 或 <code>accounts_urls_tiktok</code> 的 <code>mark</code> 参数，再次运行 <code>批量下载账号作品</code> 模式，程序会自动应用新的账号标识。</p>\n<p><strong>修改合集标识:</strong> 修改 <code>mix_urls</code> 或 <code>mix_urls_tiktok</code> 的 <code>mark</code> 参数，再次运行 <code>批量下载合集作品</code> 模式，程序会自动应用新的账号标识。</p>\n<h2>账号昵称/合集标题自动更新</h2>\n<p>在 <code>批量下载账号作品</code> 和 <code>批量下载合集作品</code> 模式下，程序会自动判断账号昵称/合集标题是否发生变化，如果发生变化，程序会自动识别已下载作品文件名称中的账号昵称/合集标题，并修改至最新账号昵称/合集标题。</p>\n<p>程序会优先使用账号标识/合集标识进行更新处理，如果账号标识/合集标识为空字符串，程序会自动使用账号昵称/合集标题进行更新处理。</p>\n<h3>映射缓存数据</h3>\n<p><strong>数据路径: <code>./Volume/DouK-Downloader.db</code> 的 <code>mapping_data</code> 数据表；</strong>\n用于记录账号 / 合集标识和账号昵称，当账号 / 合集标识或账号昵称发生变化时，程序会对相应的文件夹和文件进行重命名更新处理。</p>\n<p><strong>缓存数据仅供程序读取和修改，不建议手动编辑数据内容。</strong></p>\n\n# 构建可执行文件指南\n\n本指南将引导您通过 Fork 本仓库并执行 GitHub Actions 自动完成基于最新源码的程序构建和打包！\n\n---\n\n## 使用步骤\n\n### 1. Fork 本仓库\n\n1. 点击项目仓库右上角的 **Fork** 按钮，将本仓库 Fork 到您的个人 GitHub 账户中\n2. 您的 Fork 仓库地址将类似于：`https://github.com/your-username/this-repo`\n\n---\n\n### 2. 启用 GitHub Actions\n\n1. 前往您 Fork 的仓库页面\n2. 点击顶部的 **Settings** 选项卡\n3. 点击右侧的 **Actions** 选项卡\n4. 点击 **General** 选项\n5. 在 **Actions permissions** 下，选择 **Allow all actions and reusable workflows** 选项，点击 **Save** 按钮\n\n---\n\n### 3. 手动触发打包流程\n\n1. 在您 Fork 的仓库中，点击顶部的 **Actions** 选项卡\n2. 找到名为 **手动构建可执行文件** 的工作流\n3. 点击右侧的 **Run workflow** 按钮：\n    - 选择 **master** 或者 **develop** 分支\n    - 点击 **Run workflow**\n\n---\n\n### 4. 查看打包进度\n\n1. 在 **Actions** 页面中，您可以看到触发的工作流运行记录\n2. 点击运行记录，查看详细的日志以了解打包进度和状态\n\n---\n\n### 5. 下载打包结果\n\n1. 打包完成后，进入对应的运行记录页面\n2. 在页面底部的 **Artifacts** 部分，您将看到打包的结果文件\n3. 点击下载并保存到本地，即可获得打包好的程序\n\n---\n\n## 注意事项\n\n1. **资源使用**：\n    - Actions 的运行环境由 GitHub 免费提供，普通用户每月有一定的免费使用额度（2000 分钟）\n\n2. **代码修改**：\n    - 您可以自由修改 Fork 仓库中的代码以定制程序打包流程\n    - 修改后重新触发打包流程，您将得到自定义的构建版本\n\n3. **与主仓库保持同步**：\n    - 如果主仓库更新了代码或工作流，建议您定期同步 Fork 仓库以获取最新功能和修复\n\n---\n\n## Actions 常见问题\n\n### Q1: 为什么我无法触发工作流？\n\nA: 请确认您已按照步骤 **启用 Actions**，否则 GitHub 会禁止运行工作流\n\n### Q2: 打包流程失败怎么办？\n\nA:\n\n- 检查运行日志，了解失败原因\n- 确保代码没有语法错误或依赖问题\n- 如果问题仍未解决，可以在本仓库的 [Issues 页面](https://github.com/JoeanAmier/TikTokDownloader/issues) 提出问题\n\n### Q3: 我可以直接使用主仓库的 Actions 吗？\n\nA: 由于权限限制，您无法直接触发主仓库的 Actions。请通过 Fork 仓库的方式执行打包流程\n\n<h1>常见问题与解决方案</h1>\n<h2>响应内容不是有效的 JSON 数据</h2>\n<p>可能是 Cookie 无效或者接口失效；请尝试清除 DNS 缓存，更新 Cookie，如果仍然无法解决，可能是接口失效，请考虑向作者反馈！</p>\n<h2 id=\"twc\">获取 ttwid 参数失败</h2>\n<p>TikTok 平台的 Cookie ttwid 值无效；可能是当前账号被风控，请考虑更换账号，或者尝试设置 <code>twc_tiktok</code> 参数。</p>\n<p><code>twc_tiktok</code> 参数设置教程：</p>\n<ul>\n<li>以无痕模式打开浏览器</li>\n<li>按 <code>F12</code> 打开开发人员工具</li>\n<li>选择 <code>网络</code> 选项卡</li>\n<li>访问 <code>https://www.tiktok.com/</code></li>\n<li>在 <code>筛选器</code> 输入框输入 <code>ttwid</code></li>\n<li>在开发人员工具窗口选择任意一个数据包(如果无数据包，刷新网页)</li>\n<li>检查 <code>响应标头</code> -> <code>Set-Cookie</code></li>\n<li>复制 <code>ttwid=XXX</code> 内容</li>\n<li>粘贴至配置文件的 <code>twc_tiktok</code> 参数</li>\n</ul>\n<p><code>Set-Cookie</code> 的内容格式为：<code>ttwid=XXX; Path=/; Domain=tiktok.com; Max-Age=31536000; HttpOnly; Secure; SameSite=None</code>，复制时只需要复制 <code>ttwid=XXX</code> 部分，而不是复制全部内容！</p>\n<h2>采集数据而不下载文件</h2>\n<p>将配置文件的 <code>download</code> 参数设置为 <code>false</code>，并设置 <code>storage_format</code> 参数，程序将不会下载任何文件，仅采集数据。</p>\n<h2>请求超时：timed out</h2>\n<p>网络异常；如果您的网络需要使用代理才能访问 TikTok，请在配置文件设置 <code>proxy</code> 参数！</p>\n<h2>self 获取账号信息失败</h2>\n<p>请把配置文件的 <code>owner_url</code> 参数修改为实际的抖音主页链接，获取方式请查阅 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/issues/416\">issue</a></p>\n<h1>免责声明</h1>\n<ol>\n<li>使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。</li>\n<li>本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。</li>\n<li>本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责任。</li>\n<li>使用者在使用本项目时必须严格遵守 <a href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU\n    General Public License v3.0</a> 的要求，并在适当的地方注明使用了 <a\n        href=\"https://github.com/JoeanAmier/TikTokDownloader/blob/master/LICENSE\">GNU General Public License\n    v3.0</a> 的代码。\n</li>\n<li>使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。</li>\n<li>使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分发。</li>\n<li>本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使用者自行承担。</li>\n<li>使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。</li>\n<li>本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-Downloader 项目相关的任何商业服务。</li>\n<li>基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的各种情况负全部责任。</li>\n<li>本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣传、推广或再授权。</li>\n<li>作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁已获取的代码及衍生作品。</li>\n<li>作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修订后的条款。</li>\n</ol>\n<b>在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险和后果。</b>\n"
  },
  {
    "path": "docs/Release_Notes.md",
    "content": "**更新内容：**\n\n1. API 模式搜索接口增加 `offset` 和 `count` 参数\n2. 修复部分 TikTok 账号提取 sec_user_id 失败的问题\n3. 修复 API 模式搜索接口多页数据报错的问题\n4. 修复 API 模式搜索接口结果为空报错的问题\n5. 修复 TikTok 平台批量下载账号作品功能\n6. 修复 TikTok 平台批量下载合集作品功能\n7. TikTok 平台新增 X-Gnarly 请求参数\n8. 优化提取 secUid 的正则表达式\n9. 服务器模式默认启用局域网访问\n10. 修复请求参数编码错误的问题\n11. 修复提取作品 ID 失败的问题\n12. 更新数据接口请求参数\n13. 更新项目英语翻译\n14. 修正项目功能描述\n15. 修复其他已知问题\n"
  },
  {
    "path": "license",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "locale/README.md",
    "content": "# 命令参考\n\n**运行命令前，确保已经安装了 `gettext` 软件包，并配置好环境变量。**\n\n**Before running the command, ensure that the `gettext` package is installed and the environment variables are properly\nconfigured.**\n\n* `xgettext --files-from=py_files.txt -d tk -o tk.pot`\n* `mkdir zh_CN\\LC_MESSAGES`\n* `msginit -l zh_CN -o zh_CN/LC_MESSAGES/tk.po -i tk.pot`\n* `mkdir en_US\\LC_MESSAGES`\n* `msginit -l en_US -o en_US/LC_MESSAGES/tk.po -i tk.pot`\n* `msgmerge -U zh_CN/LC_MESSAGES/tk.po tk.pot`\n* `msgmerge -U en_US/LC_MESSAGES/tk.po tk.pot`\n\n# 翻译贡献指南\n\n* 如果想要贡献支持更多语言，请在终端切换至 `locale` 文件夹，运行命令 `msginit -l 语言代码 -o 语言代码/LC_MESSAGES/tk.po -i tk.pot`\n  生成 po 文件并编辑翻译。\n* 如果想要贡献改进翻译结果，请直接编辑 `tk.po` 文件内容。\n* 仅需提交 `tk.po` 文件，作者会转换格式并合并。\n\n# Translation Contribution Guide\n\n* If you want to contribute support for more languages, please switch to the `locale` folder in the terminal and run the\n  command `msginit -l language_code -o language_code/LC_MESSAGES/tk.po -i tk.pot` to generate the po file and edit the\n  translation.\n* If you want to contribute to improving the translation, please directly edit the content of the `tk.po` file.\n* Only the `tk.po` file needs to be submitted, and the author will convert the format and merge it.\n"
  },
  {
    "path": "locale/en_US/LC_MESSAGES/tk.po",
    "content": "# English translations for DouK-Downloader package.\n# Copyright (C) 2024 THE DouK-Downloader'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the DouK-Downloader package.\n# FIRST AUTHOR <yonglelolu@foxmail.com>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: DouK-Downloader 5.8\\n\"\n\"Report-Msgid-Bugs-To: <yonglelolu@foxmail.com>\\n\"\n\"POT-Creation-Date: 2025-11-04 10:48+0800\\n\"\n\"PO-Revision-Date: 2024-12-22 21:46+0800\\n\"\n\"Last-Translator: <yonglelolu@foxmail.com>\\n\"\n\"Language-Team: English\\n\"\n\"Language: en_US\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:41\nmsgid \"\"\n\"程序会自动检测并提取剪贴板中的抖音和 TikTok 作品链接，并自动下载作品文件；如\"\n\"需关闭，请按下 Ctrl+C，或将剪贴板内容设置为“close”以停止监听！\"\nmsgstr \"\"\n\"The program will automatically detect and extract TikTok and DouYin video \"\n\"links from the clipboard, then download the video files. To turn it off, \"\n\"press Ctrl+C or set the clipboard content to \\\"close\\\" to stop monitoring!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:129\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:941\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:968\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1288\n#, python-brace-format\nmsgid \"{url} 提取作品 ID 失败\"\nmsgstr \"Failed to extract works ID from {url}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:50\nmsgid \"验证失败！\"\nmsgstr \"Verification failed!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:106\nmsgid \"访问项目 GitHub 仓库\"\nmsgstr \"Visit the project's GitHub repository\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:107\nmsgid \"重定向至项目 GitHub 仓库主页\"\nmsgstr \"Redirect to the project's GitHub repository homepage\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:123\nmsgid \"项目\"\nmsgstr \"Project\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:115\nmsgid \"测试令牌有效性\"\nmsgstr \"Test token validity\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:128\nmsgid \"验证成功！\"\nmsgstr \"Verification successful!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:135\nmsgid \"更新项目全局配置\"\nmsgstr \"Update project global configuration\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:145\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:158\nmsgid \"配置\"\nmsgstr \"Configuration\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:156\nmsgid \"获取项目全局配置\"\nmsgstr \"Retrieve project global configuration\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:157\nmsgid \"返回项目全部配置参数\"\nmsgstr \"Return all configuration parameters of the project\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:166\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:511\nmsgid \"获取分享链接重定向的完整链接\"\nmsgstr \"Retrieve the complete link for the shared link redirect\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:175\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:206\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:233\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:259\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:299\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:342\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:379\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:419\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:449\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:477\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:501\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:190\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:444\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:885\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:961\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:43\nmsgid \"抖音\"\nmsgstr \"DouYin\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:183\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:528\nmsgid \"请求链接成功！\"\nmsgstr \"Request link successful!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:188\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:533\nmsgid \"请求链接失败！\"\nmsgstr \"Request link failed!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:195\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:540\nmsgid \"获取单个作品数据\"\nmsgstr \"Retrieve data for a single works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:216\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:561\nmsgid \"获取账号作品数据\"\nmsgstr \"Retrieve account works data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:243\nmsgid \"获取合集作品数据\"\nmsgstr \"Retrieve mix works data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:269\nmsgid \"参数错误！\"\nmsgstr \"Parameter error!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:288\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:622\nmsgid \"获取直播数据\"\nmsgstr \"Retrieve live stream data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:326\nmsgid \"获取作品评论数据\"\nmsgstr \"Retrieve work comments data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:364\nmsgid \"获取评论回复数据\"\nmsgstr \"Retrieve comment replies data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:398\nmsgid \"获取综合搜索数据\"\nmsgstr \"Retrieve general search data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:429\nmsgid \"获取视频搜索数据\"\nmsgstr \"Retrieve video search data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:459\nmsgid \"获取用户搜索数据\"\nmsgstr \"Retrieve user search data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:487\nmsgid \"获取直播搜索数据\"\nmsgstr \"Retrieve live search data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:588\nmsgid \"获取合辑作品数据\"\nmsgstr \"Retrieve mix works data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:656\nmsgid \"搜索结果为空！\"\nmsgstr \"The search result is empty!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:709\nmsgid \"获取数据成功！\"\nmsgstr \"Successfully retrieved data!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:720\nmsgid \"获取数据失败！\"\nmsgstr \"Failed to retrieve data!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:71\nmsgid \"\"\n\"未设置 storage_format 参数，无法正常使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\"The `storage_format` parameter is not set, so this feature cannot be used \"\n\"properly. Please refer to the project documentation for detailed information!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:86\nmsgid \"抖音 Cookie\"\nmsgstr \"DouYin Cookie\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:90\n#, python-brace-format\nmsgid \"{tip} 未登录，无法使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\"{tip} Not logged in, unable to use this feature. Please refer to the project \"\n\"documentation for detailed information!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:143\nmsgid \"批量下载账号作品(抖音)\"\nmsgstr \"Batch Download Account Works (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:147\nmsgid \"批量下载链接作品(抖音)\"\nmsgstr \"Batch Download Works from Links (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:151\nmsgid \"获取直播拉流地址(抖音)\"\nmsgstr \"Get live stream pull URL (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:155\nmsgid \"采集作品评论数据(抖音)\"\nmsgstr \"Collect Works Comment Data (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:159\nmsgid \"批量下载合集作品(抖音)\"\nmsgstr \"Batch Download Mix Works (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:163\nmsgid \"采集账号详细数据(抖音)\"\nmsgstr \"Collect Account Detailed Data (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:167\nmsgid \"采集搜索结果数据(抖音)\"\nmsgstr \"Collect Search Result Data (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:171\nmsgid \"采集抖音热榜数据(抖音)\"\nmsgstr \"Collect DouYin Hot Board Data (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:176\nmsgid \"批量下载收藏作品(抖音)\"\nmsgstr \"Batch Download Favorites Works (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:180\nmsgid \"批量下载收藏音乐(抖音)\"\nmsgstr \"Batch Download Favorites Music (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:185\nmsgid \"批量下载收藏夹作品(抖音)\"\nmsgstr \"Batch Download Collections Works (DouYin)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:189\nmsgid \"批量下载账号作品(TikTok)\"\nmsgstr \"Batch Download Account Works (TikTok)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:193\nmsgid \"批量下载链接作品(TikTok)\"\nmsgstr \"Batch Download Works from Links (TikTok)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:197\nmsgid \"批量下载合集作品(TikTok)\"\nmsgstr \"Batch Download Mix Works (TikTok)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:201\nmsgid \"获取直播拉流地址(TikTok)\"\nmsgstr \"Get live stream pull URL (TikTok)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:206\nmsgid \"批量下载视频原画(TikTok)\"\nmsgstr \"Batch Download Video original file (TikTok)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:211\nmsgid \"使用 accounts_urls 参数的账号链接(推荐)\"\nmsgstr \"Account links using the `accounts_urls` parameter (recommended)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:212\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:220\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:236\nmsgid \"手动输入待采集的账号链接\"\nmsgstr \"Manually enter the account links to be collected\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:213\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:221\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:237\nmsgid \"从文本文档读取待采集的账号链接\"\nmsgstr \"Read account links to be collected from a text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:217\nmsgid \"使用 accounts_urls_tiktok 参数的账号链接(推荐)\"\nmsgstr \"Account links using the `accounts_urls_tiktok` parameter (recommended)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:224\nmsgid \"使用 mix_urls 参数的合集链接(推荐)\"\nmsgstr \"Mix links using the `mix_urls` parameter (recommended)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:225\nmsgid \"获取当前账号收藏合集列表\"\nmsgstr \"Retrieve the current account's Collections Mix list\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:226\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:231\nmsgid \"手动输入待采集的合集/作品链接\"\nmsgstr \"Manually enter the Mix/Works links to be collected\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:227\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:232\nmsgid \"从文本文档读取待采集的合集/作品链接\"\nmsgstr \"Read the Mix/Works links to be collected from a text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:230\nmsgid \"使用 mix_urls_tiktok 参数的合集链接(推荐)\"\nmsgstr \"Mix links using the `mix_urls_tiktok` parameter (recommended)\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:235\nmsgid \"使用 accounts_urls 参数的账号链接\"\nmsgstr \"Account links using the `accounts_urls` parameter\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:240\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:244\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:248\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:252\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:256\nmsgid \"手动输入待采集的作品链接\"\nmsgstr \"Manually enter the works links to be collected\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:241\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:245\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:249\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:253\nmsgid \"从文本文档读取待采集的作品链接\"\nmsgstr \"Read the works links to be collected from a text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:261\nmsgid \"综合搜索数据采集\"\nmsgstr \"General Search\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:265\nmsgid \"视频搜索数据采集\"\nmsgstr \"Video Search\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:269\nmsgid \"用户搜索数据采集\"\nmsgstr \"User Search\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:273\nmsgid \"直播搜索数据采集\"\nmsgstr \"Live Search\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:283\n#, python-brace-format\nmsgid \"请输入{tip}链接: \"\nmsgstr \"Please enter the {tip} link:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:296\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:322\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:330\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1821\nmsgid \"请选择账号链接来源\"\nmsgstr \"Please select the account link source\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:300\nmsgid \"已退出批量下载账号作品(TikTok)模式\"\nmsgstr \"Exited Batch Download Account Works (TikTok) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:302\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:415\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:535\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:25\nmsgid \"账号\"\nmsgstr \"Account\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:306\n#, python-brace-format\nmsgid \"程序共处理 {0} 个{1}，成功 {2} 个，失败 {3} 个，耗时 {4} 分钟 {5} 秒\"\nmsgstr \"\"\n\"The program processed a total of {0} {1}, with {2} successes, {3} failures, \"\n\"and a duration of {4} minutes {5} seconds.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:326\nmsgid \"已退出批量下载账号作品(抖音)模式\"\nmsgstr \"Exited Batch Download Account Works (DouYin) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:382\n#, python-brace-format\nmsgid \"共有 {count} 个账号的作品等待下载\"\nmsgstr \"There are {count} accounts' works waiting to be downloaded\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:393\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 提取 sec_user_id 失败，错误配置：{data}\"\nmsgstr \"\"\n\"Failed to extract sec_user_id from the url {url} in the configuration file \"\n\"{name}, error configuration: {data}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:451\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1743\nmsgid \"账号主页\"\nmsgstr \"Account Page\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:438\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1747\n#, python-brace-format\nmsgid \"{url} 提取账号 sec_user_id 失败\"\nmsgstr \"Failed to extract account sec_user_id from {url}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:470\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:508\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1776\nmsgid \"从文本文档提取账号 sec_user_id 失败\"\nmsgstr \"Failed to extract account sec_user_id from the text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:556\n#, python-brace-format\nmsgid \"开始处理第 {index} 个账号\"\nmsgstr \"Start processing the {index}th account\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:558\nmsgid \"开始处理账号\"\nmsgstr \"Starting to process the account\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:571\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败，请检查 Cookie 登录状态！\"\nmsgstr \"\"\n\"Failed to retrieve account information for {sec_user_id}, please check the \"\n\"Cookie login status!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:582\nmsgid \"\"\n\"如果账号发布作品均为共创作品且该账号均不是作品作者时，请配置已登录的 Cookie \"\n\"后重新运行程序，其余情况请无视该提示！\"\nmsgstr \"\"\n\"If all works published by the account are co-created works and the account \"\n\"is not the author of any work, please configure a logged-in Cookie and run \"\n\"the program again. Ignore this message in all other cases!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:730\nmsgid \"开始提取作品数据\"\nmsgstr \"Starting to extract works data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:743\nmsgid \"提取账号或合集信息发生错误！\"\nmsgstr \"An error occurred while extracting account or collection information!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:825\nmsgid \"发布作品\"\nmsgstr \"Posts\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:827\nmsgid \"喜欢作品\"\nmsgstr \"Liked\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:829\nmsgid \"收藏作品\"\nmsgstr \"Favorites\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:831\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:35\nmsgid \"合集作品\"\nmsgstr \"Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:833\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:89\nmsgid \"收藏夹作品\"\nmsgstr \"Collections\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:844\n#, python-brace-format\nmsgid \"昵称/标题：{name}；标识：{mark}；ID：{id}\"\nmsgstr \"Nickname/Title: {name}; Mark: {mark}; ID: {id}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:884\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:918\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1272\nmsgid \"请选择作品链接来源\"\nmsgstr \"Please select the works link source\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:888\nmsgid \"已退出批量下载链接作品(抖音)模式\"\nmsgstr \"Exited Batch Download Works from Links (DouYin) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:898\nmsgid \"已退出批量下载链接作品(TikTok)模式\"\nmsgstr \"Exited Batch Download Works from Links (TikTok) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:905\nmsgid \"注意：本功能为实验性功能，依赖第三方 API 服务，可能不稳定或存在限制！\"\nmsgstr \"\"\n\"Note: This feature is experimental and relies on unofficial API services, \"\n\"which may be unstable or have limitations!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:911\nmsgid \"已退出批量下载视频原画(TikTok)模式\"\nmsgstr \"Exited Batch Download Video original file (TikTok) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:938\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:965\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1283\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\slides.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:38\nmsgid \"作品\"\nmsgstr \"Works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:944\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:971\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1017\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1291\n#, python-brace-format\nmsgid \"共提取到 {count} 个作品，开始处理！\"\nmsgstr \"Successfully extracted {count} works, starting to process!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:984\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1005\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1014\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1312\nmsgid \"从文本文档提取作品 ID 失败\"\nmsgstr \"Failed to extract works ID from the text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1093\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:320\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:323\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:405\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:749\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:453\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:467\nmsgid \"图集\"\nmsgstr \"Image\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1095\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:326\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:329\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:456\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:751\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:351\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:365\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:480\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:570\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:102\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:116\nmsgid \"视频\"\nmsgstr \"Video\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1105\nmsgid \"程序未检测到有效的 ffmpeg，不支持直播下载功能！\"\nmsgstr \"\"\n\"The program did not detect a valid ffmpeg tool, live streaming download \"\n\"functionality is not supported!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1109\nmsgid \"请选择下载清晰度(输入清晰度或者对应序号，直接回车代表不下载): \"\nmsgstr \"\"\n\"Please select the download resolution (enter the resolution or corresponding \"\n\"number, press Enter to skip download):\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1116\nmsgid \"未输入有效的清晰度或者序号，跳过下载！\"\nmsgstr \"No valid resolution or serial number entered, skipping download!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1149\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1164\nmsgid \"直播\"\nmsgstr \"Live\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1154\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1171\nmsgid \"获取直播数据失败\"\nmsgstr \"Failed to retrieve live stream data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1158\nmsgid \"已退出获取直播拉流地址(抖音)模式\"\nmsgstr \"Exited Get DouYin live stream pull URL mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1167\nmsgid \"{} 提取直播 ID 失败\"\nmsgstr \"Failed to extract live stream ID from {}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1181\nmsgid \"已退出获取直播拉流地址(TikTok)模式\"\nmsgstr \"Exited Get TikTok live stream pull URL mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1197\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1214\nmsgid \"直播标题:\"\nmsgstr \"Live Stream Title:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1198\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1215\nmsgid \"主播昵称:\"\nmsgstr \"Presenter Nickname:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1199\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1217\nmsgid \"在线观众:\"\nmsgstr \"Online Viewers:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1200\nmsgid \"观看次数:\"\nmsgstr \"View Count:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1202\nmsgid \"当前直播已结束！\"\nmsgstr \"The current live stream has ended!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1216\nmsgid \"开播时间:\"\nmsgstr \"Start Time:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1218\nmsgid \"点赞次数:\"\nmsgstr \"Like Count:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1223\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1242\nmsgid \"FLV 拉流地址: \"\nmsgstr \"FLV Stream push URL:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1226\nmsgid \"M3U8 拉流地址: \"\nmsgstr \"M3U8 Stream push URL:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1264\nmsgid \"已退出采集作品评论数据(TikTok)模式\"\nmsgstr \"Exited Collect Works Comment Data (TikTok) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1276\nmsgid \"已退出采集作品评论数据(抖音)模式)\"\nmsgstr \"Exited Collect Works Comment Data (DouYin) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1363\n#, python-brace-format\nmsgid \"作品评论数据已储存至 {filename}\"\nmsgstr \"Works comment data has been saved to {filename}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1364\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1374\n#, python-brace-format\nmsgid \"作品{id}_评论数据\"\nmsgstr \"Works{id}_CommentData\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1368\nmsgid \"采集评论数据失败\"\nmsgstr \"Failed to collect comment data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1423\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1434\nmsgid \"请选择合集链接来源\"\nmsgstr \"Please select the Mix link source\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1427\nmsgid \"已退出批量下载合集作品(抖音)模式\"\nmsgstr \"Exited Batch Download Mix Works (DouYin) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1438\nmsgid \"已退出批量下载合集作品(TikTok)模式\"\nmsgstr \"Exited Batch Download Mix Works (TikTok) mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1470\nmsgid \"合集或作品\"\nmsgstr \"Mix or Works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1459\n#, python-brace-format\nmsgid \"{url} 获取作品 ID 或合集 ID 失败\"\nmsgstr \"Failed to retrieve Works ID or Mix ID from {url}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1473\n#, python-brace-format\nmsgid \"{url} 获取合集 ID 失败\"\nmsgstr \"Failed to retrieve Mix ID from {url}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1502\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1509\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:150\nmsgid \"收藏合集\"\nmsgstr \"Collections Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1513\n#, python-brace-format\nmsgid \"{text}列表：\"\nmsgstr \"{text} List:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1518\n#, python-brace-format\nmsgid \"\"\n\"请输入需要下载的{item}序号(多个序号使用空格分隔，输入 ALL 下载全部{item})：\"\nmsgstr \"\"\n\"Please enter the serial numbers of the {item} to download (separate multiple \"\n\"numbers with spaces, enter ALL to download all {item}):\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1532\n#, python-brace-format\nmsgid \"{text}序号输入错误！\"\nmsgstr \"Incorrect {text} serial number input!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1540\nmsgid \"从文本文档提取作品 ID 或合集 ID 失败\"\nmsgstr \"Failed to extract Works ID or Mix ID from the text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1550\nmsgid \"从文本文档提取合集 ID 失败\"\nmsgstr \"Failed to extract Mix ID from the text file\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1590\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1650\nmsgid \"合集\"\nmsgstr \"Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1626\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 获取作品 ID 或合集 ID 失败，错误配置：{data}\"\nmsgstr \"\"\n\"Failed to obtain the work ID or collection ID from the url {url} in the \"\n\"configuration file {name}, error configuration: {data}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1668\n#, python-brace-format\nmsgid \"开始处理第 {index} 个合集\"\nmsgstr \"Starting to process the {index}th Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1670\nmsgid \"开始处理合集\"\nmsgstr \"Starting to process the Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1704\nmsgid \"采集合集作品数据失败\"\nmsgstr \"Failed to collect Mix Works data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1730\n#, python-brace-format\nmsgid \"配置文件 accounts_urls 参数第 {index} 条数据的 url 无效\"\nmsgstr \"\"\n\"The URL in parameter {index} of the `accounts_urls` in the configuration \"\n\"file is invalid\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1754\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:40\nmsgid \"请输入文本文档路径：\"\nmsgstr \"Please enter the text file path:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1761\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:47\n#, python-brace-format\nmsgid \"{path} 文件读取异常: {error}\"\nmsgstr \"File read error at {path}: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1764\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:50\n#, python-brace-format\nmsgid \"{path} 文件不存在！\"\nmsgstr \"The file {path} does not exist!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1788\n#, python-brace-format\nmsgid \"正在获取账号 {sec_user_id} 的数据\"\nmsgstr \"正在获取账号 {sec_user_id} 的数据\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1815\nmsgid \"账号数据已保存至文件\"\nmsgstr \"Account data has been saved to the file.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1825\nmsgid \"已退出采集账号详细数据模式\"\nmsgstr \"Exited Collect Account Detailed Data mode.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1832\n#, python-brace-format\nmsgid \"请输入搜索参数；参数之间使用两个空格分隔({field})：\\n\"\nmsgstr \"\"\n\"Please enter search parameters; Separate parameters with two spaces \"\n\"({field}): \\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1855\nmsgid \"请选择搜索模式\"\nmsgstr \"Please choose a search mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1934\nmsgid \"搜索结果为空\"\nmsgstr \"The search result is empty\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1956\nmsgid \"搜索数据\"\nmsgstr \"Search_Data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2023\n#, python-brace-format\nmsgid \"搜索数据已保存至 {name}\"\nmsgstr \"Search data has been saved to {name}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2032\nmsgid \"已退出采集抖音热榜数据(抖音)模式\"\nmsgstr \"Exited Collect DouYin Hot Board Data mode.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2052\n#, python-brace-format\nmsgid \"热榜数据_{time}_{name}\"\nmsgstr \"HotBoardData_{time}_{name}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2066\n#, python-brace-format\nmsgid \"热榜数据已储存至: 热榜数据_{time} + 榜单类型\"\nmsgstr \"Hot Board Data has been saved to: HotBoardData_{time} + Board Type\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2081\nmsgid \"已退出批量下载收藏作品(抖音)模式\"\nmsgstr \"Exited Batch Download Favorites Works (DouYin) mode.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2101\nmsgid \"已退出批量下载收藏夹作品(抖音)模式\"\nmsgstr \"Exited Batch Download Collections Works (DouYin) mode.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2126\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:27\nmsgid \"收藏夹\"\nmsgstr \"Collections\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2137\n#, python-brace-format\nmsgid \"配置文件 owner_url 的 url 参数 {url} 无效\"\nmsgstr \"\"\n\"The URL parameter {url} of owner_url in the configuration file is invalid.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2167\nmsgid \"已退出批量下载收藏音乐(抖音)模式\"\nmsgstr \"Exited Batch Download Collections Music (DouYin) mode.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2175\n#, python-brace-format\nmsgid \"程序运行耗时 {minutes} 分钟 {seconds} 秒\"\nmsgstr \"The program ran for {minutes} minutes {seconds} seconds\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2205\nmsgid \"开始获取收藏数据\"\nmsgstr \"Starting to retrieve Favorites data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2215\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败\"\nmsgstr \"Failed to retrieve account information for {sec_user_id}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2248\nmsgid \"开始获取收藏夹数据\"\nmsgstr \"Starting to retrieve Collections data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2301\nmsgid \"请选择采集功能\"\nmsgstr \"Please select the function menu\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:108\nmsgid \"禁用\"\nmsgstr \"Disable\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:109\nmsgid \"启用\"\nmsgstr \"Enable\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:112\nmsgid \"从剪贴板读取 Cookie (抖音)\"\nmsgstr \"Extracting cookie (DouYin) from clipboard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:113\nmsgid \"从浏览器读取 Cookie (抖音)\"\nmsgstr \"Extracting cookie (DouYin) from browser\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:115\nmsgid \"从剪贴板读取 Cookie (TikTok)\"\nmsgstr \"Extracting cookie (TikTok) from clipboard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:116\nmsgid \"从浏览器读取 Cookie (TikTok)\"\nmsgstr \"Extracting cookie (TikTok) from browser\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:117\nmsgid \"终端交互模式\"\nmsgstr \"Terminal Mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:118\nmsgid \"后台监听模式\"\nmsgstr \"Monitoring Mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:119\nmsgid \"Web API 模式\"\nmsgstr \"Web API Mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:120\nmsgid \"Web UI 模式\"\nmsgstr \"Web UI Mode\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:124\nmsgid \"{}作品下载记录\"\nmsgstr \"{} Works Download History\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:127\nmsgid \"删除作品下载记录\"\nmsgstr \"Delete Works Download History\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:129\nmsgid \"{}运行日志记录\"\nmsgstr \"{} Run Log History\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:132\nmsgid \"检查程序版本更新\"\nmsgstr \"Check for Program Version Updates\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:133\nmsgid \"切换语言\"\nmsgstr \"切换到简体中文\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:149\nmsgid \"\"\n\"访问 http://127.0.0.1:5555/docs 或者 http://127.0.0.1:5555/redoc 可以查阅 \"\n\"API 模式说明文档！\"\nmsgstr \"\"\n\"Visit http://127.0.0.1:5555/docs or http://127.0.0.1:5555/redoc to view the \"\n\"API mode documentation!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:190\nmsgid \"是否已仔细阅读上述免责声明(YES/NO): \"\nmsgstr \"Have you carefully read the above disclaimer (YES/NO):\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:224\nmsgid \"项目地址: {}\"\nmsgstr \"Project URL: {}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:225\nmsgid \"项目文档: {}\"\nmsgstr \"Project Documentation: {}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:226\nmsgid \"开源许可: {}\\n\"\nmsgstr \"Open Source License: {}\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:248\n#, python-brace-format\nmsgid \"检测到新版本: {major}.{minor}\"\nmsgstr \"New version detected: {major}.{minor}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:255\nmsgid \"当前版本为开发版, 可更新至正式版\"\nmsgstr \"\"\n\"The current version is a development version, update to the stable version.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:260\nmsgid \"当前已是最新开发版\"\nmsgstr \"The current version is the latest development version.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:264\nmsgid \"当前已是最新正式版\"\nmsgstr \"The current version is the latest stable version.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:268\nmsgid \"检测新版本失败\"\nmsgstr \"Failed to check for new version.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:280\nmsgid \"DouK-Downloader 功能选项\"\nmsgstr \"DouK-Downloader Feature Options\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:322\nmsgid \"修改设置成功！\"\nmsgstr \"Settings updated successfully!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:334\nmsgid \"Cookie 获取教程：\"\nmsgstr \"Cookie retrieval tutorial:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:340\nmsgid \"\"\n\"复制 Cookie 内容至剪贴板后，按回车键确认继续；若输入任意内容并按回车，则取消\"\n\"操作：\"\nmsgstr \"\"\n\"After pasting the cookie into the clipboard, press Enter to proceed. Enter \"\n\"any text + Enter to abort.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:379\nmsgid \"作品下载记录功能已禁用！\"\nmsgstr \"Works download history feature is disabled!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:445\nmsgid \"正在关闭程序\"\nmsgstr \"Shutting down the program\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:273\n#, python-brace-format\nmsgid \"{name} 参数格式错误\"\nmsgstr \"{name} parameter format error\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:366\n#, python-brace-format\nmsgid \"root 参数 {root} 不是有效的文件夹路径，程序将使用项目根路径作为储存路径\"\nmsgstr \"\"\n\"The root parameter {root} is not a valid folder path. The program will use \"\n\"the project root path as the storage path.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:386\n#, python-brace-format\nmsgid \"\"\n\"folder_name 参数 {folder_name} 不是有效的文件夹名称，程序将使用默认值：\"\n\"Download\"\nmsgstr \"\"\n\"folder_name parameter {folder_name} is not a valid folder name. The program \"\n\"will use the default value: Download\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:399\n#, python-brace-format\nmsgid \"\"\n\"name_format 参数 {name_format} 设置错误，程序将使用默认值：创建时间 作品类型 \"\n\"账号昵称 作品描述\"\nmsgstr \"\"\n\"name_format parameter {name_format} is set incorrectly. The program will use \"\n\"the default value: Time, Type, Nickname, Description\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:412\n#, python-brace-format\nmsgid \"\"\n\"date_format 参数 {date_format} 设置错误，程序将使用默认值：年-月-日 时:分:秒\"\nmsgstr \"\"\n\"date_format parameter {date_format} is set incorrectly. The program will use \"\n\"the default value: Year-Month-Day Hour:Minute:Second\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:421\n#, python-brace-format\nmsgid \"split 参数 {split} 包含非法字符，程序将使用默认值：-\"\nmsgstr \"\"\n\"split parameter {split} contains illegal characters. The program will use \"\n\"the default value: -\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:451\n#, python-brace-format\nmsgid \"{remark}代理参数应为字符串格式，未来不再支持字典格式\"\nmsgstr \"\"\n\"{remark} proxy parameter should be in string format. Dictionary format will \"\n\"no longer be supported in the future.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:467\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试成功\"\nmsgstr \"{remark} proxy {proxy} test successful.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:474\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试超时\"\nmsgstr \"{remark} proxy {proxy} test timed out.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:484\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试失败：{error}\"\nmsgstr \"{remark} proxy {proxy} test failed: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:519\n#, python-brace-format\nmsgid \"max_pages 参数 {max_pages} 设置错误，程序将使用默认值：99999\"\nmsgstr \"\"\n\"max_pages parameter {max_pages} is set incorrectly. The program will use the \"\n\"default value: 99999\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:543\n#, python-brace-format\nmsgid \"\"\n\"storage_format 参数 {storage_format} 设置错误，程序默认不会储存任何数据至文件\"\nmsgstr \"\"\n\"The storage_format parameter {storage_format} is set incorrectly. By \"\n\"default, the program will not store any data to files.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:561\nmsgid \"正在更新抖音参数，请稍等...\"\nmsgstr \"Updating DouYin parameters, please wait...\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:579\nmsgid \"抖音参数更新完毕！\"\nmsgstr \"DouYin parameters update completed!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:583\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:644\nmsgid \"配置文件 cookie 参数未设置，抖音平台功能可能无法正常使用\"\nmsgstr \"\"\n\"The cookie parameter is not configured in the settings file. DouYin platform \"\n\"features may not work properly.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:593\nmsgid \"正在更新 TikTok 参数，请稍等...\"\nmsgstr \"Updating TikTok parameters, please wait...\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:611\nmsgid \"TikTok 参数更新完毕！\"\nmsgstr \"TikTok parameters update completed!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:616\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:667\nmsgid \"配置文件 cookie_tiktok 参数未设置，TikTok 平台功能可能无法正常使用\"\nmsgstr \"\"\n\"The cookie_tiktok parameter is not configured in the settings file. TikTok \"\n\"platform features may not work properly.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:772\n#, python-brace-format\nmsgid \"TikTok cookie 缺少 {name} 键值对，请尝试重新写入 cookie\"\nmsgstr \"\"\n\"The TikTok cookie is missing the {name} key-value pair. Please attempt to \"\n\"rewrite the cookie.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1112\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置过小，程序将使用默认值：{default}\"\nmsgstr \"\"\n\"The parameter {key} has been set to a value ({value}) that is too small. The \"\n\"program will use the default value instead: {default}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1120\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置错误，程序将使用默认值：{default}\"\nmsgstr \"\"\n\"The parameter {key} is incorrectly configured ({value}). The program will \"\n\"use the default value: {default}.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1133\n#, python-brace-format\nmsgid \"live_qualities 参数 {live_qualities} 设置错误\"\nmsgstr \"\"\n\"The live_qualities parameter is incorrectly configured: {live_qualities}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:157\nmsgid \"\"\n\"创建默认配置文件 settings.json 成功！\\n\"\n\"请参考项目文档的快速入门部分，设置 Cookie 后重新运行程序！\\n\"\n\"建议根据实际使用需求修改配置文件 settings.json！\\n\"\nmsgstr \"\"\n\"Default configuration file settings.json created successfully!\\n\"\n\"Please refer to the quick start section of the project documentation, set \"\n\"the Cookie, and rerun the program!\\n\"\n\"It is recommended to modify the settings.json file according to your actual \"\n\"usage needs!\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:174\nmsgid \"配置文件 settings.json 格式错误，请检查 JSON 格式！\"\nmsgstr \"\"\n\"The configuration file settings.json has an error. Please check the JSON \"\n\"format!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:186\n#, python-brace-format\nmsgid \"配置文件 settings.json 缺少参数 {i}，已自动添加该参数！\"\nmsgstr \"\"\n\"The configuration file settings.json is missing the parameter \\\"{i}\\\". The \"\n\"program has automatically added this parameter.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:204\nmsgid \"保存配置成功！\"\nmsgstr \"Configuration saved successfully!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:216\n#, python-brace-format\nmsgid \"配置文件 {old} 参数已变更为 {new} 参数，请注意修改配置文件！\"\nmsgstr \"\"\n\"The configuration file parameter {old} has been changed to {new}. Please \"\n\"make sure to update the configuration file accordingly!\"\n\nmsgid \"\"\n\"关于 DouK-Downloader 的 免责声明：\\n\"\n\"\\n\"\n\"1. 使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项\"\n\"目所产生的任何损失、责任、或风险概不负责。\\n\"\n\"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术\"\n\"水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。\\n\"\n\"3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需\"\n\"自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责\"\n\"任。\\n\"\n\"4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求，\"\n\"并在适当的地方注明使用了 GNU General Public License v3.0 的代码。\\n\"\n\"5. 使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行\"\n\"为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。\\n\"\n\"6. 使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、\"\n\"传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分\"\n\"发。\\n\"\n\"7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用\"\n\"者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使\"\n\"用者自行承担。\\n\"\n\"8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行\"\n\"为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。\\n\"\n\"9. 本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-\"\n\"Downloader 项目相关的任何商业服务。\\n\"\n\"10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不\"\n\"承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的\"\n\"各种情况负全部责任。\\n\"\n\"11. 本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自\"\n\"行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣\"\n\"传、推广或再授权。\\n\"\n\"12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁\"\n\"已获取的代码及衍生作品。\\n\"\n\"13. 作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修\"\n\"订后的条款。\\n\"\n\"\\n\"\n\"在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声\"\n\"明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码\"\n\"和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险\"\n\"和后果。\\n\"\nmsgstr \"\"\n\"Disclaimer for DouK-Downloader:\\n\"\n\"\\n\"\n\"1. The use of this project is entirely at the user's own discretion and \"\n\"risk. The author assumes no responsibility or liability of any kind for any \"\n\"loss, damage, or risk arising from the user's use of this project.\\n\"\n\"2. The code and functionalities provided by the author of this project are \"\n\"developed based on current knowledge and technology. The author makes every \"\n\"effort to ensure the correctness and security of the code according to \"\n\"current technical standards but does not guarantee that the code is \"\n\"completely free of errors or defects.\\n\"\n\"3. All third-party libraries, plugins, or services used by this project \"\n\"follow their original open-source or commercial licenses. Users must review \"\n\"and comply with these license agreements accordingly. The author assumes no \"\n\"responsibility for the stability, security, or compliance of any third-party \"\n\"components.\\n\"\n\"4. When using this project, users must strictly comply with the requirements \"\n\"of the GNU General Public License v3.0 and clearly indicate in appropriate \"\n\"places that the code was used under the GNU General Public License v3.0.\\n\"\n\"5. When using the code and functionalities of this project, users must \"\n\"independently research relevant laws and regulations and ensure that their \"\n\"usage is legal and compliant. Any legal liabilities or risks arising from \"\n\"violations of laws and regulations shall be borne solely by the user.\\n\"\n\"6. Users must not use this tool to engage in any activities that infringe \"\n\"intellectual property rights, including but not limited to downloading or \"\n\"distributing copyrighted content without authorization. Developers do not \"\n\"participate in, support, or endorse the acquisition or distribution of any \"\n\"illegal or unauthorized content.\\n\"\n\"7. This project assumes no responsibility for the compliance of data \"\n\"processing activities (including collection, storage, and transmission) \"\n\"performed by users. Users must comply with relevant laws and regulations and \"\n\"ensure that such activities are lawful and proper. Legal liabilities \"\n\"resulting from non-compliant operations shall be borne by the user.\\n\"\n\"8. Under no circumstances may users associate the author, contributors, or \"\n\"other related parties of this project with their usage of the project, nor \"\n\"may they hold these parties liable for any loss or damage resulting from \"\n\"such usage.\\n\"\n\"9. The author of this project will not provide a paid version of the DouK-\"\n\"Downloader project, nor will they offer any commercial services related to \"\n\"it.\\n\"\n\"10. Any secondary development, modification, or compilation based on this \"\n\"project is unrelated to the original author. The original author assumes no \"\n\"liability for any consequences resulting from such secondary development. \"\n\"Users bear full responsibility for all outcomes arising from such \"\n\"modifications.\\n\"\n\"11. This project does not grant users any patent licenses. If the use of \"\n\"this project leads to patent disputes or infringement, the user assumes all \"\n\"associated risks and responsibilities. Without written authorization from \"\n\"the author or rights holder, users may not use this project for any \"\n\"commercial promotion, advertising, or re-licensing.\\n\"\n\"12. The author reserves the right to terminate service to any user who \"\n\"violates this disclaimer at any time and may require them to destroy all \"\n\"obtained code and derivative works.\\n\"\n\"13. The author reserves the right to update this disclaimer at any time \"\n\"without prior notice. Continued use of the project constitutes acceptance of \"\n\"the revised terms.\\n\"\n\"\\n\"\n\"Before using the code and functionalities of this project, please carefully \"\n\"consider and accept the above disclaimer. If you have any questions or \"\n\"disagree with the above statements, please do not use the code and \"\n\"functionalities of this project. If you do use the code and functionalities \"\n\"of this project, it shall be deemed that you have fully understood and \"\n\"accepted the above disclaimer and voluntarily assume all risks and \"\n\"consequences associated with its use.\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\custom\\function.py:56\n#, python-brace-format\nmsgid \"\"\n\"程序连续处理了 {batches} 个数据，为了避免请求频率过高导致账号或 IP 被风控，程\"\n\"序已经暂停运行，将在 {rest_time} 秒后恢复运行！\"\nmsgstr \"\"\n\"The program has continuously processed {batches} items. To avoid high \"\n\"request frequency that could lead to account or IP being restricted, the \"\n\"program has paused and will resume in {rest_time} seconds!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:159\nmsgid \"开始下载作品文件\"\nmsgstr \"Start downloading the works file.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:343\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:501\nmsgid \"音乐\"\nmsgstr \"Music\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:255\nmsgid \"程序将会调用 ffmpeg 下载直播，关闭 DouK-Downloader 不会中断下载！\"\nmsgstr \"\"\n\"The program will call ffmpeg to download the live stream. Closing DouK-\"\n\"Downloader will not interrupt the download!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:332\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:335\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:753\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:422\nmsgid \"实况\"\nmsgstr \"LivePhoto\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:409\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:460\n#, python-brace-format\nmsgid \"【{type}】{name} 提取文件下载地址失败，跳过下载\"\nmsgstr \"\"\n\"【{type}】{name} failed to retrieve the file download URL, skipping download.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:421\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录，跳过下载\"\nmsgstr \"【{type}】{name} has a download record, skipping download.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:428\n#, python-brace-format\nmsgid \"【{type}】{name}_{index} 文件已存在，跳过下载\"\nmsgstr \"【{type}】{name}_{index} file already exists, skipping download.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:472\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录或文件已存在，跳过下载\"\nmsgstr \"\"\n\"【{type}】{name} has a download record or file already exists, skipping \"\n\"download.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:514\n#, python-brace-format\nmsgid \"【{type}】{name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:622\nmsgid \"文件缓存异常，尝试重新下载\"\nmsgstr \"File cache exception, attempting to re-download.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:660\n#, python-brace-format\nmsgid \"网络异常: {error_repr}\"\nmsgstr \"Network exception: {error_repr}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:664\n#, python-brace-format\nmsgid \"响应码异常: {error_repr}\"\nmsgstr \"Response code exception: {error_repr}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:668\nmsgid \"\"\n\"如果 TikTok 平台作品下载功能异常，请检查配置文件中 browser_info_tiktok 的 \"\n\"device_id 参数！\"\nmsgstr \"\"\n\"If the TikTok platform's content download function is not working, please \"\n\"check the device_id parameter in browser_info_tiktok in the configuration \"\n\"file!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:679\n#, python-brace-format\nmsgid \"下载文件时发生预期之外的错误，请向作者反馈，错误信息: {error}\"\nmsgstr \"\"\n\"An unexpected error occurred while downloading the file. Please report it to \"\n\"the author, error information: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:715\n#, python-brace-format\nmsgid \"{show} 下载中断，错误信息：{error}\"\nmsgstr \"{show} download interrupted, error information: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:721\n#, python-brace-format\nmsgid \"{show} 文件下载成功\"\nmsgstr \"{show} file download successful\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:785\n#, python-brace-format\nmsgid \"UID{id_}_{name}_发布作品\"\nmsgstr \"UID{id_}_{name}_Posts\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:787\n#, python-brace-format\nmsgid \"UID{id_}_{name}_喜欢作品\"\nmsgstr \"UID{id_}_{name}_Liked\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:789\n#, python-brace-format\nmsgid \"MID{id_}_{name}_合集作品\"\nmsgstr \"MID{id_}_{name}_Mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:791\n#, python-brace-format\nmsgid \"UID{id_}_{name}_收藏作品\"\nmsgstr \"UID{id_}_{name}_Favorites\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:793\n#, python-brace-format\nmsgid \"CID{id_}_{name}_收藏夹作品\"\nmsgstr \"CID{id_}_{name}_Collections\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:850\n#, python-brace-format\nmsgid \"{file_name} 文件已删除\"\nmsgstr \"{file_name} file has been deleted\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:854\n#, python-brace-format\nmsgid \"下载视频作品 {downloaded_video_count} 个\"\nmsgstr \"Downloaded {downloaded_video_count} video works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:859\n#, python-brace-format\nmsgid \"跳过视频作品 {skipped_count} 个\"\nmsgstr \"Skipped {skipped_count} video works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:864\n#, python-brace-format\nmsgid \"下载图集作品 {downloaded_image_count} 个\"\nmsgstr \"Downloaded {downloaded_image_count} image works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:869\n#, python-brace-format\nmsgid \"跳过图集作品 {skipped_count} 个\"\nmsgstr \"Skipped {skipped_count} image works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:874\n#, python-brace-format\nmsgid \"下载实况作品 {downloaded_image_count} 个\"\nmsgstr \"Downloaded {downloaded_image_count} livePhoto works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:879\n#, python-brace-format\nmsgid \"跳过实况作品 {skipped_count} 个\"\nmsgstr \"Skipped {skipped_count} livePhoto works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:957\n#, python-brace-format\nmsgid \"未收录的文件类型：{content}\"\nmsgstr \"Unlisted file type: {content}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:967\n#, python-brace-format\nmsgid \"{show} 响应内容为空\"\nmsgstr \"{show} response content is empty\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:976\n#, python-brace-format\nmsgid \"{show} 文件大小超出限制，跳过下载\"\nmsgstr \"{show} file size exceeds limit, skipping download\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\msToken.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:42\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:93\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\webID.py:44\n#, python-brace-format\nmsgid \"获取 {name} 参数失败！\"\nmsgstr \"Failed to retrieve {name} parameter!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:99\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:110\n#, python-brace-format\nmsgid \"提取账号信息失败: {data}\"\nmsgstr \"Failed to extract account information: {data}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:217\n#, python-brace-format\nmsgid \"筛选处理后作品数量: {count}\"\nmsgstr \"Number of works after filtering: {count}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:831\nmsgid \"已注销账号\"\nmsgstr \"AccountDeactivated\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:832\nmsgid \"无效账号昵称\"\nmsgstr \"InvalidNickname\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:859\n#, python-brace-format\nmsgid \"sec_user_id {user_id} 与 {s} 不一致\"\nmsgstr \"sec_user_id {user_id} does not match {s}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:944\nmsgid \"提取账号信息或合集信息失败，请向作者反馈！\"\nmsgstr \"\"\n\"Failed to extract Account or Mix information, please report to the author!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号喜欢作品\"\nmsgstr \"Account liked works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号发布作品\"\nmsgstr \"Account post works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:68\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:85\nmsgid \"\"\n\"该账号为私密账号，需要使用登录后的 Cookie，且登录的账号需要关注该私密账号\"\nmsgstr \"\"\n\"This is a private account, you need to use the Cookie from a logged-in \"\n\"account, and the logged-in account must follow this private account.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:207\n#, python-brace-format\nmsgid \"tab 参数 {tab} 设置错误，程序将使用默认值: post\"\nmsgstr \"\"\n\"tab parameter {tab} is set incorrectly, the program will use the default \"\n\"value: post\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:212\nmsgid \"最早\"\nmsgstr \"Earliest\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:215\nmsgid \"最晚\"\nmsgstr \"Latest\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:229\n#, python-brace-format\nmsgid \"作品{tip}发布日期无效 {date}\"\nmsgstr \"{tip} publish date of the works is invalid: {date}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:234\n#, python-brace-format\nmsgid \"作品{tip}发布日期参数 {date} 类型错误\"\nmsgstr \"The {tip} publish date parameter {date} type is incorrect\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:237\n#, python-brace-format\nmsgid \"作品{tip}发布日期: {latest_date}\"\nmsgstr \"{tip} publish date of the works: {latest_date}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:261\nmsgid \"配置文件 cookie 参数未登录，数据获取已提前结束\"\nmsgstr \"\"\n\"Cookie parameter in the configuration file is not logged in, data retrieval \"\n\"has ended prematurely\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:264\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:200\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:82\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:80\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:121\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:391\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:74\n#, python-brace-format\nmsgid \"数据解析失败，请告知作者处理: {data}\"\nmsgstr \"Data parsing failed, please inform the author for handling: {data}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collection.py:26\nmsgid \"账号收藏作品\"\nmsgstr \"Account favorites works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:58\nmsgid \"当前账号无收藏夹\"\nmsgstr \"The current account has no collections\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:122\n#, python-brace-format\nmsgid \"收藏夹 {collects_id} 为空\"\nmsgstr \"The collections {collects_id} is empty\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:182\nmsgid \"当前账号无收藏合集\"\nmsgstr \"The current account has no collections mix\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:216\nmsgid \"收藏短剧\"\nmsgstr \"Favorite Series\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:237\nmsgid \"当前账号无收藏短剧\"\nmsgstr \"The current account has no favorite series\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:270\nmsgid \"收藏音乐\"\nmsgstr \"Collections music\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:291\nmsgid \"当前账号无收藏音乐\"\nmsgstr \"The current account has no collections music\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:35\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment_tiktok.py:29\nmsgid \"作品评论\"\nmsgstr \"Works comments\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:77\n#, python-brace-format\nmsgid \"作品 {item_id} 无评论\"\nmsgstr \"Works {item_id} has no comments\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:105\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:194\n#, python-brace-format\nmsgid \"正在获取{text}数据\"\nmsgstr \"Fetching {text} data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:230\nmsgid \"作品评论回复\"\nmsgstr \"Works comment replies\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:270\n#, python-brace-format\nmsgid \"评论 {comment_id} 无回复\"\nmsgstr \"Comment {comment_id} has no replies\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:18\nmsgid \"抖音热榜\"\nmsgstr \"DouYin Hot Board\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:23\nmsgid \"娱乐榜\"\nmsgstr \"EntertainmentBoard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:28\nmsgid \"社会榜\"\nmsgstr \"SocialBoard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:33\nmsgid \"挑战榜\"\nmsgstr \"ChallengeBoard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:52\nmsgid \"热榜\"\nmsgstr \"HotBoard\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:87\n#, python-brace-format\nmsgid \"{space_name}数据\"\nmsgstr \"{space_name} data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:29\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:27\nmsgid \"账号简略\"\nmsgstr \"Account summary\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:57\n#, python-brace-format\nmsgid \"获取{text}失败\"\nmsgstr \"Failed to retrieve {text}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\live_tiktok.py:57\nmsgid \"此直播可能会令部分观众感到不适，请登录后重试！\"\nmsgstr \"\"\n\"This live stream may cause discomfort to some viewers. Please log in and try \"\n\"again!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:71\nmsgid \"获取合集 ID 失败\"\nmsgstr \"Failed to retrieve Mix ID\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:32\nmsgid \"合辑作品\"\nmsgstr \"Mix Works\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:92\nmsgid \"账号合辑数据\"\nmsgstr \"Account Mix data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:18\nmsgid \"综合搜索\"\nmsgstr \"GeneralSearch\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:25\nmsgid \"视频搜索\"\nmsgstr \"VideoSearch\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:32\nmsgid \"用户搜索\"\nmsgstr \"UserSearch\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:39\nmsgid \"直播搜索\"\nmsgstr \"LiveSearch\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:60\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围  内容形式\"\nmsgstr \"\"\n\"keyword  TotalPages  SortType  PublicationTime  VideoDuration  SearchRange  \"\n\"ContentForm\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:61\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围\"\nmsgstr \"\"\n\"keyword  TotalPages  SortType  PublicationTime  VideoDuration  SearchRange\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:62\nmsgid \"关键词  总页数  粉丝数量  用户类型\"\nmsgstr \"keyword  TotalPages  Fans  AccountType\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:63\nmsgid \"关键词  总页数\"\nmsgstr \"keyword  TotalPages\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:72\nmsgid \"综合排序\"\nmsgstr \"GeneralSort\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:73\nmsgid \"最多点赞\"\nmsgstr \"MostLikes\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:74\nmsgid \"最新发布\"\nmsgstr \"Latest\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:77\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:89\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:95\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:101\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:114\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:128\nmsgid \"不限\"\nmsgstr \"Unlimited\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:78\nmsgid \"一天内\"\nmsgstr \"Intraday\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:79\nmsgid \"一周内\"\nmsgstr \"SevenDays\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:80\nmsgid \"半年内\"\nmsgstr \"HalfYear\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:90\nmsgid \"一分钟以内\"\nmsgstr \"OneMinute\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:91\nmsgid \"一到五分钟\"\nmsgstr \"OneToFive\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:92\nmsgid \"五分钟以上\"\nmsgstr \"MoreThanFive\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:96\nmsgid \"最近看过\"\nmsgstr \"RecentlyWatched\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:97\nmsgid \"还未看过\"\nmsgstr \"NotViewed\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:98\nmsgid \"关注的人\"\nmsgstr \"Followed\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:103\nmsgid \"图文\"\nmsgstr \"Image\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:115\nmsgid \"1000以下\"\nmsgstr \"Below1000\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:119\nmsgid \"100w以上\"\nmsgstr \"Over1000W\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:129\nmsgid \"普通用户\"\nmsgstr \"GeneralUser\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:130\nmsgid \"企业认证\"\nmsgstr \"Enterprise\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:131\nmsgid \"个人认证\"\nmsgstr \"Individual\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:176\n#, python-brace-format\nmsgid \"获取{self_text}数据失败\"\nmsgstr \"Failed to retrieve {self_text} data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:444\n#, python-brace-format\nmsgid \"共获取到 {count} 个{text}\"\nmsgstr \"A total of {count} {text} retrieved\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:112\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:131\nmsgid \"文件夹\"\nmsgstr \"Folder\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:208\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:218\nmsgid \"文件\"\nmsgstr \"File\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:225\n#, python-brace-format\nmsgid \"{type} {old}被占用，重命名失败: {error}\"\nmsgstr \"{type} {old} is occupied, renaming failed: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:232\n#, python-brace-format\nmsgid \"{type} {new}名称重复，重命名失败: {error}\"\nmsgstr \"{type} {new} name is duplicated, renaming failed: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:239\n#, python-brace-format\nmsgid \"处理{type} {old}时发生预期之外的错误: {error}\"\nmsgstr \"Unexpected error occurred while processing {type} {old}: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\models\\search.py:34\nmsgid \"keyword 参数无效\"\nmsgstr \"The keyword parameter is invalid\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:42\nmsgid \"当前剪贴板的内容不是有效的 Cookie 内容！\"\nmsgstr \"The current clipboard content is not valid Cookie data!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\sqlite.py:83\nmsgid \"更新数据表名称时发生错误，重命名失败，请向作者反馈以便修复问题！\"\nmsgstr \"\"\n\"An error occurred while updating the data table name. The rename operation \"\n\"failed. Please report this issue to the developer for a fix.\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\xlsx.py:62\n#, python-brace-format\nmsgid \"数据包含非法字符，保存数据失败：{error}\"\nmsgstr \"The data contains illegal characters, failed to save the data: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:80\n#, python-brace-format\nmsgid \"\"\n\"读取指定浏览器的 {platform_name} Cookie 并写入配置文件；\\n\"\n\"注意：Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏\"\n\"览器 Cookie！\\n\"\n\"{options}\\n\"\n\"请输入浏览器名称或序号：\"\nmsgstr \"\"\n\"Read the specified browser's {platform_name} Cookie and write it to the \"\n\"configuration file;\\n\"\n\"Note: On Windows, you need to run the program as administrator to read \"\n\"Chromium, Chrome, or Edge browser Cookies!\\n\"\n\"{options}\\n\"\n\"Please enter the browser name or number:\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:94\nmsgid \"读取 Cookie 成功！\"\nmsgstr \"Cookie read successfully!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:102\nmsgid \"Cookie 数据为空！\"\nmsgstr \"Cookie data is empty!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:105\nmsgid \"未选择浏览器！\"\nmsgstr \"No browser selected!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:117\nmsgid \"浏览器名称或序号输入错误！\"\nmsgstr \"Browser name or number input error!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:125\nmsgid \"读取 Cookie 失败，未找到 Cookie 数据！\"\nmsgstr \"Failed to read Cookie, no Cookie data found!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:164\nmsgid \"从浏览器读取 Cookie 功能不支持当前平台！\"\nmsgstr \"\"\n\"Reading Cookie from the browser is not supported on the current platform!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:26\nmsgid \"响应内容不是有效的 JSON 数据\"\nmsgstr \"Response content is not valid JSON data\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:28\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:50\n#, python-brace-format\nmsgid \"响应码异常：{error}\"\nmsgstr \"Response code error: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:30\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:37\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:52\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:59\n#, python-brace-format\nmsgid \"网络异常：{error}\"\nmsgstr \"Network exception: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:32\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:54\n#, python-brace-format\nmsgid \"请求超时：{error}\"\nmsgstr \"Request timed out: {error}\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:48\nmsgid \"响应内容不是有效的 JSON 数据，请尝试更新 Cookie！\"\nmsgstr \"Response content is not valid JSON data, Please try updating cookies!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\cleaner.py:46\nmsgid \"不受支持的操作系统类型，可能无法正常去除非法字符！\"\nmsgstr \"\"\n\"Unsupported operating system type, illegal characters may not be removed \"\n\"properly!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\error.py:9\nmsgid \"项目代码错误\"\nmsgstr \"Project code error\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:19\n#, python-brace-format\nmsgid \"正在进行第 {index} 次重试\"\nmsgstr \"Retrying {index} time\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:48\nmsgid \"\"\n\"如需重新尝试处理该对象，请关闭所有正在访问该对象的窗口或程序，然后直接按下回\"\n\"车键！\\n\"\n\"如需跳过处理该对象，请输入任意字符后按下回车键！\"\nmsgstr \"\"\n\"To retry processing this object, please close all windows or programs \"\n\"accessing this object, then press Enter!\\n\"\n\"To skip processing this object, please enter any character and press Enter!\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:63\nmsgid \"请关闭所有正在访问该对象的窗口或程序，然后按下回车键继续处理！\"\nmsgstr \"\"\n\"Please close all windows or programs accessing this object, then press Enter \"\n\"to continue processing!\"\n\nmsgid \"\"\n\"\\n\"\n\"项目默认无需令牌；公开部署时，建议设置令牌以防止恶意请求！\\n\"\n\"\\n\"\n\"令牌设置位置：`src/custom/function.py` - `is_valid_token()`\\n\"\nmsgstr \"\"\n\"\\n\"\n\"Project defaults to no token; when publicly deployed, it is recommended to \"\n\"set a token to prevent malicious requests!\\n\"\n\"\\n\"\n\"Token setting location: `src/custom/function.py` - `is_valid_token()`\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"更新项目配置文件 settings.json\\n\"\n\"\\n\"\n\"仅需传入需要更新的配置参数\\n\"\n\"\\n\"\n\"返回更新后的全部配置参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"Update project configuration file settings.json\\n\"\n\"\\n\"\n\"Only needs to pass the configuration parameters that need to be updated\\n\"\n\"\\n\"\n\"Returns all updated configuration parameters\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **text**: 包含分享链接的字符串；必需参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **text**: String containing the sharing link; required parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **detail_id**: DouYin work ID; required parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: 抖音账号 sec_uid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **sec_user_id**: DouYin account sec_uid; required parameter\\n\"\n\"- **tab**: Account page type; optional parameter, default value: `post`\\n\"\n\"- **earliest**: Earliest release date of works; optional parameter\\n\"\n\"- **latest**: Latest release date of works; optional parameter\\n\"\n\"- **pages**: Maximum number of request times, only valid for requesting \"\n\"account favorite page data; optional parameter\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: 抖音合集 ID\\n\"\n\"- **detail_id**: 属于合集的抖音作品 ID\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"\\n\"\n\"**`mix_id` 和 `detail_id` 二选一，只需传入其中之一即可**\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **mix_id**: DouYin collection ID\\n\"\n\"- **detail_id**: DouYin work ID that belongs to the collection\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\"\\n\"\n\"**Either `mix_id` or `detail_id` must be provided — only one of them is \"\n\"required**\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **web_rid**: DouYin live stream web_rid\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\n\"- **room_id**: 抖音直播 room_id\\n\"\n\"- **sec_user_id**: 抖音直播账号 sec_user_id\\n\"\n\"\\n\"\n\"**本接口支持两种参数传入方式**:\\n\"\n\"\\n\"\n\"- 方式一 ：传入 `web_rid`\\n\"\n\"- 方式二 ：同时传入 `room_id` 和 `sec_user_id`\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **web_rid**: DouYin live stream web_rid\\n\"\n\"- **room_id**: DouYin live stream room_id\\n\"\n\"- **sec_user_id**: DouYin live stream account sec_user_id\\n\"\n\"\\n\"\n\"**This interface supports two ways of passing parameters**:\\n\"\n\"\\n\"\n\"- **Method 1**: Pass in `web_rid`\\n\"\n\"- **Method 2**: Pass in both `room_id` and `sec_user_id`\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"- **count_reply**: 可选参数\\n\"\n\"- **reply**: 可选参数，默认值：False\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **detail_id**: DouYin work ID; required parameter\\n\"\n\"- **pages**: Maximum number of request times; optional parameter\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\"- **count_reply**: Optional parameter\\n\"\n\"- **reply**: Optional parameter, default value: False\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **comment_id**: 评论 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **detail_id**: DouYin work ID; required parameter\\n\"\n\"- **comment_id**: Comment ID; required parameter\\n\"\n\"- **pages**: Maximum number of request times; optional parameter\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"- **content_type**: 内容形式；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **keyword**: Keyword; required parameter\\n\"\n\"- **offset**: Starting page number; optional parameter\\n\"\n\"- **count**: Amount of data; optional parameter\\n\"\n\"- **pages**: Total number of pages; optional parameter\\n\"\n\"- **sort_type**: Sorting criteria; optional parameter\\n\"\n\"- **publish_time**: Publication time; optional parameter\\n\"\n\"- **duration**: Video duration; optional parameter\\n\"\n\"- **search_range**: Search scope; optional parameter\\n\"\n\"- **content_type**: Content format; optional parameter\\n\"\n\"\\n\"\n\"**Note**: For rules on passing certain parameters, please refer to the \"\n\"documentation: [Parameter meanings](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **keyword**: Keyword; required parameter\\n\"\n\"- **offset**: Starting page number; optional parameter\\n\"\n\"- **count**: Amount of data; optional parameter\\n\"\n\"- **pages**: Total number of pages; optional parameter\\n\"\n\"- **sort_type**: Sorting criteria; optional parameter\\n\"\n\"- **publish_time**: Publication time; optional parameter\\n\"\n\"- **duration**: Video duration; optional parameter\\n\"\n\"- **search_range**: Search scope; optional parameter\\n\"\n\"\\n\"\n\"**Note**: For rules on passing certain parameters, please refer to the \"\n\"documentation: [Parameter meanings](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **douyin_user_fans**: 粉丝数量；可选参数\\n\"\n\"- **douyin_user_type**: 用户类型；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **keyword**: Keyword; required parameter\\n\"\n\"- **offset**: Starting page number; optional parameter\\n\"\n\"- **count**: Amount of data; optional parameter\\n\"\n\"- **pages**: Total number of pages; optional parameter\\n\"\n\"- **douyin_user_fans**: Number of followers; optional parameter\\n\"\n\"- **douyin_user_type**: User type; optional parameter\\n\"\n\"\\n\"\n\"**Note**: For rules on passing certain parameters, please refer to the \"\n\"documentation: [Parameter meanings](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: DouYin Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **keyword**: Keyword; required parameter\\n\"\n\"- **offset**: Starting page number; optional parameter\\n\"\n\"- **count**: Amount of data; optional parameter\\n\"\n\"- **pages**: Total number of pages; optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: TikTok 作品 ID；必需参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **detail_id**: TikTok work ID; required parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: TikTok 账号 secUid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **sec_user_id**: TikTok account secUid; required parameter\\n\"\n\"- **tab**: Account page type; optional parameter, default value: `post`\\n\"\n\"- **earliest**: Earliest release date of works; optional parameter\\n\"\n\"- **latest**: Latest release date of works; optional parameter\\n\"\n\"- **pages**: Maximum number of request times, only valid for requesting \"\n\"account favorite page data; optional parameter\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: TikTok 合集 ID；必需参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **mix_id**: TikTok collection ID; required parameter\\n\"\n\"- **cursor**: Optional parameter\\n\"\n\"- **count**: Optional parameter\\n\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **room_id**: TikTok 直播 room_id；必需参数\\n\"\nmsgstr \"\"\n\"\\n\"\n\"**Parameters**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie; optional parameter\\n\"\n\"- **proxy**: Proxy; optional parameter\\n\"\n\"- **source**: Whether to return the original response data; optional \"\n\"parameter, default value: False\\n\"\n\"- **room_id**: TikTok live stream room_id; required parameter\\n\"\n\n#~ msgid \"是否返回上一级菜单(YES/NO)\"\n#~ msgstr \"Do you want to return to the previous menu (YES/NO)?\"\n\n#~ msgid \"扫码登录失败，未写入 Cookie！\"\n#~ msgstr \"QR code login failed, Cookie not written!\"\n\n#~ msgid \"提取 web_rid 或者 room_id 失败！\"\n#~ msgstr \"Failed to extract web_rid or room_id!\"\n\n#~ msgid \"本次运行将会使用各项参数默认值，程序功能可能无法正常使用！\"\n#~ msgstr \"\"\n#~ \"This run will use the default values for all parameters, and the \"\n#~ \"program's functionality may not work properly!\"\n\n#~ msgid \"写入 Cookie 成功！\"\n#~ msgstr \"Cookie written successfully!\"\n\n#~ msgid \"当前 Cookie 已登录\"\n#~ msgstr \"Current Cookie is logged in\"\n\n#~ msgid \"当前 Cookie 未登录\"\n#~ msgstr \"Current Cookie is not logged in\"\n\n#~ msgid \"正在启动服务器，如需关闭服务器，请按下 Ctrl + C\"\n#~ msgstr \"\"\n#~ \"Starting the server. To shut down the server, please press Ctrl + C.\"\n\n#~ msgid \"扫码登录获取 Cookie (抖音)\"\n#~ msgstr \"Scan code to login and get cookies (Tiktok)\"\n\n#~ msgid \"输入任意字符继续处理账号/合集，直接回车停止处理账号/合集: \"\n#~ msgstr \"\"\n#~ \"Enter any character to continue processing Accounts/Mix, press Enter to \"\n#~ \"stop processing Accounts/Mix:\"\n"
  },
  {
    "path": "locale/generate_path.py",
    "content": "from pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent\n\n\ndef find_python_files(dir_, file):\n    with open(file, \"w\", encoding=\"utf-8\") as f:\n        for py_file in dir_.rglob(\"*.py\"):  # 递归查找所有 .py 文件\n            f.write(str(py_file) + \"\\n\")  # 写入文件路径\n\n\n# 设置源目录和输出文件\nsource_directory = ROOT.joinpath(\"src\")  # 源目录\noutput_file = \"py_files.txt\"  # 输出文件名\n\nfind_python_files(source_directory, output_file)\nprint(f\"所有 .py 文件路径已保存到 {output_file}\")\n"
  },
  {
    "path": "locale/po_to_mo.py",
    "content": "from pathlib import Path\nfrom subprocess import run\n\nROOT = Path(__file__).resolve().parent\n\n\ndef scan_directory():\n    return [\n        item.joinpath(\"LC_MESSAGES/tk.po\") for item in ROOT.iterdir() if item.is_dir()\n    ]\n\n\ndef generate_map(files: list[Path]):\n    return [(i, i.with_suffix(\".mo\")) for i in files]\n\n\ndef generate_mo(maps: list[tuple[Path, Path]]):\n    for i, j in maps:\n        command = f'msgfmt --check -o \"{j}\" \"{i}\"'\n        print(run(command, shell=True, text=True))\n\n\nif __name__ == \"__main__\":\n    generate_mo(generate_map(scan_directory()))\n"
  },
  {
    "path": "locale/tk.pot",
    "content": "# SOME DESCRIPTIVE TITLE.\n# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the PACKAGE package.\n# FIRST AUTHOR <yonglelolu@foxmail.com>, YEAR.\n#\n#, fuzzy\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: DouK-Downloader 5.8\\n\"\n\"Report-Msgid-Bugs-To: <yonglelolu@foxmail.com>\\n\"\n\"POT-Creation-Date: 2025-11-04 10:48+0800\\n\"\n\"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n\"\n\"Last-Translator: FULL NAME <yonglelolu@foxmail.com>\\n\"\n\"Language-Team: LANGUAGE <LL@li.org>\\n\"\n\"Language: \\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:41\nmsgid \"\"\n\"程序会自动检测并提取剪贴板中的抖音和 TikTok 作品链接，并自动下载作品文件；如\"\n\"需关闭，请按下 Ctrl+C，或将剪贴板内容设置为“close”以停止监听！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:129\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:941\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:968\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1288\n#, python-brace-format\nmsgid \"{url} 提取作品 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:50\nmsgid \"验证失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:106\nmsgid \"访问项目 GitHub 仓库\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:107\nmsgid \"重定向至项目 GitHub 仓库主页\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:123\nmsgid \"项目\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:115\nmsgid \"测试令牌有效性\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:128\nmsgid \"验证成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:135\nmsgid \"更新项目全局配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:145\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:158\nmsgid \"配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:156\nmsgid \"获取项目全局配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:157\nmsgid \"返回项目全部配置参数\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:166\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:511\nmsgid \"获取分享链接重定向的完整链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:175\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:206\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:233\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:259\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:299\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:342\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:379\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:419\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:449\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:477\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:501\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:190\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:444\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:885\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:961\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:43\nmsgid \"抖音\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:183\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:528\nmsgid \"请求链接成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:188\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:533\nmsgid \"请求链接失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:195\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:540\nmsgid \"获取单个作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:216\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:561\nmsgid \"获取账号作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:243\nmsgid \"获取合集作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:269\nmsgid \"参数错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:288\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:622\nmsgid \"获取直播数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:326\nmsgid \"获取作品评论数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:364\nmsgid \"获取评论回复数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:398\nmsgid \"获取综合搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:429\nmsgid \"获取视频搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:459\nmsgid \"获取用户搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:487\nmsgid \"获取直播搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:588\nmsgid \"获取合辑作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:656\nmsgid \"搜索结果为空！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:709\nmsgid \"获取数据成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:720\nmsgid \"获取数据失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:71\nmsgid \"\"\n\"未设置 storage_format 参数，无法正常使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:86\nmsgid \"抖音 Cookie\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:90\n#, python-brace-format\nmsgid \"{tip} 未登录，无法使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:143\nmsgid \"批量下载账号作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:147\nmsgid \"批量下载链接作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:151\nmsgid \"获取直播拉流地址(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:155\nmsgid \"采集作品评论数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:159\nmsgid \"批量下载合集作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:163\nmsgid \"采集账号详细数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:167\nmsgid \"采集搜索结果数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:171\nmsgid \"采集抖音热榜数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:176\nmsgid \"批量下载收藏作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:180\nmsgid \"批量下载收藏音乐(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:185\nmsgid \"批量下载收藏夹作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:189\nmsgid \"批量下载账号作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:193\nmsgid \"批量下载链接作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:197\nmsgid \"批量下载合集作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:201\nmsgid \"获取直播拉流地址(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:206\nmsgid \"批量下载视频原画(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:211\nmsgid \"使用 accounts_urls 参数的账号链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:212\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:220\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:236\nmsgid \"手动输入待采集的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:213\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:221\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:237\nmsgid \"从文本文档读取待采集的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:217\nmsgid \"使用 accounts_urls_tiktok 参数的账号链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:224\nmsgid \"使用 mix_urls 参数的合集链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:225\nmsgid \"获取当前账号收藏合集列表\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:226\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:231\nmsgid \"手动输入待采集的合集/作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:227\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:232\nmsgid \"从文本文档读取待采集的合集/作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:230\nmsgid \"使用 mix_urls_tiktok 参数的合集链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:235\nmsgid \"使用 accounts_urls 参数的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:240\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:244\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:248\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:252\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:256\nmsgid \"手动输入待采集的作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:241\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:245\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:249\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:253\nmsgid \"从文本文档读取待采集的作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:261\nmsgid \"综合搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:265\nmsgid \"视频搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:269\nmsgid \"用户搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:273\nmsgid \"直播搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:283\n#, python-brace-format\nmsgid \"请输入{tip}链接: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:296\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:322\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:330\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1821\nmsgid \"请选择账号链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:300\nmsgid \"已退出批量下载账号作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:302\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:415\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:535\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:25\nmsgid \"账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:306\n#, python-brace-format\nmsgid \"程序共处理 {0} 个{1}，成功 {2} 个，失败 {3} 个，耗时 {4} 分钟 {5} 秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:326\nmsgid \"已退出批量下载账号作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:382\n#, python-brace-format\nmsgid \"共有 {count} 个账号的作品等待下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:393\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 提取 sec_user_id 失败，错误配置：{data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:451\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1743\nmsgid \"账号主页\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:438\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1747\n#, python-brace-format\nmsgid \"{url} 提取账号 sec_user_id 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:470\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:508\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1776\nmsgid \"从文本文档提取账号 sec_user_id 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:556\n#, python-brace-format\nmsgid \"开始处理第 {index} 个账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:558\nmsgid \"开始处理账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:571\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败，请检查 Cookie 登录状态！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:582\nmsgid \"\"\n\"如果账号发布作品均为共创作品且该账号均不是作品作者时，请配置已登录的 Cookie \"\n\"后重新运行程序，其余情况请无视该提示！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:730\nmsgid \"开始提取作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:743\nmsgid \"提取账号或合集信息发生错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:825\nmsgid \"发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:827\nmsgid \"喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:829\nmsgid \"收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:831\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:35\nmsgid \"合集作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:833\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:89\nmsgid \"收藏夹作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:844\n#, python-brace-format\nmsgid \"昵称/标题：{name}；标识：{mark}；ID：{id}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:884\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:918\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1272\nmsgid \"请选择作品链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:888\nmsgid \"已退出批量下载链接作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:898\nmsgid \"已退出批量下载链接作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:905\nmsgid \"注意：本功能为实验性功能，依赖第三方 API 服务，可能不稳定或存在限制！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:911\nmsgid \"已退出批量下载视频原画(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:938\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:965\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1283\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\slides.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:38\nmsgid \"作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:944\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:971\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1017\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1291\n#, python-brace-format\nmsgid \"共提取到 {count} 个作品，开始处理！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:984\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1005\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1014\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1312\nmsgid \"从文本文档提取作品 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1093\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:320\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:323\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:405\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:749\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:453\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:467\nmsgid \"图集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1095\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:326\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:329\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:456\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:751\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:351\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:365\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:480\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:570\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:102\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:116\nmsgid \"视频\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1105\nmsgid \"程序未检测到有效的 ffmpeg，不支持直播下载功能！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1109\nmsgid \"请选择下载清晰度(输入清晰度或者对应序号，直接回车代表不下载): \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1116\nmsgid \"未输入有效的清晰度或者序号，跳过下载！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1149\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1164\nmsgid \"直播\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1154\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1171\nmsgid \"获取直播数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1158\nmsgid \"已退出获取直播拉流地址(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1167\nmsgid \"{} 提取直播 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1181\nmsgid \"已退出获取直播拉流地址(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1197\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1214\nmsgid \"直播标题:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1198\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1215\nmsgid \"主播昵称:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1199\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1217\nmsgid \"在线观众:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1200\nmsgid \"观看次数:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1202\nmsgid \"当前直播已结束！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1216\nmsgid \"开播时间:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1218\nmsgid \"点赞次数:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1223\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1242\nmsgid \"FLV 拉流地址: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1226\nmsgid \"M3U8 拉流地址: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1264\nmsgid \"已退出采集作品评论数据(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1276\nmsgid \"已退出采集作品评论数据(抖音)模式)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1363\n#, python-brace-format\nmsgid \"作品评论数据已储存至 {filename}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1364\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1374\n#, python-brace-format\nmsgid \"作品{id}_评论数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1368\nmsgid \"采集评论数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1423\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1434\nmsgid \"请选择合集链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1427\nmsgid \"已退出批量下载合集作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1438\nmsgid \"已退出批量下载合集作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1470\nmsgid \"合集或作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1459\n#, python-brace-format\nmsgid \"{url} 获取作品 ID 或合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1473\n#, python-brace-format\nmsgid \"{url} 获取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1502\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1509\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:150\nmsgid \"收藏合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1513\n#, python-brace-format\nmsgid \"{text}列表：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1518\n#, python-brace-format\nmsgid \"\"\n\"请输入需要下载的{item}序号(多个序号使用空格分隔，输入 ALL 下载全部{item})：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1532\n#, python-brace-format\nmsgid \"{text}序号输入错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1540\nmsgid \"从文本文档提取作品 ID 或合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1550\nmsgid \"从文本文档提取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1590\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1650\nmsgid \"合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1626\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 获取作品 ID 或合集 ID 失败，错误配置：{data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1668\n#, python-brace-format\nmsgid \"开始处理第 {index} 个合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1670\nmsgid \"开始处理合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1704\nmsgid \"采集合集作品数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1730\n#, python-brace-format\nmsgid \"配置文件 accounts_urls 参数第 {index} 条数据的 url 无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1754\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:40\nmsgid \"请输入文本文档路径：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1761\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:47\n#, python-brace-format\nmsgid \"{path} 文件读取异常: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1764\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:50\n#, python-brace-format\nmsgid \"{path} 文件不存在！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1788\n#, python-brace-format\nmsgid \"正在获取账号 {sec_user_id} 的数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1815\nmsgid \"账号数据已保存至文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1825\nmsgid \"已退出采集账号详细数据模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1832\n#, python-brace-format\nmsgid \"请输入搜索参数；参数之间使用两个空格分隔({field})：\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1855\nmsgid \"请选择搜索模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1934\nmsgid \"搜索结果为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1956\nmsgid \"搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2023\n#, python-brace-format\nmsgid \"搜索数据已保存至 {name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2032\nmsgid \"已退出采集抖音热榜数据(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2052\n#, python-brace-format\nmsgid \"热榜数据_{time}_{name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2066\n#, python-brace-format\nmsgid \"热榜数据已储存至: 热榜数据_{time} + 榜单类型\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2081\nmsgid \"已退出批量下载收藏作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2101\nmsgid \"已退出批量下载收藏夹作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2126\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:27\nmsgid \"收藏夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2137\n#, python-brace-format\nmsgid \"配置文件 owner_url 的 url 参数 {url} 无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2167\nmsgid \"已退出批量下载收藏音乐(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2175\n#, python-brace-format\nmsgid \"程序运行耗时 {minutes} 分钟 {seconds} 秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2205\nmsgid \"开始获取收藏数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2215\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2248\nmsgid \"开始获取收藏夹数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2301\nmsgid \"请选择采集功能\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:108\nmsgid \"禁用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:109\nmsgid \"启用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:112\nmsgid \"从剪贴板读取 Cookie (抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:113\nmsgid \"从浏览器读取 Cookie (抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:115\nmsgid \"从剪贴板读取 Cookie (TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:116\nmsgid \"从浏览器读取 Cookie (TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:117\nmsgid \"终端交互模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:118\nmsgid \"后台监听模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:119\nmsgid \"Web API 模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:120\nmsgid \"Web UI 模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:124\nmsgid \"{}作品下载记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:127\nmsgid \"删除作品下载记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:129\nmsgid \"{}运行日志记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:132\nmsgid \"检查程序版本更新\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:133\nmsgid \"切换语言\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:149\nmsgid \"\"\n\"访问 http://127.0.0.1:5555/docs 或者 http://127.0.0.1:5555/redoc 可以查阅 \"\n\"API 模式说明文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:190\nmsgid \"是否已仔细阅读上述免责声明(YES/NO): \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:224\nmsgid \"项目地址: {}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:225\nmsgid \"项目文档: {}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:226\nmsgid \"开源许可: {}\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:248\n#, python-brace-format\nmsgid \"检测到新版本: {major}.{minor}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:255\nmsgid \"当前版本为开发版, 可更新至正式版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:260\nmsgid \"当前已是最新开发版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:264\nmsgid \"当前已是最新正式版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:268\nmsgid \"检测新版本失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:280\nmsgid \"DouK-Downloader 功能选项\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:322\nmsgid \"修改设置成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:334\nmsgid \"Cookie 获取教程：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:340\nmsgid \"\"\n\"复制 Cookie 内容至剪贴板后，按回车键确认继续；若输入任意内容并按回车，则取消\"\n\"操作：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:379\nmsgid \"作品下载记录功能已禁用！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:445\nmsgid \"正在关闭程序\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:273\n#, python-brace-format\nmsgid \"{name} 参数格式错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:366\n#, python-brace-format\nmsgid \"root 参数 {root} 不是有效的文件夹路径，程序将使用项目根路径作为储存路径\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:386\n#, python-brace-format\nmsgid \"\"\n\"folder_name 参数 {folder_name} 不是有效的文件夹名称，程序将使用默认值：\"\n\"Download\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:399\n#, python-brace-format\nmsgid \"\"\n\"name_format 参数 {name_format} 设置错误，程序将使用默认值：创建时间 作品类型 \"\n\"账号昵称 作品描述\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:412\n#, python-brace-format\nmsgid \"\"\n\"date_format 参数 {date_format} 设置错误，程序将使用默认值：年-月-日 时:分:秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:421\n#, python-brace-format\nmsgid \"split 参数 {split} 包含非法字符，程序将使用默认值：-\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:451\n#, python-brace-format\nmsgid \"{remark}代理参数应为字符串格式，未来不再支持字典格式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:467\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试成功\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:474\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试超时\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:484\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试失败：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:519\n#, python-brace-format\nmsgid \"max_pages 参数 {max_pages} 设置错误，程序将使用默认值：99999\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:543\n#, python-brace-format\nmsgid \"\"\n\"storage_format 参数 {storage_format} 设置错误，程序默认不会储存任何数据至文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:561\nmsgid \"正在更新抖音参数，请稍等...\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:579\nmsgid \"抖音参数更新完毕！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:583\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:644\nmsgid \"配置文件 cookie 参数未设置，抖音平台功能可能无法正常使用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:593\nmsgid \"正在更新 TikTok 参数，请稍等...\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:611\nmsgid \"TikTok 参数更新完毕！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:616\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:667\nmsgid \"配置文件 cookie_tiktok 参数未设置，TikTok 平台功能可能无法正常使用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:772\n#, python-brace-format\nmsgid \"TikTok cookie 缺少 {name} 键值对，请尝试重新写入 cookie\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1112\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置过小，程序将使用默认值：{default}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1120\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置错误，程序将使用默认值：{default}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1133\n#, python-brace-format\nmsgid \"live_qualities 参数 {live_qualities} 设置错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:157\nmsgid \"\"\n\"创建默认配置文件 settings.json 成功！\\n\"\n\"请参考项目文档的快速入门部分，设置 Cookie 后重新运行程序！\\n\"\n\"建议根据实际使用需求修改配置文件 settings.json！\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:174\nmsgid \"配置文件 settings.json 格式错误，请检查 JSON 格式！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:186\n#, python-brace-format\nmsgid \"配置文件 settings.json 缺少参数 {i}，已自动添加该参数！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:204\nmsgid \"保存配置成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:216\n#, python-brace-format\nmsgid \"配置文件 {old} 参数已变更为 {new} 参数，请注意修改配置文件！\"\nmsgstr \"\"\n\nmsgid \"\"\n\"关于 DouK-Downloader 的 免责声明：\\n\"\n\"\\n\"\n\"1. 使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项\"\n\"目所产生的任何损失、责任、或风险概不负责。\\n\"\n\"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术\"\n\"水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。\\n\"\n\"3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需\"\n\"自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责\"\n\"任。\\n\"\n\"4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求，\"\n\"并在适当的地方注明使用了 GNU General Public License v3.0 的代码。\\n\"\n\"5. 使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行\"\n\"为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。\\n\"\n\"6. 使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、\"\n\"传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分\"\n\"发。\\n\"\n\"7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用\"\n\"者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使\"\n\"用者自行承担。\\n\"\n\"8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行\"\n\"为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。\\n\"\n\"9. 本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-\"\n\"Downloader 项目相关的任何商业服务。\\n\"\n\"10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不\"\n\"承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的\"\n\"各种情况负全部责任。\\n\"\n\"11. 本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自\"\n\"行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣\"\n\"传、推广或再授权。\\n\"\n\"12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁\"\n\"已获取的代码及衍生作品。\\n\"\n\"13. 作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修\"\n\"订后的条款。\\n\"\n\"\\n\"\n\"在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声\"\n\"明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码\"\n\"和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险\"\n\"和后果。\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\custom\\function.py:56\n#, python-brace-format\nmsgid \"\"\n\"程序连续处理了 {batches} 个数据，为了避免请求频率过高导致账号或 IP 被风控，程\"\n\"序已经暂停运行，将在 {rest_time} 秒后恢复运行！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:159\nmsgid \"开始下载作品文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:343\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:501\nmsgid \"音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:255\nmsgid \"程序将会调用 ffmpeg 下载直播，关闭 DouK-Downloader 不会中断下载！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:332\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:335\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:753\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:422\nmsgid \"实况\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:409\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:460\n#, python-brace-format\nmsgid \"【{type}】{name} 提取文件下载地址失败，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:421\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:428\n#, python-brace-format\nmsgid \"【{type}】{name}_{index} 文件已存在，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:472\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录或文件已存在，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:514\n#, python-brace-format\nmsgid \"【{type}】{name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:622\nmsgid \"文件缓存异常，尝试重新下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:660\n#, python-brace-format\nmsgid \"网络异常: {error_repr}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:664\n#, python-brace-format\nmsgid \"响应码异常: {error_repr}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:668\nmsgid \"\"\n\"如果 TikTok 平台作品下载功能异常，请检查配置文件中 browser_info_tiktok 的 \"\n\"device_id 参数！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:679\n#, python-brace-format\nmsgid \"下载文件时发生预期之外的错误，请向作者反馈，错误信息: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:715\n#, python-brace-format\nmsgid \"{show} 下载中断，错误信息：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:721\n#, python-brace-format\nmsgid \"{show} 文件下载成功\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:785\n#, python-brace-format\nmsgid \"UID{id_}_{name}_发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:787\n#, python-brace-format\nmsgid \"UID{id_}_{name}_喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:789\n#, python-brace-format\nmsgid \"MID{id_}_{name}_合集作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:791\n#, python-brace-format\nmsgid \"UID{id_}_{name}_收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:793\n#, python-brace-format\nmsgid \"CID{id_}_{name}_收藏夹作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:850\n#, python-brace-format\nmsgid \"{file_name} 文件已删除\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:854\n#, python-brace-format\nmsgid \"下载视频作品 {downloaded_video_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:859\n#, python-brace-format\nmsgid \"跳过视频作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:864\n#, python-brace-format\nmsgid \"下载图集作品 {downloaded_image_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:869\n#, python-brace-format\nmsgid \"跳过图集作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:874\n#, python-brace-format\nmsgid \"下载实况作品 {downloaded_image_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:879\n#, python-brace-format\nmsgid \"跳过实况作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:957\n#, python-brace-format\nmsgid \"未收录的文件类型：{content}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:967\n#, python-brace-format\nmsgid \"{show} 响应内容为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:976\n#, python-brace-format\nmsgid \"{show} 文件大小超出限制，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\msToken.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:42\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:93\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\webID.py:44\n#, python-brace-format\nmsgid \"获取 {name} 参数失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:99\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:110\n#, python-brace-format\nmsgid \"提取账号信息失败: {data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:217\n#, python-brace-format\nmsgid \"筛选处理后作品数量: {count}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:831\nmsgid \"已注销账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:832\nmsgid \"无效账号昵称\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:859\n#, python-brace-format\nmsgid \"sec_user_id {user_id} 与 {s} 不一致\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:944\nmsgid \"提取账号信息或合集信息失败，请向作者反馈！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:68\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:85\nmsgid \"\"\n\"该账号为私密账号，需要使用登录后的 Cookie，且登录的账号需要关注该私密账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:207\n#, python-brace-format\nmsgid \"tab 参数 {tab} 设置错误，程序将使用默认值: post\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:212\nmsgid \"最早\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:215\nmsgid \"最晚\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:229\n#, python-brace-format\nmsgid \"作品{tip}发布日期无效 {date}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:234\n#, python-brace-format\nmsgid \"作品{tip}发布日期参数 {date} 类型错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:237\n#, python-brace-format\nmsgid \"作品{tip}发布日期: {latest_date}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:261\nmsgid \"配置文件 cookie 参数未登录，数据获取已提前结束\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:264\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:200\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:82\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:80\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:121\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:391\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:74\n#, python-brace-format\nmsgid \"数据解析失败，请告知作者处理: {data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collection.py:26\nmsgid \"账号收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:58\nmsgid \"当前账号无收藏夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:122\n#, python-brace-format\nmsgid \"收藏夹 {collects_id} 为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:182\nmsgid \"当前账号无收藏合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:216\nmsgid \"收藏短剧\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:237\nmsgid \"当前账号无收藏短剧\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:270\nmsgid \"收藏音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:291\nmsgid \"当前账号无收藏音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:35\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment_tiktok.py:29\nmsgid \"作品评论\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:77\n#, python-brace-format\nmsgid \"作品 {item_id} 无评论\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:105\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:194\n#, python-brace-format\nmsgid \"正在获取{text}数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:230\nmsgid \"作品评论回复\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:270\n#, python-brace-format\nmsgid \"评论 {comment_id} 无回复\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:18\nmsgid \"抖音热榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:23\nmsgid \"娱乐榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:28\nmsgid \"社会榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:33\nmsgid \"挑战榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:52\nmsgid \"热榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:87\n#, python-brace-format\nmsgid \"{space_name}数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:29\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:27\nmsgid \"账号简略\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:57\n#, python-brace-format\nmsgid \"获取{text}失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\live_tiktok.py:57\nmsgid \"此直播可能会令部分观众感到不适，请登录后重试！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:71\nmsgid \"获取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:32\nmsgid \"合辑作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:92\nmsgid \"账号合辑数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:18\nmsgid \"综合搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:25\nmsgid \"视频搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:32\nmsgid \"用户搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:39\nmsgid \"直播搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:60\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围  内容形式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:61\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:62\nmsgid \"关键词  总页数  粉丝数量  用户类型\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:63\nmsgid \"关键词  总页数\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:72\nmsgid \"综合排序\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:73\nmsgid \"最多点赞\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:74\nmsgid \"最新发布\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:77\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:89\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:95\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:101\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:114\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:128\nmsgid \"不限\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:78\nmsgid \"一天内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:79\nmsgid \"一周内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:80\nmsgid \"半年内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:90\nmsgid \"一分钟以内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:91\nmsgid \"一到五分钟\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:92\nmsgid \"五分钟以上\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:96\nmsgid \"最近看过\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:97\nmsgid \"还未看过\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:98\nmsgid \"关注的人\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:103\nmsgid \"图文\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:115\nmsgid \"1000以下\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:119\nmsgid \"100w以上\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:129\nmsgid \"普通用户\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:130\nmsgid \"企业认证\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:131\nmsgid \"个人认证\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:176\n#, python-brace-format\nmsgid \"获取{self_text}数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:444\n#, python-brace-format\nmsgid \"共获取到 {count} 个{text}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:112\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:131\nmsgid \"文件夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:208\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:218\nmsgid \"文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:225\n#, python-brace-format\nmsgid \"{type} {old}被占用，重命名失败: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:232\n#, python-brace-format\nmsgid \"{type} {new}名称重复，重命名失败: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:239\n#, python-brace-format\nmsgid \"处理{type} {old}时发生预期之外的错误: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\models\\search.py:34\nmsgid \"keyword 参数无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:42\nmsgid \"当前剪贴板的内容不是有效的 Cookie 内容！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\sqlite.py:83\nmsgid \"更新数据表名称时发生错误，重命名失败，请向作者反馈以便修复问题！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\xlsx.py:62\n#, python-brace-format\nmsgid \"数据包含非法字符，保存数据失败：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:80\n#, python-brace-format\nmsgid \"\"\n\"读取指定浏览器的 {platform_name} Cookie 并写入配置文件；\\n\"\n\"注意：Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏\"\n\"览器 Cookie！\\n\"\n\"{options}\\n\"\n\"请输入浏览器名称或序号：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:94\nmsgid \"读取 Cookie 成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:102\nmsgid \"Cookie 数据为空！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:105\nmsgid \"未选择浏览器！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:117\nmsgid \"浏览器名称或序号输入错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:125\nmsgid \"读取 Cookie 失败，未找到 Cookie 数据！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:164\nmsgid \"从浏览器读取 Cookie 功能不支持当前平台！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:26\nmsgid \"响应内容不是有效的 JSON 数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:28\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:50\n#, python-brace-format\nmsgid \"响应码异常：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:30\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:37\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:52\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:59\n#, python-brace-format\nmsgid \"网络异常：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:32\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:54\n#, python-brace-format\nmsgid \"请求超时：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:48\nmsgid \"响应内容不是有效的 JSON 数据，请尝试更新 Cookie！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\cleaner.py:46\nmsgid \"不受支持的操作系统类型，可能无法正常去除非法字符！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\error.py:9\nmsgid \"项目代码错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:19\n#, python-brace-format\nmsgid \"正在进行第 {index} 次重试\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:48\nmsgid \"\"\n\"如需重新尝试处理该对象，请关闭所有正在访问该对象的窗口或程序，然后直接按下回\"\n\"车键！\\n\"\n\"如需跳过处理该对象，请输入任意字符后按下回车键！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:63\nmsgid \"请关闭所有正在访问该对象的窗口或程序，然后按下回车键继续处理！\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"项目默认无需令牌；公开部署时，建议设置令牌以防止恶意请求！\\n\"\n\"\\n\"\n\"令牌设置位置：`src/custom/function.py` - `is_valid_token()`\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"更新项目配置文件 settings.json\\n\"\n\"\\n\"\n\"仅需传入需要更新的配置参数\\n\"\n\"\\n\"\n\"返回更新后的全部配置参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **text**: 包含分享链接的字符串；必需参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: 抖音账号 sec_uid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: 抖音合集 ID\\n\"\n\"- **detail_id**: 属于合集的抖音作品 ID\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"\\n\"\n\"**`mix_id` 和 `detail_id` 二选一，只需传入其中之一即可**\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\n\"- **room_id**: 抖音直播 room_id\\n\"\n\"- **sec_user_id**: 抖音直播账号 sec_user_id\\n\"\n\"\\n\"\n\"**本接口支持两种参数传入方式**:\\n\"\n\"\\n\"\n\"- 方式一 ：传入 `web_rid`\\n\"\n\"- 方式二 ：同时传入 `room_id` 和 `sec_user_id`\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"- **count_reply**: 可选参数\\n\"\n\"- **reply**: 可选参数，默认值：False\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **comment_id**: 评论 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"- **content_type**: 内容形式；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **douyin_user_fans**: 粉丝数量；可选参数\\n\"\n\"- **douyin_user_type**: 用户类型；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: TikTok 作品 ID；必需参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: TikTok 账号 secUid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: TikTok 合集 ID；必需参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **room_id**: TikTok 直播 room_id；必需参数\\n\"\nmsgstr \"\"\n"
  },
  {
    "path": "locale/zh_CN/LC_MESSAGES/tk.po",
    "content": "# Chinese translations for DouK-Downloader package\n# Copyright (C) 2024 THE DouK-Downloader'S COPYRIGHT HOLDER\n# This file is distributed under the same license as the DouK-Downloader package.\n# FIRST AUTHOR <yonglelolu@foxmail.com>, 2024.\n#\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: DouK-Downloader 5.8\\n\"\n\"Report-Msgid-Bugs-To: <yonglelolu@foxmail.com>\\n\"\n\"POT-Creation-Date: 2025-11-04 10:48+0800\\n\"\n\"PO-Revision-Date: 2024-12-22 21:46+0800\\n\"\n\"Last-Translator: <yonglelolu@foxmail.com>\\n\"\n\"Language-Team: Chinese (simplified)\\n\"\n\"Language: zh_CN\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: 8bit\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:41\nmsgid \"\"\n\"程序会自动检测并提取剪贴板中的抖音和 TikTok 作品链接，并自动下载作品文件；如\"\n\"需关闭，请按下 Ctrl+C，或将剪贴板内容设置为“close”以停止监听！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_monitor.py:129\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:941\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:968\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1288\n#, python-brace-format\nmsgid \"{url} 提取作品 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:50\nmsgid \"验证失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:106\nmsgid \"访问项目 GitHub 仓库\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:107\nmsgid \"重定向至项目 GitHub 仓库主页\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:123\nmsgid \"项目\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:115\nmsgid \"测试令牌有效性\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:128\nmsgid \"验证成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:135\nmsgid \"更新项目全局配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:145\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:158\nmsgid \"配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:156\nmsgid \"获取项目全局配置\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:157\nmsgid \"返回项目全部配置参数\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:166\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:511\nmsgid \"获取分享链接重定向的完整链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:175\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:206\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:233\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:259\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:299\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:342\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:379\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:419\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:449\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:477\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:501\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:190\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:444\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:885\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:961\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:43\nmsgid \"抖音\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:183\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:528\nmsgid \"请求链接成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:188\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:533\nmsgid \"请求链接失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:195\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:540\nmsgid \"获取单个作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:216\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:561\nmsgid \"获取账号作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:243\nmsgid \"获取合集作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:269\nmsgid \"参数错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:288\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:622\nmsgid \"获取直播数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:326\nmsgid \"获取作品评论数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:364\nmsgid \"获取评论回复数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:398\nmsgid \"获取综合搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:429\nmsgid \"获取视频搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:459\nmsgid \"获取用户搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:487\nmsgid \"获取直播搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:588\nmsgid \"获取合辑作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:656\nmsgid \"搜索结果为空！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:709\nmsgid \"获取数据成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_server.py:720\nmsgid \"获取数据失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:71\nmsgid \"\"\n\"未设置 storage_format 参数，无法正常使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:86\nmsgid \"抖音 Cookie\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:90\n#, python-brace-format\nmsgid \"{tip} 未登录，无法使用该功能，详细说明请查阅项目文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:143\nmsgid \"批量下载账号作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:147\nmsgid \"批量下载链接作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:151\nmsgid \"获取直播拉流地址(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:155\nmsgid \"采集作品评论数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:159\nmsgid \"批量下载合集作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:163\nmsgid \"采集账号详细数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:167\nmsgid \"采集搜索结果数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:171\nmsgid \"采集抖音热榜数据(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:176\nmsgid \"批量下载收藏作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:180\nmsgid \"批量下载收藏音乐(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:185\nmsgid \"批量下载收藏夹作品(抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:189\nmsgid \"批量下载账号作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:193\nmsgid \"批量下载链接作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:197\nmsgid \"批量下载合集作品(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:201\nmsgid \"获取直播拉流地址(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:206\nmsgid \"批量下载视频原画(TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:211\nmsgid \"使用 accounts_urls 参数的账号链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:212\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:220\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:236\nmsgid \"手动输入待采集的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:213\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:221\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:237\nmsgid \"从文本文档读取待采集的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:217\nmsgid \"使用 accounts_urls_tiktok 参数的账号链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:224\nmsgid \"使用 mix_urls 参数的合集链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:225\nmsgid \"获取当前账号收藏合集列表\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:226\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:231\nmsgid \"手动输入待采集的合集/作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:227\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:232\nmsgid \"从文本文档读取待采集的合集/作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:230\nmsgid \"使用 mix_urls_tiktok 参数的合集链接(推荐)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:235\nmsgid \"使用 accounts_urls 参数的账号链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:240\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:244\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:248\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:252\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:256\nmsgid \"手动输入待采集的作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:241\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:245\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:249\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:253\nmsgid \"从文本文档读取待采集的作品链接\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:261\nmsgid \"综合搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:265\nmsgid \"视频搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:269\nmsgid \"用户搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:273\nmsgid \"直播搜索数据采集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:283\n#, python-brace-format\nmsgid \"请输入{tip}链接: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:296\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:322\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:330\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1821\nmsgid \"请选择账号链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:300\nmsgid \"已退出批量下载账号作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:302\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:415\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:535\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:25\nmsgid \"账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:306\n#, python-brace-format\nmsgid \"程序共处理 {0} 个{1}，成功 {2} 个，失败 {3} 个，耗时 {4} 分钟 {5} 秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:326\nmsgid \"已退出批量下载账号作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:382\n#, python-brace-format\nmsgid \"共有 {count} 个账号的作品等待下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:393\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 提取 sec_user_id 失败，错误配置：{data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:451\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1743\nmsgid \"账号主页\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:438\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1747\n#, python-brace-format\nmsgid \"{url} 提取账号 sec_user_id 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:470\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:508\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1776\nmsgid \"从文本文档提取账号 sec_user_id 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:556\n#, python-brace-format\nmsgid \"开始处理第 {index} 个账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:558\nmsgid \"开始处理账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:571\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败，请检查 Cookie 登录状态！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:582\nmsgid \"\"\n\"如果账号发布作品均为共创作品且该账号均不是作品作者时，请配置已登录的 Cookie \"\n\"后重新运行程序，其余情况请无视该提示！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:730\nmsgid \"开始提取作品数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:743\nmsgid \"提取账号或合集信息发生错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:825\nmsgid \"发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:827\nmsgid \"喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:829\nmsgid \"收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:831\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:35\nmsgid \"合集作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:833\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:89\nmsgid \"收藏夹作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:844\n#, python-brace-format\nmsgid \"昵称/标题：{name}；标识：{mark}；ID：{id}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:884\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:918\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1272\nmsgid \"请选择作品链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:888\nmsgid \"已退出批量下载链接作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:898\nmsgid \"已退出批量下载链接作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:905\nmsgid \"注意：本功能为实验性功能，依赖第三方 API 服务，可能不稳定或存在限制！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:911\nmsgid \"已退出批量下载视频原画(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:938\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:965\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1283\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:24\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\slides.py:26\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:38\nmsgid \"作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:944\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:971\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1017\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1291\n#, python-brace-format\nmsgid \"共提取到 {count} 个作品，开始处理！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:984\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1005\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1014\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1312\nmsgid \"从文本文档提取作品 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1093\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:320\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:323\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:405\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:749\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:434\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:453\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:467\nmsgid \"图集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1095\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:326\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:329\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:456\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:751\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:351\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:365\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:480\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:570\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:102\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:116\nmsgid \"视频\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1105\nmsgid \"程序未检测到有效的 ffmpeg，不支持直播下载功能！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1109\nmsgid \"请选择下载清晰度(输入清晰度或者对应序号，直接回车代表不下载): \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1116\nmsgid \"未输入有效的清晰度或者序号，跳过下载！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1149\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1164\nmsgid \"直播\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1154\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1171\nmsgid \"获取直播数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1158\nmsgid \"已退出获取直播拉流地址(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1167\nmsgid \"{} 提取直播 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1181\nmsgid \"已退出获取直播拉流地址(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1197\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1214\nmsgid \"直播标题:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1198\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1215\nmsgid \"主播昵称:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1199\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1217\nmsgid \"在线观众:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1200\nmsgid \"观看次数:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1202\nmsgid \"当前直播已结束！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1216\nmsgid \"开播时间:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1218\nmsgid \"点赞次数:\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1223\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1242\nmsgid \"FLV 拉流地址: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1226\nmsgid \"M3U8 拉流地址: \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1264\nmsgid \"已退出采集作品评论数据(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1276\nmsgid \"已退出采集作品评论数据(抖音)模式)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1363\n#, python-brace-format\nmsgid \"作品评论数据已储存至 {filename}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1364\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1374\n#, python-brace-format\nmsgid \"作品{id}_评论数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1368\nmsgid \"采集评论数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1423\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1434\nmsgid \"请选择合集链接来源\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1427\nmsgid \"已退出批量下载合集作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1438\nmsgid \"已退出批量下载合集作品(TikTok)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1455\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1470\nmsgid \"合集或作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1459\n#, python-brace-format\nmsgid \"{url} 获取作品 ID 或合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1473\n#, python-brace-format\nmsgid \"{url} 获取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1502\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1509\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:150\nmsgid \"收藏合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1513\n#, python-brace-format\nmsgid \"{text}列表：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1518\n#, python-brace-format\nmsgid \"\"\n\"请输入需要下载的{item}序号(多个序号使用空格分隔，输入 ALL 下载全部{item})：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1532\n#, python-brace-format\nmsgid \"{text}序号输入错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1540\nmsgid \"从文本文档提取作品 ID 或合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1550\nmsgid \"从文本文档提取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1590\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1650\nmsgid \"合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1626\n#, python-brace-format\nmsgid \"\"\n\"配置文件 {name} 参数的 url {url} 获取作品 ID 或合集 ID 失败，错误配置：{data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1668\n#, python-brace-format\nmsgid \"开始处理第 {index} 个合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1670\nmsgid \"开始处理合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1704\nmsgid \"采集合集作品数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1730\n#, python-brace-format\nmsgid \"配置文件 accounts_urls 参数第 {index} 条数据的 url 无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1754\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:40\nmsgid \"请输入文本文档路径：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1761\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:47\n#, python-brace-format\nmsgid \"{path} 文件读取异常: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1764\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\cli_edition\\write.py:50\n#, python-brace-format\nmsgid \"{path} 文件不存在！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1788\n#, python-brace-format\nmsgid \"正在获取账号 {sec_user_id} 的数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1815\nmsgid \"账号数据已保存至文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1825\nmsgid \"已退出采集账号详细数据模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1832\n#, python-brace-format\nmsgid \"请输入搜索参数；参数之间使用两个空格分隔({field})：\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1855\nmsgid \"请选择搜索模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1934\nmsgid \"搜索结果为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:1956\nmsgid \"搜索数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2023\n#, python-brace-format\nmsgid \"搜索数据已保存至 {name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2032\nmsgid \"已退出采集抖音热榜数据(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2052\n#, python-brace-format\nmsgid \"热榜数据_{time}_{name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2066\n#, python-brace-format\nmsgid \"热榜数据已储存至: 热榜数据_{time} + 榜单类型\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2081\nmsgid \"已退出批量下载收藏作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2101\nmsgid \"已退出批量下载收藏夹作品(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2126\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:27\nmsgid \"收藏夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2137\n#, python-brace-format\nmsgid \"配置文件 owner_url 的 url 参数 {url} 无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2167\nmsgid \"已退出批量下载收藏音乐(抖音)模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2175\n#, python-brace-format\nmsgid \"程序运行耗时 {minutes} 分钟 {seconds} 秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2205\nmsgid \"开始获取收藏数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2215\n#, python-brace-format\nmsgid \"{sec_user_id} 获取账号信息失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2248\nmsgid \"开始获取收藏夹数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\main_terminal.py:2301\nmsgid \"请选择采集功能\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:108\nmsgid \"禁用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:109\nmsgid \"启用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:112\nmsgid \"从剪贴板读取 Cookie (抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:113\nmsgid \"从浏览器读取 Cookie (抖音)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:115\nmsgid \"从剪贴板读取 Cookie (TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:116\nmsgid \"从浏览器读取 Cookie (TikTok)\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:117\nmsgid \"终端交互模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:118\nmsgid \"后台监听模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:119\nmsgid \"Web API 模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:120\nmsgid \"Web UI 模式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:124\nmsgid \"{}作品下载记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:127\nmsgid \"删除作品下载记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:129\nmsgid \"{}运行日志记录\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:132\nmsgid \"检查程序版本更新\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:133\nmsgid \"切换语言\"\nmsgstr \"Switch to English\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:149\nmsgid \"\"\n\"访问 http://127.0.0.1:5555/docs 或者 http://127.0.0.1:5555/redoc 可以查阅 \"\n\"API 模式说明文档！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:190\nmsgid \"是否已仔细阅读上述免责声明(YES/NO): \"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:224\nmsgid \"项目地址: {}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:225\nmsgid \"项目文档: {}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:226\nmsgid \"开源许可: {}\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:248\n#, python-brace-format\nmsgid \"检测到新版本: {major}.{minor}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:255\nmsgid \"当前版本为开发版, 可更新至正式版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:260\nmsgid \"当前已是最新开发版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:264\nmsgid \"当前已是最新正式版\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:268\nmsgid \"检测新版本失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:280\nmsgid \"DouK-Downloader 功能选项\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:322\nmsgid \"修改设置成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:334\nmsgid \"Cookie 获取教程：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:340\nmsgid \"\"\n\"复制 Cookie 内容至剪贴板后，按回车键确认继续；若输入任意内容并按回车，则取消\"\n\"操作：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:379\nmsgid \"作品下载记录功能已禁用！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\application\\TikTokDownloader.py:445\nmsgid \"正在关闭程序\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:273\n#, python-brace-format\nmsgid \"{name} 参数格式错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:366\n#, python-brace-format\nmsgid \"root 参数 {root} 不是有效的文件夹路径，程序将使用项目根路径作为储存路径\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:386\n#, python-brace-format\nmsgid \"\"\n\"folder_name 参数 {folder_name} 不是有效的文件夹名称，程序将使用默认值：\"\n\"Download\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:399\n#, python-brace-format\nmsgid \"\"\n\"name_format 参数 {name_format} 设置错误，程序将使用默认值：创建时间 作品类型 \"\n\"账号昵称 作品描述\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:412\n#, python-brace-format\nmsgid \"\"\n\"date_format 参数 {date_format} 设置错误，程序将使用默认值：年-月-日 时:分:秒\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:421\n#, python-brace-format\nmsgid \"split 参数 {split} 包含非法字符，程序将使用默认值：-\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:451\n#, python-brace-format\nmsgid \"{remark}代理参数应为字符串格式，未来不再支持字典格式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:467\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试成功\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:474\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试超时\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:484\n#, python-brace-format\nmsgid \"{remark}代理 {proxy} 测试失败：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:519\n#, python-brace-format\nmsgid \"max_pages 参数 {max_pages} 设置错误，程序将使用默认值：99999\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:543\n#, python-brace-format\nmsgid \"\"\n\"storage_format 参数 {storage_format} 设置错误，程序默认不会储存任何数据至文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:561\nmsgid \"正在更新抖音参数，请稍等...\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:579\nmsgid \"抖音参数更新完毕！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:583\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:644\nmsgid \"配置文件 cookie 参数未设置，抖音平台功能可能无法正常使用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:593\nmsgid \"正在更新 TikTok 参数，请稍等...\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:611\nmsgid \"TikTok 参数更新完毕！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:616\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:667\nmsgid \"配置文件 cookie_tiktok 参数未设置，TikTok 平台功能可能无法正常使用\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:772\n#, python-brace-format\nmsgid \"TikTok cookie 缺少 {name} 键值对，请尝试重新写入 cookie\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1112\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置过小，程序将使用默认值：{default}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1120\n#, python-brace-format\nmsgid \"{key} 参数 {value} 设置错误，程序将使用默认值：{default}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\parameter.py:1133\n#, python-brace-format\nmsgid \"live_qualities 参数 {live_qualities} 设置错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:157\nmsgid \"\"\n\"创建默认配置文件 settings.json 成功！\\n\"\n\"请参考项目文档的快速入门部分，设置 Cookie 后重新运行程序！\\n\"\n\"建议根据实际使用需求修改配置文件 settings.json！\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:174\nmsgid \"配置文件 settings.json 格式错误，请检查 JSON 格式！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:186\n#, python-brace-format\nmsgid \"配置文件 settings.json 缺少参数 {i}，已自动添加该参数！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:204\nmsgid \"保存配置成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\config\\settings.py:216\n#, python-brace-format\nmsgid \"配置文件 {old} 参数已变更为 {new} 参数，请注意修改配置文件！\"\nmsgstr \"\"\n\nmsgid \"\"\n\"关于 DouK-Downloader 的 免责声明：\\n\"\n\"\\n\"\n\"1. 使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项\"\n\"目所产生的任何损失、责任、或风险概不负责。\\n\"\n\"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术\"\n\"水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。\\n\"\n\"3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需\"\n\"自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责\"\n\"任。\\n\"\n\"4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求，\"\n\"并在适当的地方注明使用了 GNU General Public License v3.0 的代码。\\n\"\n\"5. 使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行\"\n\"为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。\\n\"\n\"6. 使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、\"\n\"传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分\"\n\"发。\\n\"\n\"7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用\"\n\"者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使\"\n\"用者自行承担。\\n\"\n\"8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行\"\n\"为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。\\n\"\n\"9. 本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-\"\n\"Downloader 项目相关的任何商业服务。\\n\"\n\"10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不\"\n\"承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的\"\n\"各种情况负全部责任。\\n\"\n\"11. 本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自\"\n\"行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣\"\n\"传、推广或再授权。\\n\"\n\"12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁\"\n\"已获取的代码及衍生作品。\\n\"\n\"13. 作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修\"\n\"订后的条款。\\n\"\n\"\\n\"\n\"在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声\"\n\"明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码\"\n\"和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险\"\n\"和后果。\\n\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\custom\\function.py:56\n#, python-brace-format\nmsgid \"\"\n\"程序连续处理了 {batches} 个数据，为了避免请求频率过高导致账号或 IP 被风控，程\"\n\"序已经暂停运行，将在 {rest_time} 秒后恢复运行！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:159\nmsgid \"开始下载作品文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:343\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:501\nmsgid \"音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:255\nmsgid \"程序将会调用 ffmpeg 下载直播，关闭 DouK-Downloader 不会中断下载！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:332\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:335\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:753\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:422\nmsgid \"实况\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:409\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:460\n#, python-brace-format\nmsgid \"【{type}】{name} 提取文件下载地址失败，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:421\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:428\n#, python-brace-format\nmsgid \"【{type}】{name}_{index} 文件已存在，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:472\n#, python-brace-format\nmsgid \"【{type}】{name} 存在下载记录或文件已存在，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:514\n#, python-brace-format\nmsgid \"【{type}】{name}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:622\nmsgid \"文件缓存异常，尝试重新下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:660\n#, python-brace-format\nmsgid \"网络异常: {error_repr}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:664\n#, python-brace-format\nmsgid \"响应码异常: {error_repr}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:668\nmsgid \"\"\n\"如果 TikTok 平台作品下载功能异常，请检查配置文件中 browser_info_tiktok 的 \"\n\"device_id 参数！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:679\n#, python-brace-format\nmsgid \"下载文件时发生预期之外的错误，请向作者反馈，错误信息: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:715\n#, python-brace-format\nmsgid \"{show} 下载中断，错误信息：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:721\n#, python-brace-format\nmsgid \"{show} 文件下载成功\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:785\n#, python-brace-format\nmsgid \"UID{id_}_{name}_发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:787\n#, python-brace-format\nmsgid \"UID{id_}_{name}_喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:789\n#, python-brace-format\nmsgid \"MID{id_}_{name}_合集作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:791\n#, python-brace-format\nmsgid \"UID{id_}_{name}_收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:793\n#, python-brace-format\nmsgid \"CID{id_}_{name}_收藏夹作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:850\n#, python-brace-format\nmsgid \"{file_name} 文件已删除\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:854\n#, python-brace-format\nmsgid \"下载视频作品 {downloaded_video_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:859\n#, python-brace-format\nmsgid \"跳过视频作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:864\n#, python-brace-format\nmsgid \"下载图集作品 {downloaded_image_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:869\n#, python-brace-format\nmsgid \"跳过图集作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:874\n#, python-brace-format\nmsgid \"下载实况作品 {downloaded_image_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:879\n#, python-brace-format\nmsgid \"跳过实况作品 {skipped_count} 个\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:957\n#, python-brace-format\nmsgid \"未收录的文件类型：{content}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:967\n#, python-brace-format\nmsgid \"{show} 响应内容为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\downloader\\download.py:976\n#, python-brace-format\nmsgid \"{show} 文件大小超出限制，跳过下载\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\msToken.py:108\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:42\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\ttWid.py:93\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\encrypt\\webID.py:44\n#, python-brace-format\nmsgid \"获取 {name} 参数失败！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:99\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:110\n#, python-brace-format\nmsgid \"提取账号信息失败: {data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:217\n#, python-brace-format\nmsgid \"筛选处理后作品数量: {count}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:831\nmsgid \"已注销账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:832\nmsgid \"无效账号昵称\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:859\n#, python-brace-format\nmsgid \"sec_user_id {user_id} 与 {s} 不一致\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\extract\\extractor.py:944\nmsgid \"提取账号信息或合集信息失败，请向作者反馈！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号喜欢作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:41\nmsgid \"账号发布作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:68\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:85\nmsgid \"\"\n\"该账号为私密账号，需要使用登录后的 Cookie，且登录的账号需要关注该私密账号\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:207\n#, python-brace-format\nmsgid \"tab 参数 {tab} 设置错误，程序将使用默认值: post\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:212\nmsgid \"最早\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:215\nmsgid \"最晚\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:229\n#, python-brace-format\nmsgid \"作品{tip}发布日期无效 {date}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:234\n#, python-brace-format\nmsgid \"作品{tip}发布日期参数 {date} 类型错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:237\n#, python-brace-format\nmsgid \"作品{tip}发布日期: {latest_date}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:261\nmsgid \"配置文件 cookie 参数未登录，数据获取已提前结束\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\account.py:264\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:200\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail.py:82\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\detail_tiktok.py:80\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:121\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:391\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:235\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\user.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\tiktok_unofficial.py:74\n#, python-brace-format\nmsgid \"数据解析失败，请告知作者处理: {data}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collection.py:26\nmsgid \"账号收藏作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:58\nmsgid \"当前账号无收藏夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:122\n#, python-brace-format\nmsgid \"收藏夹 {collects_id} 为空\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:182\nmsgid \"当前账号无收藏合集\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:216\nmsgid \"收藏短剧\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:237\nmsgid \"当前账号无收藏短剧\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:270\nmsgid \"收藏音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\collects.py:291\nmsgid \"当前账号无收藏音乐\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:35\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment_tiktok.py:29\nmsgid \"作品评论\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:77\n#, python-brace-format\nmsgid \"作品 {item_id} 无评论\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:105\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:194\n#, python-brace-format\nmsgid \"正在获取{text}数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:230\nmsgid \"作品评论回复\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\comment.py:270\n#, python-brace-format\nmsgid \"评论 {comment_id} 无回复\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:18\nmsgid \"抖音热榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:23\nmsgid \"娱乐榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:28\nmsgid \"社会榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:33\nmsgid \"挑战榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:52\nmsgid \"热榜\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\hot.py:87\n#, python-brace-format\nmsgid \"{space_name}数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:29\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:27\nmsgid \"账号简略\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info.py:64\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\info_tiktok.py:57\n#, python-brace-format\nmsgid \"获取{text}失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\live_tiktok.py:57\nmsgid \"此直播可能会令部分观众感到不适，请登录后重试！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix.py:71\nmsgid \"获取合集 ID 失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:32\nmsgid \"合辑作品\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\mix_tiktok.py:92\nmsgid \"账号合辑数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:18\nmsgid \"综合搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:25\nmsgid \"视频搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:32\nmsgid \"用户搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:39\nmsgid \"直播搜索\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:60\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围  内容形式\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:61\nmsgid \"关键词  总页数  排序依据  发布时间  视频时长  搜索范围\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:62\nmsgid \"关键词  总页数  粉丝数量  用户类型\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:63\nmsgid \"关键词  总页数\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:72\nmsgid \"综合排序\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:73\nmsgid \"最多点赞\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:74\nmsgid \"最新发布\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:77\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:89\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:95\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:101\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:114\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:128\nmsgid \"不限\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:78\nmsgid \"一天内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:79\nmsgid \"一周内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:80\nmsgid \"半年内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:90\nmsgid \"一分钟以内\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:91\nmsgid \"一到五分钟\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:92\nmsgid \"五分钟以上\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:96\nmsgid \"最近看过\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:97\nmsgid \"还未看过\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:98\nmsgid \"关注的人\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:103\nmsgid \"图文\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:115\nmsgid \"1000以下\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:119\nmsgid \"100w以上\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:129\nmsgid \"普通用户\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:130\nmsgid \"企业认证\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\search.py:131\nmsgid \"个人认证\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:176\n#, python-brace-format\nmsgid \"获取{self_text}数据失败\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\interface\\template.py:444\n#, python-brace-format\nmsgid \"共获取到 {count} 个{text}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:112\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:131\nmsgid \"文件夹\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:208\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:218\nmsgid \"文件\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:225\n#, python-brace-format\nmsgid \"{type} {old}被占用，重命名失败: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:232\n#, python-brace-format\nmsgid \"{type} {new}名称重复，重命名失败: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\manager\\cache.py:239\n#, python-brace-format\nmsgid \"处理{type} {old}时发生预期之外的错误: {error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\models\\search.py:34\nmsgid \"keyword 参数无效\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\module\\cookie.py:42\nmsgid \"当前剪贴板的内容不是有效的 Cookie 内容！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\sqlite.py:83\nmsgid \"更新数据表名称时发生错误，重命名失败，请向作者反馈以便修复问题！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\storage\\xlsx.py:62\n#, python-brace-format\nmsgid \"数据包含非法字符，保存数据失败：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:80\n#, python-brace-format\nmsgid \"\"\n\"读取指定浏览器的 {platform_name} Cookie 并写入配置文件；\\n\"\n\"注意：Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏\"\n\"览器 Cookie！\\n\"\n\"{options}\\n\"\n\"请输入浏览器名称或序号：\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:94\nmsgid \"读取 Cookie 成功！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:102\nmsgid \"Cookie 数据为空！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:105\nmsgid \"未选择浏览器！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:117\nmsgid \"浏览器名称或序号输入错误！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:125\nmsgid \"读取 Cookie 失败，未找到 Cookie 数据！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\browser.py:164\nmsgid \"从浏览器读取 Cookie 功能不支持当前平台！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:26\nmsgid \"响应内容不是有效的 JSON 数据\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:28\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:50\n#, python-brace-format\nmsgid \"响应码异常：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:30\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:37\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:52\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:59\n#, python-brace-format\nmsgid \"网络异常：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:32\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:54\n#, python-brace-format\nmsgid \"请求超时：{error}\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\capture.py:48\nmsgid \"响应内容不是有效的 JSON 数据，请尝试更新 Cookie！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\cleaner.py:46\nmsgid \"不受支持的操作系统类型，可能无法正常去除非法字符！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\error.py:9\nmsgid \"项目代码错误\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:19\n#, python-brace-format\nmsgid \"正在进行第 {index} 次重试\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:48\nmsgid \"\"\n\"如需重新尝试处理该对象，请关闭所有正在访问该对象的窗口或程序，然后直接按下回\"\n\"车键！\\n\"\n\"如需跳过处理该对象，请输入任意字符后按下回车键！\"\nmsgstr \"\"\n\n#: C:\\Users\\You\\PycharmProjects\\TikTokDownloader\\src\\tools\\retry.py:63\nmsgid \"请关闭所有正在访问该对象的窗口或程序，然后按下回车键继续处理！\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"项目默认无需令牌；公开部署时，建议设置令牌以防止恶意请求！\\n\"\n\"\\n\"\n\"令牌设置位置：`src/custom/function.py` - `is_valid_token()`\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"更新项目配置文件 settings.json\\n\"\n\"\\n\"\n\"仅需传入需要更新的配置参数\\n\"\n\"\\n\"\n\"返回更新后的全部配置参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **text**: 包含分享链接的字符串；必需参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: 抖音账号 sec_uid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: 抖音合集 ID\\n\"\n\"- **detail_id**: 属于合集的抖音作品 ID\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"\\n\"\n\"**`mix_id` 和 `detail_id` 二选一，只需传入其中之一即可**\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **web_rid**: 抖音直播 web_rid\\n\"\n\"- **room_id**: 抖音直播 room_id\\n\"\n\"- **sec_user_id**: 抖音直播账号 sec_user_id\\n\"\n\"\\n\"\n\"**本接口支持两种参数传入方式**:\\n\"\n\"\\n\"\n\"- 方式一 ：传入 `web_rid`\\n\"\n\"- 方式二 ：同时传入 `room_id` 和 `sec_user_id`\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\n\"- **count_reply**: 可选参数\\n\"\n\"- **reply**: 可选参数，默认值：False\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: 抖音作品 ID；必需参数\\n\"\n\"- **comment_id**: 评论 ID；必需参数\\n\"\n\"- **pages**: 最大请求次数；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"- **content_type**: 内容形式；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **sort_type**: 排序依据；可选参数\\n\"\n\"- **publish_time**: 发布时间；可选参数\\n\"\n\"- **duration**: 视频时长；可选参数\\n\"\n\"- **search_range**: 搜索范围；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\n\"- **douyin_user_fans**: 粉丝数量；可选参数\\n\"\n\"- **douyin_user_type**: 用户类型；可选参数\\n\"\n\"\\n\"\n\"**部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/\"\n\"TikTokDownloader/wiki/\"\n\"Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: 抖音 Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **keyword**: 关键词；必需参数\\n\"\n\"- **offset**: 起始页码；可选参数\\n\"\n\"- **count**: 数据数量；可选参数\\n\"\n\"- **pages**: 总页数；可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **detail_id**: TikTok 作品 ID；必需参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **sec_user_id**: TikTok 账号 secUid；必需参数\\n\"\n\"- **tab**: 账号页面类型；可选参数，默认值：`post`\\n\"\n\"- **earliest**: 作品最早发布日期；可选参数\\n\"\n\"- **latest**: 作品最晚发布日期；可选参数\\n\"\n\"- **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **mix_id**: TikTok 合集 ID；必需参数\\n\"\n\"- **cursor**: 可选参数\\n\"\n\"- **count**: 可选参数\\n\"\nmsgstr \"\"\n\nmsgid \"\"\n\"\\n\"\n\"**参数**:\\n\"\n\"\\n\"\n\"- **cookie**: TikTok Cookie；可选参数\\n\"\n\"- **proxy**: 代理；可选参数\\n\"\n\"- **source**: 是否返回原始响应数据；可选参数，默认值：False\\n\"\n\"- **room_id**: TikTok 直播 room_id；必需参数\\n\"\nmsgstr \"\"\n"
  },
  {
    "path": "main.py",
    "content": "from asyncio import CancelledError\nfrom asyncio import run\n\nfrom src.application import TikTokDownloader\n\n\nasync def main():\n    async with TikTokDownloader() as downloader:\n        try:\n            await downloader.run()\n        except (\n                KeyboardInterrupt,\n                CancelledError,\n        ):\n            return\n\n\nif __name__ == \"__main__\":\n    run(main())\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"DouK-Downloader\"\nversion = \"5.8\"\ndescription = \"TikTok 发布/喜欢/合辑/直播/视频/图集/音乐；抖音发布/喜欢/收藏/收藏夹/视频/图集/实况/直播/音乐/合集/评论/账号/搜索/热榜数据采集工具\"\nauthors = [\n    { name = \"JoeanAmier\", email = \"yonglelolu@foxmail.com\" },\n]\nreadme = \"README.md\"\nlicense = \"GPL-3.0\"\nrequires-python = \">=3.12,<3.13\"\ndependencies = [\n    \"aiofiles>=25.1.0\",\n    \"aiosqlite>=0.21.0\",\n    \"emoji>=2.15.0\",\n    \"fastapi>=0.124.2\",\n    \"gmssl>=3.2.2\",\n    \"httpx[socks]>=0.28.1\",\n    \"lxml>=6.0.2\",\n    \"openpyxl>=3.1.5\",\n    \"pydantic>=2.12.5\",\n    \"pyperclip>=1.11.0\",\n    \"rich>=14.2.0\",\n    \"rookiepy>=0.5.6\",\n    \"uvicorn>=0.38.0\",\n]\n\n[project.urls]\nRepository = \"https://github.com/JoeanAmier/TikTokDownloader\"\n\n[tool.uv.pip]\nindex-url = \"https://pypi.org/simple\"\n\n[tool.ruff]\n# Exclude a variety of commonly ignored directories.\nexclude = [\n    \".bzr\",\n    \".direnv\",\n    \".eggs\",\n    \".git\",\n    \".git-rewrite\",\n    \".hg\",\n    \".ipynb_checkpoints\",\n    \".mypy_cache\",\n    \".nox\",\n    \".pants.d\",\n    \".pyenv\",\n    \".pytest_cache\",\n    \".pytype\",\n    \".ruff_cache\",\n    \".svn\",\n    \".tox\",\n    \".venv\",\n    \".vscode\",\n    \"__pypackages__\",\n    \"_build\",\n    \"buck-out\",\n    \"build\",\n    \"dist\",\n    \"node_modules\",\n    \"site-packages\",\n    \"venv\",\n]\n\n# Same as Black.\nline-length = 88\nindent-width = 4\n\n# Assume Python 3.12\ntarget-version = \"py312\"\n\n[tool.ruff.lint]\n# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default.\n# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or\n# McCabe complexity (`C901`) by default.\nselect = [\"E4\", \"E7\", \"E9\", \"F\"]\nignore = []\n\n# Allow fix for all enabled rules (when `--fix`) is provided.\nfixable = [\"ALL\"]\nunfixable = []\n\n# Allow unused variables when underscore-prefixed.\ndummy-variable-rgx = \"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$\"\n\n[tool.ruff.format]\n# Like Black, use double quotes for strings.\nquote-style = \"double\"\n\n# Like Black, indent with spaces, rather than tabs.\nindent-style = \"space\"\n\n# Like Black, respect magic trailing commas.\nskip-magic-trailing-comma = false\n\n# Like Black, automatically detect the appropriate line ending.\nline-ending = \"auto\"\n\n# Enable auto-formatting of code examples in docstrings. Markdown,\n# reStructuredText code/literal blocks and doctests are all supported.\n#\n# This is currently disabled by default, but it is planned for this\n# to be opt-out in the future.\ndocstring-code-format = false\n\n# Set the line length limit used when formatting code snippets in\n# docstrings.\n#\n# This only has an effect when the `docstring-code-format` setting is\n# enabled.\ndocstring-code-line-length = \"dynamic\"\n\n[dependency-groups]\ndev = [\n    \"pytest>=8.3.5\",\n]\n"
  },
  {
    "path": "requirements.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv pip compile pyproject.toml --no-deps --no-strip-extras -o requirements.txt\naiofiles==25.1.0\n    # via douk-downloader (pyproject.toml)\naiosqlite==0.22.1\n    # via douk-downloader (pyproject.toml)\nemoji==2.15.0\n    # via douk-downloader (pyproject.toml)\nfastapi==0.135.1\n    # via douk-downloader (pyproject.toml)\ngmssl==3.2.2\n    # via douk-downloader (pyproject.toml)\nhttpx[socks]==0.28.1\n    # via douk-downloader (pyproject.toml)\nlxml==6.0.2\n    # via douk-downloader (pyproject.toml)\nopenpyxl==3.1.5\n    # via douk-downloader (pyproject.toml)\npydantic==2.12.5\n    # via douk-downloader (pyproject.toml)\npyperclip==1.11.0\n    # via douk-downloader (pyproject.toml)\nqrcode==8.2\n    # via douk-downloader (pyproject.toml)\nrich==14.3.3\n    # via douk-downloader (pyproject.toml)\nrookiepy==0.5.6\n    # via douk-downloader (pyproject.toml)\nuvicorn==0.41.0\n    # via douk-downloader (pyproject.toml)\n"
  },
  {
    "path": "src/application/TikTokDownloader.py",
    "content": "from asyncio import CancelledError, run\nfrom threading import Event, Thread\nfrom time import sleep\n\nfrom httpx import RequestError, get\n\nfrom src.config import Parameter, Settings\nfrom src.custom import (\n    COOKIE_UPDATE_INTERVAL,\n    DISCLAIMER_TEXT,\n    DOCUMENTATION_URL,\n    LICENCE,\n    MASTER,\n    PROJECT_NAME,\n    PROJECT_ROOT,\n    RELEASES,\n    REPOSITORY,\n    SERVER_HOST,\n    SERVER_PORT,\n    TEXT_REPLACEMENT,\n    VERSION_BETA,\n    VERSION_MAJOR,\n    VERSION_MINOR,\n)\nfrom src.manager import Database, DownloadRecorder\nfrom src.module import Cookie, MigrateFolder\nfrom src.record import BaseLogger, LoggerManager\nfrom src.tools import (\n    Browser,\n    ColorfulConsole,\n    DownloaderError,\n    RenameCompatible,\n    choose,\n    remove_empty_directories,\n    safe_pop,\n)\nfrom src.translation import _, switch_language\n\nfrom .main_monitor import ClipboardMonitor\nfrom .main_server import APIServer\nfrom .main_terminal import TikTok\n\n# from typing import Type\n# from webbrowser import open\n\n__all__ = [\"TikTokDownloader\"]\n\n\nclass TikTokDownloader:\n    VERSION_MAJOR = VERSION_MAJOR\n    VERSION_MINOR = VERSION_MINOR\n    VERSION_BETA = VERSION_BETA\n    NAME = PROJECT_NAME\n    WIDTH = 50\n    LINE = \">\" * WIDTH\n\n    def __init__(\n        self,\n    ):\n        self.rename_compatible()\n        self.console = ColorfulConsole(\n            debug=self.VERSION_BETA,\n        )\n        self.logger = None\n        self.recorder = None\n        self.settings = Settings(PROJECT_ROOT, self.console)\n        self.event_cookie = Event()\n        self.cookie = Cookie(self.settings, self.console)\n        self.params_task = None\n        self.parameter = None\n        self.running = True\n        self.run_command = None\n        self.database = Database()\n        self.config = None\n        self.option = None\n        self.__function_menu = None\n\n    @staticmethod\n    def rename_compatible():\n        RenameCompatible.migration_file()\n\n    async def read_config(self):\n        self.config = self.__format_config(await self.database.read_config_data())\n        self.option = self.__format_config(await self.database.read_option_data())\n        self.set_language(self.option[\"Language\"])\n\n    @staticmethod\n    def __format_config(config: list) -> dict:\n        return {i[\"NAME\"]: i[\"VALUE\"] for i in config}\n\n    @staticmethod\n    def set_language(language: str) -> None:\n        switch_language(language)\n\n    async def __aenter__(self):\n        await self.database.__aenter__()\n        await self.read_config()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.database.__aexit__(exc_type, exc_val, exc_tb)\n        if self.parameter:\n            await self.parameter.close_client()\n            self.close()\n\n    def __update_menu(self):\n        options = {\n            1: _(\"禁用\"),\n            0: _(\"启用\"),\n        }\n        self.__function_menu = (\n            (_(\"从剪贴板读取 Cookie (抖音)\"), self.write_cookie),\n            (_(\"从浏览器读取 Cookie (抖音)\"), self.browser_cookie),\n            # (_(\"扫码登录获取 Cookie (抖音)\"), self.auto_cookie),\n            (_(\"从剪贴板读取 Cookie (TikTok)\"), self.write_cookie_tiktok),\n            (_(\"从浏览器读取 Cookie (TikTok)\"), self.browser_cookie_tiktok),\n            (_(\"终端交互模式\"), self.complete),\n            (_(\"后台监听模式\"), self.monitor),\n            (_(\"Web API 模式\"), self.server),\n            (_(\"Web UI 模式\"), self.disable_function),\n            # (_(\"Web API 模式\"), self.__api_object),\n            # (_(\"Web UI 模式\"), self.__web_ui_object),\n            (\n                _(\"{}作品下载记录\").format(options[self.config[\"Record\"]]),\n                self.__modify_record,\n            ),\n            (_(\"删除作品下载记录\"), self.delete_works_ids),\n            (\n                _(\"{}运行日志记录\").format(options[self.config[\"Logger\"]]),\n                self.__modify_logging,\n            ),\n            (_(\"检查程序版本更新\"), self.check_update),\n            (_(\"切换语言\"), self._switch_language),\n        )\n\n    async def disable_function(\n        self,\n        *args,\n        **kwargs,\n    ):\n        self.console.warning(\n            \"该功能正在重构，未来开发完成重新开放！\",\n        )\n\n    async def server(self):\n        try:\n            self.console.print(\n                _(\n                    \"访问 http://127.0.0.1:5555/docs 或者 http://127.0.0.1:5555/redoc 可以查阅 API 模式说明文档！\"\n                ),\n                highlight=True,\n            )\n            await APIServer(\n                self.parameter,\n                self.database,\n            ).run_server(\n                SERVER_HOST,\n                SERVER_PORT,\n            )\n        except KeyboardInterrupt:\n            self.running = False\n\n    async def __modify_record(self):\n        await self.change_config(\"Record\")\n\n    async def __modify_logging(self):\n        await self.change_config(\"Logger\")\n\n    async def _switch_language(\n        self,\n    ):\n        if self.option[\"Language\"] == \"zh_CN\":\n            language = \"en_US\"\n        elif self.option[\"Language\"] == \"en_US\":\n            language = \"zh_CN\"\n        else:\n            raise DownloaderError\n        await self._update_language(language)\n\n    async def _update_language(self, language: str) -> None:\n        self.option[\"Language\"] = language\n        await self.database.update_option_data(\"Language\", language)\n        self.set_language(language)\n\n    async def disclaimer(self):\n        if not self.config[\"Disclaimer\"]:\n            await self.__init_language()\n            self.console.print(_(DISCLAIMER_TEXT), style=MASTER)\n            if self.console.input(\n                _(\"是否已仔细阅读上述免责声明(YES/NO): \")\n            ).upper() not in (\"Y\", \"YES\"):\n                return False\n            await self.database.update_config_data(\"Disclaimer\", 1)\n            self.console.print()\n        return True\n\n    async def __init_language(self):\n        languages = (\n            (\n                \"简体中文\",\n                \"zh_CN\",\n            ),\n            (\n                \"English\",\n                \"en_US\",\n            ),\n        )\n        language = choose(\n            \"请选择语言(Please Select Language)\",\n            [i[0] for i in languages],\n            self.console,\n        )\n        try:\n            language = languages[int(language) - 1][1]\n            await self._update_language(language)\n        except ValueError:\n            await self.__init_language()\n\n    def project_info(self):\n        self.console.print(\n            f\"{self.LINE}\\n\\n\\n{self.NAME.center(self.WIDTH)}\\n\\n\\n{self.LINE}\\n\",\n            style=MASTER,\n        )\n        self.console.print(_(\"项目地址: {}\").format(REPOSITORY), style=MASTER)\n        self.console.print(_(\"项目文档: {}\").format(DOCUMENTATION_URL), style=MASTER)\n        self.console.print(_(\"开源许可: {}\\n\").format(LICENCE), style=MASTER)\n\n    def check_config(self):\n        self.recorder = DownloadRecorder(\n            self.database,\n            self.config[\"Record\"],\n            self.console,\n        )\n        self.logger = {1: LoggerManager, 0: BaseLogger}[self.config[\"Logger\"]]\n\n    async def check_update(self):\n        try:\n            response = get(\n                RELEASES,\n                timeout=5,\n                follow_redirects=True,\n            )\n            latest_major, latest_minor = map(\n                int, str(response.url).split(\"/\")[-1].split(\".\", 1)\n            )\n            if latest_major > self.VERSION_MAJOR or latest_minor > self.VERSION_MINOR:\n                self.console.warning(\n                    _(\"检测到新版本: {major}.{minor}\").format(\n                        major=latest_major, minor=latest_minor\n                    ),\n                )\n                self.console.print(RELEASES)\n            elif latest_minor == self.VERSION_MINOR and self.VERSION_BETA:\n                self.console.warning(\n                    _(\"当前版本为开发版, 可更新至正式版\"),\n                )\n                self.console.print(RELEASES)\n            elif self.VERSION_BETA:\n                self.console.warning(\n                    _(\"当前已是最新开发版\"),\n                )\n            else:\n                self.console.info(\n                    _(\"当前已是最新正式版\"),\n                )\n        except RequestError:\n            self.console.error(\n                _(\"检测新版本失败\"),\n            )\n\n    async def main_menu(\n        self,\n        mode=None,\n    ):\n        \"\"\"选择功能模式\"\"\"\n        while self.running:\n            self.__update_menu()\n            if not mode:\n                mode = choose(\n                    _(\"DouK-Downloader 功能选项\"),\n                    [i for i, __ in self.__function_menu],\n                    self.console,\n                    separate=(\n                        4,\n                        8,\n                    ),\n                )\n            await self.compatible(mode)\n            mode = None\n\n    async def complete(self):\n        \"\"\"终端交互模式\"\"\"\n        example = TikTok(\n            self.parameter,\n            self.database,\n        )\n        try:\n            await example.run(self.run_command)\n            self.running = example.running\n        except KeyboardInterrupt:\n            self.running = False\n\n    async def monitor(self):\n        await self.monitor_clipboard()\n\n    async def monitor_clipboard(self):\n        example = ClipboardMonitor(\n            self.parameter,\n            self.database,\n        )\n        try:\n            await example.run(self.run_command)\n        except (KeyboardInterrupt, CancelledError):\n            await example.stop_listener()\n\n    async def change_config(\n        self,\n        key: str,\n    ):\n        self.config[key] = 0 if self.config[key] else 1\n        await self.database.update_config_data(key, self.config[key])\n        self.console.print(_(\"修改设置成功！\"))\n        self.check_config()\n        await self.check_settings()\n\n    async def write_cookie(self):\n        await self.__write_cookie(False)\n\n    async def write_cookie_tiktok(self):\n        await self.__write_cookie(True)\n\n    async def __write_cookie(self, tiktok: bool):\n        self.console.print(\n            _(\"Cookie 获取教程：\")\n            + \"https://github.com/JoeanAmier/TikTokDownloader/blob/master/docs/Cookie%E8%8E%B7%E5%8F%96%E6\"\n            \"%95%99%E7%A8%8B.md\"\n        )\n        if self.console.input(\n            _(\n                \"复制 Cookie 内容至剪贴板后，按回车键确认继续；若输入任意内容并按回车，则取消操作：\"\n            )\n        ):\n            return\n        if self.cookie.run(tiktok):\n            await self.check_settings()\n\n    # async def auto_cookie(self):\n    #     self.console.error(\n    #         _(\n    #             \"该功能为实验性功能，仅适用于学习和研究目的；目前仅支持抖音平台，建议使用其他方式获取 Cookie，未来可能会禁用或移除该功能！\"\n    #         ),\n    #     )\n    #     if self.console.input(_(\"是否返回上一级菜单(YES/NO)\")).upper() != \"NO\":\n    #         return\n    #     if cookie := await Register(\n    #         self.parameter,\n    #         self.settings,\n    #     ).run():\n    #         self.cookie.extract(cookie, platform=_(\"抖音\"))\n    #         await self.check_settings()\n    #     else:\n    #         self.console.warning(\n    #             _(\"扫码登录失败，未写入 Cookie！\"),\n    #         )\n\n    async def compatible(self, mode: str):\n        if mode in {\"Q\", \"q\", \"\"}:\n            self.running = False\n        try:\n            n = int(mode) - 1\n        except ValueError:\n            return\n        if n in range(len(self.__function_menu)):\n            await self.__function_menu[n][1]()\n\n    async def delete_works_ids(self):\n        if not self.config[\"Record\"]:\n            self.console.warning(\n                _(\"作品下载记录功能已禁用！\"),\n            )\n            return\n        await self.recorder.delete_ids(self.console.input(\"请输入需要删除的作品 ID：\"))\n        self.console.info(\n            \"删除作品下载记录成功！\",\n        )\n\n    async def check_settings(self, restart=True):\n        if restart:\n            await self.parameter.close_client()\n        self.parameter = Parameter(\n            self.settings,\n            self.cookie,\n            logger=self.logger,\n            console=self.console,\n            **self.settings.read(),\n            recorder=self.recorder,\n        )\n        MigrateFolder(self.parameter).compatible()\n        self.parameter.set_headers_cookie()\n        self.restart_cycle_task(\n            restart,\n        )\n        # await self.parameter.update_params_offline()\n        if not restart:\n            self.run_command = self.parameter.run_command.copy()\n        self.parameter.CLEANER.set_rule(TEXT_REPLACEMENT, True)\n\n    async def run(self):\n        self.project_info()\n        self.check_config()\n        await self.check_settings(\n            False,\n        )\n        if await self.disclaimer():\n            await self.main_menu(safe_pop(self.run_command))\n\n    def periodic_update_params(self):\n        async def inner():\n            while not self.event_cookie.is_set():\n                await self.parameter.update_params()\n                self.event_cookie.wait(COOKIE_UPDATE_INTERVAL)\n\n        run(\n            inner(),\n        )\n\n    def restart_cycle_task(\n        self,\n        restart=True,\n    ):\n        if restart:\n            self.event_cookie.set()\n            while self.params_task.is_alive():\n                # print(\"等待子线程结束！\")  # 调试代码\n                sleep(1)\n        self.params_task = Thread(target=self.periodic_update_params)\n        self.event_cookie.clear()\n        self.params_task.start()\n\n    def close(self):\n        self.event_cookie.set()\n        if self.parameter.folder_mode:\n            remove_empty_directories(self.parameter.ROOT)\n            remove_empty_directories(self.parameter.root)\n        self.parameter.logger.info(_(\"正在关闭程序\"))\n\n    async def browser_cookie(\n        self,\n    ):\n        if Browser(self.parameter, self.cookie).run(\n            select=safe_pop(self.run_command),\n        ):\n            await self.check_settings()\n\n    async def browser_cookie_tiktok(\n        self,\n    ):\n        if Browser(self.parameter, self.cookie).run(\n            True,\n            select=safe_pop(self.run_command),\n        ):\n            await self.check_settings()\n"
  },
  {
    "path": "src/application/__init__.py",
    "content": "from .TikTokDownloader import TikTokDownloader\n\n__all__ = [\"TikTokDownloader\"]\n"
  },
  {
    "path": "src/application/main_monitor.py",
    "content": "from contextlib import suppress\nfrom typing import TYPE_CHECKING\nfrom asyncio import Event, create_task, gather, sleep, Queue, QueueEmpty\nfrom .main_terminal import TikTok\nfrom ..translation import _\nfrom pyperclip import copy, paste\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from ..manager import Database\n\n__all__ = [\"ClipboardMonitor\", \"PostMonitor\"]\n\n\nclass ClipboardMonitor(TikTok):\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n        database: \"Database\",\n        server_mode: bool = True,\n    ):\n        super().__init__(\n            parameter,\n            database,\n            server_mode,\n        )\n        self.event_clipboard = Event()\n        self.clipboard_cache = \"\"\n        self.queue_dy = Queue()\n        self.queue_tk = Queue()\n\n    async def run(self, run_command: list):\n        await self.start_listener()\n\n    async def start_listener(\n        self,\n        delay: int | float = 1,\n    ):\n        self.console.info(\n            _(\n                \"程序会自动检测并提取剪贴板中的抖音和 TikTok 作品链接，并自动下载作品文件；如需关闭，请按下 Ctrl+C，或将剪贴板内容设置为“close”以停止监听！\"\n            ),\n        )\n        copy(\"\")\n        self.event_clipboard.clear()\n        await gather(\n            self.check_clipboard(\n                delay=delay,\n            ),\n            self.deal_tasks(\n                delay=delay,\n            ),\n            self.deal_tasks_tiktok(\n                delay=delay,\n            ),\n        )\n\n    async def stop_listener(self):\n        self.console.debug(\"停止监听剪贴板！\")\n        self.event_clipboard.set()\n\n    async def check_clipboard(\n        self,\n        delay: int | float = 1,\n    ):\n        self.console.debug(\"开始监听剪贴板！\")\n        while not self.event_clipboard.is_set():\n            if (c := paste()).lower() == \"close\":\n                await self.stop_listener()\n            elif c != self.clipboard_cache:\n                self.clipboard_cache = c\n                create_task(self.check_link(c))\n            await sleep(delay)\n\n    async def check_link(\n        self,\n        text: str,\n    ):\n        links = text.split()\n        for i in links:\n            if \"douyin\" in i:\n                self.console.debug(f\"处理抖音链接: {i}\")\n                await self.queue_dy.put(i)\n            elif \"tiktok\" in i:\n                self.console.debug(f\"处理 TikTok 链接: {i}\")\n                await self.queue_tk.put(i)\n\n    async def deal_tasks(\n        self,\n        delay: int | float = 1,\n    ):\n        await self._deal_tasks(\n            self.parameter.douyin_platform,\n            self.queue_dy,\n            self.links,\n            False,\n            delay,\n        )\n\n    async def deal_tasks_tiktok(\n        self,\n        delay: int | float = 1,\n    ):\n        await self._deal_tasks(\n            self.parameter.tiktok_platform,\n            self.queue_tk,\n            self.links_tiktok,\n            True,\n            delay,\n        )\n\n    async def _deal_tasks(\n        self,\n        enable: bool,\n        queue: Queue,\n        link_object,\n        tiktok: bool,\n        delay: int | float = 1,\n    ):\n        if not enable:\n            return\n        root, params, logger = self.record.run(self.parameter, blank=True)\n        async with logger(root, console=self.console, **params) as record:\n            while not self.event_clipboard.is_set() or queue.qsize() > 0:\n                with suppress(QueueEmpty):\n                    url = queue.get_nowait()\n                    id_ = await link_object.run(url)\n                    if not any(id_):\n                        self.logger.warning(_(\"{url} 提取作品 ID 失败\").format(url=url))\n                    else:\n                        await self._handle_detail(\n                            id_,\n                            tiktok,\n                            record,\n                        )\n                await sleep(delay)\n\n\nclass PostMonitor(TikTok):\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n        database: \"Database\",\n        server_mode: bool = True,\n    ):\n        super().__init__(\n            parameter,\n            database,\n            server_mode,\n        )\n"
  },
  {
    "path": "src/application/main_server.py",
    "content": "from textwrap import dedent\nfrom typing import TYPE_CHECKING\n\nfrom fastapi import Depends, FastAPI, Header, HTTPException\nfrom fastapi.responses import RedirectResponse\nfrom uvicorn import Config, Server\n\nfrom ..custom import (\n    __VERSION__,\n    REPOSITORY,\n    SERVER_HOST,\n    SERVER_PORT,\n    VERSION_BETA,\n    is_valid_token,\n)\nfrom ..models import (\n    Account,\n    AccountTiktok,\n    Comment,\n    DataResponse,\n    Detail,\n    DetailTikTok,\n    GeneralSearch,\n    Live,\n    LiveSearch,\n    LiveTikTok,\n    Mix,\n    MixTikTok,\n    Reply,\n    Settings,\n    ShortUrl,\n    UrlResponse,\n    UserSearch,\n    VideoSearch,\n)\nfrom ..translation import _\nfrom .main_terminal import TikTok\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from ..manager import Database\n\n__all__ = [\"APIServer\"]\n\n\ndef token_dependency(token: str = Header(None)):\n    if not is_valid_token(token):\n        raise HTTPException(\n            status_code=403,\n            detail=_(\"验证失败！\"),\n        )\n\n\nclass APIServer(TikTok):\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n        database: \"Database\",\n        server_mode: bool = True,\n    ):\n        super().__init__(\n            parameter,\n            database,\n            server_mode,\n        )\n        self.server = None\n\n    async def handle_redirect(self, text: str, proxy: str = None) -> str:\n        return await self.links.run(\n            text,\n            \"\",\n            proxy,\n        )\n\n    async def handle_redirect_tiktok(self, text: str, proxy: str = None) -> str:\n        return await self.links_tiktok.run(\n            text,\n            \"\",\n            proxy,\n        )\n\n    async def run_server(\n        self,\n        host=SERVER_HOST,\n        port=SERVER_PORT,\n        log_level=\"info\",\n    ):\n        self.server = FastAPI(\n            debug=VERSION_BETA,\n            title=\"DouK-Downloader\",\n            version=__VERSION__,\n        )\n        self.setup_routes()\n        config = Config(\n            self.server,\n            host=host,\n            port=port,\n            log_level=log_level,\n        )\n        server = Server(config)\n        await server.serve()\n\n    def setup_routes(self):\n        @self.server.get(\n            \"/\",\n            summary=_(\"访问项目 GitHub 仓库\"),\n            description=_(\"重定向至项目 GitHub 仓库主页\"),\n            tags=[_(\"项目\")],\n        )\n        async def index():\n            return RedirectResponse(url=REPOSITORY)\n\n        @self.server.get(\n            \"/token\",\n            summary=_(\"测试令牌有效性\"),\n            description=_(\n                dedent(\"\"\"\n                项目默认无需令牌；公开部署时，建议设置令牌以防止恶意请求！\n                \n                令牌设置位置：`src/custom/function.py` - `is_valid_token()`\n                \"\"\")\n            ),\n            tags=[_(\"项目\")],\n            response_model=DataResponse,\n        )\n        async def handle_test(token: str = Depends(token_dependency)):\n            return DataResponse(\n                message=_(\"验证成功！\"),\n                data=None,\n                params=None,\n            )\n\n        @self.server.post(\n            \"/settings\",\n            summary=_(\"更新项目全局配置\"),\n            description=_(\n                dedent(\"\"\"\n                更新项目配置文件 settings.json\n                \n                仅需传入需要更新的配置参数\n                \n                返回更新后的全部配置参数\n                \"\"\")\n            ),\n            tags=[_(\"配置\")],\n            response_model=Settings,\n        )\n        async def handle_settings(\n            extract: Settings, token: str = Depends(token_dependency)\n        ):\n            await self.parameter.set_settings_data(extract.model_dump())\n            return Settings(**self.parameter.get_settings_data())\n\n        @self.server.get(\n            \"/settings\",\n            summary=_(\"获取项目全局配置\"),\n            description=_(\"返回项目全部配置参数\"),\n            tags=[_(\"配置\")],\n            response_model=Settings,\n        )\n        async def get_settings(token: str = Depends(token_dependency)):\n            return Settings(**self.parameter.get_settings_data())\n\n        @self.server.post(\n            \"/douyin/share\",\n            summary=_(\"获取分享链接重定向的完整链接\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **text**: 包含分享链接的字符串；必需参数\n                - **proxy**: 代理；可选参数\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=UrlResponse,\n        )\n        async def handle_share(\n            extract: ShortUrl, token: str = Depends(token_dependency)\n        ):\n            if url := await self.handle_redirect(extract.text, extract.proxy):\n                return UrlResponse(\n                    message=_(\"请求链接成功！\"),\n                    url=url,\n                    params=extract.model_dump(),\n                )\n            return UrlResponse(\n                message=_(\"请求链接失败！\"),\n                url=None,\n                params=extract.model_dump(),\n            )\n\n        @self.server.post(\n            \"/douyin/detail\",\n            summary=_(\"获取单个作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **detail_id**: 抖音作品 ID；必需参数\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_detail(\n            extract: Detail, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_detail(extract, False)\n\n        @self.server.post(\n            \"/douyin/account\",\n            summary=_(\"获取账号作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **sec_user_id**: 抖音账号 sec_uid；必需参数\n                - **tab**: 账号页面类型；可选参数，默认值：`post`\n                - **earliest**: 作品最早发布日期；可选参数\n                - **latest**: 作品最晚发布日期；可选参数\n                - **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_account(\n            extract: Account, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_account(extract, False)\n\n        @self.server.post(\n            \"/douyin/mix\",\n            summary=_(\"获取合集作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **mix_id**: 抖音合集 ID\n                - **detail_id**: 属于合集的抖音作品 ID\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                \n                **`mix_id` 和 `detail_id` 二选一，只需传入其中之一即可**\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_mix(extract: Mix, token: str = Depends(token_dependency)):\n            is_mix, id_ = self.generate_mix_params(\n                extract.mix_id,\n                extract.detail_id,\n            )\n            if not isinstance(is_mix, bool):\n                return DataResponse(\n                    message=_(\"参数错误！\"),\n                    data=None,\n                    params=extract.model_dump(),\n                )\n            if data := await self.deal_mix_detail(\n                is_mix,\n                id_,\n                api=True,\n                source=extract.source,\n                cookie=extract.cookie,\n                proxy=extract.proxy,\n                cursor=extract.cursor,\n                count=extract.count,\n            ):\n                return self.success_response(extract, data)\n            return self.failed_response(extract)\n\n        @self.server.post(\n            \"/douyin/live\",\n            summary=_(\"获取直播数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **web_rid**: 抖音直播 web_rid\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_live(extract: Live, token: str = Depends(token_dependency)):\n            # if self.check_live_params(\n            #     extract.web_rid,\n            #     extract.room_id,\n            #     extract.sec_user_id,\n            # ):\n            #     if data := await self.handle_live(\n            #         extract,\n            #     ):\n            #         return self.success_response(extract, data[0])\n            #     return self.failed_response(extract)\n            # return DataResponse(\n            #     message=_(\"参数错误！\"),\n            #     data=None,\n            #     params=extract.model_dump(),\n            # )\n            if data := await self.handle_live(\n                extract,\n            ):\n                return self.success_response(extract, data[0])\n            return self.failed_response(extract)\n\n        @self.server.post(\n            \"/douyin/comment\",\n            summary=_(\"获取作品评论数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **detail_id**: 抖音作品 ID；必需参数\n                - **pages**: 最大请求次数；可选参数\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                - **count_reply**: 可选参数\n                - **reply**: 可选参数，默认值：False\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_comment(\n            extract: Comment, token: str = Depends(token_dependency)\n        ):\n            if data := await self.comment_handle_single(\n                extract.detail_id,\n                cookie=extract.cookie,\n                proxy=extract.proxy,\n                source=extract.source,\n                pages=extract.pages,\n                cursor=extract.cursor,\n                count=extract.count,\n                count_reply=extract.count_reply,\n                reply=extract.reply,\n            ):\n                return self.success_response(extract, data)\n            return self.failed_response(extract)\n\n        @self.server.post(\n            \"/douyin/reply\",\n            summary=_(\"获取评论回复数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **detail_id**: 抖音作品 ID；必需参数\n                - **comment_id**: 评论 ID；必需参数\n                - **pages**: 最大请求次数；可选参数\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_reply(extract: Reply, token: str = Depends(token_dependency)):\n            if data := await self.reply_handle(\n                extract.detail_id,\n                extract.comment_id,\n                cookie=extract.cookie,\n                proxy=extract.proxy,\n                pages=extract.pages,\n                cursor=extract.cursor,\n                count=extract.count,\n                source=extract.source,\n            ):\n                return self.success_response(extract, data)\n            return self.failed_response(extract)\n\n        @self.server.post(\n            \"/douyin/search/general\",\n            summary=_(\"获取综合搜索数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **keyword**: 关键词；必需参数\n                - **offset**: 起始页码；可选参数\n                - **count**: 数据数量；可选参数\n                - **pages**: 总页数；可选参数\n                - **sort_type**: 排序依据；可选参数\n                - **publish_time**: 发布时间；可选参数\n                - **duration**: 视频时长；可选参数\n                - **search_range**: 搜索范围；可选参数\n                - **content_type**: 内容形式；可选参数\n                \n                **部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_search_general(\n            extract: GeneralSearch, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_search(extract)\n\n        @self.server.post(\n            \"/douyin/search/video\",\n            summary=_(\"获取视频搜索数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **keyword**: 关键词；必需参数\n                - **offset**: 起始页码；可选参数\n                - **count**: 数据数量；可选参数\n                - **pages**: 总页数；可选参数\n                - **sort_type**: 排序依据；可选参数\n                - **publish_time**: 发布时间；可选参数\n                - **duration**: 视频时长；可选参数\n                - **search_range**: 搜索范围；可选参数\n                \n                **部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_search_video(\n            extract: VideoSearch, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_search(extract)\n\n        @self.server.post(\n            \"/douyin/search/user\",\n            summary=_(\"获取用户搜索数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **keyword**: 关键词；必需参数\n                - **offset**: 起始页码；可选参数\n                - **count**: 数据数量；可选参数\n                - **pages**: 总页数；可选参数\n                - **douyin_user_fans**: 粉丝数量；可选参数\n                - **douyin_user_type**: 用户类型；可选参数\n                \n                **部分参数传入规则请查阅文档**: [参数含义](https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation#%E9%87%87%E9%9B%86%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E6%95%B0%E6%8D%AE%E6%8A%96%E9%9F%B3)\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_search_user(\n            extract: UserSearch, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_search(extract)\n\n        @self.server.post(\n            \"/douyin/search/live\",\n            summary=_(\"获取直播搜索数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n                \n                - **cookie**: 抖音 Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **keyword**: 关键词；必需参数\n                - **offset**: 起始页码；可选参数\n                - **count**: 数据数量；可选参数\n                - **pages**: 总页数；可选参数\n                \"\"\")\n            ),\n            tags=[_(\"抖音\")],\n            response_model=DataResponse,\n        )\n        async def handle_search_live(\n            extract: LiveSearch, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_search(extract)\n\n        @self.server.post(\n            \"/tiktok/share\",\n            summary=_(\"获取分享链接重定向的完整链接\"),\n            description=_(\n                dedent(\"\"\"\n            **参数**:\n\n            - **text**: 包含分享链接的字符串；必需参数\n            - **proxy**: 代理；可选参数\n            \"\"\")\n            ),\n            tags=[\"TikTok\"],\n            response_model=UrlResponse,\n        )\n        async def handle_share_tiktok(\n            extract: ShortUrl, token: str = Depends(token_dependency)\n        ):\n            if url := await self.handle_redirect_tiktok(extract.text, extract.proxy):\n                return UrlResponse(\n                    message=_(\"请求链接成功！\"),\n                    url=url,\n                    params=extract.model_dump(),\n                )\n            return UrlResponse(\n                message=_(\"请求链接失败！\"),\n                url=None,\n                params=extract.model_dump(),\n            )\n\n        @self.server.post(\n            \"/tiktok/detail\",\n            summary=_(\"获取单个作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n\n                - **cookie**: TikTok Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **detail_id**: TikTok 作品 ID；必需参数\n                \"\"\")\n            ),\n            tags=[\"TikTok\"],\n            response_model=DataResponse,\n        )\n        async def handle_detail_tiktok(\n            extract: DetailTikTok, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_detail(extract, True)\n\n        @self.server.post(\n            \"/tiktok/account\",\n            summary=_(\"获取账号作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n\n                - **cookie**: TikTok Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **sec_user_id**: TikTok 账号 secUid；必需参数\n                - **tab**: 账号页面类型；可选参数，默认值：`post`\n                - **earliest**: 作品最早发布日期；可选参数\n                - **latest**: 作品最晚发布日期；可选参数\n                - **pages**: 最大请求次数，仅对请求账号喜欢页数据有效；可选参数\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                \"\"\")\n            ),\n            tags=[\"TikTok\"],\n            response_model=DataResponse,\n        )\n        async def handle_account_tiktok(\n            extract: AccountTiktok, token: str = Depends(token_dependency)\n        ):\n            return await self.handle_account(extract, True)\n\n        @self.server.post(\n            \"/tiktok/mix\",\n            summary=_(\"获取合辑作品数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n\n                - **cookie**: TikTok Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **mix_id**: TikTok 合集 ID；必需参数\n                - **cursor**: 可选参数\n                - **count**: 可选参数\n                \"\"\")\n            ),\n            tags=[\"TikTok\"],\n            response_model=DataResponse,\n        )\n        async def handle_mix_tiktok(\n            extract: MixTikTok, token: str = Depends(token_dependency)\n        ):\n            if data := await self.deal_mix_detail(\n                True,\n                extract.mix_id,\n                api=True,\n                source=extract.source,\n                cookie=extract.cookie,\n                proxy=extract.proxy,\n                cursor=extract.cursor,\n                count=extract.count,\n            ):\n                return self.success_response(extract, data)\n            return self.failed_response(extract)\n\n        @self.server.post(\n            \"/tiktok/live\",\n            summary=_(\"获取直播数据\"),\n            description=_(\n                dedent(\"\"\"\n                **参数**:\n\n                - **cookie**: TikTok Cookie；可选参数\n                - **proxy**: 代理；可选参数\n                - **source**: 是否返回原始响应数据；可选参数，默认值：False\n                - **room_id**: TikTok 直播 room_id；必需参数\n                \"\"\")\n            ),\n            tags=[\"TikTok\"],\n            response_model=DataResponse,\n        )\n        async def handle_live_tiktok(\n            extract: Live, token: str = Depends(token_dependency)\n        ):\n            if data := await self.handle_live(\n                extract,\n                True,\n            ):\n                return self.success_response(extract, data[0])\n            return self.failed_response(extract)\n\n    async def handle_search(self, extract):\n        if isinstance(\n            data := await self.deal_search_data(\n                extract,\n                extract.source,\n            ),\n            list,\n        ):\n            return self.success_response(\n                extract,\n                *(data, None) if any(data) else (None, _(\"搜索结果为空！\")),\n            )\n        return self.failed_response(extract)\n\n    async def handle_detail(\n        self,\n        extract: Detail | DetailTikTok,\n        tiktok=False,\n    ):\n        root, params, logger = self.record.run(self.parameter)\n        async with logger(root, console=self.console, **params) as record:\n            if data := await self._handle_detail(\n                [extract.detail_id],\n                tiktok,\n                record,\n                True,\n                extract.source,\n                extract.cookie,\n                extract.proxy,\n            ):\n                return self.success_response(extract, data[0])\n            return self.failed_response(extract)\n\n    async def handle_account(\n        self,\n        extract: Account | AccountTiktok,\n        tiktok=False,\n    ):\n        if data := await self.deal_account_detail(\n            0,\n            extract.sec_user_id,\n            tab=extract.tab,\n            earliest=extract.earliest,\n            latest=extract.latest,\n            pages=extract.pages,\n            api=True,\n            source=extract.source,\n            cookie=extract.cookie,\n            proxy=extract.proxy,\n            tiktok=tiktok,\n            cursor=extract.cursor,\n            count=extract.count,\n        ):\n            return self.success_response(extract, data)\n        return self.failed_response(extract)\n\n    @staticmethod\n    def success_response(\n        extract,\n        data: dict | list[dict],\n        message: str = None,\n    ):\n        return DataResponse(\n            message=message or _(\"获取数据成功！\"),\n            data=data,\n            params=extract.model_dump(),\n        )\n\n    @staticmethod\n    def failed_response(\n        extract,\n        message: str = None,\n    ):\n        return DataResponse(\n            message=message or _(\"获取数据失败！\"),\n            data=None,\n            params=extract.model_dump(),\n        )\n\n    @staticmethod\n    def generate_mix_params(mix_id: str = None, detail_id: str = None):\n        if mix_id:\n            return True, mix_id\n        return (False, detail_id) if detail_id else (None, None)\n\n    @staticmethod\n    def check_live_params(\n        web_rid: str = None,\n        room_id: str = None,\n        sec_user_id: str = None,\n    ) -> bool:\n        return bool(web_rid or room_id and sec_user_id)\n\n    async def handle_live(self, extract: Live | LiveTikTok, tiktok=False):\n        if tiktok:\n            data = await self.get_live_data_tiktok(\n                extract.room_id,\n                extract.cookie,\n                extract.proxy,\n            )\n        else:\n            data = await self.get_live_data(\n                extract.web_rid,\n                # extract.room_id,\n                # extract.sec_user_id,\n                cookie=extract.cookie,\n                proxy=extract.proxy,\n            )\n        if extract.source:\n            return [data]\n        return await self.extractor.run(\n            [data],\n            None,\n            \"live\",\n            tiktok=tiktok,\n        )\n"
  },
  {
    "path": "src/application/main_terminal.py",
    "content": "from datetime import date, datetime\nfrom pathlib import Path\nfrom platform import system\nfrom time import time\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING, Any, Callable, Union\n\nfrom pydantic import ValidationError\n\n# from ..custom import failure_handling\nfrom ..custom import suspend\nfrom ..downloader import Downloader\nfrom ..extract import Extractor\nfrom ..interface import (\n    API,\n    Account,\n    AccountTikTok,\n    Collection,\n    # CommentTikTok,\n    Collects,\n    CollectsDetail,\n    CollectsMix,\n    # CollectsSeries,\n    CollectsMusic,\n    Comment,\n    Detail,\n    DetailTikTok,\n    HashTag,\n    Hot,\n    Info,\n    InfoTikTok,\n    Live,\n    LiveTikTok,\n    Mix,\n    MixTikTok,\n    Reply,\n    Search,\n    User,\n)\nfrom ..link import Extractor as LinkExtractor\nfrom ..link import ExtractorTikTok\nfrom ..manager import Cache\nfrom ..models import (\n    GeneralSearch,\n    LiveSearch,\n    UserSearch,\n    VideoSearch,\n)\nfrom ..module import DetailTikTokExtractor, DetailTikTokUnofficial\nfrom ..storage import RecordManager\nfrom ..tools import DownloaderError, choose, safe_pop\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from pydantic import BaseModel\n\n    from ..config import Parameter\n    from ..manager import Database\n\n__all__ = [\n    \"TikTok\",\n]\n\n\ndef check_storage_format(function):\n    async def inner(self, *args, **kwargs):\n        if self.parameter.storage_format:\n            return await function(self, *args, **kwargs)\n        self.console.warning(\n            _(\n                \"未设置 storage_format 参数，无法正常使用该功能，详细说明请查阅项目文档！\"\n            ),\n        )\n\n    return inner\n\n\ndef check_cookie_state(tiktok=False):\n    def check_cookie(function):\n        async def inner(self, *args, **kwargs):\n            if tiktok:\n                params = self.parameter.cookie_tiktok_state\n                tip = \"TikTok Cookie\"\n            else:\n                params = self.parameter.cookie_state\n                tip = _(\"抖音 Cookie\")\n            if params:\n                return await function(self, *args, **kwargs)\n            self.console.warning(\n                _(\"{tip} 未登录，无法使用该功能，详细说明请查阅项目文档！\").format(\n                    tip=tip\n                ),\n            )\n\n        return inner\n\n    return check_cookie\n\n\nclass TikTok:\n    ENCODE = \"UTF-8-SIG\" if system() == \"Windows\" else \"UTF-8\"\n\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n        database: \"Database\",\n        server_mode: bool = False,\n    ):\n        self.run_command = None\n        self.parameter = parameter\n        self.database = database\n        self.console = parameter.console\n        self.logger = parameter.logger\n        API.init_progress_object(\n            server_mode,\n        )\n        self.links = LinkExtractor(parameter)\n        self.links_tiktok = ExtractorTikTok(parameter)\n        self.downloader = Downloader(\n            parameter,\n            server_mode,\n        )\n        self.extractor = Extractor(parameter)\n        self.storage = bool(parameter.storage_format)\n        self.record = RecordManager()\n        self.settings = parameter.settings\n        self.accounts = parameter.accounts_urls\n        self.accounts_tiktok = parameter.accounts_urls_tiktok\n        self.mix = parameter.mix_urls\n        self.mix_tiktok = parameter.mix_urls_tiktok\n        self.owner = parameter.owner_url\n        self.owner_tiktok = parameter.owner_url_tiktok\n        self.running = True\n        self.ffmpeg = parameter.ffmpeg.state\n        self.cache = Cache(\n            parameter,\n            self.database,\n            \"mark\" in parameter.name_format,\n            \"nickname\" in parameter.name_format,\n        )\n        self.__function = (\n            (\n                _(\"批量下载账号作品(抖音)\"),\n                self.account_acquisition_interactive,\n            ),\n            (\n                _(\"批量下载链接作品(抖音)\"),\n                self.detail_interactive,\n            ),\n            (\n                _(\"获取直播拉流地址(抖音)\"),\n                self.live_interactive,\n            ),\n            (\n                _(\"采集作品评论数据(抖音)\"),\n                self.comment_interactive,\n            ),\n            (\n                _(\"批量下载合集作品(抖音)\"),\n                self.mix_interactive,\n            ),\n            (\n                _(\"采集账号详细数据(抖音)\"),\n                self.user_interactive,\n            ),\n            (\n                _(\"采集搜索结果数据(抖音)\"),\n                self.search_interactive,\n            ),\n            (\n                _(\"采集抖音热榜数据(抖音)\"),\n                self.hot_interactive,\n            ),\n            # (_(\"批量下载话题作品(抖音)\"),),\n            (\n                _(\"批量下载收藏作品(抖音)\"),\n                self.collection_interactive,\n            ),\n            (\n                _(\"批量下载收藏音乐(抖音)\"),\n                self.collection_music_interactive,\n            ),\n            # (_(\"批量下载收藏短剧(抖音)\"),),\n            (\n                _(\"批量下载收藏夹作品(抖音)\"),\n                self.collects_interactive,\n            ),\n            (\n                _(\"批量下载账号作品(TikTok)\"),\n                self.account_acquisition_interactive_tiktok,\n            ),\n            (\n                _(\"批量下载链接作品(TikTok)\"),\n                self.detail_interactive_tiktok,\n            ),\n            (\n                _(\"批量下载合集作品(TikTok)\"),\n                self.mix_interactive_tiktok,\n            ),\n            (\n                _(\"获取直播拉流地址(TikTok)\"),\n                self.live_interactive_tiktok,\n            ),\n            # (_(\"采集作品评论数据(TikTok)\"), self.comment_interactive_tiktok,),\n            # (\n            #     _(\"批量下载视频原画(TikTok)\"),\n            #     self.detail_interactive_tiktok_unofficial,\n            # ),\n        )\n        self.__function_account = (\n            (_(\"使用 accounts_urls 参数的账号链接(推荐)\"), self.account_detail_batch),\n            (_(\"手动输入待采集的账号链接\"), self.account_detail_inquire),\n            (_(\"从文本文档读取待采集的账号链接\"), self.account_detail_txt),\n        )\n        self.__function_account_tiktok = (\n            (\n                _(\"使用 accounts_urls_tiktok 参数的账号链接(推荐)\"),\n                self.account_detail_batch_tiktok,\n            ),\n            (_(\"手动输入待采集的账号链接\"), self.account_detail_inquire_tiktok),\n            (_(\"从文本文档读取待采集的账号链接\"), self.account_detail_txt_tiktok),\n        )\n        self.__function_mix = (\n            (_(\"使用 mix_urls 参数的合集链接(推荐)\"), self.mix_batch),\n            (_(\"获取当前账号收藏合集列表\"), self.mix_collection),\n            (_(\"手动输入待采集的合集/作品链接\"), self.mix_inquire),\n            (_(\"从文本文档读取待采集的合集/作品链接\"), self.mix_txt),\n        )\n        self.__function_mix_tiktok = (\n            (_(\"使用 mix_urls_tiktok 参数的合集链接(推荐)\"), self.mix_batch_tiktok),\n            (_(\"手动输入待采集的合集/作品链接\"), self.mix_inquire_tiktok),\n            (_(\"从文本文档读取待采集的合集/作品链接\"), self.mix_txt_tiktok),\n        )\n        self.__function_user = (\n            (_(\"使用 accounts_urls 参数的账号链接\"), self.user_batch),\n            (_(\"手动输入待采集的账号链接\"), self.user_inquire),\n            (_(\"从文本文档读取待采集的账号链接\"), self.user_txt),\n        )\n        self.__function_detail = (\n            (_(\"手动输入待采集的作品链接\"), self.__detail_inquire),\n            (_(\"从文本文档读取待采集的作品链接\"), self.__detail_txt),\n        )\n        self.__function_detail_tiktok = (\n            (_(\"手动输入待采集的作品链接\"), self.__detail_inquire_tiktok),\n            (_(\"从文本文档读取待采集的作品链接\"), self.__detail_txt_tiktok),\n        )\n        self.__function_detail_tiktok_unofficial = (\n            (_(\"手动输入待采集的作品链接\"), self.__detail_inquire_tiktok_unofficial),\n            (_(\"从文本文档读取待采集的作品链接\"), self.__detail_txt_tiktok_unofficial),\n        )\n        self.__function_comment = (\n            (_(\"手动输入待采集的作品链接\"), self.__comment_inquire),\n            (_(\"从文本文档读取待采集的作品链接\"), self.__comment_txt),\n        )\n        self.__function_comment_tiktok = (\n            (_(\"手动输入待采集的作品链接\"), self.__comment_inquire_tiktok),\n            # (_(\"从文本文档读取待采集的作品链接\"), self.__comment_txt_tiktok),\n        )\n        self.__function_search = (\n            (\n                _(\"综合搜索数据采集\"),\n                self._search_interactive_general,\n            ),\n            (\n                _(\"视频搜索数据采集\"),\n                self._search_interactive_video,\n            ),\n            (\n                _(\"用户搜索数据采集\"),\n                self._search_interactive_user,\n            ),\n            (\n                _(\"直播搜索数据采集\"),\n                self._search_interactive_live,\n            ),\n        )\n\n    def _inquire_input(\n        self,\n        tip: str = \"\",\n        problem: str = \"\",\n    ) -> str:\n        text = self.console.input(problem or _(\"请输入{tip}链接: \").format(tip=tip))\n        if not text:\n            return \"\"\n        elif text.upper() == \"Q\":\n            self.running = False\n            return \"\"\n        return text\n\n    async def account_acquisition_interactive_tiktok(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择账号链接来源\"),\n            function=self.__function_account_tiktok,\n            select=select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载账号作品(TikTok)模式\"))\n\n    def __summarize_results(self, count: SimpleNamespace, name=_(\"账号\")):\n        time_ = time() - count.time\n        self.logger.info(\n            _(\n                \"程序共处理 {0} 个{1}，成功 {2} 个，失败 {3} 个，耗时 {4} 分钟 {5} 秒\"\n            ).format(\n                count.success + count.failed,\n                name,\n                count.success,\n                count.failed,\n                int(time_ // 60),\n                int(time_ % 60),\n            )\n        )\n\n    async def account_acquisition_interactive(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择账号链接来源\"),\n            function=self.__function_account,\n            select=select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载账号作品(抖音)模式\"))\n\n    async def __secondary_menu(\n        self,\n        problem=_(\"请选择账号链接来源\"),\n        function=...,\n        select: str | int = ...,\n        *args,\n        **kwargs,\n    ):\n        if not select:\n            select = choose(\n                problem,\n                [i[0] for i in function],\n                self.console,\n            )\n        if select.upper() == \"Q\":\n            self.running = False\n        try:\n            n = int(select) - 1\n        except ValueError:\n            return\n        if n in range(len(function)):\n            await function[n][1](\n                *args,\n                **kwargs,\n            )\n\n    async def account_detail_batch(\n        self,\n        *args,\n    ):\n        await self.__account_detail_batch(\n            self.accounts,\n            \"accounts_urls\",\n            False,\n        )\n\n    async def account_detail_batch_tiktok(\n        self,\n        *args,\n    ):\n        await self.__account_detail_batch(\n            self.accounts_tiktok,\n            \"accounts_urls_tiktok\",\n            True,\n        )\n\n    async def __account_detail_batch(\n        self,\n        accounts: list[SimpleNamespace],\n        params_name: str,\n        tiktok: bool,\n    ) -> None:\n        count = SimpleNamespace(time=time(), success=0, failed=0)\n        self.logger.info(\n            _(\"共有 {count} 个账号的作品等待下载\").format(count=len(accounts))\n        )\n        for index, data in enumerate(accounts, start=1):\n            if not (\n                sec_user_id := await self.check_sec_user_id(\n                    data.url,\n                    tiktok,\n                )\n            ):\n                self.logger.warning(\n                    _(\n                        \"配置文件 {name} 参数的 url {url} 提取 sec_user_id 失败，错误配置：{data}\"\n                    ).format(\n                        name=params_name,\n                        url=data.url,\n                        data=vars(data),\n                    )\n                )\n                count.failed += 1\n                continue\n            if not await self.deal_account_detail(\n                index,\n                **vars(data) | {\"sec_user_id\": sec_user_id},\n                tiktok=tiktok,\n            ):\n                count.failed += 1\n                continue\n            # break  # 调试代码\n            count.success += 1\n            if index != len(accounts):\n                await suspend(index, self.console)\n        self.__summarize_results(\n            count,\n            _(\"账号\"),\n        )\n\n    async def check_sec_user_id(\n        self,\n        sec_user_id: str,\n        tiktok=False,\n    ) -> str:\n        match tiktok:\n            case True:\n                sec_user_id = await self.links_tiktok.run(sec_user_id, \"user\")\n            case False:\n                sec_user_id = await self.links.run(sec_user_id, \"user\")\n        return sec_user_id[0] if len(sec_user_id) > 0 else \"\"\n\n    async def account_detail_inquire(\n        self,\n        *args,\n    ):\n        while url := self._inquire_input(_(\"账号主页\")):\n            links = await self.links.run(url, \"user\")\n            if not links:\n                self.logger.warning(\n                    _(\"{url} 提取账号 sec_user_id 失败\").format(url=url)\n                )\n                continue\n            await self.__account_detail_handle(\n                links,\n                False,\n                *args,\n            )\n\n    async def account_detail_inquire_tiktok(\n        self,\n        *args,\n    ):\n        while url := self._inquire_input(_(\"账号主页\")):\n            links = await self.links_tiktok.run(url, \"user\")\n            if not links:\n                self.logger.warning(\n                    _(\"{url} 提取账号 sec_user_id 失败\").format(url=url)\n                )\n                continue\n            await self.__account_detail_handle(\n                links,\n                True,\n                *args,\n            )\n\n    async def account_detail_txt(\n        self,\n    ):\n        await self._read_from_txt(\n            tiktok=False,\n            type_=\"user\",\n            error=_(\"从文本文档提取账号 sec_user_id 失败\"),\n            callback=self.__account_detail_handle,\n        )\n\n    async def _read_from_txt(\n        self,\n        tiktok=False,\n        type_: str = ...,\n        error: str = ...,\n        callback: Callable = ...,\n        *args,\n        **kwargs,\n    ):\n        if not (url := self.txt_inquire()):\n            return\n        link_obj = self.links_tiktok if tiktok else self.links\n        links = await link_obj.run(\n            url,\n            type_,\n        )\n        if not links or not isinstance(links[0], bool | None):\n            links = [links]\n        if not links[-1]:\n            self.logger.warning(error)\n            return\n        await callback(\n            *links,\n            *args,\n            tiktok=tiktok,\n            **kwargs,\n        )\n\n    async def account_detail_txt_tiktok(\n        self,\n    ):\n        await self._read_from_txt(\n            tiktok=True,\n            type_=\"user\",\n            error=_(\"从文本文档提取账号 sec_user_id 失败\"),\n            callback=self.__account_detail_handle,\n        )\n\n    async def __account_detail_handle(\n        self,\n        links: list[str],\n        tiktok=False,\n        *args,\n        **kwargs,\n    ):\n        count = SimpleNamespace(time=time(), success=0, failed=0)\n        for index, sec in enumerate(links, start=1):\n            if not await self.deal_account_detail(\n                index,\n                sec_user_id=sec,\n                tiktok=tiktok,\n                *args,\n                **kwargs,\n            ):\n                count.failed += 1\n                continue\n            count.success += 1\n            if index != len(links):\n                await suspend(index, self.console)\n        self.__summarize_results(\n            count,\n            _(\"账号\"),\n        )\n\n    async def deal_account_detail(\n        self,\n        index: int,\n        sec_user_id: str,\n        mark=\"\",\n        tab=\"post\",\n        earliest=\"\",\n        latest=\"\",\n        pages: int = None,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n        tiktok=False,\n        *args,\n        **kwargs,\n    ):\n        self.logger.info(\n            _(\"开始处理第 {index} 个账号\").format(index=index)\n            if index\n            else _(\"开始处理账号\")\n        )\n        if api:\n            info = None\n        elif not (\n            info := await self.get_user_info_data(\n                tiktok,\n                cookie,\n                proxy,\n                sec_user_id=sec_user_id,\n            )\n        ):\n            self.logger.info(\n                _(\"{sec_user_id} 获取账号信息失败，请检查 Cookie 登录状态！\").format(\n                    sec_user_id=sec_user_id\n                )\n            )\n            if tab in {\n                \"favorite\",\n                \"collection\",\n            }:\n                return None\n            self.logger.info(\n                _(\n                    \"如果账号发布作品均为共创作品且该账号均不是作品作者时，请配置已登录的 Cookie 后重新运行程序，其余情况请无视该提示！\"\n                )\n            )\n        acquirer = self._get_account_data_tiktok if tiktok else self._get_account_data\n        account_data, earliest, latest = await acquirer(\n            cookie=cookie,\n            proxy=proxy,\n            sec_user_id=sec_user_id,\n            tab=tab,\n            earliest=earliest,\n            latest=latest,\n            pages=pages,\n            **kwargs,\n        )\n        if not any(account_data):\n            return None\n        if source:\n            return self.extractor.source_date_filter(\n                account_data,\n                earliest,\n                latest,\n                tiktok,\n            )\n        return await self._batch_process_detail(\n            account_data,\n            user_id=sec_user_id,\n            mark=mark,\n            api=api,\n            earliest=earliest,\n            latest=latest,\n            tiktok=tiktok,\n            mode=tab,\n            info=info,\n        )\n\n    async def _get_account_data(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        sec_user_id: Union[str] = ...,\n        tab: str = \"post\",\n        earliest: str = \"\",\n        latest: str = \"\",\n        pages: int = None,\n        *args,\n        **kwargs,\n    ):\n        return await Account(\n            self.parameter,\n            cookie,\n            proxy,\n            sec_user_id,\n            tab,\n            earliest,\n            latest,\n            pages,\n        ).run()\n\n    async def _get_account_data_tiktok(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        sec_user_id: Union[str] = ...,\n        tab: str = \"post\",\n        earliest: str = \"\",\n        latest: str = \"\",\n        pages: int = None,\n        *args,\n        **kwargs,\n    ):\n        return await AccountTikTok(\n            self.parameter,\n            cookie,\n            proxy,\n            sec_user_id,\n            tab,\n            earliest,\n            latest,\n            pages,\n        ).run()\n\n    async def get_user_info_data(\n        self,\n        tiktok=False,\n        cookie: str = None,\n        proxy: str = None,\n        unique_id: Union[str] = \"\",\n        sec_user_id: Union[str] = \"\",\n    ):\n        return (\n            await self._get_info_data_tiktok(\n                cookie,\n                proxy,\n                unique_id,\n                sec_user_id,\n            )\n            if tiktok\n            else await self._get_info_data(\n                cookie,\n                proxy,\n                sec_user_id,\n            )\n        )\n\n    async def _get_info_data(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        sec_user_id: Union[str, list[str]] = ...,\n    ):\n        return await Info(\n            self.parameter,\n            cookie,\n            proxy,\n            sec_user_id,\n        ).run()\n\n    async def _get_info_data_tiktok(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        unique_id: Union[str] = \"\",\n        sec_user_id: Union[str] = \"\",\n    ):\n        return await InfoTikTok(\n            self.parameter,\n            cookie,\n            proxy,\n            unique_id,\n            sec_user_id,\n        ).run()\n\n    async def _batch_process_detail(\n        self,\n        data: list[dict],\n        api: bool = False,\n        earliest: date = None,\n        latest: date = None,\n        tiktok: bool = False,\n        info: dict = None,\n        mode: str = \"\",\n        mark: str = \"\",\n        user_id: str = \"\",\n        mix_id: str = \"\",\n        mix_title: str = \"\",\n        collect_id: str = \"\",\n        collect_name: str = \"\",\n    ):\n        self.logger.info(_(\"开始提取作品数据\"))\n        id_, name, mark = self.extractor.preprocessing_data(\n            info or data,\n            tiktok,\n            mode,\n            mark,\n            user_id,\n            mix_id,\n            mix_title,\n            collect_id,\n            collect_name,\n        )\n        if not api and not all((id_, name, mark)):\n            self.logger.error(_(\"提取账号或合集信息发生错误！\"))\n            return False\n        self.__display_extracted_information(\n            id_,\n            name,\n            mark,\n        )\n        prefix = self._generate_prefix(mode)\n        suffix = self._generate_suffix(mode)\n        old_mark = (\n            f\"{m['MARK']}_{suffix}\" if (m := await self.cache.has_cache(id_)) else None\n        )\n        root, params, logger = self.record.run(\n            self.parameter,\n            blank=api,\n        )\n        async with logger(\n            root,\n            name=f\"{prefix}{id_}_{mark}_{suffix}\",\n            old=old_mark,\n            console=self.console,\n            **params,\n        ) as recorder:\n            data = await self.extractor.run(\n                data,\n                recorder,\n                type_=\"batch\",\n                tiktok=tiktok,\n                name=name,\n                mark=mark,\n                earliest=earliest or date(2016, 9, 20),\n                latest=latest or date.today(),\n                same=mode\n                in {\n                    \"post\",\n                    \"mix\",\n                },\n            )\n        if api:\n            return data\n        await self.cache.update_cache(\n            self.parameter.folder_mode,\n            prefix,\n            suffix,\n            id_,\n            name,\n            mark,\n        )\n        await self.download_detail_batch(\n            data,\n            tiktok=tiktok,\n            mode=mode,\n            mark=mark,\n            user_id=id_,\n            user_name=name,\n            mix_id=mix_id,\n            mix_title=mix_title,\n            collect_id=collect_id,\n            collect_name=collect_name,\n        )\n        return True\n\n    @staticmethod\n    def _generate_prefix(\n        mode: str,\n    ):\n        match mode:\n            case \"post\" | \"favorite\" | \"collection\":\n                return \"UID\"\n            case \"mix\":\n                return \"MID\"\n            case \"collects\":\n                return \"CID\"\n            case _:\n                raise DownloaderError\n\n    @staticmethod\n    def _generate_suffix(\n        mode: str,\n    ):\n        match mode:\n            case \"post\":\n                return _(\"发布作品\")\n            case \"favorite\":\n                return _(\"喜欢作品\")\n            case \"collection\":\n                return _(\"收藏作品\")\n            case \"mix\":\n                return _(\"合集作品\")\n            case \"collects\":\n                return _(\"收藏夹作品\")\n            case _:\n                raise DownloaderError\n\n    def __display_extracted_information(\n        self,\n        id_: str,\n        name: str,\n        mark: str,\n    ) -> None:\n        self.logger.info(\n            _(\"昵称/标题：{name}；标识：{mark}；ID：{id}\").format(\n                name=name,\n                mark=mark,\n                id=id_,\n            ),\n        )\n\n    async def download_detail_batch(\n        self,\n        data: list[dict],\n        type_: str = \"batch\",\n        tiktok: bool = False,\n        mode: str = \"\",\n        mark: str = \"\",\n        user_id: str = \"\",\n        user_name: str = \"\",\n        mix_id: str = \"\",\n        mix_title: str = \"\",\n        collect_id: str = \"\",\n        collect_name: str = \"\",\n    ):\n        await self.downloader.run(\n            data,\n            type_,\n            tiktok,\n            mode=mode,\n            mark=mark,\n            user_id=user_id,\n            user_name=user_name,\n            mix_id=mix_id,\n            mix_title=mix_title,\n            collect_id=collect_id,\n            collect_name=collect_name,\n        )\n\n    async def detail_interactive(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择作品链接来源\"),\n            self.__function_detail,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载链接作品(抖音)模式\"))\n\n    async def detail_interactive_tiktok(\n        self,\n        select=\"\",\n    ):\n        await self.__detail_secondary_menu(\n            self.__function_detail_tiktok,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载链接作品(TikTok)模式\"))\n\n    async def detail_interactive_tiktok_unofficial(\n        self,\n        select=\"\",\n    ):\n        self.console.warning(\n            _(\"注意：本功能为实验性功能，依赖第三方 API 服务，可能不稳定或存在限制！\")\n        )\n        await self.__detail_secondary_menu(\n            self.__function_detail_tiktok_unofficial,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载视频原画(TikTok)模式\"))\n\n    async def __detail_secondary_menu(self, menu, select=\"\", *args, **kwargs):\n        root, params, logger = self.record.run(self.parameter)\n        async with logger(root, console=self.console, **params) as record:\n            if not select:\n                select = choose(\n                    _(\"请选择作品链接来源\"),\n                    [i[0] for i in menu],\n                    self.console,\n                )\n            if select.upper() == \"Q\":\n                self.running = False\n            try:\n                n = int(select) - 1\n            except ValueError:\n                return\n            if n in range(len(menu)):\n                await menu[n][1](record)\n\n    async def __detail_inquire(\n        self,\n        tiktok=False,\n    ):\n        root, params, logger = self.record.run(self.parameter)\n        link_obj = self.links_tiktok if tiktok else self.links\n        async with logger(root, console=self.console, **params) as record:\n            while url := self._inquire_input(_(\"作品\")):\n                ids = await link_obj.run(url)\n                if not any(ids):\n                    self.logger.warning(_(\"{url} 提取作品 ID 失败\").format(url=url))\n                    continue\n                self.console.print(\n                    _(\"共提取到 {count} 个作品，开始处理！\").format(count=len(ids))\n                )\n                await self._handle_detail(\n                    ids,\n                    tiktok,\n                    record,\n                )\n\n    async def __detail_inquire_tiktok(\n        self,\n        tiktok=True,\n    ):\n        await self.__detail_inquire(\n            tiktok,\n        )\n\n    async def __detail_inquire_tiktok_unofficial(\n        self,\n        *args,\n        **kwargs,\n    ):\n        while url := self._inquire_input(_(\"作品\")):\n            ids = await self.links_tiktok.run(url)\n            if not any(ids):\n                self.logger.warning(_(\"{url} 提取作品 ID 失败\").format(url=url))\n                continue\n            self.console.print(\n                _(\"共提取到 {count} 个作品，开始处理！\").format(count=len(ids))\n            )\n            await self.handle_detail_unofficial(ids)\n\n    async def __detail_txt(\n        self,\n        tiktok=False,\n    ):\n        root, params, logger = self.record.run(self.parameter)\n        async with logger(root, console=self.console, **params) as record:\n            await self._read_from_txt(\n                tiktok,\n                \"detail\",\n                _(\"从文本文档提取作品 ID 失败\"),\n                self._handle_detail,\n                record=record,\n            )\n\n    async def __detail_txt_tiktok(\n        self,\n        tiktok=True,\n    ):\n        await self.__detail_txt(\n            tiktok=tiktok,\n        )\n\n    async def __detail_txt_tiktok_unofficial(\n        self,\n        *args,\n        **kwargs,\n    ):\n        await self._read_from_txt(\n            True,\n            \"detail\",\n            _(\"从文本文档提取作品 ID 失败\"),\n            self.handle_detail_unofficial,\n        )\n\n    async def __read_detail_txt(self):\n        if not (url := self.txt_inquire()):\n            return\n        ids = await self.links.run(url)\n        if not any(ids):\n            self.logger.warning(_(\"从文本文档提取作品 ID 失败\"))\n            return\n        self.console.print(\n            _(\"共提取到 {count} 个作品，开始处理！\").format(count=len(ids))\n        )\n        return ids\n\n    async def _handle_detail(\n        self,\n        ids: list[str],\n        tiktok: bool,\n        record,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        processor = DetailTikTok if tiktok else Detail\n        return await self.__handle_detail(\n            tiktok,\n            processor,\n            ids,\n            record,\n            api=api,\n            source=source,\n            cookie=cookie,\n            proxy=proxy,\n        )\n\n    async def handle_detail_single(\n        self,\n        processor: Callable,\n        cookie: str,\n        proxy: str,\n        detail_id: str,\n    ):\n        return await processor(\n            self.parameter,\n            cookie,\n            proxy,\n            detail_id,\n        ).run()\n\n    async def __handle_detail(\n        self,\n        tiktok: bool,\n        processor: Callable,\n        ids: list[str],\n        record,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        detail_data = [\n            await self.handle_detail_single(\n                processor,\n                cookie,\n                proxy,\n                i,\n            )\n            for i in ids\n        ]\n        if not any(detail_data):\n            return None\n        if source:\n            return detail_data\n        detail_data = await self.extractor.run(\n            detail_data,\n            record,\n            tiktok=tiktok,\n        )\n        if api:\n            return detail_data\n        await self.downloader.run(detail_data, \"detail\", tiktok=tiktok)\n        return self._get_preview_image(detail_data[0])\n\n    @staticmethod\n    def _get_preview_image(data: dict) -> str:\n        if data[\"type\"] == _(\"图集\"):\n            return data[\"downloads\"][0]\n        elif data[\"type\"] == _(\"视频\"):\n            return data[\"static_cover\"]\n        return \"\"\n\n    def _choice_live_quality(\n        self,\n        flv_items: dict,\n        m3u8_items: dict,\n    ) -> tuple | None:\n        if not self.ffmpeg:\n            self.logger.warning(_(\"程序未检测到有效的 ffmpeg，不支持直播下载功能！\"))\n            return None\n        try:\n            choice_ = self.parameter.live_qualities or self.console.input(\n                _(\"请选择下载清晰度(输入清晰度或者对应序号，直接回车代表不下载): \"),\n            )\n            if u := flv_items.get(choice_):\n                return u, m3u8_items.get(choice_)\n            if not 0 <= (i := int(choice_) - 1) < len(flv_items):\n                raise ValueError\n        except ValueError:\n            self.logger.info(_(\"未输入有效的清晰度或者序号，跳过下载！\"))\n            return None\n        return list(flv_items.values())[i], list(m3u8_items.values())[i]\n\n    async def get_live_data(\n        self,\n        web_rid: str = None,\n        room_id: str = None,\n        sec_user_id: str = None,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        return await Live(\n            self.parameter,\n            cookie,\n            proxy,\n            web_rid,\n            room_id,\n            sec_user_id,\n        ).run()\n\n    async def get_live_data_tiktok(\n        self,\n        room_id: str = None,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        return await LiveTikTok(self.parameter, cookie, proxy, room_id).run()\n\n    async def live_interactive(\n        self,\n        *args,\n    ):\n        while url := self._inquire_input(_(\"直播\")):\n            ids = await self.links.run(url, type_=\"live\")\n            live_data = [await self.get_live_data(i) for i in ids]\n            live_data = await self.extractor.run(live_data, None, \"live\")\n            if not [i for i in live_data if i]:\n                self.logger.warning(_(\"获取直播数据失败\"))\n                continue\n            download_tasks = self.show_live_info(live_data)\n            await self.downloader.run(download_tasks, type_=\"live\")\n        self.logger.info(_(\"已退出获取直播拉流地址(抖音)模式\"))\n\n    async def live_interactive_tiktok(\n        self,\n        *args,\n    ):\n        while url := self._inquire_input(_(\"直播\")):\n            ids = await self.links_tiktok.run(url, type_=\"live\")\n            if not ids:\n                self.logger.warning(_(\"{} 提取直播 ID 失败\").format(url=url))\n                continue\n            live_data = [await self.get_live_data_tiktok(i) for i in ids]\n            if not [i for i in live_data if i]:\n                self.logger.warning(_(\"获取直播数据失败\"))\n                continue\n            live_data = await self.extractor.run(\n                live_data,\n                None,\n                \"live\",\n                tiktok=True,\n            )\n            download_tasks = self.show_live_info_tiktok(live_data)\n            await self.downloader.run(download_tasks, type_=\"live\", tiktok=True)\n        self.logger.info(_(\"已退出获取直播拉流地址(TikTok)模式\"))\n\n    # def _generate_live_params(self, rid: bool, ids: list[list]) -> list[dict]:\n    #     if not ids:\n    #         self.console.warning(\n    #             _(\"提取 web_rid 或者 room_id 失败！\"),\n    #         )\n    #         return []\n    #     if rid:\n    #         return [{\"web_rid\": id_} for id_ in ids]\n    #     else:\n    #         return [{\"room_id\": id_[0], \"sec_user_id\": id_[1]} for id_ in ids]\n\n    def show_live_info(self, data: list[dict]) -> list[tuple]:\n        download_tasks = []\n        for item in data:\n            self.console.print(_(\"直播标题:\"), item[\"title\"])\n            self.console.print(_(\"主播昵称:\"), item[\"nickname\"])\n            self.console.print(_(\"在线观众:\"), item[\"user_count_str\"])\n            self.console.print(_(\"观看次数:\"), item[\"total_user_str\"])\n            if item[\"status\"] == 4:\n                self.console.print(_(\"当前直播已结束！\"))\n                continue\n            self.show_live_stream_url(item, download_tasks)\n        return [i for i in download_tasks if isinstance(i, tuple)]\n\n    def show_live_info_tiktok(self, data: list[dict]) -> list[tuple]:\n        download_tasks = []\n        for item in data:\n            if item[\"message\"]:\n                self.console.print(item[\"message\"])\n                self.console.print(item[\"prompts\"])\n                continue\n            self.console.print(_(\"直播标题:\"), item[\"title\"])\n            self.console.print(_(\"主播昵称:\"), item[\"nickname\"])\n            self.console.print(_(\"开播时间:\"), item[\"create_time\"])\n            self.console.print(_(\"在线观众:\"), item[\"user_count\"])\n            self.console.print(_(\"点赞次数:\"), item[\"like_count\"])\n            self.show_live_stream_url_tiktok(item, download_tasks)\n        return [i for i in download_tasks if isinstance(i, tuple)]\n\n    def show_live_stream_url(self, item: dict, tasks: list):\n        self.console.print(_(\"FLV 拉流地址: \"))\n        for i, (k, v) in enumerate(item[\"flv_pull_url\"].items(), start=1):\n            self.console.print(i, k, v)\n        self.console.print(_(\"M3U8 拉流地址: \"))\n        for i, (k, v) in enumerate(item[\"hls_pull_url_map\"].items(), start=1):\n            self.console.print(i, k, v)\n        if self.parameter.download:\n            tasks.append(\n                (item, *u)\n                if (\n                    u := self._choice_live_quality(\n                        item[\"flv_pull_url\"],\n                        item[\"hls_pull_url_map\"],\n                    )\n                )\n                else u\n            )\n\n    def show_live_stream_url_tiktok(self, item: dict, tasks: list):\n        self.console.print(_(\"FLV 拉流地址: \"))\n        for i, (k, v) in enumerate(item[\"flv_pull_url\"].items(), start=1):\n            self.console.print(i, k, v)\n        if self.parameter.download:\n            tasks.append(\n                (\n                    item,\n                    *u,\n                )\n                # TikTok 平台 暂无 m3u8 地址\n                if (\n                    u := self._choice_live_quality(\n                        item[\"flv_pull_url\"],\n                        item[\"flv_pull_url\"],\n                    )\n                )\n                else u\n            )\n\n    @check_storage_format\n    async def comment_interactive_tiktok(self, select=\"\", *args, **kwargs):\n        ...\n        self.logger.info(_(\"已退出采集作品评论数据(TikTok)模式\"))\n\n    @check_storage_format\n    async def comment_interactive(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择作品链接来源\"),\n            self.__function_comment,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出采集作品评论数据(抖音)模式)\"))\n\n    async def __comment_inquire(\n        self,\n        tiktok=False,\n    ):\n        link = self.links_tiktok if tiktok else self.links\n        while url := self._inquire_input(_(\"作品\")):\n            ids = await link.run(\n                url,\n            )\n            if not any(ids):\n                self.logger.warning(_(\"{url} 提取作品 ID 失败\").format(url=url))\n                continue\n            self.console.print(\n                _(\"共提取到 {count} 个作品，开始处理！\").format(count=len(ids))\n            )\n            await self.comment_handle(\n                ids,\n                tiktok=tiktok,\n            )\n\n    async def __comment_inquire_tiktok(\n        self,\n    ):\n        await self.__comment_inquire(\n            True,\n        )\n\n    async def __comment_txt(\n        self,\n        tiktok=False,\n    ):\n        await self._read_from_txt(\n            tiktok,\n            \"detail\",\n            _(\"从文本文档提取作品 ID 失败\"),\n            self.comment_handle,\n        )\n\n    async def comment_handle_single(\n        self,\n        detail_id: str,\n        cookie: str = None,\n        proxy: str = None,\n        source: bool = False,\n        **kwargs,\n    ) -> list:\n        if data := await Comment(\n            self.parameter,\n            cookie,\n            proxy,\n            detail_id=detail_id,\n            **kwargs,\n        ).run():\n            return data if source else await self.save_comment(detail_id, data)\n        return []\n\n    async def comment_handle_single_tiktok(\n        self,\n        detail_id: str,\n        cookie: str = None,\n        proxy: str = None,\n        source: bool = False,\n        **kwargs,\n    ) -> list: ...\n\n    async def comment_handle(\n        self,\n        ids: list,\n        tiktok=False,\n        cookie: str = None,\n        proxy: str = None,\n        **kwargs,\n    ):\n        if tiktok:\n            processor = self.comment_handle_single_tiktok\n        else:\n            processor = self.comment_handle_single\n        for i in ids:\n            if await processor(\n                i,\n                cookie,\n                proxy,\n                **kwargs,\n            ):\n                self.logger.info(\n                    _(\"作品评论数据已储存至 {filename}\").format(\n                        filename=_(\"作品{id}_评论数据\").format(id=i),\n                    )\n                )\n            else:\n                self.logger.warning(_(\"采集评论数据失败\"))\n\n    async def save_comment(self, detail_id: str, data: list[dict]) -> list:\n        root, params, logger = self.record.run(self.parameter, type_=\"comment\")\n        async with logger(\n            root,\n            name=_(\"作品{id}_评论数据\").format(\n                id=detail_id,\n            ),\n            console=self.console,\n            **params,\n        ) as record:\n            return await self.extractor.run(data, record, type_=\"comment\")\n\n    async def reply_handle(\n        self,\n        detail_id: str,\n        comment_id: str,\n        pages: int = None,\n        cursor=0,\n        count=3,\n        cookie: str = None,\n        proxy: str = None,\n        source=False,\n    ):\n        if data := await Reply(\n            self.parameter,\n            cookie,\n            proxy,\n            detail_id=detail_id,\n            comment_id=comment_id,\n            pages=pages,\n            cursor=cursor,\n            count=count,\n        ).run():\n            return data if source else await self.save_comment(detail_id, data)\n        return []\n\n    async def reply_handle_tiktok(\n        self,\n        detail_id: str,\n        comment_id: str,\n        pages: int = None,\n        cursor=0,\n        count=3,\n        cookie: str = None,\n        proxy: str = None,\n        source=False,\n    ): ...\n\n    async def mix_interactive(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择合集链接来源\"),\n            self.__function_mix,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载合集作品(抖音)模式\"))\n\n    async def mix_interactive_tiktok(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择合集链接来源\"),\n            self.__function_mix_tiktok,\n            select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出批量下载合集作品(TikTok)模式\"))\n\n    @staticmethod\n    def _generate_mix_params(mix: bool, id_: str) -> dict:\n        return (\n            {\n                \"mix_id\": id_,\n            }\n            if mix\n            else {\n                \"detail_id\": id_,\n            }\n        )\n\n    async def mix_inquire(\n        self,\n    ):\n        while url := self._inquire_input(_(\"合集或作品\")):\n            mix_id, ids = await self.links.run(url, type_=\"mix\")\n            if not ids:\n                self.logger.warning(\n                    _(\"{url} 获取作品 ID 或合集 ID 失败\").format(url=url)\n                )\n                continue\n            await self.__mix_handle(\n                mix_id,\n                ids,\n            )\n\n    async def mix_inquire_tiktok(\n        self,\n    ):\n        while url := self._inquire_input(_(\"合集或作品\")):\n            __, ids, title = await self.links_tiktok.run(url, type_=\"mix\")\n            if not ids:\n                self.logger.warning(_(\"{url} 获取合集 ID 失败\").format(url=url))\n                continue\n            await self.__mix_handle(\n                True,\n                ids,\n                title,\n                True,\n            )\n\n    @check_cookie_state(tiktok=False)\n    async def mix_collection(\n        self,\n    ):\n        if id_ := await self.mix_inquire_collection():\n            await self.__mix_handle(\n                True,\n                id_,\n            )\n\n    async def mix_inquire_collection(self) -> list[str]:\n        data = await CollectsMix(self.parameter).run()\n        if not any(data):\n            return []\n        data = self.extractor.extract_mix_collect_info(data)\n        return self.input_download_index(data)\n\n    def input_download_index(self, data: list[dict]) -> list[str] | None:\n        if d := self.__input_download_index(\n            data,\n            _(\"收藏合集\"),\n        ):\n            return [i[\"id\"] for i in d]\n\n    def __input_download_index(\n        self,\n        data: list[dict],\n        text=_(\"收藏合集\"),\n        select=\"\",\n        key=\"title\",\n    ) -> list[dict] | None:\n        self.console.print(_(\"{text}列表：\").format(text=_(text)))\n        for i, j in enumerate(data, start=1):\n            self.console.print(f\"{i}. {j[key]}\")\n        index = select or self.console.input(\n            _(\n                \"请输入需要下载的{item}序号(多个序号使用空格分隔，输入 ALL 下载全部{item})：\"\n            ).format(item=text)\n        )\n        try:\n            if not index:\n                pass\n            elif index.upper() == \"ALL\":\n                return data\n            elif index.upper() == \"Q\":\n                self.running = False\n            else:\n                index = {int(i) for i in index.split()}\n                return [j for i, j in enumerate(data, start=1) if i in index]\n        except ValueError:\n            self.console.warning(_(\"{text}序号输入错误！\").format(text=text))\n\n    async def mix_txt(\n        self,\n    ):\n        await self._read_from_txt(\n            tiktok=False,\n            type_=\"mix\",\n            error=_(\"从文本文档提取作品 ID 或合集 ID 失败\"),\n            callback=self.__mix_handle,\n        )\n\n    async def mix_txt_tiktok(\n        self,\n    ):\n        await self._read_from_txt(\n            tiktok=True,\n            type_=\"mix\",\n            error=_(\"从文本文档提取合集 ID 失败\"),\n            callback=self.__mix_handle,\n        )\n\n        if not (url := self.txt_inquire()):\n            return\n        __, ids, title = await self.links_tiktok.run(url, type_=\"mix\")\n        if not ids:\n            self.logger.warning()\n            return\n        await self.__mix_handle(\n            True,\n            ids,\n            title,\n            True,\n        )\n\n    async def __mix_handle(\n        self,\n        mix_id: bool,\n        ids: list[str],\n        mix_title_map: list[str] = None,\n        tiktok=False,\n    ):\n        count = SimpleNamespace(time=time(), success=0, failed=0)\n        for index, i in enumerate(ids, start=1):\n            if not await self.deal_mix_detail(\n                mix_id,\n                i,\n                index=index,\n                tiktok=tiktok,\n                mix_title=mix_title_map[index - 1] if mix_title_map else None,\n            ):\n                count.failed += 1\n                continue\n            count.success += 1\n            if index != len(ids):\n                await suspend(index, self.console)\n        self.__summarize_results(\n            count,\n            _(\"合集\"),\n        )\n\n    async def mix_batch(\n        self,\n    ):\n        await self.__mix_batch(\n            self.mix,\n            \"mix_urls\",\n            False,\n        )\n\n    async def mix_batch_tiktok(\n        self,\n    ):\n        await self.__mix_batch(\n            self.mix_tiktok,\n            \"mix_urls_tiktok\",\n            True,\n        )\n\n    async def __mix_batch(\n        self,\n        mix: list[SimpleNamespace],\n        params_name: str,\n        tiktok: bool,\n    ):\n        count = SimpleNamespace(time=time(), success=0, failed=0)\n        for index, data in enumerate(mix, start=1):\n            mix_id, id_, title = await self._check_mix_id(\n                data.url,\n                tiktok,\n            )\n            if not id_:\n                self.logger.warning(\n                    _(\n                        \"配置文件 {name} 参数的 url {url} 获取作品 ID 或合集 ID 失败，错误配置：{data}\"\n                    ).format(\n                        name=params_name,\n                        url=data.url,\n                        data=vars(data),\n                    )\n                )\n                count.failed += 1\n                continue\n            if not await self.deal_mix_detail(\n                mix_id,\n                id_,\n                data.mark,\n                index,\n                tiktok=tiktok,\n                mix_title=title,\n            ):\n                count.failed += 1\n                continue\n            count.success += 1\n            if index != len(mix):\n                await suspend(index, self.console)\n        self.__summarize_results(\n            count,\n            _(\"合集\"),\n        )\n\n    async def deal_mix_detail(\n        self,\n        mix_id: bool = None,\n        id_: str = None,\n        mark=\"\",\n        index: int = 0,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n        tiktok=False,\n        mix_title: str = \"\",\n        **kwargs,\n    ):\n        self.logger.info(\n            _(\"开始处理第 {index} 个合集\").format(index=index)\n            if index\n            else _(\"开始处理合集\")\n        )\n        mix_params = self._generate_mix_params(mix_id, id_)\n        if tiktok:\n            mix_obj = MixTikTok(\n                self.parameter,\n                cookie,\n                proxy,\n                mix_title=mix_title,\n                **mix_params,\n                **kwargs,\n            )\n        else:\n            mix_obj = Mix(\n                self.parameter,\n                cookie,\n                proxy,\n                **mix_params,\n                **kwargs,\n            )\n        if any(mix_data := await mix_obj.run()):\n            return (\n                mix_data\n                if source\n                else await self._batch_process_detail(\n                    mix_data,\n                    mode=\"mix\",\n                    mix_id=mix_obj.mix_id,\n                    mix_title=mix_obj.mix_title,\n                    mark=mark,\n                    api=api,\n                    tiktok=tiktok,\n                )\n            )\n        self.logger.warning(_(\"采集合集作品数据失败\"))\n\n    async def _check_mix_id(\n        self,\n        url: str,\n        tiktok: bool,\n    ) -> tuple[bool, str, str]:\n        match tiktok:\n            case True:\n                _, ids, title = await self.links_tiktok.run(url, type_=\"mix\")\n                return (True, ids[0], title[0]) if len(ids) > 0 else (None, \"\", \"\")\n            case False:\n                mix_id, ids = await self.links.run(url, type_=\"mix\")\n                return (mix_id, ids[0], \"\") if len(ids) > 0 else (mix_id, \"\", \"\")\n            case _:\n                raise DownloaderError\n\n    async def user_batch(\n        self,\n        *args,\n        **kwargs,\n    ):\n        users = []\n        for index, data in enumerate(self.accounts, start=1):\n            if not (sec_user_id := await self.check_sec_user_id(data.url)):\n                self.logger.warning(\n                    _(\"配置文件 accounts_urls 参数第 {index} 条数据的 url 无效\").format(\n                        index=index\n                    ),\n                )\n                continue\n            users.append(await self._get_user_data(sec_user_id))\n        await self._deal_user_data([i for i in users if i])\n\n    async def user_inquire(\n        self,\n        *args,\n        **kwargs,\n    ):\n        while url := self._inquire_input(_(\"账号主页\")):\n            sec_user_ids = await self.links.run(url, type_=\"user\")\n            if not sec_user_ids:\n                self.logger.warning(\n                    _(\"{url} 提取账号 sec_user_id 失败\").format(url=url)\n                )\n                continue\n            users = [await self._get_user_data(i) for i in sec_user_ids]\n            await self._deal_user_data([i for i in users if i])\n\n    def txt_inquire(self) -> str:\n        if path := self.console.input(_(\"请输入文本文档路径：\")):\n            if (t := Path(path.replace('\"', \"\"))).is_file():\n                try:\n                    with t.open(\"r\", encoding=self.ENCODE) as f:\n                        return f.read()\n                except UnicodeEncodeError as e:\n                    self.logger.warning(\n                        _(\"{path} 文件读取异常: {error}\").format(path=path, error=e)\n                    )\n            else:\n                self.console.print(_(\"{path} 文件不存在！\").format(path=path))\n        return \"\"\n\n    async def user_txt(\n        self,\n        *args,\n        **kwargs,\n    ):\n        if not (url := self.txt_inquire()):\n            return\n        sec_user_ids = await self.links.run(url, type_=\"user\")\n        if not sec_user_ids:\n            self.logger.warning(_(\"从文本文档提取账号 sec_user_id 失败\"))\n            return\n        users = [await self._get_user_data(i) for i in sec_user_ids]\n        await self._deal_user_data([i for i in users if i])\n\n    async def _get_user_data(\n        self,\n        sec_user_id: str,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        self.logger.info(\n            _(\"正在获取账号 {sec_user_id} 的数据\").format(sec_user_id=sec_user_id)\n        )\n        data = await User(\n            self.parameter,\n            cookie,\n            proxy,\n            sec_user_id,\n        ).run()\n        return data or {}\n\n    async def _deal_user_data(\n        self,\n        data: list[dict],\n        source=False,\n    ):\n        if not any(data):\n            return None\n        if source:\n            return data\n        root, params, logger = self.record.run(\n            self.parameter,\n            type_=\"user\",\n        )\n        async with logger(\n            root, name=\"UserData\", console=self.console, **params\n        ) as recorder:\n            data = await self.extractor.run(data, recorder, type_=\"user\")\n        self.logger.info(_(\"账号数据已保存至文件\"))\n        return data\n\n    @check_storage_format\n    async def user_interactive(self, select=\"\", *args, **kwargs):\n        await self.__secondary_menu(\n            _(\"请选择账号链接来源\"),\n            function=self.__function_user,\n            select=select or safe_pop(self.run_command),\n        )\n        self.logger.info(_(\"已退出采集账号详细数据模式\"))\n\n    def _enter_search_criteria(\n        self,\n        field: str,\n    ) -> list[Any]:\n        criteria = self.console.input(\n            _(\"请输入搜索参数；参数之间使用两个空格分隔({field})：\\n\").format(\n                field=field\n            ),\n        )\n        if criteria.upper() == \"Q\":\n            self.running = False\n            return []\n        return criteria.split(\"  \") if criteria else []\n\n    @staticmethod\n    def fill_search_criteria(criteria: list[Any]) -> list[Any]:\n        if len(criteria) == 1:\n            criteria.append(1)\n        while len(criteria) < 9:\n            criteria.append(0)\n        return criteria\n\n    @check_storage_format\n    async def search_interactive(\n        self,\n        select=\"\",\n    ):\n        await self.__secondary_menu(\n            _(\"请选择搜索模式\"),\n            function=self.__function_search,\n            select=select or safe_pop(self.run_command),\n        )\n        self.logger.info(\"已退出采集搜索结果数据模式\")\n\n    @staticmethod\n    def generate_model(\n        channel: int,\n        keyword: str,\n        pages: int = 1,\n        sort_type: int = 0,\n        publish_time: int = 0,\n        duration: int = 0,\n        search_range: int = 0,\n        content_type: int = 0,\n        douyin_user_fans: int = 0,\n        douyin_user_type: int = 0,\n    ) -> Union[\"BaseModel\", str]:\n        try:\n            match channel:\n                case 0:\n                    return GeneralSearch(\n                        keyword=keyword,\n                        pages=pages,\n                        sort_type=sort_type,\n                        publish_time=publish_time,\n                        duration=duration,\n                        search_range=search_range,\n                        content_type=content_type,\n                    )\n                case 1:\n                    return VideoSearch(\n                        keyword=keyword,\n                        pages=pages,\n                        sort_type=sort_type,\n                        publish_time=publish_time,\n                        duration=duration,\n                        search_range=search_range,\n                    )\n                case 2:\n                    return UserSearch(\n                        keyword=keyword,\n                        pages=pages,\n                        douyin_user_fans=douyin_user_fans,\n                        douyin_user_type=douyin_user_type,\n                    )\n                case 3:\n                    return LiveSearch(\n                        keyword=keyword,\n                        pages=pages,\n                    )\n                case _:\n                    raise DownloaderError\n        except ValidationError as e:\n            return repr(e)\n\n    async def _search_interactive_general(\n        self,\n        index=0,\n    ):\n        while criteria := self._enter_search_criteria(Search.search_criteria[index]):\n            criteria = self.fill_search_criteria(criteria)\n            if isinstance(\n                model := self.generate_model(\n                    index,\n                    *criteria,\n                ),\n                str,\n            ):\n                self.logger.warning(model)\n                continue\n            self.logger.info(f\"搜索参数: {model.model_dump()}\", False)\n            if isinstance(\n                r := await self.deal_search_data(\n                    model,\n                ),\n                list,\n            ) and not any(r):\n                self.logger.warning(_(\"搜索结果为空\"))\n\n    async def _search_interactive_video(self):\n        await self._search_interactive_general(\n            1,\n        )\n\n    async def _search_interactive_user(self):\n        await self._search_interactive_general(\n            2,\n        )\n\n    async def _search_interactive_live(self):\n        await self._search_interactive_general(\n            3,\n        )\n\n    @staticmethod\n    def _generate_search_name(\n        model: \"BaseModel\",\n    ) -> str:\n        name = [\n            _(\"搜索数据\"),\n            f\"{datetime.now():%Y_%m_%d_%H_%M_%S}\",\n            Search.search_params[model.channel].note,\n        ]\n        match model.channel:\n            case 0:\n                name.extend(\n                    [\n                        model.keyword,\n                        Search.sort_type_help[model.sort_type],\n                        Search.publish_time_help[model.publish_time],\n                        Search.duration_help[model.duration],\n                        Search.search_range_help[model.search_range],\n                        Search.content_type_help[model.content_type],\n                    ]\n                )\n            case 1:\n                name.extend(\n                    [\n                        model.keyword,\n                        Search.sort_type_help[model.sort_type],\n                        Search.publish_time_help[model.publish_time],\n                        Search.duration_help[model.duration],\n                        Search.search_range_help[model.search_range],\n                    ]\n                )\n            case 2:\n                name.extend(\n                    [\n                        model.keyword,\n                        Search.douyin_user_fans_help[model.douyin_user_fans],\n                        Search.douyin_user_type_help[model.douyin_user_type],\n                    ]\n                )\n            case 3:\n                name.append(\n                    model.keyword,\n                )\n        return \"_\".join(name)\n\n    async def deal_search_data(\n        self,\n        model: \"BaseModel\",\n        source=False,\n    ):\n        data = await Search(\n            self.parameter,\n            **model.model_dump(),\n        ).run()\n        if len(data) != 1 and not any(data):\n            return None\n        if source or not any(data):\n            return data\n        root, params, logger = self.record.run(\n            self.parameter,\n            type_=Search.search_data_field[model.channel],\n        )\n        name = self._generate_search_name(\n            model,\n        )\n        async with logger(root, name=name, console=self.console, **params) as logger:\n            search_data = await self.extractor.run(\n                data,\n                logger,\n                type_=\"search\",\n                tab=model.channel,\n            )\n            self.logger.info(_(\"搜索数据已保存至 {name}\").format(name=name))\n        return search_data\n\n    @check_storage_format\n    async def hot_interactive(\n        self,\n        *args,\n    ):\n        await self._deal_hot_data()\n        self.logger.info(_(\"已退出采集抖音热榜数据(抖音)模式\"))\n\n    async def _deal_hot_data(\n        self,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n    ):\n        time_, board = await Hot(\n            self.parameter,\n            cookie,\n            proxy,\n        ).run()\n        if not any(board):\n            return None, None\n        if source:\n            return time_, [{Hot.board_params[i].name: j} for i, j in board]\n        root, params, logger = self.record.run(self.parameter, type_=\"hot\")\n        data = []\n        for i, j in board:\n            name = _(\"热榜数据_{time}_{name}\").format(\n                time=time_, name=Hot.board_params[i].name\n            )\n            async with logger(\n                root, name=name, console=self.console, **params\n            ) as record:\n                data.append(\n                    {\n                        Hot.board_params[i].name: await self.extractor.run(\n                            j, record, type_=\"hot\"\n                        )\n                    }\n                )\n        self.logger.info(\n            _(\"热榜数据已储存至: 热榜数据_{time} + 榜单类型\").format(time=time_)\n        )\n        return time_, data\n\n    @check_cookie_state(tiktok=False)\n    async def collection_interactive(\n        self,\n        *args,\n    ):\n        if sec_user_id := await self.__check_owner_url():\n            start = time()\n            await self._deal_collection_data(\n                sec_user_id,\n            )\n            self._time_statistics(start)\n        self.logger.info(_(\"已退出批量下载收藏作品(抖音)模式\"))\n\n    @check_cookie_state(tiktok=False)\n    async def collects_interactive(\n        self,\n        select=\"\",\n        key: str = \"name\",\n    ):\n        if c := await self.__get_collects_list(\n            select=select,\n            key=key,\n        ):\n            start = time()\n            for i in c:\n                await self._deal_collects_data(\n                    i[key],\n                    i[\"id\"],\n                )\n            self._time_statistics(start)\n        else:\n            self.logger.info(_(\"已退出批量下载收藏夹作品(抖音)模式\"))\n\n    async def __get_collects_list(\n        self,\n        cookie: str = None,\n        proxy: str | dict = None,\n        # api=False,\n        source=False,\n        select=\"\",\n        key: str = \"name\",\n        *args,\n        **kwargs,\n    ):\n        collects = await Collects(\n            self.parameter,\n            cookie,\n            proxy,\n        ).run()\n        if not any(collects):\n            return None\n        if source:\n            return collects\n        data = self.extractor.extract_collects_info(collects)\n        return self.__input_download_index(\n            data,\n            _(\"收藏夹\"),\n            select,\n            key,\n        )\n\n    async def __check_owner_url(\n        self,\n        tiktok=False,\n    ):\n        if not (sec_user_id := await self.check_sec_user_id(self.owner.url)):\n            self.logger.warning(\n                _(\"配置文件 owner_url 的 url 参数 {url} 无效\").format(\n                    url=self.owner.url\n                ),\n            )\n            # if self.console.input(\n            #         _(\"程序无法获取账号信息，建议修改配置文件后重新运行，是否返回上一级菜单(YES/NO)\")\n            # ).upper != \"NO\":\n            #     return None\n            return \"\"\n        return sec_user_id\n\n    @check_cookie_state(tiktok=False)\n    async def collection_music_interactive(\n        self,\n        *args,\n    ):\n        start = time()\n        if data := await self.__handle_collection_music(\n            *args,\n        ):\n            data = await self.extractor.run(\n                data,\n                None,\n                \"music\",\n            )\n            await self.downloader.run(\n                data,\n                type_=\"music\",\n            )\n        self._time_statistics(start)\n        self.logger.info(_(\"已退出批量下载收藏音乐(抖音)模式\"))\n\n    def _time_statistics(\n        self,\n        start: float,\n    ):\n        time_ = time() - start\n        self.logger.info(\n            _(\"程序运行耗时 {minutes} 分钟 {seconds} 秒\").format(\n                minutes=int(time_ // 60), seconds=int(time_ % 60)\n            )\n        )\n\n    async def __handle_collection_music(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        data = await CollectsMusic(\n            self.parameter,\n            cookie,\n            proxy,\n            *args,\n            **kwargs,\n        ).run()\n        return data if any(data) else None\n\n    async def _deal_collection_data(\n        self,\n        sec_user_id: str,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n        tiktok=False,\n    ):\n        self.logger.info(_(\"开始获取收藏数据\"))\n        if not (\n            info := await self.get_user_info_data(\n                tiktok,\n                cookie,\n                proxy,\n                sec_user_id=sec_user_id,\n            )\n        ):\n            self.logger.warning(\n                _(\"{sec_user_id} 获取账号信息失败\").format(sec_user_id=sec_user_id)\n            )\n            return\n        collection = await Collection(\n            self.parameter,\n            cookie,\n            proxy,\n            sec_user_id,\n        ).run()\n        if not any(collection):\n            return None\n        if source:\n            return collection\n        await self._batch_process_detail(\n            collection,\n            api,\n            tiktok=tiktok,\n            mode=\"collection\",\n            mark=self.owner.mark,\n            user_id=sec_user_id,\n            info=info,\n        )\n\n    async def _deal_collects_data(\n        self,\n        name: str,\n        id_: str,\n        api=False,\n        source=False,\n        cookie: str = None,\n        proxy: str = None,\n        tiktok=False,\n    ):\n        self.logger.info(_(\"开始获取收藏夹数据\"))\n        data = await CollectsDetail(\n            self.parameter,\n            cookie,\n            proxy,\n            id_,\n        ).run()\n        if not any(data):\n            return None\n        if source:\n            return data\n        await self._batch_process_detail(\n            data,\n            mode=\"collects\",\n            collect_id=id_,\n            collect_name=name,\n            api=api,\n            tiktok=tiktok,\n        )\n\n    async def hashtag_interactive(\n        self,\n        cookie: str = None,\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        await HashTag(\n            self.parameter,\n            cookie,\n            proxy,\n        ).run()\n\n    async def handle_detail_unofficial(\n        self,\n        ids: list[str],\n        *args,\n        **kwargs,\n    ):\n        extractor = DetailTikTokExtractor(self.parameter)\n        for i in ids:\n            if data := await DetailTikTokUnofficial(\n                self.parameter,\n                detail_id=i,\n            ).run():\n                if data := extractor.run(data):\n                    await self.downloader.run([data], \"detail\", tiktok=True)\n\n    async def run(self, run_command: list):\n        self.run_command = run_command\n        while self.running:\n            if not (select := safe_pop(self.run_command)):\n                select = choose(\n                    _(\"请选择采集功能\"),\n                    [i for i, __ in self.__function],\n                    self.console,\n                    (11,),\n                )\n            if select in {\n                \"Q\",\n                \"q\",\n            }:\n                self.running = False\n            try:\n                n = int(select) - 1\n            except ValueError:\n                break\n            if n in range(len(self.__function)):\n                await self.__function[n][1](safe_pop(self.run_command))\n"
  },
  {
    "path": "src/cli_edition/__init__.py",
    "content": "from .main_cli import cli\n\n__all__ = [\"cli\"]\n"
  },
  {
    "path": "src/cli_edition/main_cli.py",
    "content": "__all__ = [\"cli\"]\n\n\nclass Cli:\n    pass\n\n\ndef cli():\n    pass\n"
  },
  {
    "path": "src/cli_edition/write.py",
    "content": "from pathlib import Path\n\nfrom src.config import Settings\nfrom src.custom import PROJECT_ROOT\nfrom src.tools import ColorfulConsole\nfrom src.translation import _\nfrom src.custom import VERSION_BETA\n\n\nclass Write:\n    def __init__(\n        self,\n    ):\n        self.console = ColorfulConsole(\n            debug=VERSION_BETA,\n        )\n        self.settings = Settings(PROJECT_ROOT, self.console)\n        self.data = self.settings.read()\n\n    def run(self):\n        data = self.txt_inquire()\n        self.generate_data(data)\n        self.settings.update(self.data)\n\n    def generate_data(self, data: str):\n        for i in data.split(\"\\n\"):\n            if i.strip():\n                self.data[\"accounts_urls_tiktok\"].append(\n                    {\n                        \"mark\": \"\",\n                        \"url\": i,\n                        \"tab\": \"post\",\n                        \"earliest\": \"\",\n                        \"latest\": \"\",\n                        \"enable\": True,\n                    }\n                )\n\n    def txt_inquire(self) -> str:\n        if path := self.console.input(_(\"请输入文本文档路径：\")):\n            if (t := Path(path.replace('\"', \"\"))).is_file():\n                try:\n                    with t.open(\"r\", encoding=self.settings.encode) as f:\n                        return f.read()\n                except UnicodeEncodeError as e:\n                    self.console.warning(\n                        _(\"{path} 文件读取异常: {error}\").format(path=path, error=e)\n                    )\n            else:\n                self.console.print(_(\"{path} 文件不存在！\").format(path=path))\n        return \"\"\n\n\nif __name__ == \"__main__\":\n    Write().run()\n"
  },
  {
    "path": "src/config/__init__.py",
    "content": "from .parameter import Parameter\nfrom .settings import Settings\n\n__all__ = [\"Parameter\", \"Settings\"]\n"
  },
  {
    "path": "src/config/parameter.py",
    "content": "from pathlib import Path\nfrom shutil import move\nfrom time import localtime, strftime\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING, Any, Type\n\nfrom httpx import HTTPStatusError, RequestError, TimeoutException, get\n\nfrom ..custom import (\n    BLANK_PREVIEW,\n    DATA_HEADERS,\n    DATA_HEADERS_TIKTOK,\n    DOWNLOAD_HEADERS,\n    DOWNLOAD_HEADERS_TIKTOK,\n    PARAMS_HEADERS,\n    PARAMS_HEADERS_TIKTOK,\n    PROJECT_ROOT,\n    QRCODE_HEADERS,\n    TIMEOUT,\n    USERAGENT,\n)\nfrom ..encrypt import (\n    ABogus,\n    MsToken,\n    MsTokenTikTok,\n    TtWid,\n    TtWidTikTok,\n    XBogus,\n    XGnarly,\n)\nfrom ..extract import Extractor\nfrom ..interface import API, APITikTok\nfrom ..module import FFMPEG\nfrom ..record import BaseLogger, LoggerManager\nfrom ..storage import RecordManager\nfrom ..tools import Cleaner, DownloaderError, cookie_dict_to_str, create_client\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from ..manager import DownloadRecorder\n    from ..module import Cookie\n    from ..tools import ColorfulConsole\n    from .settings import Settings\n\n__all__ = [\"Parameter\"]\n\n\nclass Parameter:\n    NAME_KEYS = (\n        \"id\",\n        \"desc\",\n        \"create_time\",\n        \"nickname\",\n        \"uid\",\n        \"mark\",\n        \"type\",\n    )\n    CLEANER = Cleaner()\n    HEADERS = {\"User-Agent\": USERAGENT}\n    NO_PROXY = {\n        \"http://\": None,\n        \"https://\": None,\n    }\n\n    def __init__(\n        self,\n        settings: \"Settings\",\n        cookie_object: \"Cookie\",\n        logger: Type[BaseLogger | LoggerManager],\n        console: \"ColorfulConsole\",\n        cookie: dict | str,\n        cookie_tiktok: dict | str,\n        root: str,\n        accounts_urls: list[dict],\n        accounts_urls_tiktok: list[dict],\n        mix_urls: list[dict],\n        mix_urls_tiktok: list[dict],\n        folder_name: str,\n        name_format: str,\n        desc_length: int,\n        name_length: int,\n        date_format: str,\n        split: str,\n        music: bool,\n        folder_mode: bool,\n        truncate: int,\n        storage_format: str,\n        dynamic_cover: bool,\n        static_cover: bool,\n        proxy: str | None | dict,\n        proxy_tiktok: str | None | dict,\n        twc_tiktok: str,\n        download: bool,\n        max_size: int,\n        chunk: int,\n        max_retry: int,\n        max_pages: int,\n        run_command: str,\n        owner_url: dict,\n        owner_url_tiktok: dict,\n        live_qualities: str,\n        ffmpeg: str,\n        recorder: \"DownloadRecorder\",\n        browser_info: dict,\n        browser_info_tiktok: dict,\n        timeout=10,\n        douyin_platform=True,\n        tiktok_platform=True,\n        **kwargs,\n    ):\n        self.settings = settings\n        self.cookie_object = cookie_object\n        self.ROOT = PROJECT_ROOT  # 项目根路径\n        self.cache = PROJECT_ROOT.joinpath(\"Cache\")  # 缓存路径\n        self.logger = logger(PROJECT_ROOT, console)\n        self.logger.run()\n        self.ab = ABogus()\n        self.xb = XBogus()\n        self.xg = XGnarly()\n        self.console = console\n        self.recorder = recorder\n        self.preview = BLANK_PREVIEW\n        self.ms_token = \"\"\n        self.ms_token_tiktok = \"\"\n\n        self.headers = DATA_HEADERS\n        self.headers_tiktok = DATA_HEADERS_TIKTOK\n        self.headers_download = DOWNLOAD_HEADERS\n        self.headers_download_tiktok = DOWNLOAD_HEADERS_TIKTOK\n        self.headers_params = PARAMS_HEADERS\n        self.headers_params_tiktok = PARAMS_HEADERS_TIKTOK\n        self.headers_qrcode = QRCODE_HEADERS\n\n        self.accounts_urls: list[SimpleNamespace] = self.check_urls_params(\n            accounts_urls\n        )\n        self.accounts_urls_tiktok: list[SimpleNamespace] = self.check_urls_params(\n            accounts_urls_tiktok\n        )\n        self.mix_urls: list[SimpleNamespace] = self.check_urls_params(mix_urls)\n        self.mix_urls_tiktok: list[SimpleNamespace] = self.check_urls_params(\n            mix_urls_tiktok\n        )\n        self.owner_url: SimpleNamespace = self.check_url_params(owner_url)\n        self.owner_url_tiktok: SimpleNamespace | None = None\n\n        self.cookie_dict, self.cookie_str = self.__check_cookie(cookie)\n        self.cookie_dict_tiktok, self.cookie_str_tiktok = self.__check_cookie_tiktok(\n            cookie_tiktok,\n        )\n        self.cookie_state: bool = self.__check_cookie_state()\n        self.cookie_tiktok_state: bool = self.__check_cookie_state(True)\n        self.set_uif_id()\n        # self.set_download_headers()\n\n        self.root = self.__check_root(root)\n        self.folder_name = self.__check_folder_name(folder_name)\n        self.name_format = self.__check_name_format(name_format)\n        self.desc_length = self.__check_desc_length(desc_length)\n        self.name_length = self.__check_name_length(name_length)\n        self.date_format = self.__check_date_format(date_format)\n        self.split = self.__check_split(split)\n        self.folder_mode = self.check_bool_false(folder_mode)\n        self.music = self.check_bool_false(music)\n        self.truncate = self.__check_truncate(truncate)\n        self.storage_format = self.__check_storage_format(storage_format)\n        self.dynamic_cover = self.check_bool_false(dynamic_cover)\n        self.static_cover = self.check_bool_false(static_cover)\n        self.twc_tiktok = self.check_str(twc_tiktok)\n        self.download = self.check_bool_true(download)\n        self.max_size = self.__check_max_size(max_size)\n        self.chunk = self.__check_chunk(chunk)\n        self.timeout = self.__check_timeout(timeout)\n        self.max_retry = self.__check_max_retry(max_retry)\n        self.max_pages = self.__check_max_pages(max_pages)\n        self.run_command = self.__check_run_command(run_command)\n        self.ffmpeg = self.__generate_ffmpeg_object(ffmpeg)\n        self.live_qualities = self.__check_live_qualities(live_qualities)\n        self.douyin_platform = self.check_bool_true(\n            douyin_platform,\n        )\n        self.tiktok_platform = self.check_bool_true(\n            tiktok_platform,\n        )\n\n        self.browser_info = self.merge_browser_info(\n            browser_info,\n            {},\n        )\n        self.browser_info_tiktok = self.merge_browser_info(\n            browser_info_tiktok,\n            {},\n        )\n        self.__set_browser_info(self.browser_info)\n        self.__set_browser_info_tiktok(self.browser_info_tiktok)\n\n        self.proxy: str | None = self.__check_proxy(\n            proxy,\n            remark=_(\"抖音\"),\n            enable=self.douyin_platform,\n        )\n        self.proxy_tiktok: str | None = self.__check_proxy_tiktok(proxy_tiktok)\n        self.client = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy,\n        )\n        self.client_tiktok = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy_tiktok,\n        )\n\n        self.__generate_folders()\n\n        # self.__URLS_PARAMS = {\n        #     \"accounts_urls\": None,\n        #     \"accounts_urls_tiktok\": None,\n        #     \"mix_urls\": None,\n        #     \"mix_urls_tiktok\": None,\n        #     \"owner_url\": None,\n        #     \"owner_url_tiktok\": None,\n        # }\n        self.__CHECK = {\n            \"root\": self.__check_root,\n            \"folder_name\": self.__check_folder_name,\n            \"name_format\": self.__check_name_format,\n            \"desc_length\": self.__check_desc_length,\n            \"name_length\": self.__check_name_length,\n            \"date_format\": self.__check_date_format,\n            \"split\": self.__check_split,\n            \"folder_mode\": self.check_bool_false,\n            \"music\": self.check_bool_false,\n            \"truncate\": self.__check_truncate,\n            \"storage_format\": self.__check_storage_format,\n            \"dynamic_cover\": self.check_bool_false,\n            \"static_cover\": self.check_bool_false,\n            \"twc_tiktok\": self.check_str,\n            \"download\": self.check_bool_true,\n            \"max_size\": self.__check_max_size,\n            \"chunk\": self.__check_chunk,\n            \"timeout\": self.__check_timeout,\n            \"max_retry\": self.__check_max_retry,\n            \"max_pages\": self.__check_max_pages,\n            \"run_command\": self.__check_run_command,\n            \"ffmpeg\": self.__generate_ffmpeg_object,\n            \"live_qualities\": self.__check_live_qualities,\n            \"douyin_platform\": self.check_bool_true,\n            \"tiktok_platform\": self.check_bool_true,\n        }\n        # self.__BROWSER_INFO = {\n        #     \"browser_info\": None,\n        #     \"browser_info_tiktok\": None,\n        # }\n\n    @staticmethod\n    def check_bool_false(\n        value: bool,\n    ) -> bool:\n        return value if isinstance(value, bool) else False\n\n    @staticmethod\n    def check_bool_true(\n        value: bool,\n    ) -> bool:\n        return value if isinstance(value, bool) else True\n\n    def __check_cookie_tiktok(\n        self,\n        cookie: dict | str,\n    ) -> tuple[dict, str]:\n        # if isinstance(cookie, str):\n        #     self.console.print(\n        #         \"参数 cookie_tiktok 应为字典格式！请修改配置文件后重新运行程序！\",\n        #         style=ERROR)\n        return self.__check_cookie(cookie, name=\"cookie_tiktok\")\n\n    def __check_cookie(self, cookie: dict | str, name=\"cookie\") -> tuple[dict, str]:\n        if isinstance(cookie, dict):\n            return cookie, \"\"\n        elif isinstance(cookie, str):\n            return {}, cookie\n        else:\n            self.logger.warning(_(\"{name} 参数格式错误\").format(name=name))\n        return {}, \"\"\n\n    def __get_cookie(\n        self,\n        cookie: dict,\n    ) -> dict:\n        return self.__check_cookie(cookie)[0]\n\n    def __get_cookie_cache(\n        self,\n        cookie: str,\n    ) -> str:\n        return self.__check_cookie(cookie)[1]\n\n    def __get_cookie_tiktok(\n        self,\n        cookie: dict,\n    ) -> dict:\n        return self.__check_cookie_tiktok(cookie)[0]\n\n    def __get_cookie_tiktok_cache(\n        self,\n        cookie: str,\n    ) -> str:\n        return self.__check_cookie_tiktok(cookie)[1]\n\n    def __add_cookie(\n        self,\n        parameters: tuple[dict, ...],\n        cookie: dict | str,\n    ) -> None | str:\n        if isinstance(cookie, dict):\n            for i in parameters:\n                if i:\n                    self.logger.info(\n                        f\"参数: {i}\",\n                        False,\n                    )\n                    cookie |= i\n            return None\n        elif isinstance(cookie, str):\n            for i in parameters:\n                if i:\n                    self.logger.info(\n                        f\"参数: {i}\",\n                        False,\n                    )\n                    cookie += f\"; {cookie_dict_to_str(i)}\"\n            return cookie\n        raise DownloaderError\n\n    async def __get_tt_wid_params(self) -> dict:\n        if tt_wid := await TtWid.get_tt_wid(\n            self.logger,\n            self.headers_params,\n            proxy=self.proxy,\n        ):\n            self.logger.info(f\"抖音 {TtWid.NAME} 请求值: {tt_wid[TtWid.NAME]}\", False)\n            return tt_wid\n        return {}\n\n    async def __get_tt_wid_params_tiktok(self) -> dict:\n        if tt_wid := await TtWidTikTok.get_tt_wid(\n            self.logger,\n            self.headers_params_tiktok,\n            self.twc_tiktok\n            or f\"{TtWidTikTok.NAME}={\n                self.cookie_dict_tiktok.get(TtWidTikTok.NAME, '')\n                or self.get_cookie_value(\n                    self.cookie_str_tiktok,\n                    TtWidTikTok.NAME,\n                )\n            }\",\n            proxy=self.proxy_tiktok,\n        ):\n            self.logger.info(\n                f\"TikTok {TtWidTikTok.NAME} 请求值: {tt_wid[TtWidTikTok.NAME]}\", False\n            )\n            return tt_wid\n        return {}\n\n    def __check_root(self, root: str) -> Path:\n        if not root:\n            return self.ROOT\n        if (r := Path(root)).is_dir():\n            self.logger.info(f\"root 参数已设置为 {root}\", False)\n            return r\n        if r := self.__check_root_again(r):\n            self.logger.info(f\"root 参数已设置为 {r}\", False)\n            return r\n        self.logger.warning(\n            _(\n                \"root 参数 {root} 不是有效的文件夹路径，程序将使用项目根路径作为储存路径\"\n            ).format(root=root),\n        )\n        return self.ROOT\n\n    @staticmethod\n    def __check_root_again(root: Path) -> bool | Path:\n        if root.resolve().parent.is_dir():\n            root.mkdir()\n            return root\n        return False\n\n    def __check_folder_name(self, folder_name: str) -> str:\n        if folder_name := self.CLEANER.filter_name(\n            folder_name,\n        ):\n            self.logger.info(f\"folder_name 参数已设置为 {folder_name}\", False)\n            return folder_name\n        self.logger.warning(\n            _(\n                \"folder_name 参数 {folder_name} 不是有效的文件夹名称，程序将使用默认值：Download\"\n            ).format(folder_name=folder_name),\n        )\n        return \"Download\"\n\n    def __check_name_format(self, name_format: str) -> list[str]:\n        name_keys = name_format.strip().split(\" \")\n        if all(i in self.NAME_KEYS for i in name_keys):\n            self.logger.info(f\"name_format 参数已设置为 {name_format}\", False)\n            return name_keys\n        else:\n            self.logger.warning(\n                _(\n                    \"name_format 参数 {name_format} 设置错误，程序将使用默认值：创建时间 作品类型 账号昵称 作品描述\"\n                ).format(name_format=name_format)\n            )\n            return [\"create_time\", \"type\", \"nickname\", \"desc\"]\n\n    def __check_date_format(self, date_format: str) -> str:\n        try:\n            strftime(date_format, localtime())\n            self.logger.info(f\"date_format 参数已设置为 {date_format}\", False)\n            return date_format\n        except ValueError:\n            self.logger.warning(\n                _(\n                    \"date_format 参数 {date_format} 设置错误，程序将使用默认值：年-月-日 时:分:秒\"\n                ).format(date_format=date_format),\n            )\n            return \"%Y-%m-%d %H:%M:%S\"\n\n    def __check_split(self, split: str) -> str:\n        for i in split:\n            if i in self.CLEANER.rule.keys():\n                self.logger.warning(\n                    _(\"split 参数 {split} 包含非法字符，程序将使用默认值：-\").format(\n                        split=split\n                    )\n                )\n                return \"-\"\n        self.logger.info(f\"split 参数已设置为 {split}\", False)\n        return split\n\n    def __check_proxy_tiktok(\n        self,\n        proxy: str | None | dict,\n    ) -> str | None:\n        return self.__check_proxy(\n            proxy,\n            \"https://www.tiktok.com/explore\",\n            \"TikTok\",\n            self.tiktok_platform,\n        )\n\n    def __check_proxy(\n        self,\n        proxy: str | None | dict,\n        url=\"https://www.douyin.com/?recommend=1\",\n        remark=_(\"抖音\"),\n        enable=True,\n    ) -> str | None:\n        if enable and proxy:\n            # 暂时兼容旧版配置；未来将会移除\n            if isinstance(proxy, dict):\n                self.console.warning(\n                    _(\"{remark}代理参数应为字符串格式，未来不再支持字典格式\").format(\n                        remark=remark\n                    )\n                )\n                if not (proxy := proxy.get(\"https://\")):\n                    return None\n            try:\n                response = get(\n                    url,\n                    headers=self.HEADERS,\n                    follow_redirects=True,\n                    timeout=TIMEOUT,\n                    proxy=proxy,\n                )\n                response.raise_for_status()\n                self.logger.info(\n                    _(\"{remark}代理 {proxy} 测试成功\").format(\n                        remark=remark, proxy=proxy\n                    )\n                )\n                return proxy\n            except TimeoutException:\n                self.logger.warning(\n                    _(\"{remark}代理 {proxy} 测试超时\").format(\n                        remark=remark, proxy=proxy\n                    )\n                )\n                return None\n            except (\n                RequestError,\n                HTTPStatusError,\n            ) as e:\n                self.logger.warning(\n                    _(\"{remark}代理 {proxy} 测试失败：{error}\").format(\n                        remark=remark, proxy=proxy, error=e\n                    ),\n                )\n                return None\n        return None\n\n    def __check_max_size(self, max_size: int) -> int:\n        max_size = max(max_size, 0)\n        self.logger.info(f\"max_size 参数已设置为 {max_size}\", False)\n        return max_size\n\n    def __check_chunk(self, chunk: int) -> int:\n        return self.__check_number_value(\n            chunk,\n            \"chunk\",\n            1024 * 128,\n            1024 * 1024 * 2,\n        )\n\n    def __check_max_retry(self, max_retry: int) -> int:\n        return self.__check_number_value(\n            max_retry,\n            \"max_retry\",\n            0,\n            5,\n        )\n\n    def __check_max_pages(self, max_pages: int) -> int:\n        if isinstance(max_pages, int) and max_pages > 0:\n            self.logger.info(f\"max_pages 参数已设置为 {max_pages}\", False)\n            return max_pages\n        elif max_pages != 0:\n            self.logger.warning(\n                _(\n                    \"max_pages 参数 {max_pages} 设置错误，程序将使用默认值：99999\"\n                ).format(max_pages=max_pages),\n            )\n        return 99999\n\n    def __check_timeout(self, timeout: int | float) -> int | float:\n        return self.__check_number_value(\n            timeout,\n            \"timeout\",\n            2,\n            10,\n        )\n\n    def __check_storage_format(self, storage_format: str) -> str:\n        if storage_format in RecordManager.DataLogger.keys():\n            self.logger.info(f\"storage_format 参数已设置为 {storage_format}\", False)\n            return storage_format\n        if not storage_format:\n            self.logger.info(\n                \"storage_format 参数未设置，程序不会储存任何数据至文件\", False\n            )\n        else:\n            self.logger.warning(\n                _(\n                    \"storage_format 参数 {storage_format} 设置错误，程序默认不会储存任何数据至文件\"\n                ).format(storage_format=storage_format),\n            )\n        return \"\"\n\n    @staticmethod\n    def __check_run_command(run_command: str) -> list:\n        return run_command.split()[::-1] if run_command else []\n\n    async def update_params(self) -> None:\n        if self.douyin_platform:\n            if any(\n                (\n                    self.cookie_dict,\n                    self.cookie_str,\n                )\n            ):\n                self.console.info(\n                    _(\"正在更新抖音参数，请稍等...\"),\n                )\n                ms_token = await self.__get_token_params()\n                tt_wid = await self.__get_tt_wid_params()\n                API.params[\"msToken\"] = ms_token.get(MsToken.NAME, \"\")\n                await self.__update_cookie(\n                    (\n                        ms_token,\n                        tt_wid,\n                    ),\n                    (\n                        self.headers,\n                        self.headers_download,\n                    ),\n                    self.cookie_dict,\n                    self.cookie_str,\n                )\n                self.console.info(\n                    _(\"抖音参数更新完毕！\"),\n                )\n            else:\n                self.logger.warning(\n                    _(\"配置文件 cookie 参数未设置，抖音平台功能可能无法正常使用\")\n                )\n        if self.tiktok_platform:\n            if any(\n                (\n                    self.cookie_dict_tiktok,\n                    self.cookie_str_tiktok,\n                )\n            ):\n                self.console.info(\n                    _(\"正在更新 TikTok 参数，请稍等...\"),\n                )\n                ms_token = await self.__get_token_params_tiktok()\n                tt_wid = await self.__get_tt_wid_params_tiktok()\n                APITikTok.params[\"msToken\"] = ms_token.get(MsTokenTikTok.NAME, \"\")\n                await self.__update_cookie(\n                    (\n                        ms_token,\n                        tt_wid,\n                    ),\n                    (\n                        self.headers_tiktok,\n                        self.headers_download_tiktok,\n                    ),\n                    self.cookie_dict_tiktok,\n                    self.cookie_str_tiktok,\n                )\n                self.console.info(\n                    _(\"TikTok 参数更新完毕！\"),\n                )\n            else:\n                self.logger.warning(\n                    _(\n                        \"配置文件 cookie_tiktok 参数未设置，TikTok 平台功能可能无法正常使用\"\n                    )\n                )\n\n    async def update_params_offline(self) -> None:\n        if self.douyin_platform:\n            if any(\n                (\n                    self.cookie_dict,\n                    self.cookie_str,\n                )\n            ):\n                ms_token = self.cookie_dict.get(MsToken.NAME) or self.get_cookie_value(\n                    self.cookie_str,\n                    MsToken.NAME,\n                )\n                API.params[\"msToken\"] = ms_token\n                await self.__update_cookie(\n                    ({MsToken.NAME: ms_token},),\n                    (\n                        self.headers,\n                        self.headers_download,\n                    ),\n                    self.cookie_dict,\n                    self.cookie_str,\n                )\n            else:\n                self.logger.warning(\n                    _(\"配置文件 cookie 参数未设置，抖音平台功能可能无法正常使用\")\n                )\n        if self.tiktok_platform:\n            if any(\n                (\n                    self.cookie_dict_tiktok,\n                    self.cookie_str_tiktok,\n                )\n            ):\n                ms_token = await self.__get_token_params_tiktok()\n                APITikTok.params[\"msToken\"] = ms_token.get(MsTokenTikTok.NAME, \"\")\n                await self.__update_cookie(\n                    (ms_token,),\n                    (\n                        self.headers_tiktok,\n                        self.headers_download_tiktok,\n                    ),\n                    self.cookie_dict_tiktok,\n                    self.cookie_str_tiktok,\n                )\n            else:\n                self.logger.warning(\n                    _(\n                        \"配置文件 cookie_tiktok 参数未设置，TikTok 平台功能可能无法正常使用\"\n                    )\n                )\n\n    async def __update_cookie(\n        self,\n        parameters: tuple[dict, ...],\n        headers: tuple[dict, ...],\n        cookie_dict: dict,\n        cookie_str: str,\n    ) -> None:\n        cookie = self.__add_cookie(\n            parameters,\n            cookie_dict or cookie_str,\n        )\n        if not isinstance(cookie, str):\n            cookie = cookie_dict_to_str(cookie_dict)\n        for i in headers:\n            i[\"Cookie\"] = cookie\n\n    def set_headers_cookie(\n        self,\n    ) -> None:\n        if self.cookie_dict:\n            cookie = cookie_dict_to_str(self.cookie_dict)\n            self.headers[\"Cookie\"] = cookie\n            self.headers_download[\"Cookie\"] = cookie\n        elif self.cookie_str:\n            self.headers[\"Cookie\"] = self.cookie_str\n            self.headers_download[\"Cookie\"] = self.cookie_str\n        if self.cookie_dict_tiktok:\n            cookie = cookie_dict_to_str(self.cookie_dict_tiktok)\n            self.headers_tiktok[\"Cookie\"] = cookie\n            self.headers_download_tiktok[\"Cookie\"] = cookie\n        elif self.cookie_str_tiktok:\n            self.headers_tiktok[\"Cookie\"] = self.cookie_str_tiktok\n            self.headers_download_tiktok[\"Cookie\"] = self.cookie_str_tiktok\n\n    def set_download_headers(self) -> None:\n        self.__update_download_headers()\n        self.__update_download_headers_tiktok()\n\n    def __update_download_headers(self) -> None:\n        self.headers_download[\"Cookie\"] = \"dy_swidth=1536; dy_sheight=864\"\n\n    def __update_download_headers_tiktok(self) -> None:\n        key = \"tt_chain_token\"\n        if tk := self.cookie_dict_tiktok.get(\n            key,\n        ):\n            self.headers_download_tiktok[\"Cookie\"] = f\"{key}={tk}\"\n        else:\n            self.headers_download_tiktok[\"Cookie\"] = self.cookie_str_tiktok\n        # self.headers_download_tiktok[\"Cookie\"] = self.headers_tiktok.get(\n        #     \"Cookie\", \"\")\n\n    async def __get_token_params(self) -> dict:\n        # if not (\n        #     m := (\n        #         self.cookie_dict.get(MsToken.NAME)\n        #         or self.get_cookie_value(\n        #             self.cookie_str,\n        #             MsToken.NAME,\n        #         )\n        #     )\n        # ):\n        #     self.logger.warning(\n        #         _(\"抖音 cookie 缺少 {name} 键值对，请尝试重新写入 cookie\").format(\n        #             name=MsToken.NAME\n        #         )\n        #     )\n        #     return {}\n        if d := await MsToken.get_real_ms_token(\n            self.logger,\n            self.headers_params,\n            # m,\n            proxy=self.proxy,\n        ):\n            self.logger.info(\n                f\"抖音 MsToken 请求值: {d[MsToken.NAME]}\",\n                False,\n            )\n            return d\n        else:\n            ms_token = self.cookie_dict.get(MsToken.NAME) or self.get_cookie_value(\n                self.cookie_str,\n                MsToken.NAME,\n            )\n            self.logger.info(\n                f\"抖音 MsToken 本地值: {ms_token}\",\n                False,\n            )\n            return {MsToken.NAME: ms_token}\n\n    async def __get_token_params_tiktok(self) -> dict:\n        if not (\n            m := (\n                self.cookie_dict_tiktok.get(MsTokenTikTok.NAME)\n                or self.get_cookie_value(\n                    self.cookie_str_tiktok,\n                    MsTokenTikTok.NAME,\n                )\n            )\n        ):\n            self.logger.warning(\n                _(\"TikTok cookie 缺少 {name} 键值对，请尝试重新写入 cookie\").format(\n                    name=MsTokenTikTok.NAME\n                )\n            )\n            return {}\n        # if d := await MsTokenTikTok.get_long_ms_token(\n        #     self.logger,\n        #     self.headers_params_tiktok,\n        #     m,\n        #     proxy=self.proxy_tiktok,\n        # ):\n        #     self.logger.info(\n        #         f\"TikTok MsToken 请求值: {d[MsTokenTikTok.NAME]}\",\n        #         False,\n        #     )\n        #     return d\n        # else:\n        #     self.logger.info(\n        #         f\"TikTok MsToken 本地值: {m}\",\n        #         False,\n        #     )\n        #     return {MsTokenTikTok.NAME: m}\n        return {MsTokenTikTok.NAME: m}\n\n    def set_uif_id(\n        self,\n    ) -> None:\n        if self.cookie_dict:\n            API.params[\"uifid\"] = self.cookie_dict.get(\"UIFID\", \"\")\n        elif self.cookie_str:\n            API.params[\"uifid\"] = self.get_cookie_value(\n                self.cookie_str,\n                \"UIFID\",\n            )\n\n    @staticmethod\n    def __generate_ffmpeg_object(ffmpeg_path: str) -> FFMPEG:\n        return FFMPEG(ffmpeg_path)\n\n    def get_settings_data(self) -> dict:\n        return {\n            \"accounts_urls\": [vars(i) for i in self.accounts_urls],\n            \"accounts_urls_tiktok\": [vars(i) for i in self.accounts_urls_tiktok],\n            \"mix_urls\": [vars(i) for i in self.mix_urls],\n            \"mix_urls_tiktok\": [vars(i) for i in self.mix_urls_tiktok],\n            \"owner_url\": vars(self.owner_url),\n            \"owner_url_tiktok\": self.owner_url_tiktok,\n            \"root\": str(self.root.resolve()),\n            \"folder_name\": self.folder_name,\n            \"name_format\": \" \".join(self.name_format),\n            \"desc_length\": self.desc_length,\n            \"name_length\": self.name_length,\n            \"date_format\": self.date_format,\n            \"split\": self.split,\n            \"folder_mode\": self.folder_mode,\n            \"music\": self.music,\n            \"truncate\": self.truncate,\n            \"storage_format\": self.storage_format,\n            \"cookie\": self.cookie_str or self.cookie_dict,\n            \"cookie_tiktok\": self.cookie_str_tiktok or self.cookie_dict_tiktok,\n            \"dynamic_cover\": self.dynamic_cover,\n            \"static_cover\": self.static_cover,\n            \"proxy\": self.proxy,\n            \"proxy_tiktok\": self.proxy_tiktok,\n            \"twc_tiktok\": self.twc_tiktok,\n            \"download\": self.download,\n            \"max_size\": self.max_size,\n            \"chunk\": self.chunk,\n            \"max_retry\": self.max_retry,\n            \"max_pages\": self.max_pages,\n            \"run_command\": \" \".join(self.run_command[::-1]),\n            \"ffmpeg\": self.ffmpeg.path or \"\",\n        }\n\n    async def set_settings_data(\n        self,\n        data: dict,\n    ) -> None:\n        self.set_urls_params(\n            data.pop(\"accounts_urls\"),\n            data.pop(\"mix_urls\"),\n            data.pop(\"owner_url\"),\n            data.pop(\"accounts_urls_tiktok\"),\n            data.pop(\"mix_urls_tiktok\"),\n            data.pop(\"owner_url_tiktok\"),\n        )\n        self.set_cookie(\n            data.pop(\n                \"cookie\",\n            ),\n            data.pop(\n                \"cookie_tiktok\",\n            ),\n        )\n        self.set_browser_info(\n            data.pop(\n                \"browser_info\",\n            ),\n            data.pop(\n                \"browser_info_tiktok\",\n            ),\n        )\n        await self.set_proxy(\n            data.pop(\n                \"proxy\",\n            ),\n            data.pop(\n                \"proxy_tiktok\",\n            ),\n        )\n        self.set_general_params(data)\n\n    async def __update_cookie_data(self, data: dict) -> None:\n        for i, j in zip((\"cookie\", \"cookie_tiktok\"), (_(\"抖音\"), \"TikTok\")):\n            if c := data.get(i):\n                setattr(\n                    self, i, self.cookie_object.extract(c, False, key=i, platform=j)\n                )\n        await self.update_params()\n\n    @staticmethod\n    def check_urls_params(data: list[dict]) -> list[SimpleNamespace]:\n        items = []\n        for item in data:\n            if not item.get(\"url\") or not item.get(\"enable\", True):\n                continue\n            if not isinstance(item.get(\"mark\"), str):\n                item[\"mark\"] = \"\"\n            items.append(item)\n        return Extractor.generate_data_object(items)\n\n    @staticmethod\n    def check_url_params(data: dict) -> SimpleNamespace:\n        if not data.get(\"url\"):\n            return SimpleNamespace(\n                mark=\"\",\n                url=\"\",\n            )\n        if not isinstance(data.get(\"mark\"), str):\n            data[\"mark\"] = \"\"\n        return Extractor.generate_data_object(data)\n\n    def set_urls_params(\n        self,\n        accounts_urls: list[dict],\n        mix_urls: list[dict],\n        owner_url: dict,\n        accounts_urls_tiktok: list[dict],\n        mix_urls_tiktok: list[dict],\n        owner_url_tiktok: dict,\n    ):\n        if accounts_urls:\n            self.accounts_urls = self.check_urls_params(accounts_urls)\n        if accounts_urls_tiktok:\n            self.accounts_urls_tiktok = self.check_urls_params(accounts_urls_tiktok)\n        if mix_urls:\n            self.mix_urls = self.check_urls_params(mix_urls)\n        if mix_urls_tiktok:\n            self.mix_urls_tiktok = self.check_urls_params(mix_urls_tiktok)\n        if owner_url:\n            self.owner_url = self.check_url_params(owner_url)\n        # if owner_url_tiktok:\n        #     self.owner_url_tiktok = self.check_url_params(owner_url_tiktok)\n\n    def set_cookie(\n        self, cookie: str | dict[str, str], cookie_tiktok: str | dict[str, str]\n    ):\n        if cookie:\n            self.cookie_dict, self.cookie_str = self.__check_cookie(cookie)\n            self.cookie_state: bool = self.__check_cookie_state()\n            self.set_uif_id()\n        if cookie_tiktok:\n            self.cookie_dict_tiktok, self.cookie_str_tiktok = (\n                self.__check_cookie_tiktok(\n                    cookie_tiktok,\n                )\n            )\n            self.cookie_tiktok_state: bool = self.__check_cookie_state(True)\n            self.__update_download_headers_tiktok()\n\n    def set_general_params(self, data: dict[str, Any]) -> None:\n        for i, j in data.items():\n            if j is not None:\n                self.__CHECK[i](j)\n\n    async def set_proxy(self, proxy: str | None, proxy_tiktok: str | None):\n        if isinstance(proxy, str):\n            self.proxy: str | None = self.__check_proxy(\n                proxy,\n                remark=_(\"抖音\"),\n                enable=self.douyin_platform,\n            )\n        if isinstance(proxy_tiktok, str):\n            self.proxy_tiktok: str | None = self.__check_proxy_tiktok(proxy_tiktok)\n        await self.close_client()\n        self.client = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy,\n        )\n        self.client_tiktok = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy_tiktok,\n        )\n\n    @staticmethod\n    def merge_browser_info(\n        browser_info: dict,\n        new_info: dict,\n    ) -> dict:\n        return browser_info | new_info\n\n    def set_browser_info(self, browser_info: dict, browser_info_tiktok: dict):\n        self.browser_info = self.merge_browser_info(\n            self.browser_info,\n            browser_info or {},\n        )\n        self.browser_info_tiktok = self.merge_browser_info(\n            self.browser_info_tiktok,\n            browser_info_tiktok or {},\n        )\n        self.__set_browser_info(self.browser_info)\n        self.__set_browser_info_tiktok(self.browser_info_tiktok)\n\n    @staticmethod\n    def check_str(value: str) -> str:\n        return value if isinstance(value, str) else \"\"\n\n    async def close_client(self) -> None:\n        await self.client.aclose()\n        await self.client_tiktok.aclose()\n\n    def __generate_folders(self):\n        self.compatible()\n        self.cache.mkdir(exist_ok=True)\n\n    def __set_browser_info(\n        self,\n        info: dict[str, str],\n    ) -> None:\n        self.logger.info(f\"抖音浏览器信息: {info}\", False)\n        if ua := info.get(\n            \"User-Agent\",\n        ):\n            for i in (\n                self.headers,\n                self.headers_download,\n                self.headers_params,\n                self.headers_qrcode,\n            ):\n                i[\"User-Agent\"] = ua\n        else:\n            ua = USERAGENT\n        for i in (\n            \"pc_libra_divert\",\n            \"browser_language\",\n            \"browser_platform\",\n            \"browser_name\",\n            \"browser_version\",\n            \"engine_name\",\n            \"engine_version\",\n            \"os_name\",\n            \"os_version\",\n            # 'webid',\n        ):\n            if v := info.get(\n                i,\n            ):\n                API.params[i] = v\n        self.ab = ABogus(\n            ua,\n            info.get(\n                \"browser_platform\",\n            ),\n        )\n\n    def __set_browser_info_tiktok(\n        self,\n        info: dict,\n    ):\n        self.logger.info(f\"TikTok 浏览器信息: {info}\", False)\n        if ua := info.get(\n            \"User-Agent\",\n        ):\n            for i in (\n                self.headers_tiktok,\n                self.headers_download_tiktok,\n                self.headers_params_tiktok,\n            ):\n                i[\"User-Agent\"] = ua\n        for i in (\n            \"app_language\",\n            \"browser_language\",\n            \"browser_name\",\n            \"browser_platform\",\n            \"browser_version\",\n            \"language\",\n            \"os\",\n            \"priority_region\",\n            \"region\",\n            \"tz_name\",\n            \"webcast_language\",\n            \"device_id\",\n        ):\n            if v := info.get(\n                i,\n            ):\n                APITikTok.params[i] = v\n\n    def __check_truncate(self, truncate: int) -> int:\n        return self.__check_number_value(\n            truncate,\n            \"truncate\",\n            25,\n            50,\n        )\n\n    def __check_name_length(self, name_length: int) -> int:\n        return self.__check_number_value(\n            name_length,\n            \"name_length\",\n            32,\n            128,\n        )\n\n    def __check_desc_length(self, desc_length: int) -> int:\n        return self.__check_number_value(\n            desc_length,\n            \"desc_length\",\n            16,\n            64,\n        )\n\n    def __check_number_value(\n        self, value: int, name: str, minimum: int, default: int\n    ) -> int:\n        if isinstance(value, int):\n            if value >= minimum:\n                self.logger.info(f\"{name} 参数已设置为 {value}\", False)\n                return value\n            self.logger.warning(\n                _(\"{key} 参数 {value} 设置过小，程序将使用默认值：{default}\").format(\n                    key=name,\n                    value=value,\n                    default=default,\n                ),\n            )\n            return default\n        self.logger.warning(\n            _(\"{key} 参数 {value} 设置错误，程序将使用默认值：{default}\").format(\n                key=name,\n                value=value,\n                default=default,\n            ),\n        )\n        return default\n\n    def __check_live_qualities(self, live_qualities: str) -> str:\n        if isinstance(live_qualities, str):\n            self.logger.info(f\"live_qualities 参数已设置为 {live_qualities}\", False)\n            return live_qualities\n        self.logger.warning(\n            _(\"live_qualities 参数 {live_qualities} 设置错误\").format(\n                live_qualities=live_qualities\n            ),\n        )\n        return \"\"\n\n    def __check_cookie_state(self, tiktok=False) -> bool:\n        if tiktok:\n            return (self.cookie_object.STATE_KEY in self.cookie_dict_tiktok) or (\n                self.cookie_object.STATE_KEY in self.cookie_str_tiktok\n            )\n        return (self.cookie_object.STATE_KEY in self.cookie_dict) or (\n            self.cookie_object.STATE_KEY in self.cookie_str\n        )\n\n    @staticmethod\n    def get_cookie_value(cookie_str: str, key: str, return_key=False) -> str:\n        \"\"\"\n        解析cookie字符串并返回指定键的值或键值对\n\n        :param cookie_str: cookie字符串（格式如 \"name=John; age=30;\"）\n        :param key: 需要获取的键名\n        :param return_key: 是否返回键值对格式，默认为False\n        :return: 键值对字符串或值（若不存在返回None）\n        \"\"\"\n        cookies = {}\n        for pair in cookie_str.split(\";\"):\n            pair = pair.strip()\n            if not pair:\n                continue\n            # 分割键值（最多分割一次，应对含等号的值）\n            key_value = pair.split(\"=\", 1)\n            if len(key_value) != 2:\n                continue  # 跳过无效格式\n            k, v = key_value[0].strip(), key_value[1].strip()\n            cookies[k] = v\n\n        value = cookies.get(key)\n        if value is None:\n            return \"\"\n\n        return f\"{key}={value}\" if return_key else value\n\n    def compatible(self):\n        if (\n            old := self.ROOT.parent.joinpath(\"Cache\")\n        ).exists() and not self.cache.exists():\n            move(old, self.cache)\n"
  },
  {
    "path": "src/config/settings.py",
    "content": "from json import dump, load\nfrom json.decoder import JSONDecodeError\nfrom platform import system\nfrom shutil import move\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING\n\nfrom ..custom import USERAGENT\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    from ..tools import ColorfulConsole\n\n__all__ = [\"Settings\"]\n\n\nclass Settings:\n    encode = \"UTF-8-SIG\" if system() == \"Windows\" else \"UTF-8\"\n    default = {\n        \"accounts_urls\": [\n            {\n                \"mark\": \"\",\n                \"url\": \"\",\n                \"tab\": \"\",\n                \"earliest\": \"\",\n                \"latest\": \"\",\n                \"enable\": True,\n            },\n        ],\n        \"accounts_urls_tiktok\": [\n            {\n                \"mark\": \"\",\n                \"url\": \"\",\n                \"tab\": \"\",\n                \"earliest\": \"\",\n                \"latest\": \"\",\n                \"enable\": True,\n            },\n        ],\n        \"mix_urls\": [\n            {\n                \"mark\": \"\",\n                \"url\": \"\",\n                \"enable\": True,\n            },\n        ],\n        \"mix_urls_tiktok\": [\n            {\n                \"mark\": \"\",\n                \"url\": \"\",\n                \"enable\": True,\n            },\n        ],\n        \"owner_url\": {\n            \"mark\": \"\",\n            \"url\": \"\",\n            \"uid\": \"\",\n            \"sec_uid\": \"\",\n            \"nickname\": \"\",\n        },\n        \"owner_url_tiktok\": None,\n        \"root\": \"\",\n        \"folder_name\": \"Download\",\n        \"name_format\": \"create_time type nickname desc\",\n        \"desc_length\": 64,\n        \"name_length\": 128,\n        \"date_format\": \"%Y-%m-%d %H:%M:%S\",\n        \"split\": \"-\",\n        \"folder_mode\": False,\n        \"music\": False,\n        \"truncate\": 50,\n        \"storage_format\": \"\",\n        \"cookie\": \"\",\n        \"cookie_tiktok\": \"\",\n        \"dynamic_cover\": False,\n        \"static_cover\": False,\n        \"proxy\": \"\",\n        \"proxy_tiktok\": \"\",\n        \"twc_tiktok\": \"\",\n        \"download\": True,\n        \"max_size\": 0,\n        \"chunk\": 1024 * 1024 * 2,  # 每次从服务器接收的数据块大小\n        \"timeout\": 10,\n        \"max_retry\": 5,  # 重试最大次数\n        \"max_pages\": 0,\n        \"run_command\": \"\",\n        \"ffmpeg\": \"\",\n        \"live_qualities\": \"\",\n        \"douyin_platform\": True,\n        \"tiktok_platform\": True,\n        \"browser_info\": {\n            \"User-Agent\": USERAGENT,\n            \"pc_libra_divert\": \"Windows\",\n            \"browser_language\": \"zh-CN\",\n            \"browser_platform\": \"Win32\",\n            \"browser_name\": \"Chrome\",\n            \"browser_version\": \"139.0.0.0\",\n            \"engine_name\": \"Blink\",\n            \"engine_version\": \"139.0.0.0\",\n            \"os_name\": \"Windows\",\n            \"os_version\": \"10\",\n            \"webid\": \"\",\n        },\n        \"browser_info_tiktok\": {\n            \"User-Agent\": USERAGENT,\n            \"app_language\": \"zh-Hans\",\n            \"browser_language\": \"zh-CN\",\n            \"browser_name\": \"Mozilla\",\n            \"browser_platform\": \"Win32\",\n            \"browser_version\": \"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n            \"language\": \"zh-Hans\",\n            \"os\": \"windows\",\n            \"priority_region\": \"US\",\n            \"region\": \"US\",\n            \"tz_name\": \"Asia/Shanghai\",\n            \"webcast_language\": \"zh-Hans\",\n            \"device_id\": \"\",\n        },\n    }  # 默认配置\n    rename_params = (\n        (\n            \"default_mode\",\n            \"run_command\",\n            \"\",\n        ),\n        (\n            \"update_cookie\",\n            \"douyin_platform\",\n            True,\n        ),\n        (\n            \"update_cookie_tiktok\",\n            \"tiktok_platform\",\n            True,\n        ),\n        (\n            \"original_cover\",\n            \"static_cover\",\n            False,\n        ),\n    )  # 兼容旧版本配置文件\n\n    def __init__(self, root: \"Path\", console: \"ColorfulConsole\"):\n        self.root = root\n        self.file = \"settings.json\"\n        self.path = root.joinpath(self.file)  # 配置文件\n        self.console = console\n\n    def __create(self) -> dict:\n        \"\"\"创建默认配置文件\"\"\"\n        with self.path.open(\"w\", encoding=self.encode) as f:\n            dump(self.default, f, indent=4, ensure_ascii=False)\n        self.console.info(\n            _(\n                \"创建默认配置文件 settings.json 成功！\\n\"\n                \"请参考项目文档的快速入门部分，设置 Cookie 后重新运行程序！\\n\"\n                \"建议根据实际使用需求修改配置文件 settings.json！\\n\"\n            ),\n        )\n        return self.default\n\n    def read(self) -> dict:\n        \"\"\"读取配置文件，如果没有配置文件，则生成配置文件\"\"\"\n        self.compatible()\n        try:\n            if self.path.exists():\n                with self.path.open(\"r\", encoding=self.encode) as f:\n                    return self.__check(load(f))\n            return self.__create()  # 生成的默认配置文件必须设置 cookie 才可以正常运行\n        except JSONDecodeError:\n            self.console.error(\n                _(\"配置文件 settings.json 格式错误，请检查 JSON 格式！\"),\n            )\n            return self.default  # 读取配置文件发生错误时返回空配置\n\n    def __check(self, data: dict) -> dict:\n        data = self.__compatible_with_old_settings(data)\n        update = False\n        for i, j in self.default.items():\n            if i not in data:\n                data[i] = j\n                update = True\n                self.console.info(\n                    _(\"配置文件 settings.json 缺少参数 {i}，已自动添加该参数！\").format(\n                        i=i\n                    ),\n                )\n        if update:\n            self.update(data)\n        return data\n\n    def update(self, settings: dict | SimpleNamespace):\n        \"\"\"更新配置文件\"\"\"\n        with self.path.open(\"w\", encoding=self.encode) as f:\n            dump(\n                settings if isinstance(settings, dict) else vars(settings),\n                f,\n                indent=4,\n                ensure_ascii=False,\n            )\n        self.console.info(\n            _(\"保存配置成功！\"),\n        )\n\n    def __compatible_with_old_settings(\n        self,\n        data: dict,\n    ) -> dict:\n        \"\"\"兼容旧版本配置文件\"\"\"\n        for old, new_, default in self.rename_params:\n            if old in data:\n                self.console.info(\n                    _(\n                        \"配置文件 {old} 参数已变更为 {new} 参数，请注意修改配置文件！\"\n                    ).format(old=old, new=new_),\n                )\n                data[new_] = data.get(\n                    new_,\n                    data.get(\n                        old,\n                        default,\n                    ),\n                )\n        return data\n\n    def compatible(self):\n        if (\n            old := self.root.parent.joinpath(self.file)\n        ).exists() and not self.path.exists():\n            move(old, self.path)\n"
  },
  {
    "path": "src/custom/__init__.py",
    "content": "from .function import (\n    wait,\n    failure_handling,\n    condition_filter,\n    suspend,\n    is_valid_token,\n)\nfrom .internal import (\n    DISCLAIMER_TEXT,\n    PROJECT_ROOT,\n    VERSION_MAJOR,\n    VERSION_MINOR,\n    VERSION_BETA,\n    RELEASES,\n    REPOSITORY,\n    LICENCE,\n    DOCUMENTATION_URL,\n    USERAGENT,\n    RETRY,\n    BLANK_PREVIEW,\n    TIMEOUT,\n    PROJECT_NAME,\n    DATA_HEADERS,\n    PARAMS_HEADERS,\n    DOWNLOAD_HEADERS,\n    QRCODE_HEADERS,\n    DOWNLOAD_HEADERS_TIKTOK,\n    PHONE_HEADERS,\n    PARAMS_HEADERS_TIKTOK,\n    DATA_HEADERS_TIKTOK,\n    VIDEO_INDEX,\n    VIDEO_TIKTOK_INDEX,\n    IMAGE_INDEX,\n    IMAGE_TIKTOK_INDEX,\n    VIDEOS_INDEX,\n    DYNAMIC_COVER_INDEX,\n    STATIC_COVER_INDEX,\n    MUSIC_INDEX,\n    COMMENT_IMAGE_INDEX,\n    COMMENT_STICKER_INDEX,\n    LIVE_COVER_INDEX,\n    AUTHOR_COVER_INDEX,\n    HOT_WORD_COVER_INDEX,\n    COMMENT_IMAGE_LIST_INDEX,\n    BITRATE_INFO_TIKTOK_INDEX,\n    LIVE_DATA_INDEX,\n    AVATAR_LARGER_INDEX,\n    AUTHOR_COVER_URL_INDEX,\n    SEARCH_USER_INDEX,\n    SEARCH_AVATAR_INDEX,\n    MUSIC_COLLECTION_COVER_INDEX,\n    MUSIC_COLLECTION_DOWNLOAD_INDEX,\n    __VERSION__,\n    BLANK_HEADERS,\n)\nfrom .static import (\n    MAX_WORKERS,\n    TEXT_REPLACEMENT,\n    SERVER_HOST,\n    SERVER_PORT,\n    MASTER,\n    PROMPT,\n    WARNING,\n    ERROR,\n    INFO,\n    GENERAL,\n    PROGRESS,\n    DEBUG,\n    COOKIE_UPDATE_INTERVAL,\n    FILE_SIGNATURES,\n    FILE_SIGNATURES_LENGTH,\n)\n"
  },
  {
    "path": "src/custom/function.py",
    "content": "from asyncio import sleep\nfrom random import randint\nfrom typing import TYPE_CHECKING\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.tools import ColorfulConsole\n\n\nasync def wait() -> None:\n    \"\"\"\n    设置网络请求间隔时间，仅对获取数据生效，不影响下载文件\n    \"\"\"\n    # 随机延时\n    await sleep(randint(5, 20) * 0.1)\n    # 固定延时\n    # await sleep(1)\n    # 取消延时\n    # pass\n\n\ndef failure_handling() -> bool:\n    \"\"\"批量下载账号作品模式 和 批量下载合集作品模式 获取数据失败时，是否继续执行\"\"\"\n    # 询问用户\n    # return bool(input(_(\"输入任意字符继续处理账号/合集，直接回车停止处理账号/合集: \")))\n    # 继续执行\n    return True\n    # 结束执行\n    # return False\n\n\ndef condition_filter(data: dict) -> bool:\n    \"\"\"\n    自定义作品筛选规则，例如：筛选作品点赞数、作品类型、视频分辨率等\n    需要排除的作品返回 False，否则返回 True\n    \"\"\"\n    # if data[\"ratio\"] in (\"720p\", \"540p\"):\n    #     return False  # 过滤低分辨率的视频作品\n    return True\n\n\nasync def suspend(count: int, console: \"ColorfulConsole\") -> None:\n    \"\"\"\n    如需采集大量数据，请启用该函数，可以在处理指定数量的数据后，暂停一段时间，然后继续运行\n    batches: 每次处理的数据数量上限，比如：每次处理 10 个数据，就会暂停程序\n    rest_time: 程序暂停的时间，单位：秒；比如：每处理 10 个数据，就暂停 5 分钟\n    仅对 批量下载账号作品模式 和 批量下载合集作品模式 生效\n    说明: 此处的一个数据代表一个账号或者一个合集，并非代表一个数据包\n    \"\"\"\n    # 启用该函数\n    batches = 10  # 根据实际需求修改\n    if not count % batches:\n        rest_time = 60 * 5  # 根据实际需求修改\n        console.print(\n            _(\n                \"程序连续处理了 {batches} 个数据，为了避免请求频率过高导致账号或 IP 被风控，\"\n                \"程序已经暂停运行，将在 {rest_time} 秒后恢复运行！\"\n            ).format(batches=batches, rest_time=rest_time),\n        )\n        await sleep(rest_time)\n    # 禁用该函数\n    # pass\n\n\ndef is_valid_token(token: str) -> bool:\n    \"\"\"Web API 接口模式 和 Web UI 交互模式 token 参数验证\"\"\"\n    return True\n"
  },
  {
    "path": "src/custom/internal.py",
    "content": "from pathlib import Path\n\nPROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.joinpath(\"Volume\")\nPROJECT_ROOT.mkdir(exist_ok=True)\nVERSION_MAJOR = 5\nVERSION_MINOR = 8\nVERSION_BETA = True\n__VERSION__ = f\"{VERSION_MAJOR}.{VERSION_MINOR}.{'beta' if VERSION_BETA else 'stable'}\"\nPROJECT_NAME = f\"DouK-Downloader V{VERSION_MAJOR}.{VERSION_MINOR} {\n    'Beta' if VERSION_BETA else 'Stable'\n}\"\n\nREPOSITORY = \"https://github.com/JoeanAmier/TikTokDownloader\"\nLICENCE = \"GNU General Public License v3.0\"\nDOCUMENTATION_URL = \"https://github.com/JoeanAmier/TikTokDownloader/wiki/Documentation\"\nRELEASES = \"https://github.com/JoeanAmier/TikTokDownloader/releases/latest\"\n\nDISCLAIMER_TEXT = (\n    \"关于 DouK-Downloader 的 免责声明：\\n\"\n    \"\\n\"\n    \"1. 使用者对本项目的使用由使用者自行决定，并自行承担风险。作者对使用者使用本项目所产生的任何损失、责任、或风险概不负责。\\n\"\n    \"2. 本项目的作者提供的代码和功能是基于现有知识和技术的开发成果。作者按现有技术水平努力确保代码的正确性和安全性，但不保证代码完全没有错误或缺陷。\\n\"\n    \"3. 本项目依赖的所有第三方库、插件或服务各自遵循其原始开源或商业许可，使用者需自行查阅并遵守相应协议，作者不对第三方组件的稳定性、安全性及合规性承担任何责任。\\n\"\n    \"4. 使用者在使用本项目时必须严格遵守 GNU General Public License v3.0 的要求，并在适当的地方注明使用了 GNU General Public License v3.0 的代码。\\n\"\n    \"5. 使用者在使用本项目的代码和功能时，必须自行研究相关法律法规，并确保其使用行为合法合规。任何因违反法律法规而导致的法律责任和风险，均由使用者自行承担。\\n\"\n    \"6. 使用者不得使用本工具从事任何侵犯知识产权的行为，包括但不限于未经授权下载、传播受版权保护的内容，开发者不参与、不支持、不认可任何非法内容的获取或分发。\\n\"\n    \"7. 本项目不对使用者涉及的数据收集、存储、传输等处理活动的合规性承担责任。使用者应自行遵守相关法律法规，确保处理行为合法正当；因违规操作导致的法律责任由使用者自行承担。\\n\"\n    \"8. 使用者在任何情况下均不得将本项目的作者、贡献者或其他相关方与使用者的使用行为联系起来，或要求其对使用者使用本项目所产生的任何损失或损害负责。\\n\"\n    \"9. 本项目的作者不会提供 DouK-Downloader 项目的付费版本，也不会提供与 DouK-Downloader 项目相关的任何商业服务。\\n\"\n    \"10. 基于本项目进行的任何二次开发、修改或编译的程序与原创作者无关，原创作者不承担与二次开发行为或其结果相关的任何责任，使用者应自行对因二次开发可能带来的各种情况负全部责任。\\n\"\n    \"11. 本项目不授予使用者任何专利许可；若使用本项目导致专利纠纷或侵权，使用者自行承担全部风险和责任。未经作者或权利人书面授权，不得使用本项目进行任何商业宣传、推广或再授权。\\n\"\n    \"12. 作者保留随时终止向任何违反本声明的使用者提供服务的权利，并可能要求其销毁已获取的代码及衍生作品。\\n\"\n    \"13. 作者保留在不另行通知的情况下更新本声明的权利，使用者持续使用即视为接受修订后的条款。\\n\"\n    \"\\n\"\n    \"在使用本项目的代码和功能之前，请您认真考虑并接受以上免责声明。如果您对上述声\"\n    \"明有任何疑问或不同意，请不要使用本项目的代码和功能。如果您使用了本项目的代码\"\n    \"和功能，则视为您已完全理解并接受上述免责声明，并自愿承担使用本项目的一切风险\"\n    \"和后果。\\n\"\n)\n\nRETRY = 5\nTIMEOUT = 10\n\nPHONE_HEADERS = {\n    \"User-Agent\": \"Mozilla/5.0 (iPhone; CPU iPhone OS 17_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) \"\n    \"CriOS/125.0.6422.51 Mobile/15E148 Safari/604.1\",\n}\nUSERAGENT = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\"\nBLANK_HEADERS = {\n    \"User-Agent\": USERAGENT,\n}\nREFERER = \"https://www.douyin.com/?recommend=1\"\nREFERER_TIKTOK = \"https://www.tiktok.com/explore\"\nPARAMS_HEADERS = {\n    \"Accept\": \"*/*\",\n    \"Accept-Encoding\": \"*/*\",\n    \"Content-Type\": \"text/plain;charset=UTF-8\",\n    \"Referer\": REFERER,\n    \"User-Agent\": USERAGENT,\n}\nPARAMS_HEADERS_TIKTOK = PARAMS_HEADERS | {\n    \"Referer\": REFERER_TIKTOK,\n}\nDATA_HEADERS = {\n    \"Accept\": \"*/*\",\n    \"Accept-Encoding\": \"*/*\",\n    \"Referer\": REFERER,\n    \"User-Agent\": USERAGENT,\n}\nDATA_HEADERS_TIKTOK = DATA_HEADERS | {\n    \"Referer\": REFERER_TIKTOK,\n}\nDOWNLOAD_HEADERS = {\n    \"Accept\": \"*/*\",\n    \"Range\": \"bytes=0-\",\n    \"Referer\": REFERER,\n    \"User-Agent\": USERAGENT,\n}\nDOWNLOAD_HEADERS_TIKTOK = DOWNLOAD_HEADERS | {\n    \"Referer\": REFERER_TIKTOK,\n}\nQRCODE_HEADERS = {\n    \"Accept\": \"*/*\",\n    \"Accept-Encoding\": \"*/*\",\n    \"Referer\": REFERER,\n    \"User-Agent\": USERAGENT,\n}\n\nBLANK_PREVIEW = \"static/images/blank.png\"\n\nVIDEO_INDEX: int = -1\nVIDEO_TIKTOK_INDEX: int = 0\nIMAGE_INDEX: int = -1\nIMAGE_TIKTOK_INDEX: int = -1\nVIDEOS_INDEX: int = -1\nDYNAMIC_COVER_INDEX: int = -1\nSTATIC_COVER_INDEX: int = -1\nMUSIC_INDEX: int = -1\nCOMMENT_IMAGE_INDEX: int = -1\nCOMMENT_STICKER_INDEX: int = -1\nLIVE_COVER_INDEX: int = -1\nAUTHOR_COVER_INDEX: int = -1\nHOT_WORD_COVER_INDEX: int = -1\nCOMMENT_IMAGE_LIST_INDEX: int = 0\nBITRATE_INFO_TIKTOK_INDEX: int = 0\nLIVE_DATA_INDEX: int = 0\nAVATAR_LARGER_INDEX: int = 0\nAUTHOR_COVER_URL_INDEX: int = 0\nSEARCH_USER_INDEX: int = 0\nSEARCH_AVATAR_INDEX: int = 0\nMUSIC_COLLECTION_COVER_INDEX: int = 0\nMUSIC_COLLECTION_DOWNLOAD_INDEX: int = 0\n\nif __name__ == \"__main__\":\n    print(__VERSION__)\n"
  },
  {
    "path": "src/custom/static.py",
    "content": "# 同时下载作品文件的最大任务数，对直播无效\nMAX_WORKERS = 4\n\n# 非法字符替换规则，key 为替换前的文本，value 为替换后的文本\nTEXT_REPLACEMENT = {\n    \" \": \" \",\n}\n\n# 服务器模式主机，对 Web API 接口模式、Web UI 交互模式 生效，设置为 \"127.0.0.1\" 代表仅本地可用\nSERVER_HOST = \"0.0.0.0\"\n\n# 服务器模式端口，对 Web API 接口模式、Web UI 交互模式 生效\nSERVER_PORT = 5555\n\n# Cookie 更新间隔，单位：秒\nCOOKIE_UPDATE_INTERVAL = 15 * 60\n\n# 彩色交互提示颜色设置，支持标准颜色名称、Hex、RGB 格式\nMASTER = \"b #fff200\"\nPROMPT = \"b turquoise2\"\nGENERAL = \"b bright_white\"\nPROGRESS = \"b bright_magenta\"\nERROR = \"b bright_red\"\nWARNING = \"b bright_yellow\"\nINFO = \"b bright_green\"\nDEBUG = \"b dark_orange\"\n\n# 文件类型签名\nFILE_SIGNATURES: tuple[\n    tuple[\n        int,\n        bytes,\n        str,\n    ],\n    ...,\n] = (\n    # 分别为偏移量(字节)、十六进制签名、后缀\n    # 参考：https://en.wikipedia.org/wiki/List_of_file_signatures\n    # 参考：https://www.garykessler.net/library/file_sigs.html\n    (0, b\"\\xff\\xd8\\xff\", \"jpg\"),\n    (0, b\"\\x89\\x50\\x4e\\x47\\x0d\\x0a\\x1a\\x0a\", \"png\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x61\\x76\\x69\\x66\", \"avif\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x68\\x65\\x69\\x63\", \"heic\"),\n    (8, b\"\\x57\\x45\\x42\\x50\", \"webp\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x4d\\x53\\x4e\\x56\", \"mp4\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x69\\x73\\x6f\\x6d\", \"mp4\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x6d\\x70\\x34\\x32\", \"m4v\"),\n    (4, b\"\\x66\\x74\\x79\\x70\\x71\\x74\\x20\\x20\", \"mov\"),\n    (0, b\"\\x1a\\x45\\xdf\\xa3\", \"mkv\"),\n    (0, b\"\\x00\\x00\\x01\\xb3\", \"mpg\"),\n    (0, b\"\\x00\\x00\\x01\\xba\", \"mpg\"),\n    (0, b\"\\x46\\x4c\\x56\\x01\", \"flv\"),\n    (8, b\"\\x41\\x56\\x49\\x20\", \"avi\"),\n)\nFILE_SIGNATURES_LENGTH = max(\n    offset + len(signature) for offset, signature, _ in FILE_SIGNATURES\n)\n"
  },
  {
    "path": "src/downloader/__init__.py",
    "content": "from .download import Downloader\n\n__all__ = [\"Downloader\"]\n"
  },
  {
    "path": "src/downloader/download.py",
    "content": "from asyncio import Semaphore, gather\nfrom datetime import datetime\nfrom pathlib import Path\nfrom shutil import move\nfrom time import time\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING, Callable, Union\n\nfrom aiofiles import open\nfrom httpx import HTTPStatusError, RequestError, StreamError\nfrom rich.progress import (\n    BarColumn,\n    DownloadColumn,\n    Progress,\n    SpinnerColumn,\n    TextColumn,\n    TimeElapsedColumn,\n    TimeRemainingColumn,\n    TransferSpeedColumn,\n)\n\nfrom ..custom import (\n    MAX_WORKERS,\n    PROGRESS,\n)\nfrom ..tools import (\n    CacheError,\n    DownloaderError,\n    FakeProgress,\n    Retry,\n    beautify_string,\n    format_size,\n)\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from httpx import AsyncClient\n\n    from ..config import Parameter\n\n__all__ = [\"Downloader\"]\n\n\nclass Downloader:\n    semaphore = Semaphore(MAX_WORKERS)\n    CONTENT_TYPE_MAP = {\n        \"image/png\": \"png\",\n        \"image/jpeg\": \"jpeg\",\n        \"image/webp\": \"webp\",\n        \"video/mp4\": \"mp4\",\n        \"video/quicktime\": \"mov\",\n        \"audio/mp4\": \"m4a\",\n        \"audio/mpeg\": \"mp3\",\n    }\n\n    def __init__(\n        self,\n        params: \"Parameter\",\n        server_mode: bool = False,\n    ):\n        self.cleaner = params.CLEANER\n        self.client: \"AsyncClient\" = params.client\n        self.client_tiktok: \"AsyncClient\" = params.client_tiktok\n        self.headers = params.headers_download\n        self.headers_tiktok = params.headers_download_tiktok\n        self.log = params.logger\n        self.xb = params.xb\n        self.console = params.console\n        self.root = params.root\n        self.folder_name = params.folder_name\n        self.name_format = params.name_format\n        self.desc_length = params.desc_length\n        self.name_length = params.name_length\n        self.split = params.split\n        self.folder_mode = params.folder_mode\n        self.music = params.music\n        self.dynamic_cover = params.dynamic_cover\n        self.static_cover = params.static_cover\n        # self.cookie = params.cookie\n        # self.cookie_tiktok = params.cookie_tiktok\n        self.proxy = params.proxy\n        self.proxy_tiktok = params.proxy_tiktok\n        self.download = params.download\n        self.max_size = params.max_size\n        self.chunk = params.chunk\n        self.max_retry = params.max_retry\n        self.recorder = params.recorder\n        self.timeout = params.timeout\n        self.ffmpeg = params.ffmpeg\n        self.cache = params.cache\n        self.truncate = params.truncate\n        self.general_progress_object: Callable = self.init_general_progress(\n            server_mode,\n        )\n\n    def init_general_progress(\n        self,\n        server_mode: bool = False,\n    ) -> Callable:\n        if server_mode:\n            return self.__fake_progress_object\n        return self.__general_progress_object\n\n    @staticmethod\n    def __fake_progress_object(\n        *args,\n        **kwargs,\n    ):\n        return FakeProgress()\n\n    def __general_progress_object(self):\n        \"\"\"文件下载进度条\"\"\"\n        return Progress(\n            TextColumn(\n                \"[progress.description]{task.description}\",\n                style=PROGRESS,\n                justify=\"left\",\n            ),\n            SpinnerColumn(),\n            BarColumn(bar_width=20),\n            \"[progress.percentage]{task.percentage:>3.1f}%\",\n            \"•\",\n            DownloadColumn(binary_units=True),\n            \"•\",\n            TimeRemainingColumn(),\n            console=self.console,\n            transient=True,\n            expand=True,\n        )\n\n    def __live_progress_object(self):\n        \"\"\"直播下载进度条\"\"\"\n        return Progress(\n            TextColumn(\n                \"[progress.description]{task.description}\",\n                style=PROGRESS,\n                justify=\"left\",\n            ),\n            SpinnerColumn(),\n            BarColumn(bar_width=20),\n            \"•\",\n            TransferSpeedColumn(),\n            \"•\",\n            TimeElapsedColumn(),\n            console=self.console,\n            transient=True,\n            expand=True,\n        )\n\n    async def run(\n        self,\n        data: Union[list[dict], list[tuple]],\n        type_: str,\n        tiktok=False,\n        **kwargs,\n    ) -> None:\n        if not self.download or not data:\n            return\n        self.log.info(_(\"开始下载作品文件\"))\n        match type_:\n            case \"batch\":\n                await self.run_batch(data, tiktok, **kwargs)\n            case \"detail\":\n                await self.run_general(data, tiktok, **kwargs)\n            case \"music\":\n                await self.run_music(data, **kwargs)\n            case \"live\":\n                await self.run_live(data, tiktok, **kwargs)\n            case _:\n                raise ValueError\n\n    async def run_batch(\n        self,\n        data: list[dict],\n        tiktok: bool,\n        mode: str = \"\",\n        mark: str = \"\",\n        user_id: str = \"\",\n        user_name: str = \"\",\n        mix_id: str = \"\",\n        mix_title: str = \"\",\n        collect_id: str = \"\",\n        collect_name: str = \"\",\n    ):\n        root = self.storage_folder(\n            mode,\n            *self.data_classification(\n                mode,\n                mark,\n                user_id,\n                user_name,\n                mix_id,\n                mix_title,\n                collect_id,\n                collect_name,\n            ),\n        )\n        await self.batch_processing(\n            data,\n            root,\n            tiktok=tiktok,\n        )\n\n    async def run_general(self, data: list[dict], tiktok: bool, **kwargs):\n        root = self.storage_folder(mode=\"detail\")\n        await self.batch_processing(\n            data,\n            root,\n            tiktok=tiktok,\n        )\n\n    async def run_music(\n        self,\n        data: list[dict],\n        **kwargs,\n    ):\n        root = self.root.joinpath(\"Music\")\n        tasks = []\n        for i in data:\n            name = self.generate_music_name(i)\n            temp_root, actual_root = self.deal_folder_path(\n                root,\n                name,\n                False,\n            )\n            self.download_music(\n                tasks,\n                name,\n                i[\"id\"],\n                i,\n                temp_root,\n                actual_root,\n                \"download\",\n                True,\n                type_=_(\"音乐\"),\n            )\n        await self.downloader_chart(\n            tasks, SimpleNamespace(), self.general_progress_object(), **kwargs\n        )\n\n    async def run_live(\n        self,\n        data: list[tuple],\n        tiktok=False,\n        **kwargs,\n    ):\n        if not data or not self.download:\n            return\n        download_command = []\n        self.generate_live_commands(\n            data,\n            download_command,\n        )\n        self.console.info(\n            _(\"程序将会调用 ffmpeg 下载直播，关闭 DouK-Downloader 不会中断下载！\"),\n        )\n        self.__download_live(download_command, tiktok)\n\n    def generate_live_commands(\n        self,\n        data: list[tuple],\n        commands: list,\n        suffix: str = \"mp4\",\n    ):\n        root = self.root.joinpath(\"Live\")\n        root.mkdir(exist_ok=True)\n        for i, f, m in data:\n            name = self.cleaner.filter_name(\n                f\"{i['title']}{self.split}{i['nickname']}{self.split}{datetime.now():%Y-%m-%d %H.%M.%S}.{suffix}\",\n                f\"{int(time())}{self.split}{datetime.now():%Y-%m-%d %H.%M.%S}.{suffix}\",\n            )\n            path = root.joinpath(name)\n            commands.append(\n                (\n                    m,\n                    str(path.resolve()),\n                )\n            )\n\n    def __download_live(\n        self,\n        commands: list,\n        tiktok: bool,\n    ):\n        self.ffmpeg.download(\n            commands,\n            self.proxy_tiktok if tiktok else self.proxy,\n            self.headers[\"User-Agent\"],\n        )\n\n    async def batch_processing(self, data: list[dict], root: Path, **kwargs):\n        count = SimpleNamespace(\n            downloaded_image=set(),\n            skipped_image=set(),\n            downloaded_video=set(),\n            skipped_video=set(),\n            downloaded_live=set(),\n            skipped_live=set(),\n        )\n        tasks = []\n        for item in data:\n            item[\"desc\"] = beautify_string(\n                item[\"desc\"],\n                self.desc_length,\n            )\n            name = self.generate_detail_name(item)\n            temp_root, actual_root = self.deal_folder_path(\n                root,\n                name,\n                self.folder_mode,\n            )\n            params = {\n                \"tasks\": tasks,\n                \"name\": name,\n                \"id_\": item[\"id\"],\n                \"item\": item,\n                \"temp_root\": temp_root,\n                \"actual_root\": actual_root,\n            }\n            if (t := item[\"type\"]) == _(\"图集\"):\n                await self.download_image(\n                    **params,\n                    type_=_(\"图集\"),\n                    skipped=count.skipped_image,\n                )\n            elif t == _(\"视频\"):\n                await self.download_video(\n                    **params,\n                    type_=_(\"视频\"),\n                    skipped=count.skipped_video,\n                )\n            elif t == _(\"实况\"):\n                await self.download_image(\n                    suffix=\"mp4\",\n                    type_=_(\"实况\"),\n                    **params,\n                    skipped=count.skipped_live,\n                )\n            else:\n                raise DownloaderError\n            self.download_music(\n                **params,\n                type=_(\"音乐\"),\n            )\n            self.download_cover(**params)\n        await self.downloader_chart(\n            tasks, count, self.general_progress_object(), **kwargs\n        )\n        self.statistics_count(count)\n\n    async def downloader_chart(\n        self,\n        tasks: list[tuple],\n        count: SimpleNamespace,\n        progress: Progress,\n        semaphore: Semaphore = None,\n        **kwargs,\n    ):\n        with progress:\n            tasks = [\n                self.request_file(\n                    *task,\n                    count=count,\n                    **kwargs,\n                    progress=progress,\n                    semaphore=semaphore,\n                )\n                for task in tasks\n            ]\n            await gather(*tasks)\n\n    def deal_folder_path(\n        self,\n        root: Path,\n        name: str,\n        folder_mode=False,\n    ) -> tuple[Path, Path]:\n        \"\"\"生成文件的临时路径和目标路径\"\"\"\n        root = self.create_detail_folder(root, name, folder_mode)\n        root.mkdir(exist_ok=True)\n        cache = self.cache.joinpath(name)\n        actual = root.joinpath(name)\n        return cache, actual\n\n    async def is_downloaded(self, id_: str) -> bool:\n        return await self.recorder.has_id(id_)\n\n    @staticmethod\n    def is_exists(path: Path) -> bool:\n        return path.exists()\n\n    async def is_skip(self, id_: str, path: Path) -> bool:\n        return await self.is_downloaded(id_) or self.is_exists(path)\n\n    async def download_image(\n        self,\n        tasks: list,\n        name: str,\n        id_: str,\n        item: SimpleNamespace,\n        skipped: set,\n        temp_root: Path,\n        actual_root: Path,\n        suffix: str = \"jpeg\",\n        type_: str = _(\"图集\"),\n    ) -> None:\n        if not item[\"downloads\"]:\n            self.log.error(\n                _(\"【{type}】{name} 提取文件下载地址失败，跳过下载\").format(\n                    type=type_, name=name\n                )\n            )\n            return\n        for index, img in enumerate(\n            item[\"downloads\"],\n            start=1,\n        ):\n            if await self.is_downloaded(id_):\n                skipped.add(id_)\n                self.log.info(\n                    _(\"【{type}】{name} 存在下载记录，跳过下载\").format(\n                        type=type_, name=name\n                    )\n                )\n                break\n            elif self.is_exists(p := actual_root.with_name(f\"{name}_{index}.{suffix}\")):\n                self.log.info(\n                    _(\"【{type}】{name}_{index} 文件已存在，跳过下载\").format(\n                        type=type_, name=name, index=index\n                    )\n                )\n                self.log.info(f\"文件路径: {p.resolve()}\", False)\n                skipped.add(id_)\n                continue\n            tasks.append(\n                (\n                    img,\n                    temp_root.with_name(f\"{name}_{index}.{suffix}\"),\n                    p,\n                    f\"【{type_}】{name}_{index}\",\n                    id_,\n                    suffix,\n                )\n            )\n\n    async def download_video(\n        self,\n        tasks: list,\n        name: str,\n        id_: str,\n        item: SimpleNamespace,\n        skipped: set,\n        temp_root: Path,\n        actual_root: Path,\n        suffix: str = \"mp4\",\n        type_: str = _(\"视频\"),\n    ) -> None:\n        if not item[\"downloads\"]:\n            self.log.error(\n                _(\"【{type}】{name} 提取文件下载地址失败，跳过下载\").format(\n                    type=type_, name=name\n                )\n            )\n            return\n        if await self.is_skip(\n            id_,\n            p := actual_root.with_name(\n                f\"{name}.{suffix}\",\n            ),\n        ):\n            self.log.info(\n                _(\"【{type}】{name} 存在下载记录或文件已存在，跳过下载\").format(\n                    type=type_, name=name\n                )\n            )\n            self.log.info(f\"文件路径: {p.resolve()}\", False)\n            skipped.add(id_)\n            return\n        tasks.append(\n            (\n                item[\"downloads\"],\n                temp_root.with_name(f\"{name}.{suffix}\"),\n                p,\n                f\"【{type_}】{name}\",\n                id_,\n                suffix,\n            )\n        )\n\n    def download_music(\n        self,\n        tasks: list,\n        name: str,\n        id_: str,\n        item: dict,\n        temp_root: Path,\n        actual_root: Path,\n        key: str = \"music_url\",\n        switch: bool = False,\n        suffix: str = \"mp3\",\n        type_: str = _(\"音乐\"),\n        **kwargs,\n    ) -> None:\n        if self.check_deal_music(\n            url := item[key],\n            p := actual_root.with_name(f\"{name}.{suffix}\"),\n            switch,\n        ):\n            tasks.append(\n                (\n                    url,\n                    temp_root.with_name(f\"{name}.{suffix}\"),\n                    p,\n                    _(\"【{type}】{name}\").format(\n                        type=type_,\n                        name=name,\n                    ),\n                    id_,\n                    suffix,\n                )\n            )\n\n    def download_cover(\n        self,\n        tasks: list,\n        name: str,\n        id_: str,\n        item: SimpleNamespace,\n        temp_root: Path,\n        actual_root: Path,\n        static_suffix: str = \"jpeg\",\n        dynamic_suffix: str = \"webp\",\n        **kwargs,\n    ) -> None:\n        if all(\n            (\n                self.static_cover,\n                url := item[\"static_cover\"],\n                not self.is_exists(\n                    p := actual_root.with_name(f\"{name}.{static_suffix}\")\n                ),\n            )\n        ):\n            tasks.append(\n                (\n                    url,\n                    temp_root.with_name(f\"{name}.{static_suffix}\"),\n                    p,\n                    f\"【封面】{name}\",\n                    id_,\n                    static_suffix,\n                )\n            )\n        if all(\n            (\n                self.dynamic_cover,\n                url := item[\"dynamic_cover\"],\n                not self.is_exists(\n                    p := actual_root.with_name(f\"{name}.{dynamic_suffix}\")\n                ),\n            )\n        ):\n            tasks.append(\n                (\n                    url,\n                    temp_root.with_name(f\"{name}.{dynamic_suffix}\"),\n                    p,\n                    f\"【动图】{name}\",\n                    id_,\n                    dynamic_suffix,\n                )\n            )\n\n    def check_deal_music(\n        self,\n        url: str,\n        path: Path,\n        switch=False,\n    ) -> bool:\n        \"\"\"未传入 switch 参数则判断音乐下载开关设置\"\"\"\n        return all((switch or self.music, url, not self.is_exists(path)))\n\n    @Retry.retry\n    async def request_file(\n        self,\n        url: str,\n        temp: Path,\n        actual: Path,\n        show: str,\n        id_: str,\n        suffix: str,\n        count: SimpleNamespace,\n        progress: Progress,\n        headers: dict = None,\n        tiktok=False,\n        unknown_size=False,\n        semaphore: Semaphore = None,\n    ) -> bool | None:\n        async with semaphore or self.semaphore:\n            client = self.client_tiktok if tiktok else self.client\n            headers = self.__adapter_headers(\n                headers,\n                tiktok,\n            )\n            self.__record_request_messages(\n                show,\n                url,\n                headers,\n            )\n            try:\n                # length, suffix = await self.__head_file(client, url, headers, suffix, )\n                position = self.__update_headers_range(\n                    headers,\n                    temp,\n                )\n                async with client.stream(\n                    \"GET\",\n                    url,\n                    headers=headers,\n                ) as response:\n                    if response.status_code == 416:\n                        raise CacheError(_(\"文件缓存异常，尝试重新下载\"))\n                    response.raise_for_status()\n                    length, suffix = self._extract_content(\n                        response.headers,\n                        suffix,\n                    )\n                    length += position\n                    self._record_response(\n                        response,\n                        show,\n                        length,\n                    )\n                    match self._download_initial_check(\n                        length,\n                        unknown_size,\n                        show,\n                    ):\n                        case 1:\n                            return await self.download_file(\n                                temp,\n                                actual.with_suffix(\n                                    f\".{suffix}\",\n                                ),\n                                show,\n                                id_,\n                                response,\n                                length,\n                                position,\n                                count,\n                                progress,\n                            )\n                        case 0:\n                            return True\n                        case -1:\n                            return False\n                        case _:\n                            raise DownloaderError\n            except RequestError as e:\n                self.log.warning(_(\"网络异常: {error_repr}\").format(error_repr=repr(e)))\n                return False\n            except HTTPStatusError as e:\n                self.log.warning(\n                    _(\"响应码异常: {error_repr}\").format(error_repr=repr(e))\n                )\n                self.console.warning(\n                    _(\n                        \"如果 TikTok 平台作品下载功能异常，请检查配置文件中 browser_info_tiktok 的 device_id 参数！\"\n                    ),\n                )\n                return False\n            except CacheError as e:\n                self.delete(temp)\n                self.log.error(str(e))\n                return False\n            except Exception as e:\n                self.log.error(\n                    _(\n                        \"下载文件时发生预期之外的错误，请向作者反馈，错误信息: {error}\"\n                    ).format(error=repr(e)),\n                )\n                self.log.error(f\"URL: {url}\", False)\n                self.log.error(f\"Headers: {headers}\", False)\n                return False\n\n    async def download_file(\n        self,\n        cache: Path,\n        actual: Path,\n        show: str,\n        id_: str,\n        response,\n        content: int,\n        position: int,\n        count: SimpleNamespace,\n        progress: Progress,\n    ) -> bool:\n        task_id = progress.add_task(\n            beautify_string(show, self.truncate),\n            total=content or None,\n            completed=position,\n        )\n        try:\n            async with open(cache, \"ab\") as f:\n                async for chunk in response.aiter_bytes(self.chunk):\n                    await f.write(chunk)\n                    progress.update(task_id, advance=len(chunk))\n                progress.remove_task(task_id)\n        except (\n            RequestError,\n            StreamError,\n        ) as e:\n            progress.remove_task(task_id)\n            self.log.warning(\n                _(\"{show} 下载中断，错误信息：{error}\").format(show=show, error=e)\n            )\n            # self.delete_file(cache)\n            await self.recorder.delete_id(id_)\n            return False\n        self.save_file(cache, actual)\n        self.log.info(_(\"{show} 文件下载成功\").format(show=show))\n        self.log.info(f\"文件路径 {actual.resolve()}\", False)\n        await self.recorder.update_id(id_)\n        self.add_count(show, id_, count)\n        return True\n\n    def __record_request_messages(\n        self,\n        show: str,\n        url: str,\n        headers: dict,\n    ):\n        self.log.info(f\"{show} URL: {url}\", False)\n        # 请求头脱敏处理，不记录 Cookie\n        desensitize = {k: v for k, v in headers.items() if k != \"Cookie\"}\n        self.log.info(f\"{show} Headers: {desensitize}\", False)\n\n    def __adapter_headers(\n        self,\n        headers: dict,\n        tiktok: bool,\n        *args,\n        **kwargs,\n    ) -> dict:\n        return (headers or self.headers_tiktok if tiktok else self.headers).copy()\n\n    @staticmethod\n    def add_count(show: str, id_: str, count: SimpleNamespace):\n        if show.startswith(f\"【{_('图集')}】\"):\n            count.downloaded_image.add(id_)\n        elif show.startswith(f\"【{_('视频')}】\"):\n            count.downloaded_video.add(id_)\n        elif show.startswith(f\"【{_('实况')}】\"):\n            count.downloaded_live.add(id_)\n\n    @staticmethod\n    def data_classification(\n        mode: str = \"\",\n        mark: str = \"\",\n        user_id: str = \"\",\n        user_name: str = \"\",\n        mix_id: str = \"\",\n        mix_title: str = \"\",\n        collect_id: str = \"\",\n        collect_name: str = \"\",\n    ) -> tuple[str, str]:\n        match mode:\n            case \"post\" | \"favorite\" | \"collection\":\n                return user_id, mark or user_name\n            case \"mix\":\n                return mix_id, mark or mix_title\n            case \"collects\":\n                return collect_id, mark or collect_name\n            case _:\n                raise DownloaderError\n\n    def storage_folder(\n        self,\n        mode: str = \"\",\n        id_: str = \"\",\n        name: str = \"\",\n    ) -> Path:\n        match mode:\n            case \"post\":\n                folder_name = _(\"UID{id_}_{name}_发布作品\").format(id_=id_, name=name)\n            case \"favorite\":\n                folder_name = _(\"UID{id_}_{name}_喜欢作品\").format(id_=id_, name=name)\n            case \"mix\":\n                folder_name = _(\"MID{id_}_{name}_合集作品\").format(id_=id_, name=name)\n            case \"collection\":\n                folder_name = _(\"UID{id_}_{name}_收藏作品\").format(id_=id_, name=name)\n            case \"collects\":\n                folder_name = _(\"CID{id_}_{name}_收藏夹作品\").format(id_=id_, name=name)\n            case \"detail\":\n                folder_name = self.folder_name\n            case _:\n                raise DownloaderError\n        folder = self.root.joinpath(folder_name)\n        folder.mkdir(exist_ok=True)\n        return folder\n\n    def generate_detail_name(self, data: dict) -> str:\n        \"\"\"生成作品文件名称\"\"\"\n        return beautify_string(\n            self.cleaner.filter_name(\n                self.split.join(data[i] for i in self.name_format),\n                data[\"id\"],\n            ),\n            length=self.name_length,\n        )\n\n    def generate_music_name(self, data: dict) -> str:\n        \"\"\"生成音乐文件名称\"\"\"\n        return beautify_string(\n            self.cleaner.filter_name(\n                self.split.join(\n                    data[i]\n                    for i in (\n                        \"author\",\n                        \"title\",\n                        \"id\",\n                    )\n                ),\n                default=str(time())[:10],\n            ),\n            length=self.name_length,\n        )\n\n    @staticmethod\n    def create_detail_folder(\n        root: Path,\n        name: str,\n        folder_mode=False,\n    ) -> Path:\n        return root.joinpath(name) if folder_mode else root\n\n    @staticmethod\n    def delete(\n        temp: \"Path\",\n    ):\n        if temp.is_file():\n            temp.unlink()\n\n    @staticmethod\n    def save_file(cache: Path, actual: Path):\n        move(cache.resolve(), actual.resolve())\n\n    def delete_file(self, path: Path):\n        path.unlink()\n        self.log.info(_(\"{file_name} 文件已删除\").format(file_name=path.name))\n\n    def statistics_count(self, count: SimpleNamespace):\n        self.log.info(\n            _(\"下载视频作品 {downloaded_video_count} 个\").format(\n                downloaded_video_count=len(count.downloaded_video)\n            ),\n        )\n        self.log.info(\n            _(\"跳过视频作品 {skipped_count} 个\").format(\n                skipped_count=len(count.skipped_video)\n            )\n        )\n        self.log.info(\n            _(\"下载图集作品 {downloaded_image_count} 个\").format(\n                downloaded_image_count=len(count.downloaded_image)\n            ),\n        )\n        self.log.info(\n            _(\"跳过图集作品 {skipped_count} 个\").format(\n                skipped_count=len(count.skipped_image)\n            )\n        )\n        self.log.info(\n            _(\"下载实况作品 {downloaded_image_count} 个\").format(\n                downloaded_image_count=len(count.downloaded_live)\n            ),\n        )\n        self.log.info(\n            _(\"跳过实况作品 {skipped_count} 个\").format(\n                skipped_count=len(count.skipped_live)\n            )\n        )\n\n    def _record_response(\n        self,\n        response,\n        show: str,\n        length: int,\n    ):\n        self.log.info(f\"{show} Response URL: {response.url}\", False)\n        self.log.info(f\"{show} Response Code: {response.status_code}\", False)\n        self.log.info(f\"{show} Response Headers: {response.headers}\", False)\n        self.log.info(\n            f\"{show} 文件大小 {format_size(length)}\",\n            False,\n        )\n\n    async def __head_file(\n        self,\n        client: \"AsyncClient\",\n        url: str,\n        headers: dict,\n        suffix: str,\n    ) -> tuple[int, str]:\n        response = await client.head(\n            url,\n            headers=headers,\n        )\n        if response.status_code == 405:\n            return 0, suffix\n        response.raise_for_status()\n        return self._extract_content(\n            response.headers,\n            suffix,\n        )\n\n    def _extract_content(\n        self,\n        headers: dict,\n        suffix: str,\n    ) -> tuple[int, str]:\n        suffix = (\n            self.__extract_type(\n                headers.get(\"Content-Type\"),\n            )\n            or suffix\n        )\n        length = headers.get(\n            \"Content-Length\",\n            0,\n        )\n        return int(length), suffix\n\n    @staticmethod\n    def __get_resume_byte_position(file: Path) -> int:\n        return file.stat().st_size if file.is_file() else 0\n\n    def __update_headers_range(\n        self,\n        headers: dict,\n        file: Path,\n        length: int = 0,\n    ) -> int:\n        position = self.__get_resume_byte_position(file)\n        # if length and position >= length:\n        #     self.delete(file)\n        #     position = 0\n        headers[\"Range\"] = f\"bytes={position}-\"\n        return position\n\n    def __extract_type(self, content: str) -> str:\n        if not (s := self.CONTENT_TYPE_MAP.get(content)):\n            return self.__unknown_type(content)\n        return s\n\n    def __unknown_type(self, content: str) -> str:\n        self.log.warning(_(\"未收录的文件类型：{content}\").format(content=content))\n        return \"\"\n\n    def _download_initial_check(\n        self,\n        length: int,\n        unknown_size: bool,\n        show: str,\n    ) -> int:\n        if not length and not unknown_size:  # 响应内容大小判断\n            self.log.warning(_(\"{show} 响应内容为空\").format(show=show))\n            return -1  # 执行重试\n        if all(\n            (\n                self.max_size,\n                length,\n                length > self.max_size,\n            )\n        ):  # 文件下载跳过判断\n            self.log.info(_(\"{show} 文件大小超出限制，跳过下载\").format(show=show))\n            return 0  # 跳过下载\n        return 1  # 继续下载\n"
  },
  {
    "path": "src/encrypt/__init__.py",
    "content": "from .aBogus import ABogus\nfrom .device_id import DeviceId\nfrom .msToken import MsToken, MsTokenTikTok\nfrom .ttWid import TtWid, TtWidTikTok\nfrom .verifyFp import VerifyFp\nfrom .webID import WebId\nfrom .xBogus import XBogus, XBogusTikTok\nfrom .xGnarly import XGnarly\n"
  },
  {
    "path": "src/encrypt/aBogus.py",
    "content": "from random import choice, randint, random\nfrom re import compile\nfrom time import time\nfrom urllib.parse import quote, urlencode\n\nfrom gmssl import func, sm3\n\nfrom src.custom import USERAGENT\n\n__all__ = [\n    \"ABogus\",\n]\n\n\nclass ABogus:\n    __filter = compile(r\"%([0-9A-F]{2})\")\n    __arguments = [0, 1, 14]\n    __ua_key = \"\\u0000\\u0001\\u000e\"\n    __end_string = \"cus\"\n    __version = [1, 0, 1, 5]\n    __browser = \"1536|742|1536|864|0|0|0|0|1536|864|1536|864|1536|742|24|24|Win32\"\n    __reg = [\n        1937774191,\n        1226093241,\n        388252375,\n        3666478592,\n        2842636476,\n        372324522,\n        3817729613,\n        2969243214,\n    ]\n    __str = {\n        \"s0\": \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\",\n        \"s1\": \"Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=\",\n        \"s2\": \"Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=\",\n        \"s3\": \"ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe\",\n        \"s4\": \"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe\",\n    }\n\n    def __init__(\n        self,\n        user_agent: str = USERAGENT,\n        platform: str = None,\n    ):\n        self.chunk = []\n        self.size = 0\n        self.reg = self.__reg[:]\n        self.ua_code = self.generate_ua_code(user_agent)\n        self.browser = (\n            self.generate_browser_info(platform) if platform else self.__browser\n        )\n        self.browser_len = len(self.browser)\n        self.browser_code = self.char_code_at(self.browser)\n\n    @classmethod\n    def list_1(\n        cls,\n        random_num=None,\n        a=170,\n        b=85,\n        c=45,\n    ) -> list:\n        return cls.random_list(\n            random_num,\n            a,\n            b,\n            1,\n            2,\n            5,\n            c & a,\n        )\n\n    @classmethod\n    def list_2(\n        cls,\n        random_num=None,\n        a=170,\n        b=85,\n    ) -> list:\n        return cls.random_list(\n            random_num,\n            a,\n            b,\n            1,\n            0,\n            0,\n            0,\n        )\n\n    @classmethod\n    def list_3(\n        cls,\n        random_num=None,\n        a=170,\n        b=85,\n    ) -> list:\n        return cls.random_list(\n            random_num,\n            a,\n            b,\n            1,\n            0,\n            5,\n            0,\n        )\n\n    @staticmethod\n    def random_list(\n        a: float = None,\n        b=170,\n        c=85,\n        d=0,\n        e=0,\n        f=0,\n        g=0,\n    ) -> list:\n        r = a or (random() * 10000)\n        v = [\n            r,\n            int(r) & 255,\n            int(r) >> 8,\n        ]\n        s = v[1] & b | d\n        v.append(s)\n        s = v[1] & c | e\n        v.append(s)\n        s = v[2] & b | f\n        v.append(s)\n        s = v[2] & c | g\n        v.append(s)\n        return v[-4:]\n\n    @staticmethod\n    def from_char_code(*args):\n        return \"\".join(chr(code) for code in args)\n\n    @classmethod\n    def generate_string_1(\n        cls,\n        random_num_1=None,\n        random_num_2=None,\n        random_num_3=None,\n    ):\n        return (\n            cls.from_char_code(*cls.list_1(random_num_1))\n            + cls.from_char_code(*cls.list_2(random_num_2))\n            + cls.from_char_code(*cls.list_3(random_num_3))\n        )\n\n    def generate_string_2(\n        self,\n        url_params: str,\n        method=\"GET\",\n        start_time=0,\n        end_time=0,\n    ) -> str:\n        a = self.generate_string_2_list(\n            url_params,\n            method,\n            start_time,\n            end_time,\n        )\n        e = self.end_check_num(a)\n        a.extend(self.browser_code)\n        a.append(e)\n        return self.rc4_encrypt(self.from_char_code(*a), \"y\")\n\n    def generate_ua_code(self, user_agent: str) -> list[int]:\n        u = self.rc4_encrypt(user_agent, self.__ua_key)\n        u = self.generate_result(u, \"s3\")\n        return self.sum(u)\n\n    def generate_string_2_list(\n        self,\n        url_params: str,\n        method=\"GET\",\n        start_time=0,\n        end_time=0,\n    ) -> list:\n        start_time = start_time or int(time() * 1000)\n        end_time = end_time or (start_time + randint(4, 8))\n        params_array = self.generate_params_code(url_params)\n        method_array = self.generate_method_code(method)\n        return self.list_4(\n            (end_time >> 24) & 255,\n            params_array[21],\n            self.ua_code[23],\n            (end_time >> 16) & 255,\n            params_array[22],\n            self.ua_code[24],\n            (end_time >> 8) & 255,\n            (end_time >> 0) & 255,\n            (start_time >> 24) & 255,\n            (start_time >> 16) & 255,\n            (start_time >> 8) & 255,\n            (start_time >> 0) & 255,\n            method_array[21],\n            method_array[22],\n            int(end_time / 256 / 256 / 256 / 256) >> 0,\n            int(start_time / 256 / 256 / 256 / 256) >> 0,\n            self.browser_len,\n        )\n\n    @staticmethod\n    def reg_to_array(a):\n        o = [0] * 32\n        for i in range(8):\n            c = a[i]\n            o[4 * i + 3] = 255 & c\n            c >>= 8\n            o[4 * i + 2] = 255 & c\n            c >>= 8\n            o[4 * i + 1] = 255 & c\n            c >>= 8\n            o[4 * i] = 255 & c\n\n        return o\n\n    def compress(self, a):\n        f = self.generate_f(a)\n        i = self.reg[:]\n        for o in range(64):\n            c = self.de(i[0], 12) + i[4] + self.de(self.pe(o), o)\n            c = c & 0xFFFFFFFF\n            c = self.de(c, 7)\n            s = (c ^ self.de(i[0], 12)) & 0xFFFFFFFF\n\n            u = self.he(o, i[0], i[1], i[2])\n            u = (u + i[3] + s + f[o + 68]) & 0xFFFFFFFF\n\n            b = self.ve(o, i[4], i[5], i[6])\n            b = (b + i[7] + c + f[o]) & 0xFFFFFFFF\n\n            i[3] = i[2]\n            i[2] = self.de(i[1], 9)\n            i[1] = i[0]\n            i[0] = u\n\n            i[7] = i[6]\n            i[6] = self.de(i[5], 19)\n            i[5] = i[4]\n            i[4] = (b ^ self.de(b, 9) ^ self.de(b, 17)) & 0xFFFFFFFF\n\n        for l in range(8):\n            self.reg[l] = (self.reg[l] ^ i[l]) & 0xFFFFFFFF\n\n    @classmethod\n    def generate_f(cls, e):\n        r = [0] * 132\n\n        for t in range(16):\n            r[t] = (\n                (e[4 * t] << 24)\n                | (e[4 * t + 1] << 16)\n                | (e[4 * t + 2] << 8)\n                | e[4 * t + 3]\n            )\n            r[t] &= 0xFFFFFFFF\n\n        for n in range(16, 68):\n            a = r[n - 16] ^ r[n - 9] ^ cls.de(r[n - 3], 15)\n            a = a ^ cls.de(a, 15) ^ cls.de(a, 23)\n            r[n] = (a ^ cls.de(r[n - 13], 7) ^ r[n - 6]) & 0xFFFFFFFF\n\n        for n in range(68, 132):\n            r[n] = (r[n - 68] ^ r[n - 64]) & 0xFFFFFFFF\n\n        return r\n\n    @staticmethod\n    def pad_array(arr, length=60):\n        while len(arr) < length:\n            arr.append(0)\n        return arr\n\n    def fill(self, length=60):\n        size = 8 * self.size\n        self.chunk.append(128)\n        self.chunk = self.pad_array(self.chunk, length)\n        for i in range(4):\n            self.chunk.append((size >> 8 * (3 - i)) & 255)\n\n    @staticmethod\n    def list_4(\n        a: int,\n        b: int,\n        c: int,\n        d: int,\n        e: int,\n        f: int,\n        g: int,\n        h: int,\n        i: int,\n        j: int,\n        k: int,\n        m: int,\n        n: int,\n        o: int,\n        p: int,\n        q: int,\n        r: int,\n    ) -> list:\n        return [\n            44,\n            a,\n            0,\n            0,\n            0,\n            0,\n            24,\n            b,\n            n,\n            0,\n            c,\n            d,\n            0,\n            0,\n            0,\n            1,\n            0,\n            239,\n            e,\n            o,\n            f,\n            g,\n            0,\n            0,\n            0,\n            0,\n            h,\n            0,\n            0,\n            14,\n            i,\n            j,\n            0,\n            k,\n            m,\n            3,\n            p,\n            1,\n            q,\n            1,\n            r,\n            0,\n            0,\n            0,\n        ]\n\n    @staticmethod\n    def end_check_num(a: list):\n        r = 0\n        for i in a:\n            r ^= i\n        return r\n\n    @classmethod\n    def decode_string(\n        cls,\n        url_string,\n    ):\n        decoded = cls.__filter.sub(cls.replace_func, url_string)\n        return decoded\n\n    @staticmethod\n    def replace_func(match):\n        return chr(int(match.group(1), 16))\n\n    @staticmethod\n    def de(e, r):\n        r %= 32\n        return ((e << r) & 0xFFFFFFFF) | (e >> (32 - r))\n\n    @staticmethod\n    def pe(e):\n        return 2043430169 if 0 <= e < 16 else 2055708042\n\n    @staticmethod\n    def he(e, r, t, n):\n        if 0 <= e < 16:\n            return (r ^ t ^ n) & 0xFFFFFFFF\n        elif 16 <= e < 64:\n            return (r & t | r & n | t & n) & 0xFFFFFFFF\n        raise ValueError\n\n    @staticmethod\n    def ve(e, r, t, n):\n        if 0 <= e < 16:\n            return (r ^ t ^ n) & 0xFFFFFFFF\n        elif 16 <= e < 64:\n            return (r & t | ~r & n) & 0xFFFFFFFF\n        raise ValueError\n\n    @staticmethod\n    def convert_to_char_code(a):\n        d = []\n        for i in a:\n            d.append(ord(i))\n        return d\n\n    @staticmethod\n    def split_array(arr, chunk_size=64):\n        result = []\n        for i in range(0, len(arr), chunk_size):\n            result.append(arr[i : i + chunk_size])\n        return result\n\n    @staticmethod\n    def char_code_at(s):\n        return [ord(char) for char in s]\n\n    def write(\n        self,\n        e,\n    ):\n        self.size = len(e)\n        if isinstance(e, str):\n            e = self.decode_string(e)\n            e = self.char_code_at(e)\n        if len(e) <= 64:\n            self.chunk = e\n        else:\n            chunks = self.split_array(e, 64)\n            for i in chunks[:-1]:\n                self.compress(i)\n            self.chunk = chunks[-1]\n\n    def reset(\n        self,\n    ):\n        self.chunk = []\n        self.size = 0\n        self.reg = self.__reg[:]\n\n    def sum(self, e, length=60):\n        self.reset()\n        self.write(e)\n        self.fill(length)\n        self.compress(self.chunk)\n        return self.reg_to_array(self.reg)\n\n    @classmethod\n    def generate_result_unit(cls, n, s):\n        r = \"\"\n        for i, j in zip(range(18, -1, -6), (16515072, 258048, 4032, 63)):\n            r += cls.__str[s][(n & j) >> i]\n        return r\n\n    @classmethod\n    def generate_result_end(cls, s, e=\"s4\"):\n        r = \"\"\n        b = ord(s[120]) << 16\n        r += cls.__str[e][(b & 16515072) >> 18]\n        r += cls.__str[e][(b & 258048) >> 12]\n        r += \"==\"\n        return r\n\n    @classmethod\n    def generate_result(cls, s, e=\"s4\"):\n        # r = \"\"\n        # for i in range(len(s)//4):\n        #     b = ((ord(s[i * 3]) << 16) | (ord(s[i * 3 + 1]))\n        #          << 8) | ord(s[i * 3 + 2])\n        #     r += cls.generate_result_unit(b, e)\n        # return r\n\n        r = []\n\n        for i in range(0, len(s), 3):\n            if i + 2 < len(s):\n                n = (ord(s[i]) << 16) | (ord(s[i + 1]) << 8) | ord(s[i + 2])\n            elif i + 1 < len(s):\n                n = (ord(s[i]) << 16) | (ord(s[i + 1]) << 8)\n            else:\n                n = ord(s[i]) << 16\n\n            for j, k in zip(range(18, -1, -6), (0xFC0000, 0x03F000, 0x0FC0, 0x3F)):\n                if j == 6 and i + 1 >= len(s):\n                    break\n                if j == 0 and i + 2 >= len(s):\n                    break\n                r.append(cls.__str[e][(n & k) >> j])\n\n        r.append(\"=\" * ((4 - len(r) % 4) % 4))\n        return \"\".join(r)\n\n    @classmethod\n    def generate_args_code(cls):\n        a = []\n        for j in range(24, -1, -8):\n            a.append(cls.__arguments[0] >> j)\n        a.append(cls.__arguments[1] / 256)\n        a.append(cls.__arguments[1] % 256)\n        a.append(cls.__arguments[1] >> 24)\n        a.append(cls.__arguments[1] >> 16)\n        for j in range(24, -1, -8):\n            a.append(cls.__arguments[2] >> j)\n        return [int(i) & 255 for i in a]\n\n    def generate_method_code(self, method: str = \"GET\") -> list[int]:\n        return self.sm3_to_array(self.sm3_to_array(method + self.__end_string))\n        # return self.sum(self.sum(method + self.__end_string))\n\n    def generate_params_code(self, params: str) -> list[int]:\n        return self.sm3_to_array(self.sm3_to_array(params + self.__end_string))\n        # return self.sum(self.sum(params + self.__end_string))\n\n    @classmethod\n    def sm3_to_array(cls, data: str | list) -> list[int]:\n        \"\"\"\n        代码参考: https://github.com/Johnserf-Seed/f2/blob/main/f2/utils/abogus.py\n\n        计算请求体的 SM3 哈希值，并将结果转换为整数数组\n        Calculate the SM3 hash value of the request body and convert the result to an array of integers\n\n        Args:\n            data (Union[str, List[int]]): 输入数据 (Input data).\n\n        Returns:\n            List[int]: 哈希值的整数数组 (Array of integers representing the hash value).\n        \"\"\"\n\n        if isinstance(data, str):\n            b = data.encode(\"utf-8\")\n        else:\n            b = bytes(data)  # 将 List[int] 转换为字节数组\n\n        # 将字节数组转换为适合 sm3.sm3_hash 函数处理的列表格式\n        h = sm3.sm3_hash(func.bytes_to_list(b))\n\n        # 将十六进制字符串结果转换为十进制整数列表\n        return [int(h[i : i + 2], 16) for i in range(0, len(h), 2)]\n\n    @classmethod\n    def generate_browser_info(cls, platform: str = \"Win32\") -> str:\n        inner_width = randint(1280, 1920)\n        inner_height = randint(720, 1080)\n        outer_width = randint(inner_width, 1920)\n        outer_height = randint(inner_height, 1080)\n        screen_x = 0\n        screen_y = choice((0, 30))\n        value_list = [\n            inner_width,\n            inner_height,\n            outer_width,\n            outer_height,\n            screen_x,\n            screen_y,\n            0,\n            0,\n            outer_width,\n            outer_height,\n            outer_width,\n            outer_height,\n            inner_width,\n            inner_height,\n            24,\n            24,\n            platform,\n        ]\n        return \"|\".join(str(i) for i in value_list)\n\n    @staticmethod\n    def rc4_encrypt(plaintext, key):\n        s = list(range(256))\n        j = 0\n\n        for i in range(256):\n            j = (j + s[i] + ord(key[i % len(key)])) % 256\n            s[i], s[j] = s[j], s[i]\n\n        i = 0\n        j = 0\n        cipher = []\n\n        for k in range(len(plaintext)):\n            i = (i + 1) % 256\n            j = (j + s[i]) % 256\n            s[i], s[j] = s[j], s[i]\n            t = (s[i] + s[j]) % 256\n            cipher.append(chr(s[t] ^ ord(plaintext[k])))\n\n        return \"\".join(cipher)\n\n    def get_value(\n        self,\n        url_params: dict | str,\n        method=\"GET\",\n        start_time=0,\n        end_time=0,\n        random_num_1=None,\n        random_num_2=None,\n        random_num_3=None,\n    ) -> str:\n        string_1 = self.generate_string_1(\n            random_num_1,\n            random_num_2,\n            random_num_3,\n        )\n        string_2 = self.generate_string_2(\n            urlencode(\n                url_params,\n                quote_via=quote,\n            )\n            if isinstance(url_params, dict)\n            else url_params,\n            method,\n            start_time,\n            end_time,\n        )\n        string = string_1 + string_2\n        # return self.generate_result(\n        #     string, \"s4\") + self.generate_result_end(string, \"s4\")\n        return self.generate_result(string, \"s4\")\n"
  },
  {
    "path": "src/encrypt/device_id.py",
    "content": "from asyncio import run\nfrom re import compile\nfrom typing import TYPE_CHECKING, Union\n\nfrom src.custom import PARAMS_HEADERS_TIKTOK\nfrom src.tools import request_params\n\nif TYPE_CHECKING:\n    from src.record import BaseLogger, LoggerManager\n    from src.testers import Logger\n\n\nclass DeviceId:\n    NAME = \"device_id\"\n    URL = \"https://www.tiktok.com/explore\"\n    DEVICE_ID = compile(r'\"wid\":\"(\\d{19})\"')\n\n    @classmethod\n    async def get_device_id(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        **kwargs,\n    ) -> [str, str]:\n        response = await request_params(\n            logger,\n            cls.URL,\n            \"GET\",\n            headers=headers,\n            resp=\"response\",\n            **kwargs,\n        )\n        response.raise_for_status()\n        device_id = d.group(1) if (d := cls.DEVICE_ID.search(response.text)) else \"\"\n        cookie = \"; \".join(\n            [f\"{key}={value}\" for key, value in response.cookies.items()]\n        )\n        return device_id, cookie\n\n    @classmethod\n    async def get_device_ids(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        number: int,\n        **kwargs,\n    ) -> [[str, str]]:\n        return [\n            await cls.get_device_id(\n                logger,\n                headers,\n                **kwargs,\n            )\n            for _ in range(number)\n        ]\n\n\nasync def test():\n    from src.testers import Logger\n\n    print(\n        await DeviceId.get_device_id(\n            Logger(),\n            PARAMS_HEADERS_TIKTOK,\n            proxy=\"http://127.0.0.1:10809\",\n        )\n    )\n    # print(await DeviceId.get_device_ids(\n    #     Logger(),\n    #     PARAMS_HEADERS_TIKTOK,\n    #     5,\n    #     proxy=\"http://127.0.0.1:10809\",\n    # ))\n\n\nif __name__ == \"__main__\":\n    run(test())\n"
  },
  {
    "path": "src/encrypt/msToken.py",
    "content": "from asyncio import run\nfrom json import dumps\nfrom random import randint\nfrom string import ascii_lowercase, ascii_uppercase, digits\nfrom time import time\nfrom typing import TYPE_CHECKING, Union\nfrom urllib.parse import quote\n\nfrom src.custom import PARAMS_HEADERS, PARAMS_HEADERS_TIKTOK, USERAGENT\nfrom src.encrypt.ttWid import TtWid\nfrom src.encrypt.xBogus import XBogusTikTok\nfrom src.tools import request_params\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.record import BaseLogger, LoggerManager\n    from src.testers import Logger\n\n__all__ = [\"MsToken\", \"MsTokenTikTok\"]\n\n\nclass MsToken:\n    NAME = \"msToken\"\n    # API = \"https://mssdk.bytedance.com/web/report\"\n    API = \"https://mssdk.bytedance.com/web/common\"\n    DATA = {\n        \"magic\": 538969122,\n        \"version\": 1,\n        \"dataType\": 8,\n        \"strData\": \"fWOdJTQR3/jwmZqBBsPO6tdNEc1jX7YTwPg0Z8CT+j3HScLFbj2Zm1XQ7/lqgSutntVKLJWaY3Hc/+vc0h+So9N1t6EqiImu5\"\n        \"jKyUa+S4NPy6cNP0x9CUQQgb4+RRihCgsn4QyV8jivEFOsj3N5zFQbzXRyOV+9aG5B5EAnwpn8C70llsWq0zJz1VjN6y2KZiB\"\n        \"ZRyonAHE8feSGpwMDeUTllvq6BG3AQZz7RrORLWNCLEoGzM6bMovYVPRAJipuUML4Hq/568bNb5vqAo0eOFpvTZjQFgbB7f/C\"\n        \"tAYYmnOYlvfrHKBKvb0TX6AjYrw2qmNNEer2ADJosmT5kZeBsogDui8rNiI/OOdX9PVotmcSmHOLRfw1cYXTgwHXr6cJeJveu\"\n        \"ipgwtUj2FNT4YCdZfUGGyRDz5bR5bdBuYiSRteSX12EktobsKPksdhUPGGv99SI1QRVmR0ETdWqnKWOj/7ujFZsNnfCLxNfqx\"\n        \"QYEZEp9/U01CHhWLVrdzlrJ1v+KJH9EA4P1Wo5/2fuBFVdIz2upFqEQ11DJu8LSyD43qpTok+hFG3Moqrr81uPYiyPHnUvTFg\"\n        \"wA/TIE11mTc/pNvYIb8IdbE4UAlsR90eYvPkI+rK9KpYN/l0s9ti9sqTth12VAw8tzCQvhKtxevJRQntU3STeZ3coz9Dg8qkv\"\n        \"aSNFWuBDuyefZBGVSgILFdMy33//l/eTXhQpFrVc9OyxDNsG6cvdFwu7trkAENHU5eQEWkFSXBx9Ml54+fa3LvJBoacfPViyv\"\n        \"zkJworlHcYYTG392L4q6wuMSSpYUconb+0c5mwqnnLP6MvRdm/bBTaY2Q6RfJcCxyLW0xsJMO6fgLUEjAg/dcqGxl6gDjUVRW\"\n        \"bCcG1NAwPCfmYARTuXQYbFc8LO+r6WQTWikO9Q7Cgda78pwH07F8bgJ8zFBbWmyrghilNXENNQkyIzBqOQ1V3w0WXF9+Z3vG3\"\n        \"aBKCjIENqAQM9qnC14WMrQkfCHosGbQyEH0n/5R2AaVTE/ye2oPQBWG1m0Gfcgs/96f6yYrsxbDcSnMvsA+okyd6GfWsdZYTI\"\n        \"K1E97PYHlncFeOjxySjPpfy6wJc4UlArJEBZYmgveo1SZAhmXl3pJY3yJa9CmYImWkhbpwsVkSmG3g11JitJXTGLIfqKXSAhh\"\n        \"+7jg4HTKe+5KNir8xmbBI/DF8O/+diFAlD+BQd3cV0G4mEtCiPEhOvVLKV1pE+fv7nKJh0t38wNVdbs3qHtiQNN7JhY4uWZAo\"\n        \"sMuBXSjpEtoNUndI+o0cjR8XJ8tSFnrAY8XihiRzLMfeisiZxWCvVwIP3kum9MSHXma75cdCQGFBfFRj0jPn1JildrTh2vRgw\"\n        \"G+KeDZ33BJ2VGw9PgRkztZ2l/W5d32jc7H91FftFFhwXil6sA23mr6nNp6CcrO7rOblcm5SzXJ5MA601+WVicC/g3p6A0lAnh\"\n        \"jsm37qP+xGT+cbCFOfjexDYEhnqz0QZm94CCSnilQ9B/HBLhWOddp9GK0SABIk5i3xAH701Xb4HCcgAulvfO5EK0RL2eN4fb+\"\n        \"CccgZQeO1Zzo4qsMHc13UG0saMgBEH8SqYlHz2S0CVHuDY5j1MSV0nsShjM01vIynw6K0T8kmEyNjt1eRGlleJ5lvE8vonJv7\"\n        \"rAeaVRZ06rlYaxrMT6cK3RSHd2liE50Z3ik3xezwWoaY6zBXvCzljyEmqjNFgAPU3gI+N1vi0MsFmwAwFzYqqWdk3jwRoWLp/\"\n        \"/FnawQX0g5T64CnfAe/o2e/8o5/bvz83OsAAwZoR48GZzPu7KCIN9q4GBjyrePNx5Csq2srblifmzSKwF5MP/RLYsk6mEE15j\"\n        \"pCMKOVlHcu0zhJybNP3AKMVllF6pvn+HWvUnLXNkt0A6zsfvjAva/tbLQiiiYi6vtheasIyDz3HpODlI+BCkV6V8lkTt7m8QJ\"\n        \"1IcgTfqjQBummyjYTSwsQji3DdNCnlKYd13ZQa545utqu837FFAzOZQhbnC3bKqeJqO2sE3m7WBUMbRWLflPRqp/PsklN+9jB\"\n        \"PADKxKPl8g6/NZVq8fB1w68D5EJlGExdDhglo4B0aihHhb1u3+zJ2DqkxkPCGBAZ2AcuFIDzD53yS4NssoWb4HJ7YyzPaJro+\"\n        \"tgG9TshWRBtUw8Or3m0OtQtX+rboYn3+GxvD1O8vWInrg5qxnepelRcQzmnor4rHF6ZNhAJZAf18Rjncra00HPJBugY5rD+Ew\"\n        \"nN9+mGQo43b01qBBRYEnxy9JJYuvXxNXxe47/MEPOw6qsxN+dmyIWZSuzkw8K+iBM/anE11yfU4qTFt0veCaVprK6tXaFK0Zh\"\n        \"GXDOYJd70sjIP4UrPhatp8hqIXSJ2cwi70B+TvlDk/o19CA3bH6YxrAAVeag1P9hmNlfJ7NxK3Jp7+Ny1Vd7JHWVF+R6rSJiX\"\n        \"XPfsXi3ZEy0klJAjI51NrDAnzNtgIQf0V8OWeEVv7F8Rsm3/GKnjdNOcDKymi9agZUgtctENWbCXGFnI40NHuVHtBRZeYAYtw\"\n        \"fV7v6U0bP9s7uZGpkp+OETHMv3AyV0MVbZwQvarnjmct4Z3Vma+DvT+Z4VlMVnkC2x2FLt26K3SIMz+KV2XLv5ocEdPFSn1vM\"\n        \"R7zruCWC8XqAG288biHo/soldmb/nlw8o8qlfZj4h296K3hfdFubGIUtqgsrZCrLCkkRC08Cv1ozEX/y6t2YrQepwiNmwDVk5\"\n        \"IufStVvJMj+y2r9TcYLv7UKWXx3P6aySvM2ZHPaZhv+6Z/A/jIMBSvOizn4qG11iK7Oo6JYhxCSMJZsetjsnL4ecSIAufEmoF\"\n        \"lAScWBh6nFArRpVLvkAZ3tej7H2lWFRXIU7x7mdBfGqU82PpM6znKMMZCpEsvHqpkSPSL+Kwz2z1f5wW7BKcKK4kNZ8iveg9V\"\n        \"zY1NNjs91qU8DJpUnGyM04C7KNMpeilEmoOxvyelMQdi85ndOVmigVKmy5JYlODNX744sHpeqmMEK/ux3xY5O406lm7dZlyGP\"\n        \"SMrFWbm4rzqvSEIskP43+9xVP8L84GeHE4RpOHg3qh/shx+/WnT1UhKuKpByHCpLoEo144udpzZswCYSMp58uPrlwdVF31//A\"\n        \"acTRk8dUP3tBlnSQPa1eTpXWFCn7vIiqOTXaRL//YQK+e7ssrgSUnwhuGKJ8aqNDgdsL+haVZnV9g5Qrju643adyNixvYFEp0\"\n        \"uxzOzVkekOMh2FYnFVIL2mJYGpZEXlAIC0zQbb54rSP89j0G7soJ2HcOkD0NmMEWj/7hUdTuMin1lRNde/qmHjwhbhqL8Z9ME\"\n        \"O/YG3iLMgFTgSNQQhyE8AZAAKnehmzjORJfbK+qxyiJ07J843EDduzOoYt9p/YLqyTFmAgpdfK0uYrtAJ47cbl5WWhVXp5/XU\"\n        \"xwWdL7TvQB0Xh6ir1/XBRcsVSDrR7cPE221ThmW1EPzD+SPf2L2gS0WromZqj1PhLgk92YnnR9s7/nLBXZHPKy+fDbJT16Qqa\"\n        \"bFKqAl9G0blyf+R5UGX2kN+iQp4VGXEoH5lXxNNTlgRskzrW7KliQXcac20oimAHUE8Phf+rXXglpmSv4XN3eiwfXwvOaAMVj\"\n        \"MRmRxsKitl5iZnwpcdbsC4jt16g2r/ihlKzLIYju+XZej4dNMlkftEidyNg24IVimJthXY1H15RZ8Hm7mAM/JZrsxiAVI0A49\"\n        \"pWEiUk3cyZcBzq/vVEjHUy4r6IZnKkRvLjqsvqWE95nAGMor+F0GLHWfBCVkuI51EIOknwSB1eTvLgwgRepV4pdy9cdp6iR8T\"\n        \"ZndPVCikflXYVMlMEJ2bJ2c0Swiq57ORJW6vQwnkxtPudpFRc7tNNDzz4LKEznJxAwGi6pBR7/co2IUgRw1ijLFTHWHQJOjgc\"\n        \"7KaduHI0C6a+BJb4Y8IWuIk2u2qCMF1HNKFAUn/J1gTcqtIJcvK5uykpfJFCYc899TmUc8LMKI9nu57m0S44Y2hPPYeW4XSak\"\n        \"Scsg8bJHMkcXk3Tbs9b4eqiD+kHUhTS2BGfsHadR3d5j8lNhBPzA5e+mE==\",\n        \"tspFromClient\": 0,\n        \"ulr\": 0,\n    }\n    TOKEN = (\n        \"9cguMjz4GIfQV50B_D49quM-cEyIvWMwWi0gj1bf\"\n        \"-4YprIjt29ZrAxmDb5oIhmzEhwvcmcC4BR_kEZGmXdS1q7Ad3V94izdpXwtxgPPpozVUzQVm7KDrc5H9nfN3pLw=\"\n    )\n\n    @staticmethod\n    def get_fake_ms_token(key=\"msToken\", size=156) -> dict:\n        \"\"\"\n        根据传入长度产生随机字符串\n        \"\"\"\n        base_str = digits + ascii_uppercase + ascii_lowercase\n        length = len(base_str) - 1\n        return {key: \"\".join(base_str[randint(0, length)] for _ in range(size))}\n\n    @classmethod\n    async def _get_ms_token(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        params: dict,\n        headers: dict,\n        proxy: str,\n        **kwargs,\n    ) -> dict | None:\n        if response := await request_params(\n            logger,\n            cls.API,\n            data=dumps(cls.DATA | {\"tspFromClient\": int(time() * 1000)}),\n            headers=headers,\n            params=params,\n            proxy=proxy,\n            **kwargs,\n        ):\n            return TtWid.extract(logger, response, cls.NAME)\n        logger.error(_(\"获取 {name} 参数失败！\").format(name=cls.NAME))\n\n    @classmethod\n    async def get_real_ms_token(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        token=\"\",\n        proxy: str = None,\n        **kwargs,\n    ) -> dict | None:\n        params = {cls.NAME: token}\n        return await cls._get_ms_token(\n            logger,\n            params,\n            headers,\n            proxy,\n            **kwargs,\n        )\n\n    @classmethod\n    async def get_long_ms_token(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        token=\"\",\n        proxy: str = None,\n        **kwargs,\n    ) -> dict | None:\n        return await cls.get_real_ms_token(\n            logger,\n            headers,\n            token or cls.TOKEN,\n            proxy,\n            **kwargs,\n        )\n\n\nclass MsTokenTikTok(MsToken):\n    REFERER = \"https://www.tiktok.com/\"\n    API = \"https://mssdk-ttp2.tiktokw.us/web/report\"\n    DATA = {\n        \"magic\": 538969122,\n        \"version\": 1,\n        \"dataType\": 8,\n        \"strData\": \"3DWMSoJNifh/BoM1CDv7lbH3G7vd6C7zPt0YWMVrYRi369yWaBxCOhq+WMznjr1QWKkr/uLgcnRh+LQDtMl/JDLHSPlEqNPz\"\n        \"/iuxeOktia3YM/pJtUX4EQYqBMW8uAx4qFcN8M5H5XhB1FEkk76W09Xq5DwtcjoO4dpH18G3UcI1hasCXVW8B\"\n        \"+igwPIeEuOIayxuf3OZlTmZbNI1guSUBbccxoph0SEb1TVc4/DeQjQvXkXZOmuN144LcENdtflWmcQPqcwnfD2bWGuR4\"\n        \"+LUgRke1GcyVYa440PH/VOm+DYNcbKeBG87gqTHg+Y724ph1RQKlKX4nsi7Wa+V08ESimNbT8DMsbA\"\n        \"//MovFbr0CiVmvqtXg6VLloJH7UlZRQTC7T0l90KssOt0Y4T/H2EbU5XywcZd8OpICK4wB\"\n        \"/m8KuHGzrheYGmIfxUQtWhrlJdtqzoNI/GiEceTHxp4NahNof4KH6+BZMv87B7nYyE2x7eH2AaeG8iVoiyYKrE7ckQX8mjvj12\"\n        \"+BIkhUiKhpe3SGewK0iEB8NYH3fSqap/QnGsYcSy3lCwHlq7wHUcNdwhKFXkMS65Op\"\n        \"/zpS4uOEZqK9a0v8iGwBrd1VSfFki7sXGUFm5fGMh1Z9Z+4tycL7MYk5fzdUkZ+e56h4p5vPg4qpG17ntvn1LcXR/HXZKgMlx\"\n        \"+qqd3hOpnFOGcC2PahUp+zQ3Y/pZ3Jr+0XRmlHm4zpDmYJkqo3XZrTetOI5JwBkTN/GUkWyVC8hV48WyXpUxUiSHSBeN17735\"\n        \"+PrijcAZh/1+R\"\n        \"+gjnTkcAfm4pxlMfEur85pvI9K0qZbosPV3cgd3T3R5djejTilcyJ3wOC29pV4U193BEXqZnfIPYHFxXc5dlxYlq6tGHbdXsih1b\"\n        \"bguCXchz6byslGKDnWTSHA+QufOcfIh6HNijtM1iHNhAz/BkpiehN8u27ntq5p9VH0Um3Q6yh6lmcR5Jexry8l6zXT5HAbImhKK\"\n        \"F2GhMzznMaSFASYTTIyzwLVtZab+9HIEnlRmSg/B2Vrc0M0r+qsucb4vji4q/oWh7SUeqcstUXKt86dSUi0xmH1tRDbK9Gb8Avp\"\n        \"ef5tITSPqwuI9A6uqHctCCC54XMw6RPmmzueXJYM8hRF7PpjK76zxtPImLeg1zxwjnb8GsoaTnNsrDVboTpFtbcA6c3IEYvqZ/Z\"\n        \"OmJww74eMhDAuc0SGnF7RgIeHxHHc1mdoK6lmzjI4c2S7nYusgcLGzzSJm3D98AncBkOQ3BONTCAnb7era4absFz4jPTFWGPN5Z\"\n        \"3xhD5h5E9dHX1V05MUCzcTV+ooEtlcgLfW4nt+CPWeyxfekrlqMZPuwlvgepIOIj2dnYckVbCcXqIhNPVAzDzt847IzPnQGViT8\"\n        \"5VH6n6NKdA0c1om130oa+Zu5kWrzXqekOAkN1K7xlQlqD+t2QFGVZLtZoAUWF6+nAyI3Zz4+7fT/RAzsmRFSCWMiKsSK96tBLNZ\"\n        \"5GXupRlQ/Ns7MH5FCduL+l3I2Dfwas2M+qLr/gTJ/wRGGI4KhXNHlQzzJmOG8VOrwV4hyHBvl1B3j6R+7UZ/Jo57BZIHG+cui3o\"\n        \"AGqCreMByWLy3L+/38MkCCACw6YGvhccrYkjSIcmNv3qbQv1WoLXrGq0k9IOB69KWB7bX0kFUnr1l3Gwvc47U3IJIbGmSOYumtv\"\n        \"naumgZqcyWitMud0kOGW1wCvpyY1+tv1AZtsCIdLJYqj4M1u+iQ/GuJGQlyFWVY/3gcurWoFqhOl156t9mkXhHZeILv2Y/L+IEs\"\n        \"zW6cwu/N0tukf1kgBlLwtmfMQA/rvzn6ueAYNQ0A0KNSfm5ndiZaCxBlHlUBCa4Fe7vMxLhKZ44ffcu0D5RMESjduuykgInMrSp\"\n        \"vE88hHs01A2NL8HiRBQTjBWAiP273kWun6DWecqqkw0kr5ZjVGCYZPFVlLUL9JGcWcTmUZa96bTu7hKB14+P9tOjK+N95tuQnqL\"\n        \"DPS849ceh3qPX8PPn5PgmExPjd7OfMmbn39XCBZQBnMuuV8ceanDlmfnqhtqaEk3jRnkvXn3lDFY5EYw2Uja3XgkgBTyf3hsm7Y\"\n        \"mlqrGR/1mt0WyJiDW2sF+veKVxirGdv3GHJ2IRDo1lb6W/ZEIHiGimteqBXyYE7JdJxeFcrU2+NWoFvP2TX3DJAIEFaFcEQRkZA\"\n        \"+gzR2pCu3jfazUOEP3nKLE6If30xeUClWLC5qZXsRwIjjj5+CvtRNrEkAcnpQenq8RgTQ0fu9CvJZ9bcRWuItsZdjh+ll0+dFs7\"\n        \"yI9Qhus3ccl09aGUc6+EomD96DBuW9B6bEWmKnVJuqZJgeH6v+oCYinFjMdhrGPHf05U06Bu7NCHN361aqE/XKAyN9GmUZTHsp9\"\n        \"8hEbZqaWycuDGJ1PFc54dUEfaACa30WbEkT2zqzq3A9zHx7Rr87h79+t1yjz5CEvU7xfc1WXjV0vFr3+B8yJFhT0fWhNZ7fP/LL\"\n        \"3C/Zcy/3qznQFxavc0TacInSDLqfz8ju31N7LJs9js1Xd7UVyvqOq6nu0YOI1lDAl8xaetH6rAIsEr+IuOKvTVYuUXVjaTnMa2Q\"\n        \"xkORw2l+tfl9QgRF1csGofJl5K1tuSjTMbMxEGhoLcjzPEtqiyXF96CJSbquCwhw8tzQIXoajUgY9wrnUalSARMaXkhUejOMNqH\"\n        \"/0c5S6cyP5p4zk1cfUihY6W2vcNsrdILAib4dMVflXulaTBopkvh6fD6DiWHw36nQeLT9WfvZ3xwUeNjeQca8fWV0950GUNbVk8\"\n        \"Iq35ltuGdFhSiE+6wgXoq78NS5WB4iChkZ5/IIVvfU/0To32SEiHMRINQTZFXPZWjjIdxwkdmOvEbqD4Bfu4jWRSC5pzTN0bTU3\"\n        \"ax+hCYWDAVxsZi7HwkeMnDUueBaXt9QbeH0cA0XJELudePlsfYaqhEytDKG6PyQjROnQKZMDgBdsGi7kbcIJvsq9ldvI4XrYFfL\"\n        \"nNese4Hveij58+Rw0j6wO+7EjiWAEow5Q2Yqlgk2jNgB8xorpUaxxyIfe/rSNs7I0VhynwqJXENKq/ZWlf72liv1g1hMGDy8x9X\"\n        \"Q+x+pefBJ5h0r1Jd+FTE7Dpk7B57zAefH/9uAE/IUS21i78INIYa8QtORZOuLmW27y5fBjD4BdpPb8hYSjX56zHLkGjUNEXEj9C\"\n        \"HKns7tse8zAKUleMVTw5+3juYjsCVvPYntqx9Hbgc3QEG9zWoS6feX1aBIpIRR2M8dn8pWI8WmHWCa1cO/5DAMas83sExxMER4/\"\n        \"dXMIn7mLnsojNje1+XiAF9o2wt7rksJazO+nAxULLLWiMAsd6BpK6GgHZUgFFihSIYZaOrjE/TVoDREEuznHEdHiZMYdjAk9Gq4\"\n        \"SEUmeujJFyXHSQ6yYjpxSlQKFLTUAlYf+j9c55RoYO+/Wy0nb5Gwkzl8GEwa9SsWi/9prCJCNOvlwix5VPqerBpJvFF8dPJizXQ\"\n        \"85ZYJUknOOCxZViPwxsZaRbItUKO/7MVMBfK0Nde/AGrkCFMlwU45NvD0PrXWOIZMZW0Z5vtboqS1yMOHjBV97he4IXThAuLzjB\"\n        \"mzdtmUvIHgdxg1Fx+u//Qmbnqn00e4yqTQUpnfF5jCvRfUtacc6SfT0KbsFyUe4JRa5ZAhZ1OzeiqBOKm+NRF3ko7lnt70Tjwnt\"\n        \"Gcf2YK03kN5VEKYDEIFbQjmlktyxeUpiEW+ZdD7/A0jrC8ob3JhCzsrnntkt9vNK4NI8woIDKvDPAbbEKm4FsTsLfnJrbEL0qs1\"\n        \"n/0ISRhXH0XLYx5sLrVDzXjY6BwC51pkMBvmDT+EOpvln0Ya6+pAd1tuuWjbz3cZvFUe/V+808hjMPnf8ieuunjBKdW/zSDVul9\"\n        \"I/gIOzpJwmujzZh6FHDrAR0oMqyOC27kTfoEBy49s1JK+cvpx6+uUmGfuqEJuKemzHl3F0+4EF32fXngQcMPf2W0V0j5jgccde/\"\n        \"r7ga4Af3uEJNqYBfxX6L+r2aIPlGFvwQw2VLuhIKSiVaqhFrJbb4xYHSFhomTLgEQoxIB6sS4CXAg+sg33xtAwmtgdTFYtvuvYn\"\n        \"qFzB54DIcx/FNPzTUzwh/vhfup4HUWgL1lHnE/uaCZnceQXHxoymjfyBctHqmopigJI4arMEu3Db+xGclUpIrgmxMWs0CaG+yMp\"\n        \"33Ulmay3bNlhBpFzDSzRaMsNa0sk8L5MM0QCeKaTqaRx2qfaLuWlURXflBGRxApIZbMi9lIg119/QuKaXhtdFP00RYzYk03cTNi\"\n        \"MUlm0lKg/DGyOLWTp+huhZHg0umkQHDi0wbLDfwXrTZowQdim9iYPOJaLOUr1rqODk2dHe/gTLcErlAT/OL6MRmOvtwlMfpbN0L\"\n        \"n6xh11L4+WWJFNFT3lCXsFaybLh8R2MxllwT32EjAXSiLrd8rh05PBKGQJE7eg9hScjdNS4UUc8rSTf7pidBbSbMbfDJDWixSBT\"\n        \"nzkLD2Om7etBZ2yw/F14uK9sgtuRkNegmyazk84MChAL3gCCRKoDnwvc/3VhhJYmXyzDyQSZkVfUfr9Vm9TWhKjS7eyor8D/Rc9\"\n        \"K4NCGUQ3EOMnkxi1E3Ae52ZboKci/rZtqhaOZuwxD+fFXT4hXWA5OxK3++LxsKu0tnVRoufxjvDEIW4MfWqfsOOdnUreBJlB5uq\"\n        \"xqtYoGlBfgCntLU/F80FDgAfVDUqWr49fuRdOjsuZm\",\n    }\n    TOKEN = (\n        \"DFrAJZtLAY2Lrd8Tvmh5cqHYng42N9aIQxG0Rhos9kNznkm4oSeGUOmPptqIveuXzrQARNP\"\n        \"-F08uUkIaCQo_kaYSN6d7X5pQIM8pOFckqCgBLbTMqTZC9rEheMlW88EOKPMVBJ7t-CGQDTTfx0k8tEyx\"\n    )\n\n    @classmethod\n    async def get_real_ms_token(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        token=\"\",\n        proxy: str = None,\n        **kwargs,\n    ) -> dict | None:\n        params = {cls.NAME: token}\n        if token:\n            headers |= {\"Cookie\": f\"{cls.NAME}={token}\"}\n            params[\"X-Bogus\"] = quote(\n                XBogusTikTok().get_x_bogus(\n                    params, user_agent=headers.get(\"User-Agent\", USERAGENT)\n                ),\n                safe=\"\",\n            )\n        return await cls._get_ms_token(\n            logger,\n            params,\n            headers,\n            proxy,\n            **kwargs,\n        )\n\n\nasync def test():\n    from src.testers import Logger\n\n    print(\"抖音\", await MsToken.get_real_ms_token(Logger(), PARAMS_HEADERS, proxy=None))\n    print(\n        \"抖音\",\n        await MsToken.get_long_ms_token(\n            Logger(),\n            PARAMS_HEADERS,\n            proxy=None,\n        ),\n    )\n    print(\n        \"TikTok\",\n        await MsTokenTikTok.get_real_ms_token(\n            Logger(), PARAMS_HEADERS_TIKTOK, proxy=\"http://127.0.0.1:10809\"\n        ),\n    )\n    print(\n        \"TikTok\",\n        await MsTokenTikTok.get_long_ms_token(\n            Logger(), PARAMS_HEADERS_TIKTOK, proxy=\"http://127.0.0.1:10809\"\n        ),\n    )\n\n\nif __name__ == \"__main__\":\n    run(test())\n"
  },
  {
    "path": "src/encrypt/ttWid.py",
    "content": "from asyncio import run\nfrom http import cookies\nfrom json import dumps\nfrom typing import TYPE_CHECKING, Union\n\nfrom src.custom import PARAMS_HEADERS, PARAMS_HEADERS_TIKTOK\nfrom src.tools import request_params\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.record import BaseLogger, LoggerManager\n    from src.testers import Logger\n\n__all__ = [\"TtWid\", \"TtWidTikTok\"]\n\n\nclass TtWid:\n    NAME = \"ttwid\"\n    API = \"https://ttwid.bytedance.com/ttwid/union/register/\"\n    DATA = (\n        '{\"region\":\"cn\",\"aid\":1768,\"needFid\":false,\"service\":\"www.ixigua.com\",\"migrate_info\":{\"ticket\":\"\",'\n        '\"source\":\"node\"},\"cbUrlProtocol\":\"https\",\"union\":true}'\n    )\n\n    @classmethod\n    async def get_tt_wid(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        proxy: str = None,\n        **kwargs,\n    ) -> dict | None:\n        if response := await request_params(\n            logger,\n            cls.API,\n            data=cls.DATA,\n            headers=headers,\n            proxy=proxy,\n            **kwargs,\n        ):\n            return cls.extract(logger, response, cls.NAME)\n        logger.error(_(\"获取 {name} 参数失败！\").format(name=cls.NAME))\n\n    @staticmethod\n    def extract(\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"], headers, key: str\n    ) -> dict | None:\n        if c := headers.get(\"Set-Cookie\"):\n            cookie_jar = cookies.SimpleCookie()\n            cookie_jar.load(c)\n            if v := cookie_jar.get(key):\n                return {key: v.value}\n        logger.error(f\"获取 {key} 参数失败！\")\n\n\nclass TtWidTikTok(TtWid):\n    API = \"https://www.tiktok.com/ttwid/check/\"\n    DATA = dumps(\n        {\n            \"aid\": 1988,\n            \"service\": \"www.tiktok.com\",\n            \"union\": False,\n            \"unionHost\": \"\",\n            \"needFid\": False,\n            \"fid\": \"\",\n            \"migrate_priority\": 0,\n        },\n        separators=(\",\", \":\"),\n    )\n\n    @classmethod\n    async def get_tt_wid(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        cookie: str = \"\",\n        proxy: str = None,\n        **kwargs,\n    ) -> dict | None:\n        if response := await request_params(\n            logger,\n            cls.API,\n            data=cls.DATA,\n            headers=headers\n            | {\n                \"Cookie\": cookie,\n                \"Content-Type\": \"application/x-www-form-urlencoded\",\n            },\n            proxy=proxy,\n            **kwargs,\n        ):\n            return cls.extract(logger, response, cls.NAME)\n        logger.error(_(\"获取 {name} 参数失败！\").format(name=cls.NAME))\n\n\nasync def test():\n    from src.testers import Logger\n\n    print(\"抖音\", await TtWid.get_tt_wid(Logger(), PARAMS_HEADERS, proxy=None))\n    print(\n        \"TikTok\",\n        await TtWidTikTok.get_tt_wid(\n            Logger(),\n            PARAMS_HEADERS_TIKTOK,\n            cookie=\"ttwid=\",\n            proxy=\"http://localhost:10809\",\n        ),\n    )\n\n\nif __name__ == \"__main__\":\n    run(test())\n"
  },
  {
    "path": "src/encrypt/verifyFp.py",
    "content": "from random import random\nfrom string import ascii_lowercase\nfrom string import ascii_uppercase\nfrom string import digits\nfrom time import time\n\nfrom rich import print\n\n__all__ = [\n    \"VerifyFp\",\n]\n\n\nclass VerifyFp:\n    \"\"\"\n    var xi = function() {\n        return Pi.get(Si) || (null === localStorage || void 0 === localStorage ? void 0 : localStorage.getItem(Si)) || function() {\n            var e = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\".split(\"\")\n              , t = e.length\n              , n = Date.now().toString(36)\n              , r = [];\n            r[8] = r[13] = r[18] = r[23] = \"_\",\n            r[14] = \"4\";\n            for (var o = 0, i = void 0; o < 36; o++)\n                r[o] || (i = 0 | Math.random() * t,\n                r[o] = e[19 == o ? 3 & i | 8 : i]);\n            return \"verify_\" + n + \"_\" + r.join(\"\")\n        }()\n    }\n    \"\"\"\n\n    @staticmethod\n    def get_verify_fp(timestamp: int = None):\n        base_str = digits + ascii_uppercase + ascii_lowercase\n        t = len(base_str)\n        milliseconds = timestamp or int(round(time() * 1000))\n        base36 = \"\"\n\n        # 转换为 base36\n        while milliseconds > 0:\n            milliseconds, remainder = divmod(milliseconds, 36)\n            if remainder < 10:\n                base36 = str(remainder) + base36\n            else:\n                base36 = chr(ord(\"a\") + remainder - 10) + base36\n\n        # 设置固定字符\n        o = [\"\"] * 36\n        o[8] = o[13] = o[18] = o[23] = \"_\"\n        o[14] = \"4\"\n\n        # 随机填充缺失的字符\n        for i in range(36):\n            if not o[i]:\n                n = int(random() * t)  # 优化随机数生成方式\n                if i == 19:\n                    n = 3 & n | 8\n                o[i] = base_str[n]\n\n        # 组合最终字符串\n        return f\"verify_{base36}_\" + \"\".join(o)\n\n\nif __name__ == \"__main__\":\n    params = 1710413848097\n    print(VerifyFp.get_verify_fp(params))\n"
  },
  {
    "path": "src/encrypt/webID.py",
    "content": "from asyncio import run\nfrom typing import TYPE_CHECKING, Union\n\nfrom src.custom import PARAMS_HEADERS\nfrom src.tools import request_params\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.record import BaseLogger, LoggerManager\n    from src.testers import Logger\n\n__all__ = [\"WebId\"]\n\n\nclass WebId:\n    NAME = \"webid\"\n    API = \"https://mcs.zijieapi.com/webid\"\n    PARAMS = {\"aid\": \"6383\", \"sdk_version\": \"5.1.18_zip\", \"device_platform\": \"web\"}\n\n    @classmethod\n    async def get_web_id(\n        cls,\n        logger: Union[\"BaseLogger\", \"LoggerManager\", \"Logger\"],\n        headers: dict,\n        proxy: str = None,\n        **kwargs,\n    ) -> str | None:\n        user_agent = headers.get(\"User-Agent\")\n        data = (\n            f'{{\"app_id\":6383,\"url\":\"https://www.douyin.com/\",\"user_agent\":\"{user_agent}\",\"referer\":\"https://www'\n            f'.douyin.com/\",\"user_unique_id\":\"\"}}'\n        )\n        if response := await request_params(\n            logger,\n            cls.API,\n            params=cls.PARAMS,\n            data=data,\n            headers=headers,\n            resp=\"json\",\n            proxy=proxy,\n            **kwargs,\n        ):\n            return response.get(\"web_id\")\n        logger.error(_(\"获取 {name} 参数失败！\").format(name=cls.NAME))\n\n\nasync def test():\n    from src.testers import Logger\n\n    print(await WebId.get_web_id(Logger(), PARAMS_HEADERS, proxy=None))\n\n\nif __name__ == \"__main__\":\n    run(test())\n"
  },
  {
    "path": "src/encrypt/xBogus.py",
    "content": "from base64 import b64encode\nfrom hashlib import md5\nfrom time import time\nfrom urllib.parse import quote, urlencode\n\nfrom ..custom import USERAGENT\n\n__all__ = [\"XBogus\", \"XBogusTikTok\"]\n\n\nclass XBogus:\n    __string = \"Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=\"\n    __array = (\n        [None for _ in range(48)]\n        + list(range(10))\n        + [None for _ in range(39)]\n        + list(range(10, 16))\n    )\n    __canvas = 3873194319\n\n    @staticmethod\n    def disturb_array(a, b, e, d, c, f, t, n, o, i, r, _, x, u, s, l, v, h, g):\n        array = [0] * 19\n        array[0] = a\n        array[10] = b\n        array[1] = e\n        array[11] = d\n        array[2] = c\n        array[12] = f\n        array[3] = t\n        array[13] = n\n        array[4] = o\n        array[14] = i\n        array[5] = r\n        array[15] = _\n        array[6] = x\n        array[16] = u\n        array[7] = s\n        array[17] = l\n        array[8] = v\n        array[18] = h\n        array[9] = g\n        return array\n\n    @staticmethod\n    def generate_garbled_1(a, b, e, d, c, f, t, n, o, i, r, _, x, u, s, l, v, h, g):\n        array = [0] * 19\n        array[0] = a\n        array[1] = r\n        array[2] = b\n        array[3] = _\n        array[4] = e\n        array[5] = x\n        array[6] = d\n        array[7] = u\n        array[8] = c\n        array[9] = s\n        array[10] = f\n        array[11] = l\n        array[12] = t\n        array[13] = v\n        array[14] = n\n        array[15] = h\n        array[16] = o\n        array[17] = g\n        array[18] = i\n        return \"\".join(map(chr, map(int, array)))\n\n    @staticmethod\n    def generate_num(text):\n        return [\n            ord(text[i]) << 16 | ord(text[i + 1]) << 8 | ord(text[i + 2]) << 0\n            for i in range(0, 21, 3)\n        ]\n\n    @staticmethod\n    def generate_garbled_2(a, b, c):\n        return chr(a) + chr(b) + c\n\n    @staticmethod\n    def generate_garbled_3(a, b):\n        d = list(range(256))\n        c = 0\n        f = \"\"\n        for a_idx in range(256):\n            d[a_idx] = a_idx\n        for b_idx in range(256):\n            c = (c + d[b_idx] + ord(a[b_idx % len(a)])) % 256\n            e = d[b_idx]\n            d[b_idx] = d[c]\n            d[c] = e\n        t = 0\n        c = 0\n        for b_idx in range(len(b)):\n            t = (t + 1) % 256\n            c = (c + d[t]) % 256\n            e = d[t]\n            d[t] = d[c]\n            d[c] = e\n            f += chr(ord(b[b_idx]) ^ d[(d[t] + d[c]) % 256])\n        return f\n\n    def calculate_md5(self, input_string):\n        if isinstance(input_string, str):\n            array = self.md5_to_array(input_string)\n        elif isinstance(input_string, list):\n            array = input_string\n        else:\n            raise TypeError\n\n        md5_hash = md5()\n        md5_hash.update(bytes(array))\n        return md5_hash.hexdigest()\n\n    def md5_to_array(self, md5_str):\n        if isinstance(md5_str, str) and len(md5_str) > 32:\n            return [ord(char) for char in md5_str]\n        else:\n            return [\n                (self.__array[ord(md5_str[index])] << 4)\n                | self.__array[ord(md5_str[index + 1])]\n                for index in range(0, len(md5_str), 2)\n            ]\n\n    def process_url_path(self, url_path):\n        return self.md5_to_array(\n            self.calculate_md5(self.md5_to_array(self.calculate_md5(url_path)))\n        )\n\n    def generate_str(self, num):\n        string = [num & 16515072, num & 258048, num & 4032, num & 63]\n        string = [i >> j for i, j in zip(string, range(18, -1, -6))]\n        return \"\".join([self.__string[i] for i in string])\n\n    @staticmethod\n    def handle_ua(a, b):\n        d = list(range(256))\n        c = 0\n        result = bytearray(len(b))\n\n        for i in range(256):\n            c = (c + d[i] + ord(a[i % len(a)])) % 256\n            d[i], d[c] = d[c], d[i]\n\n        t = 0\n        c = 0\n\n        for i in range(len(b)):\n            t = (t + 1) % 256\n            c = (c + d[t]) % 256\n            d[t], d[c] = d[c], d[t]\n            result[i] = b[i] ^ d[(d[t] + d[c]) % 256]\n\n        return result\n\n    def generate_ua_array(self, user_agent: str, params: int) -> list:\n        ua_key = [\"\\u0000\", \"\\u0001\", chr(params)]\n        value = self.handle_ua(ua_key, user_agent.encode(\"utf-8\"))\n        value = b64encode(value)\n        return list(md5(value).digest())\n\n    def generate_x_bogus(\n        self, query: list, params: int, user_agent: str, timestamp: int\n    ):\n        ua_array = self.generate_ua_array(user_agent, params)\n        array = [\n            64,\n            0.00390625,\n            1,\n            params,\n            query[-2],\n            query[-1],\n            69,\n            63,\n            ua_array[-2],\n            ua_array[-1],\n            timestamp >> 24 & 255,\n            timestamp >> 16 & 255,\n            timestamp >> 8 & 255,\n            timestamp >> 0 & 255,\n            self.__canvas >> 24 & 255,\n            self.__canvas >> 16 & 255,\n            self.__canvas >> 8 & 255,\n            self.__canvas >> 0 & 255,\n            None,\n        ]\n        zero = 0\n        for i in array[:-1]:\n            if isinstance(i, float):\n                i = int(i)\n            zero ^= i\n        array[-1] = zero\n        garbled = self.generate_garbled_1(*self.disturb_array(*array))\n        garbled = self.generate_garbled_2(2, 255, self.generate_garbled_3(\"ÿ\", garbled))\n        return \"\".join(self.generate_str(i) for i in self.generate_num(garbled))\n\n    def get_x_bogus(\n        self, query: dict | str, params=8, user_agent=USERAGENT, test_time=None\n    ):\n        timestamp = int(test_time or time())\n        query = self.process_url_path(\n            urlencode(query, quote_via=quote) if isinstance(query, dict) else query\n        )\n        return self.generate_x_bogus(query, params, user_agent, timestamp)\n\n\nclass XBogusTikTok(XBogus):\n    pass\n"
  },
  {
    "path": "src/encrypt/xGnarly.py",
    "content": "from hashlib import md5\nfrom random import randint\nfrom time import time\n\nfrom src.custom import USERAGENT\n\n\nclass XGnarly:\n    _AA = [\n        0xFFFFFFFF,\n        138,\n        1498001188,\n        211147047,\n        253,\n        None,\n        203,\n        288,\n        9,\n        1196819126,\n        3212677781,\n        135,\n        263,\n        193,\n        58,\n        18,\n        244,\n        2931180889,\n        240,\n        173,\n        268,\n        2157053261,\n        261,\n        175,\n        14,\n        5,\n        171,\n        270,\n        156,\n        258,\n        13,\n        15,\n        3732962506,\n        185,\n        169,\n        2,\n        6,\n        132,\n        162,\n        200,\n        3,\n        160,\n        217618912,\n        62,\n        2517678443,\n        44,\n        164,\n        4,\n        96,\n        183,\n        2903579748,\n        3863347763,\n        119,\n        181,\n        10,\n        190,\n        8,\n        2654435769,\n        259,\n        104,\n        230,\n        128,\n        2633865432,\n        225,\n        1,\n        257,\n        143,\n        179,\n        16,\n        600974999,\n        185100057,\n        32,\n        188,\n        53,\n        2718276124,\n        177,\n        196,\n        4294967296,\n        147,\n        117,\n        17,\n        49,\n        7,\n        28,\n        12,\n        266,\n        216,\n        11,\n        0,\n        45,\n        166,\n        247,\n        1451689750,\n    ]\n    _OT = [_AA[9], _AA[69], _AA[51], _AA[92]]\n    _MASK32 = 0xFFFFFFFF\n    _BASE64_ALPHABET = (\n        \"u09tbS3UvgDEe6r-ZVMXzLpsAohTn7mdINQlW412GqBjfYiyk8JORCF5/xKHwacP=\"\n    )\n\n    def __init__(self):\n        \"\"\"\n        初始化 XGnarly 实例，并创建其唯一的 PRNG 状态。\n        \"\"\"\n        self.St = None\n        self._init_prng_state()\n\n    def _init_prng_state(self):\n        \"\"\"\n        设置 PRNG 的初始状态，此状态将在此实例的生命周期内持续存在。\n        \"\"\"\n        now_ms = int(time() * 1000)\n        self.kt = [\n            self._AA[44],\n            self._AA[74],\n            self._AA[10],\n            self._AA[62],\n            self._AA[42],\n            self._AA[17],\n            self._AA[2],\n            self._AA[21],\n            self._AA[3],\n            self._AA[70],\n            self._AA[50],\n            self._AA[32],\n            self._AA[0] & now_ms,\n            randint(0, self._AA[77]),\n            randint(0, self._AA[77]),\n            randint(0, self._AA[77]),\n        ]\n        self.St = self._AA[88]  # position pointer, starts at 0\n\n    # ── BIT HELPERS ────────────────────────────────────────\n    @classmethod\n    def _u32(cls, x: int) -> int:\n        return x & cls._MASK32\n\n    @classmethod\n    def _rotl(cls, x: int, n: int) -> int:\n        return cls._u32(((x << n) & cls._MASK32) | (x >> (32 - n)))\n\n    # ── CHACHA CORE ────────────────────────────────────────\n    @classmethod\n    def _quarter(cls, st: list[int], a: int, b: int, c: int, d: int):\n        st[a] = cls._u32(st[a] + st[b])\n        st[d] = cls._rotl(st[d] ^ st[a], 16)\n        st[c] = cls._u32(st[c] + st[d])\n        st[b] = cls._rotl(st[b] ^ st[c], 12)\n        st[a] = cls._u32(st[a] + st[b])\n        st[d] = cls._rotl(st[d] ^ st[a], 8)\n        st[c] = cls._u32(st[c] + st[d])\n        st[b] = cls._rotl(st[b] ^ st[c], 7)\n\n    @classmethod\n    def _chacha_block(cls, state: list[int], rounds: int) -> list[int]:\n        w = state.copy()\n        r = 0\n        while r < rounds:\n            cls._quarter(w, 0, 4, 8, 12)\n            cls._quarter(w, 1, 5, 9, 13)\n            cls._quarter(w, 2, 6, 10, 14)\n            cls._quarter(w, 3, 7, 11, 15)\n            r += 1\n            if r >= rounds:\n                break\n            cls._quarter(w, 0, 5, 10, 15)\n            cls._quarter(w, 1, 6, 11, 12)\n            cls._quarter(w, 2, 7, 12, 13)\n            cls._quarter(w, 3, 4, 13, 14)\n            r += 1\n        for i in range(16):\n            w[i] = cls._u32(w[i] + state[i])\n        return w\n\n    def _bump_counter(self):\n        self.kt[12] = self._u32(self.kt[12] + 1)\n\n    # ── JS-faithful PRNG (rand) ────────────────────────────\n    def rand(self) -> float:\n        e = self._chacha_block(self.kt, 8)\n        t = e[self.St]\n        r = (e[self.St + 8] & 0xFFFFFFF0) >> 11\n        if self.St == 7:\n            self._bump_counter()\n            self.St = 0\n        else:\n            self.St += 1\n        return (t + 4294967296 * r) / (2**53)\n\n    # ── UTILITIES ──────────────────────────────────────────\n    @staticmethod\n    def _num_to_bytes(val: int) -> list[int]:\n        if val < 65535:\n            return [(val >> 8) & 0xFF, val & 0xFF]\n        return [(val >> 24) & 0xFF, (val >> 16) & 0xFF, (val >> 8) & 0xFF, val & 0xFF]\n\n    @staticmethod\n    def _be_int_from_str(s: str) -> int:\n        b = s.encode(\"utf-8\")[:4]\n        acc = 0\n        for x in b:\n            acc = (acc << 8) | x\n        return acc & XGnarly._MASK32\n\n    # ── MESSAGE ENCRYPTION ──────────────────────────────\n    def _encrypt_chacha(self, key_words: list[int], rounds: int, data: list[int]):\n        n_full = len(data) // 4\n        leftover = len(data) % 4\n        words = [0] * ((len(data) + 3) // 4)\n\n        for i in range(n_full):\n            j = 4 * i\n            words[i] = (\n                data[j] | (data[j + 1] << 8) | (data[j + 2] << 16) | (data[j + 3] << 24)\n            )\n        if leftover:\n            v = 0\n            base = 4 * n_full\n            for c in range(leftover):\n                v |= data[base + c] << (8 * c)\n            words[n_full] = v\n\n        o = 0\n        state = key_words.copy()\n        while o + 16 < len(words):\n            stream = self._chacha_block(state, rounds)\n            state[12] = self._u32(state[12] + 1)\n            for k in range(16):\n                words[o + k] ^= stream[k]\n            o += 16\n\n        if o < len(words):\n            stream = self._chacha_block(state, rounds)\n            for k in range(len(words) - o):\n                words[o + k] ^= stream[k]\n\n        for i in range(n_full):\n            w = words[i]\n            j = 4 * i\n            data[j : j + 4] = [\n                w & 0xFF,\n                (w >> 8) & 0xFF,\n                (w >> 16) & 0xFF,\n                (w >> 24) & 0xFF,\n            ]\n        if leftover:\n            w = words[n_full]\n            base = 4 * n_full\n            for c in range(leftover):\n                data[base + c] = (w >> (8 * c)) & 0xFF\n\n    def _ab22(self, key12_words: list[int], rounds: int, s: str) -> str:\n        state = self._OT + key12_words\n        data = [ord(ch) for ch in s]\n        self._encrypt_chacha(state, rounds, data)\n        return \"\".join(chr(x) for x in data)\n\n    # ── MAIN API ───────────────────────────────────────────\n    def generate(\n        self,\n        query_string: str,\n        body: str = \"\",\n        user_agent: str = USERAGENT,\n        envcode: int = 0,\n        version: str = \"5.1.1\",\n    ) -> str:\n        timestamp_ms = int(time() * 1000)\n\n        obj = {\n            1: 1,\n            2: envcode,\n            3: md5(query_string.encode()).hexdigest(),\n            4: md5(body.encode()).hexdigest(),\n            5: md5(user_agent.encode()).hexdigest(),\n            6: timestamp_ms // 1000,\n            7: 1508145731,\n            8: int((timestamp_ms * 1000) % 2147483648),\n            9: version,\n        }\n\n        if version == \"5.1.1\":\n            obj[10] = \"1.0.0.314\"\n            obj[11] = 1\n            v12 = 0\n            for i in range(1, 12):\n                v = obj[i]\n                to_xor = v if isinstance(v, int) else self._be_int_from_str(v)\n                v12 ^= to_xor\n            obj[12] = v12 & self._MASK32\n        elif version != \"5.1.0\":\n            raise ValueError(f\"Unsupported version: {version}\")\n\n        v0 = 0\n        for i in range(1, len(obj) + 1):\n            v = obj[i]\n            if isinstance(v, int):\n                v0 ^= v\n        obj[0] = v0 & self._MASK32\n\n        payload = [len(obj)]\n        for k, v in obj.items():\n            payload.append(k)\n            val_bytes = (\n                self._num_to_bytes(v) if isinstance(v, int) else list(v.encode(\"utf-8\"))\n            )\n            payload.extend(self._num_to_bytes(len(val_bytes)))\n            payload.extend(val_bytes)\n        base_str = \"\".join(chr(x) for x in payload)\n\n        key_words = []\n        key_bytes = []\n        round_accum = 0\n        for _ in range(12):\n            word = int(self.rand() * 4294967296) & self._MASK32\n            key_words.append(word)\n            round_accum = (round_accum + (word & 15)) & 15\n            key_bytes.extend(\n                [\n                    word & 0xFF,\n                    (word >> 8) & 0xFF,\n                    (word >> 16) & 0xFF,\n                    (word >> 24) & 0xFF,\n                ]\n            )\n        rounds = round_accum + 5\n\n        enc = self._ab22(key_words, rounds, base_str)\n\n        insert_pos = 0\n        for b in key_bytes:\n            insert_pos = (insert_pos + b) % (len(enc) + 1)\n        for ch in enc:\n            insert_pos = (insert_pos + ord(ch)) % (len(enc) + 1)\n\n        key_bytes_str = \"\".join(chr(b) for b in key_bytes)\n        final_str = (\n            chr(((1 << 6) ^ (1 << 3) ^ 3) & 0xFF)\n            + enc[:insert_pos]\n            + key_bytes_str\n            + enc[insert_pos:]\n        )\n\n        out = []\n        full_len = (len(final_str) // 3) * 3\n        for i in range(0, full_len, 3):\n            block = (\n                (ord(final_str[i]) << 16)\n                | (ord(final_str[i + 1]) << 8)\n                | ord(final_str[i + 2])\n            )\n            out.extend(\n                [\n                    self._BASE64_ALPHABET[(block >> 18) & 63],\n                    self._BASE64_ALPHABET[(block >> 12) & 63],\n                    self._BASE64_ALPHABET[(block >> 6) & 63],\n                    self._BASE64_ALPHABET[block & 63],\n                ]\n            )\n\n        return \"\".join(out)\n"
  },
  {
    "path": "src/extract/__init__.py",
    "content": "from .extractor import Extractor\n\n__all__ = [\"Extractor\"]\n"
  },
  {
    "path": "src/extract/extractor.py",
    "content": "from datetime import datetime\nfrom json import dumps\nfrom time import localtime, strftime\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import urlparse\n\nfrom ..custom import (\n    AUTHOR_COVER_INDEX,\n    AUTHOR_COVER_URL_INDEX,\n    AVATAR_LARGER_INDEX,\n    BITRATE_INFO_TIKTOK_INDEX,\n    COMMENT_IMAGE_INDEX,\n    COMMENT_IMAGE_LIST_INDEX,\n    COMMENT_STICKER_INDEX,\n    DYNAMIC_COVER_INDEX,\n    HOT_WORD_COVER_INDEX,\n    IMAGE_INDEX,\n    IMAGE_TIKTOK_INDEX,\n    LIVE_COVER_INDEX,\n    LIVE_DATA_INDEX,\n    MUSIC_COLLECTION_COVER_INDEX,\n    MUSIC_COLLECTION_DOWNLOAD_INDEX,\n    MUSIC_INDEX,\n    SEARCH_AVATAR_INDEX,\n    SEARCH_USER_INDEX,\n    STATIC_COVER_INDEX,\n    VIDEO_INDEX,\n    VIDEO_TIKTOK_INDEX,\n    condition_filter,\n)\nfrom ..tools import DownloaderError\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from datetime import date\n\n    from ..config import Parameter\n\n__all__ = [\"Extractor\"]\n\n\nclass Extractor:\n    statistics_keys = (\n        \"digg_count\",\n        \"comment_count\",\n        \"collect_count\",\n        \"share_count\",\n        \"play_count\",\n    )\n    statistics_keys_tiktok = (\n        \"diggCount\",\n        \"commentCount\",\n        \"collectCount\",\n        \"shareCount\",\n        \"playCount\",\n    )\n    detail_necessary_keys = \"id\"\n    comment_necessary_keys = \"cid\"\n    user_necessary_keys = \"sec_uid\"\n    extract_params_tiktok = {\n        \"sec_uid\": \"author.secUid\",\n        \"mix_id\": \"playlistId\",\n        \"uid\": \"author.id\",\n        \"nickname\": \"author.nickname\",\n        \"mix_title\": \"playlistId\",  # TikTok 不返回合辑标题\n    }\n    extract_params = {\n        \"sec_uid\": \"author.sec_uid\",\n        \"mix_id\": \"mix_info.mix_id\",\n        \"uid\": \"author.uid\",\n        \"nickname\": \"author.nickname\",\n        \"mix_title\": \"mix_info.mix_name\",\n    }\n\n    def __init__(self, params: \"Parameter\"):\n        self.log = params.logger\n        self.date_format = params.date_format\n        self.cleaner = params.CLEANER\n        self.type = {\n            \"batch\": self.__batch,\n            \"detail\": self.__detail,\n            \"comment\": self.__comment,\n            \"live\": self.__live,\n            \"user\": self.__user,\n            \"search\": self.__search,\n            \"hot\": self.__hot,\n            \"music\": self.__music,\n        }\n\n    def get_user_info(self, data: dict) -> dict:\n        try:\n            return {\n                \"nickname\": data[\"nickname\"],\n                \"sec_uid\": data[\"sec_uid\"],\n                \"uid\": data[\"uid\"],\n            }\n        except (KeyError, TypeError):\n            self.log.error(_(\"提取账号信息失败: {data}\").format(data=data))\n            return {}\n\n    def get_user_info_tiktok(self, data: dict) -> dict:\n        try:\n            return {\n                \"nickname\": data[\"user\"][\"nickname\"],\n                \"sec_uid\": data[\"user\"][\"secUid\"],\n                \"uid\": data[\"user\"][\"id\"],\n            }\n        except (KeyError, TypeError):\n            self.log.error(_(\"提取账号信息失败: {data}\").format(data=data))\n            return {}\n\n    @staticmethod\n    def generate_data_object(\n        data: dict | list,\n    ) -> SimpleNamespace | list[SimpleNamespace]:\n        def depth_conversion(element):\n            if isinstance(element, dict):\n                return SimpleNamespace(\n                    **{k: depth_conversion(v) for k, v in element.items()}\n                )\n            elif isinstance(element, list):\n                return [depth_conversion(item) for item in element]\n            else:\n                return element\n\n        return depth_conversion(data)\n\n    @staticmethod\n    def safe_extract(\n        data: SimpleNamespace | list[SimpleNamespace],\n        attribute_chain: str,\n        default: str | int | list | dict | SimpleNamespace = \"\",\n    ):\n        attributes = attribute_chain.split(\".\")\n        for attribute in attributes:\n            if \"[\" in attribute:\n                parts = attribute.split(\"[\", 1)\n                attribute = parts[0]\n                index = parts[1].split(\"]\", 1)[0]\n                try:\n                    index = int(index)\n                    data = getattr(data, attribute, None)[index]\n                except (IndexError, TypeError, ValueError):\n                    return default\n            else:\n                data = getattr(data, attribute, None)\n                if not data:\n                    return default\n        return data or default\n\n    async def run(\n        self,\n        data: list[dict],\n        recorder,\n        type_=\"detail\",\n        tiktok=False,\n        **kwargs,\n    ) -> list[dict]:\n        if type_ not in self.type.keys():\n            raise DownloaderError\n        return await self.type[type_](data, recorder, tiktok, **kwargs)\n\n    async def __batch(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n        name: str,\n        mark: str,\n        earliest,\n        latest,\n        same=True,\n    ) -> list[dict]:\n        \"\"\"批量下载作品\"\"\"\n        container = SimpleNamespace(\n            all_data=[],\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n            cache=None,\n            name=name,\n            mark=mark,\n            same=same,  # 是否相同作者\n            earliest=earliest,\n            latest=latest,\n        )\n        self.__platform_classify_detail(\n            data,\n            container,\n            tiktok,\n        )\n        container.all_data = self.__clean_extract_data(\n            container.all_data,\n            self.detail_necessary_keys,\n        )\n        self.__extract_item_records(container.all_data)\n        await self.__record_data(recorder, container.all_data)\n        self.__date_filter(container)\n        self.__condition_filter(container)\n        self.__summary_detail(container.all_data)\n        return container.all_data\n\n    @staticmethod\n    def __condition_filter(\n        container: SimpleNamespace,\n    ):\n        \"\"\"自定义筛选作品\"\"\"\n        result = [i for i in container.all_data if condition_filter(i)]\n        container.all_data = result\n\n    def __summary_detail(\n        self,\n        data: list[dict],\n    ):\n        \"\"\"汇总作品数量\"\"\"\n        self.log.info(_(\"筛选处理后作品数量: {count}\").format(count=len(data)))\n\n    def __extract_batch(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ) -> None:\n        \"\"\"批量提取作品信息\"\"\"\n        container.cache = container.template.copy()\n        self.__extract_detail_info(container.cache, data)\n        self.__extract_account_info(container, data)\n        self.__extract_music(container.cache, data)\n        self.__extract_statistics(container.cache, data)\n        self.__extract_tags(container.cache, data)\n        self.__extract_extra_info(container.cache, data)\n        self.__extract_additional_info(container.cache, data)\n        container.all_data.append(container.cache)\n\n    def __extract_batch_tiktok(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ) -> None:\n        \"\"\"批量提取作品信息\"\"\"\n        container.cache = container.template.copy()\n        self.__extract_detail_info_tiktok(container.cache, data)\n        self.__extract_account_info_tiktok(container, data)\n        self.__extract_music(container.cache, data, True)\n        self.__extract_statistics_tiktok(container.cache, data)\n        self.__extract_tags_tiktok(container.cache, data)\n        self.__extract_extra_info_tiktok(container.cache, data)\n        self.__extract_additional_info(container.cache, data, True)\n        container.all_data.append(container.cache)\n\n    def __extract_extra_info(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        if e := self.safe_extract(data, \"anchor_info\"):\n            extra = dumps(e, ensure_ascii=False, indent=2, default=lambda x: vars(x))\n        else:\n            extra = \"\"\n        item[\"extra\"] = extra\n\n    def __extract_extra_info_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        # TODO: 尚未适配 TikTok 额外信息\n        item[\"extra\"] = \"\"\n\n    def __extract_commodity_data(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        pass\n\n    def __extract_game_data(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        pass\n\n    def __extract_description(self, data: SimpleNamespace) -> str:\n        # 2023/11/11: 抖音不再折叠过长的作品描述\n        return self.safe_extract(data, \"desc\")\n        # if len(desc := self.safe_extract(data, \"desc\")) < 107:\n        #     return desc\n        # long_desc = self.safe_extract(data, \"share_info.share_link_desc\")\n        # return long_desc.split(\n        #     \"  \", 1)[-1].split(\"  %s\", 1)[0].replace(\"# \", \"#\")\n\n    def __clean_description(self, desc: str) -> str:\n        return self.cleaner.clear_spaces(self.cleaner.filter(desc))\n\n    def __format_date(\n        self,\n        data: int,\n    ) -> str:\n        return strftime(\n            self.date_format,\n            localtime(data or None),\n        )\n\n    def __extract_detail_info(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        item[\"id\"] = self.safe_extract(data, \"aweme_id\")\n        item[\"desc\"] = (\n            self.__clean_description(\n                self.__extract_description(data),\n            )\n            or item[\"id\"]\n        )\n        item[\"create_timestamp\"] = self.safe_extract(data, \"create_time\")\n        item[\"create_time\"] = self.__format_date(item[\"create_timestamp\"])\n        self.__extract_text_extra(item, data)\n        self.__classifying_detail(item, data)\n\n    def __extract_detail_info_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        item[\"id\"] = self.safe_extract(data, \"id\")\n        item[\"desc\"] = (\n            self.__clean_description(self.__extract_description(data)) or item[\"id\"]\n        )\n        item[\"create_timestamp\"] = self.safe_extract(\n            data,\n            \"createTime\",\n        )\n        item[\"create_time\"] = self.__format_date(item[\"create_timestamp\"])\n        self.__extract_text_extra_tiktok(item, data)\n        self.__classifying_detail_tiktok(item, data)\n\n    def __classifying_detail(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        # 作品分类\n        if images := self.safe_extract(data, \"images\"):\n            self.__extract_image_info(item, data, images)\n        else:\n            self.__extract_video_info(\n                item,\n                data,\n                _(\"视频\"),\n            )\n\n    def __classifying_detail_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        if images := self.safe_extract(data, \"imagePost.images\"):\n            self.__extract_image_info_tiktok(item, data, images)\n        else:\n            self.__extract_video_info_tiktok(\n                item,\n                data,\n                _(\"视频\"),\n            )\n\n    def __extract_additional_info(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        tiktok=False,\n    ):\n        # item[\"ratio\"] = self.safe_extract(data, \"video.ratio\")\n        item[\"share_url\"] = self.__generate_link(\n            item[\"type\"],\n            item[\"id\"],\n            item[\"unique_id\"] if tiktok else None,\n        )\n\n    @staticmethod\n    def __generate_link(\n        type_: str,\n        id_: str,\n        unique_id: str = None,\n    ) -> str:\n        match bool(unique_id), type_:\n            case True, \"视频\":\n                return f\"https://www.tiktok.com/@{unique_id}/video/{id_}\"\n            case True, \"图集\":\n                return f\"https://www.tiktok.com/@{unique_id}/photo/{id_}\"\n            case False, \"视频\":\n                return f\"https://www.douyin.com/video/{id_}\"\n            case False, \"图集\" | \"实况\":\n                return f\"https://www.douyin.com/note/{id_}\"\n            case _:\n                return \"\"\n\n    @staticmethod\n    def __clean_share_url(url: str) -> str:\n        if not url:\n            return url\n        parsed_url = urlparse(url)\n        return f\"{parsed_url.scheme}://{parsed_url.netloc}{parsed_url.path}\"\n\n    def __extract_image_info(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        images: list[SimpleNamespace],\n    ) -> None:\n        if any(\n            self.safe_extract(\n                i,\n                \"video\",\n            )\n            for i in images\n        ):\n            self.__set_blank_data(\n                item,\n                data,\n                _(\"实况\"),\n            )\n            item[\"downloads\"] = [\n                self.__classify_slides_item(\n                    i,\n                )\n                for i in images\n            ]\n        else:\n            self.__set_blank_data(\n                item,\n                data,\n                _(\"图集\"),\n            )\n            item[\"downloads\"] = [\n                self.safe_extract(\n                    i,\n                    f\"url_list[{IMAGE_INDEX}]\",\n                )\n                for i in images\n            ]\n\n    def __extract_image_info_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        images: list,\n    ) -> None:\n        self.__set_blank_data(\n            item,\n            data,\n            _(\"图集\"),\n        )\n        item[\"downloads\"] = [\n            self.safe_extract(\n                i,\n                f\"imageURL.urlList[{IMAGE_TIKTOK_INDEX}]\",\n            )\n            for i in images\n        ]\n\n    def __set_blank_data(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        type_=_(\"图集\"),\n    ):\n        item[\"type\"] = type_\n        item[\"duration\"] = \"00:00:00\"\n        item[\"uri\"] = \"\"\n        item[\"height\"] = -1\n        item[\"width\"] = -1\n        self.__extract_cover(item, data)\n\n    def __extract_video_info(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        type_=_(\"视频\"),\n    ) -> None:\n        item[\"type\"] = type_\n        item[\"height\"], item[\"width\"], item[\"downloads\"] = (\n            self.__extract_video_download(\n                data,\n            )\n        )\n        item[\"duration\"] = self.time_conversion(\n            self.safe_extract(data, \"video.duration\", 0)\n        )\n        item[\"uri\"] = self.safe_extract(data, \"video.play_addr.uri\")\n        self.__extract_cover(item, data, True)\n\n    def __classify_slides_item(\n        self,\n        item: SimpleNamespace,\n    ) -> str:\n        if self.safe_extract(item, \"video\"):\n            return self.__extract_video_download(\n                item,\n            )[-1]\n        return self.safe_extract(item, f\"url_list[{IMAGE_INDEX}]\")\n\n    def __extract_video_download(\n        self,\n        data: SimpleNamespace,\n    ) -> tuple[int, int, str]:\n        bit_rate: list[SimpleNamespace] = self.safe_extract(\n            data,\n            \"video.bit_rate\",\n            [],\n        )\n        try:\n            bit_rate: list[tuple[int, int, int, int, int, list[str]]] = [\n                (\n                    i.FPS,\n                    i.bit_rate,\n                    i.play_addr.data_size,\n                    i.play_addr.height,\n                    i.play_addr.width,\n                    i.play_addr.url_list,\n                )\n                for i in bit_rate\n            ]\n            bit_rate.sort(\n                key=lambda x: (\n                    max(\n                        x[3],\n                        x[4],\n                    ),\n                    x[0],\n                    x[1],\n                    x[2],\n                ),\n            )\n            return (\n                (\n                    bit_rate[-1][-3],\n                    bit_rate[-1][-2],\n                    bit_rate[-1][-1][VIDEO_INDEX],\n                )\n                if bit_rate\n                else (-1, -1, \"\")\n            )\n        except AttributeError:\n            self.log.error(\n                f\"视频下载地址解析失败: {data}\",\n                False,\n            )\n            height = self.safe_extract(\n                bit_rate[0],\n                \"play_addr.height\",\n                -1,\n            )\n            width = self.safe_extract(\n                bit_rate[0],\n                \"play_addr.width\",\n                -1,\n            )\n            url = self.safe_extract(\n                bit_rate[0],\n                f\"play_addr.url_list[{VIDEO_INDEX}]\",\n            )\n            return height, width, url\n\n    def __extract_video_info_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        type_=_(\"视频\"),\n    ) -> None:\n        item[\"type\"] = type_\n        # item[\"downloads\"] = self.safe_extract(\n        #     data,\n        #     \"video.playAddr\",\n        # )  # 视频文件大小优先\n        item[\"height\"], item[\"width\"], item[\"downloads\"] = (\n            self.__extract_video_download_tiktok(\n                data,\n            )\n        )  # 视频分辨率优先\n        item[\"duration\"] = self.time_conversion_tiktok(\n            self.safe_extract(\n                data,\n                \"video.duration\",\n                0,\n            )\n        )\n        item[\"uri\"] = self.safe_extract(\n            data,\n            f\"video.bitrateInfo[{BITRATE_INFO_TIKTOK_INDEX}].PlayAddr.Uri\",\n        )\n        self.__extract_cover_tiktok(item, data, True)\n\n    def __extract_video_download_tiktok(\n        self,\n        data: SimpleNamespace,\n    ) -> tuple[int, int, str]:\n        bitrate_info: list[SimpleNamespace] = self.safe_extract(\n            data,\n            \"video.bitrateInfo\",\n            [],\n        )\n        try:\n            bitrate_info: list[tuple[int, str, int, int, list[str]]] = [\n                (\n                    i.Bitrate,\n                    i.PlayAddr.DataSize,\n                    i.PlayAddr.Height,\n                    i.PlayAddr.Width,\n                    i.PlayAddr.UrlList,\n                )\n                for i in bitrate_info\n            ]\n            bitrate_info.sort(\n                key=lambda x: (\n                    max(\n                        x[2],\n                        x[3],\n                    ),\n                    x[0],\n                    x[1],\n                ),\n            )\n            return (\n                (\n                    bitrate_info[-1][-3],\n                    bitrate_info[-1][-2],\n                    bitrate_info[-1][-1][VIDEO_TIKTOK_INDEX],\n                )\n                if bitrate_info\n                else (-1, -1, \"\")\n            )\n        except AttributeError:\n            self.log.error(\n                f\"视频下载地址解析失败: {data}\",\n                False,\n            )\n            height = self.safe_extract(\n                bitrate_info[0],\n                \"PlayAddr.Height\",\n                -1,\n            )\n            width = self.safe_extract(\n                bitrate_info[0],\n                \"PlayAddr.Width\",\n                -1,\n            )\n            url = self.safe_extract(\n                bitrate_info[0],\n                f\"PlayAddr.UrlList[{VIDEO_TIKTOK_INDEX}]\",\n            )\n            return height, width, url\n\n    @staticmethod\n    def time_conversion(time_: int) -> str:\n        second = time_ // 1000\n        return f\"{second // 3600:0>2d}:{second % 3600 // 60:0>2d}:{second % 3600 % 60:0>2d}\"\n\n    @staticmethod\n    def time_conversion_tiktok(seconds: int) -> str:\n        minutes, seconds = divmod(seconds, 60)\n        hours, minutes = divmod(minutes, 60)\n        return \"{:02d}:{:02d}:{:02d}\".format(int(hours), int(minutes), int(seconds))\n\n    def __extract_text_extra(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        \"\"\"作品标签\"\"\"\n        text = [\n            self.safe_extract(i, \"hashtag_name\")\n            for i in self.safe_extract(data, \"text_extra\", [])\n        ]\n        item[\"text_extra\"] = [i for i in text if i]\n\n    def __extract_text_extra_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ):\n        \"\"\"作品标签\"\"\"\n        text = [\n            self.safe_extract(i, \"hashtagName\")\n            for i in self.safe_extract(data, \"textExtra\", [])\n        ]\n        item[\"text_extra\"] = [i for i in text if i]\n\n    def __extract_cover(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        has=False,\n    ) -> None:\n        if has:\n            # 动态封面图链接\n            item[\"dynamic_cover\"] = self.safe_extract(\n                data, f\"video.dynamic_cover.url_list[{DYNAMIC_COVER_INDEX}]\"\n            )\n            # 静态封面图链接\n            item[\"static_cover\"] = self.safe_extract(\n                data, f\"video.cover.url_list[{STATIC_COVER_INDEX}]\"\n            )\n        else:\n            item[\"dynamic_cover\"], item[\"static_cover\"] = \"\", \"\"\n\n    def __extract_cover_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        has=False,\n    ) -> None:\n        if has:\n            # 动态封面图链接\n            item[\"dynamic_cover\"] = self.safe_extract(data, \"video.dynamicCover\")\n            # 静态封面图链接\n            item[\"static_cover\"] = self.safe_extract(data, \"video.cover\")\n        else:\n            item[\"dynamic_cover\"], item[\"static_cover\"] = \"\", \"\"\n\n    def __extract_music(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n        tiktok=False,\n    ) -> None:\n        if music_data := self.safe_extract(data, \"music\"):\n            if tiktok:\n                author = self.safe_extract(music_data, \"authorName\")\n                title = self.safe_extract(music_data, \"title\")\n                url = self.safe_extract(music_data, \"playUrl\")\n            else:\n                author = self.safe_extract(music_data, \"author\")\n                title = self.safe_extract(music_data, \"title\")\n                url = self.safe_extract(\n                    music_data,\n                    f\"play_url.url_list[{MUSIC_INDEX}]\",\n                )  # 部分作品的音乐无法下载\n\n        else:\n            author, title, url = \"\", \"\", \"\"\n        item[\"music_author\"] = author\n        item[\"music_title\"] = title\n        item[\"music_url\"] = url\n\n    def __extract_statistics(self, item: dict, data: SimpleNamespace) -> None:\n        data = self.safe_extract(data, \"statistics\")\n        for i in self.statistics_keys:\n            item[i] = self.safe_extract(\n                data,\n                i,\n                -1,\n            )\n\n    def __extract_statistics_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        data = self.safe_extract(data, \"stats\")\n        for i, j in enumerate(self.statistics_keys_tiktok):\n            item[self.statistics_keys[i]] = self.safe_extract(\n                data,\n                j,\n                -1,\n            )\n\n    def __extract_tags(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        if not (t := self.safe_extract(data, \"video_tag\")):\n            item[\"tag\"] = []\n        else:\n            item[\"tag\"] = [self.safe_extract(i, \"tag_name\") for i in t]\n\n    def __extract_tags_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        if not (t := self.safe_extract(data, \"textExtra\")):\n            item[\"tag\"] = []\n        else:\n            item[\"tag\"] = [self.safe_extract(i, \"hashtagName\") for i in t]\n\n    def __extract_account_info(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n        key=\"author\",\n    ) -> None:\n        data = self.safe_extract(data, key)\n        container.cache[\"uid\"] = self.safe_extract(data, \"uid\")\n        container.cache[\"sec_uid\"] = self.safe_extract(data, \"sec_uid\")\n        # container.cache[\"short_id\"] = self.safe_extract(data, \"short_id\")\n        container.cache[\"unique_id\"] = self.safe_extract(\n            data,\n            \"unique_id\",\n        )\n        container.cache[\"signature\"] = self.safe_extract(data, \"signature\")\n        container.cache[\"user_age\"] = self.safe_extract(data, \"user_age\", -1)\n        self.__extract_nickname_info(container, data)\n\n    def __extract_account_info_tiktok(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n        key=\"author\",\n    ) -> None:\n        data = self.safe_extract(data, key)\n        container.cache[\"uid\"] = self.safe_extract(data, \"id\")\n        container.cache[\"sec_uid\"] = self.safe_extract(data, \"secUid\")\n        container.cache[\"unique_id\"] = self.safe_extract(data, \"uniqueId\")\n        container.cache[\"signature\"] = self.safe_extract(data, \"signature\")\n        container.cache[\"user_age\"] = -1\n        self.__extract_nickname_info(container, data)\n\n    def __extract_nickname_info(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ) -> None:\n        if container.same:\n            container.cache[\"nickname\"] = container.name\n            container.cache[\"mark\"] = container.mark or container.name\n        else:\n            name = self.cleaner.filter_name(\n                self.safe_extract(data, \"nickname\", _(\"已注销账号\")),\n                default=_(\"无效账号昵称\"),\n            )\n            container.cache[\"nickname\"] = name\n            container.cache[\"mark\"] = name\n\n    def preprocessing_data(\n        self,\n        data: list[dict] | dict,\n        tiktok: bool = False,\n        mode: str = ...,\n        mark: str = \"\",\n        user_id: str = \"\",\n        mix_id: str = \"\",\n        mix_title: str = \"\",\n        collect_id: str = \"\",\n        collect_name: str = \"\",\n    ) -> tuple[\n        str,\n        str,\n        str,\n    ]:\n        if isinstance(data, dict):\n            info = (\n                self.get_user_info_tiktok(data) if tiktok else self.get_user_info(data)\n            )\n            if user_id != (s := info.get(\"sec_uid\")):\n                self.log.error(\n                    _(\"sec_user_id {user_id} 与 {s} 不一致\").format(\n                        user_id=user_id, s=s\n                    ),\n                )\n                return \"\", \"\", \"\"\n            name = self.cleaner.filter_name(\n                info[\"nickname\"],\n                info[\"uid\"],\n            )\n            mark = self.cleaner.filter_name(\n                mark,\n                name,\n            )\n            return (\n                info[\"uid\"],\n                name,\n                mark,\n            )\n        elif isinstance(data, list):\n            match mode:\n                case \"post\":\n                    item = self.__select_item(\n                        data,\n                        user_id,\n                        (self.extract_params_tiktok if tiktok else self.extract_params)[\n                            \"sec_uid\"\n                        ],\n                    )\n                    id_, name, mark = self.__extract_pretreatment_data(\n                        item,\n                        (self.extract_params_tiktok if tiktok else self.extract_params)[\n                            \"uid\"\n                        ],\n                        (self.extract_params_tiktok if tiktok else self.extract_params)[\n                            \"nickname\"\n                        ],\n                        mark,\n                    )\n                    return id_, name, mark\n                case \"mix\":\n                    if tiktok:\n                        id_ = mix_id\n                        name = self.cleaner.filter_name(\n                            mix_title,\n                        ).strip()\n                        mark = self.cleaner.filter_name(\n                            mark,\n                            name,\n                        ).strip()\n                    else:\n                        item = self.__select_item(\n                            data,\n                            mix_id,\n                            self.extract_params[\"mix_id\"],\n                        )\n                        id_, name, mark = self.__extract_pretreatment_data(\n                            item,\n                            self.extract_params[\"mix_id\"],\n                            self.extract_params[\"mix_title\"],\n                            mark,\n                            mix_title,\n                        )\n                    return id_, name, mark\n                case \"collects\":\n                    collect_name = self.cleaner.filter_name(\n                        collect_name,\n                        collect_id,\n                    )\n                    return collect_id, collect_name, collect_name\n                case _:\n                    raise DownloaderError\n        else:\n            raise DownloaderError\n\n    def __select_item(\n        self,\n        data: list[dict],\n        id_: str,\n        key: str,\n    ):\n        \"\"\"从多个数据返回对象\"\"\"\n        for item in data:\n            item = self.generate_data_object(item)\n            if id_ == self.safe_extract(item, key):\n                return item\n        raise DownloaderError(_(\"提取账号信息或合集信息失败，请向作者反馈！\"))\n\n    def __extract_pretreatment_data(\n        self,\n        item: SimpleNamespace,\n        id_: str,\n        name: str,\n        mark: str,\n        title: str = None,  # TikTok 合辑需要直接传入标题\n    ):\n        id_ = self.safe_extract(item, id_)\n        name = self.cleaner.filter_name(\n            title\n            or self.safe_extract(\n                item,\n                name,\n                id_,\n            ),\n        )\n        mark = self.cleaner.filter_name(\n            mark,\n            name,\n        )\n        return id_, name.strip(), mark.strip()\n\n    def __platform_classify_detail(\n        self,\n        data: list[dict],\n        container: SimpleNamespace,\n        tiktok: bool,\n    ) -> None:\n        if tiktok:\n            [\n                self.__extract_batch_tiktok(\n                    container,\n                    self.generate_data_object(item),\n                )\n                for item in data\n            ]\n        else:\n            [\n                self.__extract_batch(\n                    container,\n                    self.generate_data_object(item),\n                )\n                for item in data\n            ]\n\n    async def __detail(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n    ) -> list[dict]:\n        container = SimpleNamespace(\n            all_data=[],\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n            cache=None,\n            same=False,\n        )\n        self.__platform_classify_detail(\n            data,\n            container,\n            tiktok,\n        )\n        container.all_data = self.__clean_extract_data(\n            container.all_data, self.detail_necessary_keys\n        )\n        self.__extract_item_records(container.all_data)\n        await self.__record_data(recorder, container.all_data)\n        self.__condition_filter(container)\n        return container.all_data\n\n    async def __comment(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n        source=False,\n    ) -> list[dict]:\n        if not any(data):\n            return []\n        container = SimpleNamespace(\n            all_data=[],\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n            cache=None,\n            same=False,\n        )\n        if source:\n            container.all_data = data\n        else:\n            [\n                self.__extract_comments_data(container, self.generate_data_object(i))\n                for i in data\n            ]\n            container.all_data = self.__clean_extract_data(\n                container.all_data, self.comment_necessary_keys\n            )\n            await self.__record_data(recorder, container.all_data)\n        return container.all_data\n\n    def __extract_comments_data(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        container.cache = container.template.copy()\n        container.cache[\"create_timestamp\"] = self.safe_extract(data, \"create_time\")\n        container.cache[\"create_time\"] = self.__format_date(\n            container.cache[\"create_timestamp\"]\n        )\n        container.cache[\"ip_label\"] = self.safe_extract(data, \"ip_label\", \"未知\")\n        container.cache[\"text\"] = self.safe_extract(data, \"text\")\n        container.cache[\"image\"] = self.safe_extract(\n            data,\n            f\"image_list[{COMMENT_IMAGE_LIST_INDEX}].origin_url.url_list[{COMMENT_IMAGE_INDEX}]\",\n        )\n        container.cache[\"sticker\"] = self.safe_extract(\n            data, f\"sticker.static_url.url_list[{COMMENT_STICKER_INDEX}]\"\n        )\n        container.cache[\"digg_count\"] = self.safe_extract(data, \"digg_count\", -1)\n        container.cache[\"reply_to_reply_id\"] = self.safe_extract(\n            data, \"reply_to_reply_id\"\n        )\n        container.cache[\"reply_comment_total\"] = self.safe_extract(\n            data, \"reply_comment_total\", 0\n        )\n        container.cache[\"reply_id\"] = self.safe_extract(data, \"reply_id\")\n        container.cache[\"cid\"] = self.safe_extract(data, \"cid\")\n        self.__extract_account_info(container, data, \"user\")\n        container.all_data.append(container.cache)\n\n    @classmethod\n    def extract_reply_ids(cls, data: list[dict]) -> list[str]:\n        container = SimpleNamespace(\n            reply_ids=[],\n            cache=None,\n        )\n        for item in data:\n            item = cls.generate_data_object(item)\n            container.cache = {\n                \"reply_comment_total\": cls.safe_extract(\n                    item,\n                    \"reply_comment_total\",\n                    0,\n                ),\n                \"cid\": cls.safe_extract(item, \"cid\"),\n            }\n            cls.__filter_reply_ids(container)\n        return container.reply_ids\n\n    @staticmethod\n    def __filter_reply_ids(container: SimpleNamespace):\n        if container.cache[\"reply_comment_total\"] > 0:\n            container.reply_ids.append(container.cache[\"cid\"])\n\n    async def __live(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n        *args,\n    ) -> list[dict]:\n        container = SimpleNamespace(all_data=[])\n        if tiktok:\n            [\n                self.__extract_live_data_tiktok(container, self.generate_data_object(i))\n                for i in data\n            ]\n        else:\n            [\n                self.__extract_live_data(container, self.generate_data_object(i))\n                for i in data\n            ]\n        return container.all_data\n\n    def __extract_live_data(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        if data := self.safe_extract(\n            data, f\"data.data[{LIVE_DATA_INDEX}]\"\n        ) or self.safe_extract(data, \"data.room\"):\n            live_data = {\n                \"status\": self.safe_extract(data, \"status\"),\n                \"nickname\": self.safe_extract(data, \"owner.nickname\"),\n                \"title\": self.safe_extract(data, \"title\"),\n                \"flv_pull_url\": vars(\n                    self.safe_extract(\n                        data,\n                        \"stream_url.flv_pull_url\",\n                        SimpleNamespace(),\n                    )\n                ),\n                \"hls_pull_url_map\": vars(\n                    self.safe_extract(\n                        data,\n                        \"stream_url.hls_pull_url_map\",\n                        SimpleNamespace(),\n                    )\n                ),\n                \"cover\": self.safe_extract(data, f\"cover.url_list[{LIVE_COVER_INDEX}]\"),\n                \"total_user_str\": self.safe_extract(data, \"stats.total_user_str\"),\n                \"user_count_str\": self.safe_extract(data, \"stats.user_count_str\"),\n            }\n            container.all_data.append(live_data)\n\n    def __extract_live_data_tiktok(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        data = self.safe_extract(data, \"data\")\n        live_data = {\n            \"create_time\": datetime.fromtimestamp(t)\n            if (t := self.safe_extract(data, \"create_time\"))\n            else \"未知\",\n            \"id_str\": self.safe_extract(data, \"id_str\"),\n            \"like_count\": self.safe_extract(data, \"like_count\"),\n            \"nickname\": self.safe_extract(data, \"owner.nickname\"),\n            \"display_id\": self.safe_extract(data, \"owner.display_id\"),\n            \"title\": self.safe_extract(data, \"title\"),\n            \"user_count\": self.safe_extract(data, \"user_count\"),\n            \"flv_pull_url\": vars(self.safe_extract(data, \"stream_url.flv_pull_url\")),\n            \"message\": self.safe_extract(data, \"message\"),\n            \"prompts\": self.safe_extract(data, \"prompts\"),\n        }\n        container.all_data.append(live_data)\n\n    async def __user(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n    ) -> list[dict]:\n        container = SimpleNamespace(\n            all_data=[],\n            cache=None,\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n        )\n        [\n            self.__extract_user_data(container, self.generate_data_object(i))\n            for i in data\n        ]\n        container.all_data = self.__clean_extract_data(\n            container.all_data, self.user_necessary_keys\n        )\n        await self.__record_data(recorder, container.all_data)\n        return container.all_data\n\n    def __extract_user_data(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        container.cache = container.template.copy()\n        container.cache[\"avatar\"] = self.safe_extract(\n            data, f\"avatar_larger.url_list[{AVATAR_LARGER_INDEX}]\"\n        )\n        container.cache[\"city\"] = self.safe_extract(data, \"city\")\n        container.cache[\"country\"] = self.safe_extract(data, \"country\")\n        container.cache[\"district\"] = self.safe_extract(data, \"district\")\n        container.cache[\"favoriting_count\"] = self.safe_extract(\n            data, \"favoriting_count\", -1\n        )\n        container.cache[\"follower_count\"] = self.safe_extract(\n            data, \"follower_count\", -1\n        )\n        container.cache[\"max_follower_count\"] = self.safe_extract(\n            data, \"max_follower_count\", -1\n        )\n        container.cache[\"following_count\"] = self.safe_extract(\n            data, \"following_count\", -1\n        )\n        container.cache[\"total_favorited\"] = self.safe_extract(\n            data, \"total_favorited\", -1\n        )\n        container.cache[\"gender\"] = {1: \"男\", 2: \"女\"}.get(\n            self.safe_extract(data, \"gender\"),\n            \"未知\",\n        )\n        container.cache[\"ip_location\"] = self.safe_extract(data, \"ip_location\")\n        container.cache[\"nickname\"] = self.safe_extract(data, \"nickname\")\n        container.cache[\"province\"] = self.safe_extract(data, \"province\")\n        container.cache[\"school_name\"] = self.safe_extract(data, \"school_name\")\n        container.cache[\"sec_uid\"] = self.safe_extract(data, \"sec_uid\")\n        container.cache[\"signature\"] = self.safe_extract(data, \"signature\")\n        container.cache[\"uid\"] = self.safe_extract(data, \"uid\")\n        container.cache[\"unique_id\"] = self.safe_extract(data, \"unique_id\")\n        container.cache[\"user_age\"] = self.safe_extract(data, \"user_age\", -1)\n        container.cache[\"cover\"] = self.safe_extract(\n            data, f\"cover_url[{AUTHOR_COVER_URL_INDEX}].url_list[{AUTHOR_COVER_INDEX}]\"\n        )\n        container.cache[\"short_id\"] = self.safe_extract(data, \"short_id\")\n        container.cache[\"aweme_count\"] = self.safe_extract(data, \"aweme_count\", -1)\n        container.cache[\"verify\"] = self.safe_extract(data, \"custom_verify\", \"无\")\n        container.cache[\"enterprise\"] = self.safe_extract(\n            data, \"enterprise_verify_reason\", \"无\"\n        )\n        container.cache[\"url\"] = (\n            f\"https://www.douyin.com/user/{container.cache['sec_uid']}\"\n        )\n        container.all_data.append(container.cache)\n\n    async def __search(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n        tab: int,\n    ) -> list[dict]:\n        if tab in {0, 1}:\n            return await self.__search_general(data, recorder)\n        elif tab == 2:\n            return await self.__search_user(data, recorder)\n        elif tab == 3:\n            return await self.__search_live(data, recorder)\n\n    async def __search_general(\n        self,\n        data: list[dict],\n        recorder,\n    ) -> list[dict]:\n        container = SimpleNamespace(\n            all_data=[],\n            cache=None,\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n            same=False,\n        )\n        [\n            self.__search_result_classify(container, self.generate_data_object(i))\n            for i in data\n        ]\n        await self.__record_data(recorder, container.all_data)\n        return container.all_data\n\n    def __search_result_classify(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        if d := self.safe_extract(data, \"aweme_info\"):\n            self.__extract_batch(container, d)\n        elif d := self.safe_extract(data, \"aweme_mix_info.mix_items\"):\n            [self.__extract_batch(container, i) for i in d]\n        elif d := self.safe_extract(data, \"card_info.attached_info.aweme_list\"):\n            [self.__extract_batch(container, i) for i in d]\n        elif d := self.safe_extract(data, f\"user_list[{SEARCH_USER_INDEX}].items\"):\n            [self.__extract_batch(container, i) for i in d]\n        # elif d := self.safe_extract(data, \"user_list.user_info\"):\n        #     pass\n        # elif d := self.safe_extract(data, \"music_list\"):\n        #     pass\n        # elif d := self.safe_extract(data, \"common_aladdin\"):\n        #     pass\n        else:\n            self.log.error(f\"Unreported search results: {data}\", False)\n\n    async def __search_user(\n        self,\n        data: list[dict],\n        recorder,\n    ) -> list[dict]:\n        container = SimpleNamespace(\n            all_data=[],\n            cache=None,\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n        )\n        [\n            self.__deal_search_user_live(\n                container, self.generate_data_object(i[\"user_info\"])\n            )\n            for i in data\n        ]\n        await self.__record_data(recorder, container.all_data)\n        return container.all_data\n\n    def __deal_search_user_live(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n        user=True,\n    ):\n        if user:\n            container.cache = container.template.copy()\n        container.cache[\"avatar\"] = self.safe_extract(\n            data,\n            f\"{'avatar_thumb' if user else 'avatar_larger'}.url_list[{SEARCH_AVATAR_INDEX}]\",\n        )\n        container.cache[\"nickname\"] = self.safe_extract(data, \"nickname\")\n        container.cache[\"sec_uid\"] = self.safe_extract(data, \"sec_uid\")\n        container.cache[\"signature\"] = self.safe_extract(data, \"signature\")\n        container.cache[\"uid\"] = self.safe_extract(data, \"uid\")\n        container.cache[\"short_id\"] = self.safe_extract(data, \"short_id\")\n        container.cache[\"verify\"] = self.safe_extract(data, \"custom_verify\", \"无\")\n        container.cache[\"enterprise\"] = self.safe_extract(\n            data, \"enterprise_verify_reason\", \"无\"\n        )\n        if user:\n            container.cache[\"follower_count\"] = self.safe_extract(\n                data, \"follower_count\", -1\n            )\n            container.cache[\"total_favorited\"] = self.safe_extract(\n                data, \"total_favorited\", -1\n            )\n            container.cache[\"unique_id\"] = self.safe_extract(data, \"unique_id\")\n            container.all_data.append(container.cache)\n        # else:\n        #     pass\n\n    async def __search_live(\n        self,\n        data: list[dict],\n        recorder,\n    ) -> list[dict]:\n        container = SimpleNamespace(\n            all_data=[],\n            cache=None,\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n        )\n        [self.__deal_search_live(container, self.generate_data_object(i)) for i in data]\n        await self.__record_data(recorder, container.all_data)\n        return container.all_data\n\n    def __deal_search_live(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        container.cache = container.template.copy()\n        self.__deal_search_user_live(\n            container, self.safe_extract(data, \"author\"), False\n        )\n        container.cache[\"room_id\"] = self.safe_extract(data, \"aweme_id\")\n        container.all_data.append(container.cache)\n\n    async def __hot(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok: bool,\n    ) -> list[dict]:\n        all_data = []\n        [self.__deal_hot_data(all_data, self.generate_data_object(i)) for i in data]\n        await self.__record_data(recorder, all_data)\n        return all_data\n\n    def __deal_hot_data(self, container: list, data: SimpleNamespace):\n        cache = {\n            \"position\": str(self.safe_extract(data, \"position\", -1)),\n            \"sentence_id\": self.safe_extract(data, \"sentence_id\"),\n            \"word\": self.safe_extract(data, \"word\"),\n            \"video_count\": str(self.safe_extract(data, \"video_count\", -1)),\n            \"event_time\": self.__format_date(self.safe_extract(data, \"event_time\")),\n            \"view_count\": str(self.safe_extract(data, \"view_count\", -1)),\n            \"hot_value\": str(self.safe_extract(data, \"hot_value\", -1)),\n            \"cover\": self.safe_extract(\n                data, f\"word_cover.url_list[{HOT_WORD_COVER_INDEX}]\"\n            ),\n        }\n        container.append(cache)\n\n    async def __record_data(self, record, data: list[dict]):\n        # 记录数据\n        for i in data:\n            await record.save(self.__extract_values(record, i))\n\n    @staticmethod\n    def __extract_values(record, data: dict) -> list:\n        return [data[key] for key in record.field_keys]\n\n    @staticmethod\n    def __date_filter(container: SimpleNamespace):\n        # print(\"前\", len(container.all_data))  # 调试代码\n        result = []\n        for item in container.all_data:\n            create_time = datetime.fromtimestamp(item[\"create_timestamp\"]).date()\n            if container.earliest <= create_time <= container.latest:\n                result.append(item)\n            # else:\n            #     print(\"丢弃\", item)  # 调试代码\n        # print(\"后\", len(result))  # 调试代码\n        container.all_data = result\n\n    def source_date_filter(\n        self,\n        data: list[dict],\n        earliest: \"date\",\n        latest: \"date\",\n        tiktok=False,\n    ) -> list[dict]:\n        if tiktok:\n            return self.__source_date_filter(\n                data,\n                \"createTime\",\n                earliest=earliest,\n                latest=latest,\n            )\n        return self.__source_date_filter(\n            data,\n            earliest=earliest,\n            latest=latest,\n        )\n\n    def __source_date_filter(\n        self,\n        data: list[dict],\n        key: str = \"create_time\",\n        earliest: \"date\" = ...,\n        latest: \"date\" = ...,\n    ) -> list[dict]:\n        result = []\n        for item in data:\n            if not (create_time := item.get(key, 0)):\n                result.append(item)\n                continue\n            create_time = datetime.fromtimestamp(create_time).date()\n            if earliest <= create_time <= latest:\n                result.append(item)\n        self.__summary_detail(result)\n        return result\n\n    @classmethod\n    def extract_mix_id(cls, data: dict) -> str:\n        data = cls.generate_data_object(data)\n        return cls.safe_extract(data, \"mix_info.mix_id\")\n\n    def __extract_item_records(self, data: list[dict]):\n        # 记录提取成功的条目\n        for i in data:\n            self.log.info(f\"{i['type']} {i['id']} 数据提取成功\", False)\n\n    @classmethod\n    def extract_mix_collect_info(cls, data: list[dict]) -> list[dict]:\n        data = cls.generate_data_object(data)\n        return [\n            {\n                \"title\": Extractor.safe_extract(i, \"mix_name\"),\n                \"id\": Extractor.safe_extract(i, \"mix_id\"),\n            }\n            for i in data\n        ]\n\n    @classmethod\n    def extract_collects_info(cls, data: list[dict]) -> list[dict]:\n        data = cls.generate_data_object(data)\n        return [\n            {\n                \"name\": Extractor.safe_extract(i, \"collects_name\"),\n                \"id\": Extractor.safe_extract(i, \"collects_id_str\"),\n            }\n            for i in data\n        ]\n\n    @staticmethod\n    def __clean_extract_data(data: list[dict], key: str) -> list[dict]:\n        # 去除无效数据\n        return [i for i in data if i.get(key)]\n\n    async def __music(\n        self,\n        data: list[dict],\n        recorder,\n        tiktok=False,\n    ) -> list[dict]:\n        \"\"\"暂不记录收藏音乐数据\"\"\"\n        container = SimpleNamespace(\n            all_data=[],\n            template={\n                \"collection_time\": datetime.now().strftime(self.date_format),\n            },\n            cache=None,\n            same=False,\n        )\n        [\n            self.__extract_collection_music(\n                container,\n                self.generate_data_object(item),\n            )\n            for item in data\n        ]\n        return container.all_data\n\n    def __extract_collection_music(\n        self,\n        container: SimpleNamespace,\n        data: SimpleNamespace,\n    ):\n        container.cache = container.template.copy()\n        container.cache[\"id\"] = self.safe_extract(data, \"id_str\")\n        container.cache[\"title\"] = self.safe_extract(data, \"title\")\n        container.cache[\"author\"] = self.safe_extract(data, \"author\")\n        container.cache[\"album\"] = self.safe_extract(data, \"album\")\n        container.cache[\"cover\"] = self.safe_extract(\n            data, f\"cover_hd.url_list[{MUSIC_COLLECTION_COVER_INDEX}]\"\n        )\n        container.cache[\"download\"] = self.safe_extract(\n            data, f\"play_url.url_list[{MUSIC_COLLECTION_DOWNLOAD_INDEX}]\"\n        )\n        container.cache[\"duration\"] = self.time_conversion(\n            self.safe_extract(data, \"duration\", 0)\n        )\n        container.all_data.append(container.cache)\n"
  },
  {
    "path": "src/gui_edition/__init__.py",
    "content": ""
  },
  {
    "path": "src/interface/__init__.py",
    "content": "from ..interface.account import Account\nfrom ..interface.account_tiktok import AccountTikTok\nfrom ..interface.collection import Collection\nfrom ..interface.collects import (\n    Collects,\n    CollectsDetail,\n    CollectsMix,\n    CollectsMusic,\n    CollectsSeries,\n)\nfrom ..interface.comment import Comment, Reply\nfrom ..interface.comment_tiktok import CommentTikTok, ReplyTikTok\nfrom ..interface.detail import Detail\nfrom ..interface.detail_tiktok import DetailTikTok\nfrom ..interface.hashtag import HashTag\nfrom ..interface.hot import Hot\nfrom ..interface.info import Info\nfrom ..interface.info_tiktok import InfoTikTok\nfrom ..interface.live import Live\nfrom ..interface.live_tiktok import LiveTikTok\nfrom ..interface.mix import Mix\nfrom ..interface.mix_tiktok import MixListTikTok\nfrom ..interface.mix_tiktok import MixTikTok\nfrom ..interface.search import Search\nfrom ..interface.template import API\nfrom ..interface.template import APITikTok\nfrom ..interface.user import User\n"
  },
  {
    "path": "src/interface/account.py",
    "content": "from datetime import date, datetime, timedelta\nfrom typing import TYPE_CHECKING, Callable, Coroutine, Type, Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Account(API):\n    post_api = f\"{API.domain}aweme/v1/web/aweme/post/\"\n    favorite_api = f\"{API.domain}aweme/v1/web/aweme/favorite/\"\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: str = ...,\n        tab=\"post\",\n        earliest: str | float | int = \"\",\n        latest: str | float | int = \"\",\n        pages: int = None,\n        cursor=0,\n        count=18,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.sec_user_id = sec_user_id\n        self.api, self.favorite, self.pages = self.check_type(\n            tab, pages or params.max_pages\n        )\n        # TODO: 重构数据验证逻辑\n        self.latest: date = self.check_latest(latest)\n        self.earliest: date = self.check_earliest(earliest)\n        self.cursor = cursor\n        self.count = count\n        self.text = _(\"账号喜欢作品\") if self.favorite else _(\"账号发布作品\")\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"max_cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        if self.favorite:\n            self.set_referer(f\"{self.domain}user/{self.sec_user_id}?showTab=like\")\n        else:\n            self.set_referer(f\"{self.domain}user/{self.sec_user_id}\")\n        match single_page:\n            case True:\n                await self.run_single(\n                    data_key,\n                    error_text\n                    or _(\n                        \"该账号为私密账号，需要使用登录后的 Cookie，且登录的账号需要关注该私密账号\"\n                    ),\n                    cursor,\n                    has_more,\n                    params,\n                    data,\n                    method,\n                    headers,\n                    *args,\n                    **kwargs,\n                )\n                return self.response\n            case False:\n                await self.run_batch(\n                    data_key,\n                    error_text\n                    or _(\n                        \"该账号为私密账号，需要使用登录后的 Cookie，且登录的账号需要关注该私密账号\"\n                    ),\n                    cursor,\n                    has_more,\n                    params,\n                    data,\n                    method,\n                    headers,\n                    *args,\n                    **kwargs,\n                )\n                return self.response, self.earliest, self.latest\n        raise ValueError\n\n    async def run_single(\n        self,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"max_cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        await super().run_single(\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n    async def run_batch(\n        self,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"max_cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        await super().run_batch(\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            callback=callback or self.early_stop,\n            *args,\n            **kwargs,\n        )\n        self.summary_works()\n\n    async def early_stop(self):\n        \"\"\"如果获取数据的发布日期已经早于限制日期，就不需要再获取下一页的数据了\"\"\"\n        if (\n            not self.favorite\n            and self.earliest\n            > datetime.fromtimestamp(max(int(self.cursor) / 1000, 0)).date()\n        ):\n            self.finished = True\n\n    def generate_params(\n        self,\n    ) -> dict:\n        match self.favorite:\n            case True:\n                return self.generate_favorite_params()\n            case False:\n                return self.generate_post_params()\n        return {}\n\n    def generate_favorite_params(self) -> dict:\n        return self.params | {\n            \"sec_user_id\": self.sec_user_id,\n            \"max_cursor\": self.cursor,\n            \"min_cursor\": \"0\",\n            \"whale_cut_token\": \"\",\n            \"cut_version\": \"1\",\n            \"count\": self.count,\n            \"publish_video_strategy_type\": \"2\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    def generate_post_params(self) -> dict:\n        return self.params | {\n            \"sec_user_id\": self.sec_user_id,\n            \"max_cursor\": self.cursor,\n            \"locate_query\": \"false\",\n            \"show_live_replay_strategy\": \"1\",\n            \"need_time_list\": \"1\",\n            \"time_list_query\": \"0\",\n            \"whale_cut_token\": \"\",\n            \"cut_version\": \"1\",\n            \"count\": self.count,\n            \"publish_video_strategy_type\": \"2\",\n        }\n\n    def check_type(self, tab: str, pages: int) -> tuple[str, bool, int]:\n        match tab:\n            case \"favorite\":\n                return self.favorite_api, True, pages\n            case \"post\":\n                pass\n            case _:\n                self.log.warning(\n                    _(\"tab 参数 {tab} 设置错误，程序将使用默认值: post\").format(tab=tab)\n                )\n        return self.post_api, False, 99999\n\n    def check_earliest(self, date_: str | float | int) -> date:\n        return self.check_date(date(2016, 9, 20), self.latest, _(\"最早\"), date_)\n\n    def check_latest(self, date_: str | float | int) -> date:\n        return self.check_date(date.today(), date.today(), _(\"最晚\"), date_)\n\n    def check_date(\n        self, default: date, start: date, tip: str, value: str | float | int\n    ) -> date:\n        if not value:\n            return default\n        if isinstance(value, (int, float)):\n            date_ = start - timedelta(days=value)\n        elif isinstance(value, str):\n            try:\n                date_ = datetime.strptime(value, \"%Y/%m/%d\").date()\n            except ValueError:\n                self.log.warning(\n                    _(\"作品{tip}发布日期无效 {date}\").format(tip=tip, date=value)\n                )\n                return default\n        else:\n            raise ValueError(\n                _(\"作品{tip}发布日期参数 {date} 类型错误\").format(tip=tip, date=value)\n            )\n        self.log.info(\n            _(\"作品{tip}发布日期: {latest_date}\").format(tip=tip, latest_date=date_)\n        )\n        return date_  # 返回 date 对象\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[data_key]):\n                self.log.warning(error_text)\n                self.finished = True\n            else:\n                self.cursor = data_dict[cursor]\n                self.append_response(d)\n                self.finished = not data_dict[has_more]\n        except KeyError:\n            if data_dict.get(\"status_code\") == 0:\n                self.log.warning(_(\"配置文件 cookie 参数未登录，数据获取已提前结束\"))\n            else:\n                self.log.error(\n                    _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n                )\n            self.finished = True\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Account(\n            params,\n            sec_user_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/account_tiktok.py",
    "content": "from typing import TYPE_CHECKING, Callable, Coroutine, Type, Union\n\nfrom src.interface.account import Account\nfrom src.interface.template import APITikTok\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass AccountTikTok(\n    Account,\n    APITikTok,\n):\n    post_api = f\"{APITikTok.domain}api/post/item_list/\"\n    favorite_api = f\"{APITikTok.domain}api/favorite/item_list/\"\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: str = ...,\n        tab=\"post\",\n        earliest: str | float | int = \"\",\n        latest: str | float | int = \"\",\n        pages: int = None,\n        cursor=0,\n        count=16,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(\n            params,\n            cookie,\n            proxy,\n            sec_user_id,\n            tab,\n            earliest,\n            latest,\n            pages,\n            cursor,\n            count,\n            *args,\n            **kwargs,\n        )\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"itemList\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"hasMore\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        self.set_referer(referer)\n        match single_page:\n            case True:\n                await self.run_single(\n                    data_key,\n                    error_text=error_text,\n                    cursor=cursor,\n                    has_more=has_more,\n                    params=params,\n                    data=data,\n                    method=method,\n                    headers=headers,\n                    *args,\n                    **kwargs,\n                )\n                return self.response\n            case False:\n                await self.run_batch(\n                    data_key,\n                    error_text=error_text,\n                    cursor=cursor,\n                    has_more=has_more,\n                    params=params,\n                    data=data,\n                    method=method,\n                    headers=headers,\n                    *args,\n                    **kwargs,\n                )\n                return self.response, self.earliest, self.latest\n        raise ValueError\n\n    async def run_batch(\n        self,\n        data_key: str = \"itemList\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"hasMore\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        await super().run_batch(\n            data_key=data_key,\n            error_text=error_text,\n            cursor=cursor,\n            has_more=has_more,\n            params=params,\n            data=data,\n            method=method,\n            headers=headers,\n            callback=callback,\n            *args,\n            **kwargs,\n        )\n\n    def generate_favorite_params(self) -> dict:\n        return self.generate_post_params()\n\n    def generate_post_params(self) -> dict:\n        return self.params | {\n            \"secUid\": self.sec_user_id,\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n            \"coverFormat\": \"2\",\n            \"post_item_list_request_type\": \"0\",\n            \"needPinnedItemIds\": \"true\",\n            \"video_encoding\": \"mp4\",\n        }\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        AccountTikTok.params[\"msToken\"] = params.msToken_tiktok\n        i = AccountTikTok(\n            params,\n            sec_user_id=\"\",\n            earliest=15,\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/collection.py",
    "content": "from typing import TYPE_CHECKING, Callable, Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Collection(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: str = \"\",\n        count=10,\n        cursor=0,\n        pages: int = None,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.api = f\"{self.domain}aweme/v1/web/aweme/listcollection/\"\n        self.text = _(\"账号收藏作品\")\n        self.count = count\n        self.cursor = cursor\n        self.pages = pages or params.max_pages\n        self.sec_user_id = sec_user_id\n\n    async def run(\n        self,\n        referer: str = \"\",\n        single_page=False,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"POST\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        await super().run(\n            referer or f\"{self.domain}user/self?showTab=favorite_collection\",\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n        # await self.get_owner_data()\n        return self.response\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"publish_video_strategy_type\": \"2\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    def generate_data(\n        self,\n    ) -> dict:\n        return {\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n        }\n\n    async def request_data(\n        self,\n        url: str,\n        params: dict = None,\n        data: dict = None,\n        method=\"GET\",\n        headers: dict = None,\n        encryption=\"GET\",\n        finished=False,\n        *args,\n        **kwargs,\n    ):\n        return await super().request_data(\n            url,\n            params,\n            data,\n            method,\n            headers,\n            encryption,\n            finished,\n            *args,\n            **kwargs,\n        )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        c = Collection(\n            params,\n            pages=1,\n        )\n        print(await c.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/collects.py",
    "content": "from typing import TYPE_CHECKING, Callable, Union\n\nfrom src.interface.collection import Collection\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Collects(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        cursor=0,\n        count=10,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}aweme/v1/web/collects/list/\"\n        self.text = _(\"收藏夹\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/user/self?showTab=favorite_collection\",\n        single_page=False,\n        data_key: str = \"collects_list\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text or _(\"当前账号无收藏夹\"),\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n\nclass CollectsDetail(Collection, API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        collects_id: str = ...,\n        pages: int = None,\n        cursor=0,\n        count=10,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, None, *args, **kwargs)\n        self.collects_id = collects_id\n        self.pages = pages or params.max_pages\n        self.api = f\"{self.domain}aweme/v1/web/collects/video/list/\"\n        self.cursor = cursor\n        self.count = count\n        self.text = _(\"收藏夹作品\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"collects_id\": self.collects_id,\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/user/self?showTab=favorite_collection\",\n        single_page=False,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        await super(Collection, self).run(\n            referer,\n            single_page,\n            data_key,\n            error_text\n            or _(\"收藏夹 {collects_id} 为空\").format(collects_id=self.collects_id),\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n        return self.response\n\n\nclass CollectsMix(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        cursor=0,\n        count=12,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}aweme/v1/web/mix/listcollection/\"\n        self.text = _(\"收藏合集\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/user/self?showTab=favorite_collection\",\n        single_page=False,\n        data_key: str = \"mix_infos\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text or _(\"当前账号无收藏合集\"),\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            proxy,\n            *args,\n            **kwargs,\n        )\n\n\nclass CollectsSeries(CollectsMix):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        cursor=0,\n        count=12,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(\n            params,\n            cookie,\n            proxy,\n            *args,\n            **kwargs,\n        )\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}aweme/v1/web/series/collections/\"\n        self.text = _(\"收藏短剧\")\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/user/self?showTab=favorite_collection\",\n        single_page=False,\n        data_key: str = \"series_infos\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text or _(\"当前账号无收藏短剧\"),\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n\nclass CollectsMusic(CollectsMix):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        cursor=0,\n        count=20,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(\n            params,\n            cookie,\n            proxy,\n            *args,\n            **kwargs,\n        )\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}aweme/v1/web/music/listcollection/\"\n        self.text = _(\"收藏音乐\")\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/user/self?showTab=favorite_collection\",\n        single_page=False,\n        data_key: str = \"mc_list\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text or _(\"当前账号无收藏音乐\"),\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        c = Collects(\n            params,\n        )\n        print(await c.run())\n        c = CollectsDetail(params, collects_id=\"\")\n        print(await c.run())\n        c = CollectsMix(\n            params,\n        )\n        print(await c.run())\n        c = CollectsMusic(\n            params,\n        )\n        print(await c.run())\n        c = CollectsSeries(\n            params,\n        )\n        print(await c.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/comment.py",
    "content": "from typing import TYPE_CHECKING, Callable, Coroutine, Type, Union\n\nfrom src.extract import Extractor\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Comment(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = ...,\n        pages: int = None,\n        cursor: int = 0,\n        count: int = 20,\n        count_reply: int = 3,\n        reply: bool = False,\n    ):\n        super().__init__(params, cookie, proxy)\n        self.params_object = params\n        self.cookie = cookie\n        self.proxy = proxy\n        self.item_id = detail_id\n        self.pages = pages or params.max_pages\n        self.cursor = cursor\n        self.count = count\n        self.count_reply = count_reply\n        self.api = f\"{self.domain}aweme/v1/web/comment/list/\"\n        self.text = _(\"作品评论\")\n        self.current_page = []\n        self.progress = None\n        self.task_id = None\n        self.reply = reply\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"aweme_id\": self.item_id,\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"item_type\": \"0\",\n            \"insert_ids\": \"\",\n            \"whale_cut_token\": \"\",\n            \"cut_version\": \"1\",\n            \"rcFT\": \"\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"comments\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ) -> list[dict]:\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text=error_text\n            or _(\"作品 {item_id} 无评论\").format(item_id=self.item_id),\n            cursor=cursor,\n            has_more=has_more,\n            data=data,\n            params=params,\n            method=method,\n            headers=headers,\n            callback=self.run_reply,\n            *args,\n            **kwargs,\n        )\n\n    async def run_batch(\n        self,\n        data_key: str = \"comments\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        with self.progress_object() as self.progress:\n            self.task_id = self.progress.add_task(\n                _(\"正在获取{text}数据\").format(text=self.text),\n                total=None,\n            )\n            await self.update_progress(\n                data_key,\n                error_text,\n                cursor,\n                has_more,\n                params,\n                data,\n                method,\n                headers,\n                callback,\n                *args,\n                **kwargs,\n            )\n\n    async def update_progress(\n        self,\n        data_key: str = \"comments\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        while not self.finished and self.pages > 0:\n            self.progress.update(self.task_id)\n            await self.run_single(\n                data_key,\n                error_text,\n                cursor,\n                has_more,\n                params,\n                data,\n                method,\n                headers,\n                *args,\n                **kwargs,\n            )\n            self.pages -= 1\n            if callback:\n                await callback()\n\n    async def run_reply(\n        self,\n    ):\n        if not self.reply:\n            return\n        reply_ids = Extractor.extract_reply_ids(self.current_page)\n        for reply_id in reply_ids:\n            reply = Reply(\n                self.params_object,\n                self.cookie,\n                self.proxy,\n                self.item_id,\n                reply_id,\n                self.pages,\n                cursor=0,\n                count=self.count_reply,\n                progress=self.progress,\n                task_id=self.task_id,\n            )\n            self.response.extend(await reply.run())\n            if (p := reply.pages) > 1:\n                self.pages = p\n            else:\n                break\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[data_key]):\n                self.log.info(error_text)\n                self.finished = True\n            else:\n                self.cursor = data_dict[cursor]\n                self.current_page = d\n                self.append_response(d)\n                self.finished = not data_dict[has_more]\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n            self.finished = True\n\n\nclass Reply(Comment):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = ...,\n        comment_id: str = ...,\n        pages: int = None,\n        cursor=0,\n        count=3,\n        progress=None,\n        task_id=None,\n    ):\n        super().__init__(\n            params,\n            cookie,\n            proxy,\n        )\n        self.item_id = detail_id\n        self.comment_id = comment_id\n        self.pages = pages or params.max_pages\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}aweme/v1/web/comment/list/reply/\"\n        self.text = _(\"作品评论回复\")\n        self.progress = progress\n        self.task_id = task_id\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"item_id\": self.item_id,\n            \"comment_id\": self.comment_id,\n            \"cut_version\": \"1\",\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"item_type\": \"0\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n            \"support_h265\": \"0\",\n            \"support_dash\": \"0\",\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"comments\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super(Comment, self).run(\n            referer,\n            single_page=single_page,\n            data_key=data_key,\n            error_text=error_text\n            or _(\"评论 {comment_id} 无回复\").format(comment_id=self.comment_id),\n            cursor=cursor,\n            has_more=has_more,\n            params=params,\n            data=data,\n            method=method,\n            headers=headers,\n            *args,\n            **kwargs,\n        )\n\n    async def run_batch(\n        self,\n        data_key: str = \"comments\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        if not self.progress:\n            return await super(Comment, self).run_batch(\n                data_key,\n                error_text,\n                cursor,\n                has_more,\n                params,\n                data,\n                method,\n                headers,\n                callback,\n                *args,\n                **kwargs,\n            )\n        return await self.update_progress(\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            callback,\n            *args,\n            **kwargs,\n        )\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        return super(Comment, self).check_response(\n            data_dict,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            *args,\n            **kwargs,\n        )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Comment(\n            params,\n            detail_id=\"\",\n        )\n        print(await i.run())\n        i = Reply(\n            params,\n            detail_id=\"\",\n            comment_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/comment_tiktok.py",
    "content": "from typing import TYPE_CHECKING, Union\n\nfrom src.interface.comment import Comment, Reply\nfrom src.interface.template import APITikTok\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass CommentTikTok(Comment, APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = ...,\n        pages: int = None,\n        cursor=0,\n        count=20,\n        count_reply=3,\n    ):\n        super().__init__(\n            params, cookie, proxy, detail_id, pages, cursor, count, count_reply\n        )\n        self.api = f\"{self.domain}api/comment/list/\"\n        self.text = _(\"作品评论\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"aweme_id\": self.item_id,\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n            \"enter_from\": \"tiktok_web\",\n            \"is_non_personalized\": \"false\",\n            \"fromWeb\": \"1\",\n            \"from_page\": \"video\",\n        }\n\n\nclass ReplyTikTok(Reply, CommentTikTok, APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = \"\",\n        comment_id: str = \"\",\n        pages: int = None,\n        cursor=0,\n        count=3,\n        progress=None,\n        task_id=None,\n    ):\n        super().__init__(\n            params,\n            cookie,\n            proxy,\n            detail_id,\n            comment_id,\n            pages,\n            cursor,\n            count,\n            progress,\n            task_id,\n        )\n        self.api = f\"{self.domain}api/comment/list/reply/\"\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"comment_id\": self.comment_id,\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n            \"fromWeb\": \"1\",\n            \"from_page\": \"video\",\n            \"item_id\": self.item_id,\n        }\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        CommentTikTok.params[\"msToken\"] = params.msToken_tiktok\n        ReplyTikTok.params[\"msToken\"] = params.msToken_tiktok\n        i = CommentTikTok(\n            params,\n            detail_id=\"\",\n        )\n        print(await i.run())\n        i = ReplyTikTok(\n            params,\n            detail_id=\"\",\n            comment_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/detail.py",
    "content": "from typing import Callable\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Detail(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = ...,\n    ):\n        super().__init__(params, cookie, proxy)\n        self.detail_id = detail_id\n        self.api = f\"{self.domain}aweme/v1/web/aweme/detail/\"\n        self.text = _(\"作品\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"aweme_id\": self.detail_id,\n            \"version_code\": \"190500\",\n            \"version_name\": \"19.5.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=True,\n        data_key: str = \"aweme_detail\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[data_key]):\n                self.log.warning(error_text)\n            else:\n                self.response = d\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Detail(\n            params,\n            detail_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/detail_tiktok.py",
    "content": "from typing import TYPE_CHECKING, Callable, Union\n\nfrom src.interface.template import APITikTok\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass DetailTikTok(APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        detail_id: str = ...,\n    ):\n        super().__init__(params, cookie, proxy)\n        self.detail_id = detail_id\n        self.api = f\"{self.domain}/api/item/detail/\"\n        self.text = _(\"作品\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"itemId\": self.detail_id,\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=True,\n        data_key: str = None,\n        error_text=\"\",\n        cursor=None,\n        has_more=None,\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str = None,\n        error_text=\"\",\n        cursor=None,\n        has_more=None,\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[\"itemInfo\"][\"itemStruct\"]):\n                self.log.info(error_text)\n            else:\n                self.response = d\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        DetailTikTok.params[\"msToken\"] = params.msToken_tiktok\n        i = DetailTikTok(\n            params,\n            detail_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/hashtag.py",
    "content": "from typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import API\n\n# from src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass HashTag(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n\n    async def run(self, *args, **kwargs):\n        pass\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = HashTag(\n            params,\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/hot.py",
    "content": "from datetime import datetime\nfrom types import SimpleNamespace\nfrom typing import Callable\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Hot(API):\n    board_params = (\n        SimpleNamespace(\n            name=_(\"抖音热榜\"),\n            type=0,\n            sub_type=\"\",\n        ),\n        SimpleNamespace(\n            name=_(\"娱乐榜\"),\n            type=2,\n            sub_type=2,\n        ),\n        SimpleNamespace(\n            name=_(\"社会榜\"),\n            type=2,\n            sub_type=4,\n        ),\n        SimpleNamespace(\n            name=_(\"挑战榜\"),\n            type=2,\n            sub_type=\"hotspot_challenge\",\n        ),\n    )\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.headers = self.headers | {\n            \"Cookie\": \"\",\n        }\n        self.api = f\"{self.domain}aweme/v1/web/hot/search/list/\"\n        self.text = _(\"热榜\")\n        self.index = None\n        self.time = None\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"detail_list\": \"1\",\n            \"source\": \"6\",\n            \"board_type\": self.board_params[self.index].type,\n            \"board_sub_type\": self.board_params[self.index].sub_type,\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = \"https://www.douyin.com/discover\",\n        single_page=True,\n        data_key: str = None,\n        error_text=None,\n        cursor=None,\n        has_more=None,\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        self.time = f\"{datetime.now():%Y_%m_%d_%H_%M_%S}\"\n        self.set_referer(referer)\n        for index, space in enumerate(self.board_params):\n            self.index = index\n            self.text = _(\"{space_name}数据\").format(space_name=space.name)\n            await self.run_single(\n                data_key,\n                \"\",\n                cursor,\n                has_more,\n                params=self.generate_params,\n                data=data,\n                method=method,\n                headers=headers,\n                index=index,\n                *args,\n                **kwargs,\n            )\n        return self.time, self.response\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str = None,\n        error_text=None,\n        cursor=None,\n        has_more=None,\n        index: int = None,\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[\"data\"][\"word_list\"]):\n                self.log.info(error_text)\n            else:\n                self.response.append((index, d))\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Hot(\n            params,\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/info.py",
    "content": "from typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Info(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: Union[str, list[str], tuple[str]] = ...,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.api = f\"{self.domain}aweme/v1/web/im/user/info/\"\n        self.sec_user_id = sec_user_id\n        self.static_params = self.params | {\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n        self.text = _(\"账号简略\")\n\n    async def run(\n        self,\n        first=True,\n        *args,\n        **kwargs,\n    ) -> dict | list[dict]:\n        self.set_referer()\n        await self.run_single()\n        if first:\n            return self.response[0] if self.response else {}\n        return self.response\n\n    async def run_single(\n        self,\n        *args,\n        **kwargs,\n    ):\n        await super().run_single(\n            \"\",\n            params=lambda: self.static_params,\n            data=self.__generate_data,\n            method=\"POST\",\n        )\n\n    def check_response(\n        self,\n        data_dict: dict,\n        *args,\n        **kwargs,\n    ):\n        if d := data_dict.get(\"data\"):\n            self.append_response(d)\n        else:\n            self.log.warning(_(\"获取{text}失败\").format(text=self.text))\n\n    def __generate_data(\n        self,\n    ) -> dict:\n        if isinstance(self.sec_user_id, str):\n            self.sec_user_id = [self.sec_user_id]\n        value = f\"[{','.join(f'\"{i}\"' for i in self.sec_user_id)}]\"\n        return {\n            \"sec_user_ids\": value,\n        }\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Info(\n            params,\n            sec_user_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/info_tiktok.py",
    "content": "from typing import TYPE_CHECKING, Union\n\nfrom src.interface.template import APITikTok\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass InfoTikTok(APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        unique_id: Union[str] = \"\",\n        sec_user_id: Union[str] = \"\",\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.api = f\"{self.domain}api/user/detail/\"\n        self.unique_id = unique_id\n        self.sec_user_id = sec_user_id\n        self.text = _(\"账号简略\")\n\n    async def run(\n        self,\n        # first=True,\n        *args,\n        **kwargs,\n    ) -> dict | list[dict]:\n        self.set_referer()\n        await self.run_single()\n        return self.response[0] if self.response else {}\n\n    async def run_single(\n        self,\n        *args,\n        **kwargs,\n    ):\n        await super().run_single(\n            \"\",\n        )\n\n    def check_response(\n        self,\n        data_dict: dict,\n        *args,\n        **kwargs,\n    ):\n        if d := data_dict.get(\"userInfo\"):\n            self.append_response(d)\n        else:\n            self.log.warning(_(\"获取{text}失败\").format(text=self.text))\n\n    def append_response(\n        self,\n        data: dict,\n        *args,\n        **kwargs,\n    ) -> None:\n        self.response.append(data)\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"abTestVersion\": \"[object Object]\",\n            \"appType\": \"m\",\n            \"secUid\": self.sec_user_id,\n            \"uniqueId\": self.unique_id,\n            \"user\": \"[object Object]\",\n        }\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        InfoTikTok.params[\"msToken\"] = params.msToken_tiktok\n        i = InfoTikTok(\n            params,\n            unique_id=\"\",\n            sec_user_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/live.py",
    "content": "from typing import TYPE_CHECKING, Union\n\nfrom src.interface.template import API\nfrom src.tools import DownloaderError\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Live(API):\n    live_api = \"https://live.douyin.com/webcast/room/web/enter/\"\n    live_api_share = \"https://webcast.amemv.com/webcast/room/reflow/info/\"\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        web_rid: str = ...,\n        room_id: str = ...,\n        sec_user_id: str = \"\",\n    ):\n        super().__init__(params, cookie, proxy)\n        self.black_headers = params.headers_download\n        self.web_rid = web_rid\n        self.room_id = room_id\n        self.sec_user_id = sec_user_id\n\n    async def run(\n        self,\n        *args,\n        **kwargs,\n    ) -> dict:\n        if isinstance(self.web_rid, str):\n            return await self.with_web_rid()\n        elif self.room_id:\n            return await self.with_room_id()\n        else:\n            raise DownloaderError\n\n    async def with_web_rid(self) -> dict:\n        self.set_referer(\"https://live.douyin.com/\")\n        params = {  # TODO: 参数固定\n            \"aid\": \"6383\",\n            \"app_name\": \"douyin_web\",\n            \"live_id\": \"1\",\n            \"device_platform\": \"web\",\n            \"language\": \"zh-CN\",\n            \"enter_from\": \"web_share_link\",\n            \"cookie_enabled\": \"true\",\n            \"screen_width\": \"1536\",\n            \"screen_height\": \"864\",\n            \"browser_language\": \"zh-CN\",\n            \"browser_platform\": \"Win32\",\n            \"browser_name\": \"Edge\",\n            \"browser_version\": \"139.0.0.0\",\n            \"web_rid\": self.web_rid,\n            # \"room_id_str\": \"\",\n            \"enter_source\": \"\",\n            \"is_need_double_stream\": \"false\",\n            \"insert_task_id\": \"\",\n            \"live_reason\": \"\",\n        }\n        return await self.request_data(\n            self.live_api,\n            params,\n        )\n\n    async def with_room_id(self) -> dict:\n        params = {\n            \"type_id\": \"0\",\n            \"live_id\": \"1\",\n            \"room_id\": self.room_id,\n            \"sec_user_id\": self.sec_user_id,\n            \"app_id\": \"1128\",\n        }\n        return await self.request_data(\n            self.live_api_share,\n            params,\n            headers=self.black_headers,\n        )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Live(\n            params,\n            room_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/live_tiktok.py",
    "content": "from typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import APITikTok\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from src.testers import Params\n\n\nclass LiveTikTok(APITikTok):\n    live_api = \"https://webcast.us.tiktok.com/webcast/room/enter/\"\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        room_id: str = ...,\n    ):\n        super().__init__(params, cookie, proxy)\n        self.black_headers = params.headers_download\n        self.room_id = room_id\n\n    async def run(\n        self,\n        *args,\n        **kwargs,\n    ) -> dict:\n        response = await self.with_room_id()\n        return self.check_response(response)\n\n    async def with_room_id(self) -> dict:\n        return await self.request_data(\n            self.live_api,\n            self.params,\n            method=\"POST\",\n            data=self.__generate_room_id_data(),\n        )\n\n    def __generate_room_id_data(\n        self,\n    ) -> dict:\n        return {\n            \"enter_source\": \"others-others\",\n            \"room_id\": self.room_id,\n        }\n\n    def check_response(\n        self,\n        data_dict: dict,\n        *args,\n        **kwargs,\n    ):\n        if data_dict and \"prompt\" in data_dict[\"data\"]:\n            self.console.warning(_(\"此直播可能会令部分观众感到不适，请登录后重试！\"))\n            return {}\n        return data_dict\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = LiveTikTok(\n            params,\n            room_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/mix.py",
    "content": "from typing import Callable\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.extract import Extractor\nfrom src.interface.detail import Detail\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Mix(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        mix_id: str = None,\n        detail_id: str = None,\n        cursor=0,\n        count=12,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.mix_title = None\n        self.mix_id = mix_id\n        self.detail_id = detail_id\n        self.count = count\n        self.cursor = cursor\n        self.api = f\"{self.domain}aweme/v1/web/mix/aweme/\"\n        self.text = _(\"合集作品\")\n        self.detail = Detail(\n            params,\n            cookie,\n            proxy,\n            self.detail_id,\n        )\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"mix_id\": self.mix_id,\n            \"cursor\": self.cursor,\n            \"count\": self.count,\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"aweme_list\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        await self.__get_mix_id()\n        if not self.mix_id:\n            self.log.warning(_(\"获取合集 ID 失败\"))\n            return self.response\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n    async def __get_mix_id(self):\n        if not self.mix_id:\n            self.mix_id = Extractor.extract_mix_id(await self.detail.run())\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Mix(\n            params,\n            mix_id=\"\",\n            detail_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/mix_tiktok.py",
    "content": "from typing import TYPE_CHECKING, Callable, Union\n\nfrom src.interface.template import APITikTok\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass MixTikTok(APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        mix_title: str = ...,\n        mix_id: str = ...,\n        # detail_id: str = None,\n        cursor=0,\n        count=30,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.mix_title = mix_title\n        self.mix_id = mix_id\n        # self.detail_id = detail_id  # 未使用\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}api/collection/item_list/\"\n        self.text = _(\"合辑作品\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n            \"collectionId\": self.mix_id,\n            \"sourceType\": \"113\",\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"itemList\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"hasMore\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n\nclass MixListTikTok(APITikTok):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: str = \"\",\n        cursor=0,\n        count=20,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.sec_user_id = sec_user_id\n        self.cursor = cursor\n        self.count = count\n        self.api = f\"{self.domain}api/user/playlist/\"\n        self.text = _(\"账号合辑数据\")\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"count\": self.count,\n            \"cursor\": self.cursor,\n            \"secUid\": self.sec_user_id,\n        }\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"playList\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"hasMore\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        return await super().run(\n            referer,\n            single_page,\n            data_key,\n            error_text,\n            cursor,\n            has_more,\n            params,\n            data,\n            method,\n            headers,\n            *args,\n            **kwargs,\n        )\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        MixTikTok.params[\"msToken\"] = params.msToken_tiktok\n        MixListTikTok.params[\"msToken\"] = params.msToken_tiktok\n        # i = MixTikTok(\n        #     params,\n        #     mix_id=\"\",\n        # )\n        # print(await i.run())\n        i = MixListTikTok(\n            params,\n            sec_user_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/search.py",
    "content": "from json import dumps\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING, Union\nfrom urllib.parse import quote\n\nfrom src.interface.template import API\nfrom src.tools import DownloaderError\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass Search(API):\n    search_params = (\n        SimpleNamespace(\n            note=_(\"综合搜索\"),\n            api=f\"{API.domain}aweme/v1/web/general/search/single/\",\n            channel=\"aweme_general\",\n            type=\"general\",\n            key=\"data\",\n        ),\n        SimpleNamespace(\n            note=_(\"视频搜索\"),\n            api=f\"{API.domain}aweme/v1/web/search/item/\",\n            channel=\"aweme_video_web\",\n            type=\"video\",\n            key=\"data\",\n        ),\n        SimpleNamespace(\n            note=_(\"用户搜索\"),\n            api=f\"{API.domain}aweme/v1/web/discover/search/\",\n            channel=\"aweme_user_web\",\n            type=\"user\",\n            key=\"user_list\",\n        ),\n        SimpleNamespace(\n            note=_(\"直播搜索\"),\n            api=f\"{API.domain}aweme/v1/web/live/search/\",\n            channel=\"aweme_live\",\n            type=\"live\",\n            key=\"data\",\n        ),\n        SimpleNamespace(\n            note=None,\n            api=None,\n            channel=None,\n            type=None,\n            key=None,\n        ),\n    )\n    search_data_field = {\n        0: \"search_general\",\n        1: \"search_general\",\n        2: \"search_user\",\n        3: \"search_live\",\n    }\n    search_criteria = {\n        0: _(\"关键词  总页数  排序依据  发布时间  视频时长  搜索范围  内容形式\"),\n        1: _(\"关键词  总页数  排序依据  发布时间  视频时长  搜索范围\"),\n        2: _(\"关键词  总页数  粉丝数量  用户类型\"),\n        3: _(\"关键词  总页数\"),\n    }\n    channel_map = {\n        0: search_params[0],\n        1: search_params[1],\n        2: search_params[2],\n        3: search_params[3],\n    }\n    sort_type_help = {\n        0: _(\"综合排序\"),\n        1: _(\"最多点赞\"),\n        2: _(\"最新发布\"),\n    }\n    publish_time_help = {\n        0: _(\"不限\"),\n        1: _(\"一天内\"),\n        7: _(\"一周内\"),\n        180: _(\"半年内\"),\n    }\n    duration_map = {\n        0: \"\",\n        1: \"0-1\",\n        2: \"1-5\",\n        3: \"5-10000\",\n    }\n    duration_help = {\n        0: _(\"不限\"),\n        1: _(\"一分钟以内\"),\n        2: _(\"一到五分钟\"),\n        3: _(\"五分钟以上\"),\n    }\n    search_range_help = {\n        0: _(\"不限\"),\n        1: _(\"最近看过\"),\n        2: _(\"还未看过\"),\n        3: _(\"关注的人\"),\n    }\n    content_type_help = {\n        0: _(\"不限\"),\n        1: _(\"视频\"),\n        2: _(\"图文\"),\n    }\n    douyin_user_fans_map = {\n        0: [\"\"],\n        1: [\"0_1k\"],\n        2: [\"1k_1w\"],\n        3: [\"1w_10w\"],\n        4: [\"10w_100w\"],\n        5: [\"100w_\"],\n    }\n    douyin_user_fans_help = {\n        0: _(\"不限\"),\n        1: _(\"1000以下\"),\n        2: \"1000-1w\",\n        3: \"1w-10w\",\n        4: \"10w-100w\",\n        5: _(\"100w以上\"),\n    }\n    douyin_user_type_map = {\n        0: [\"\"],\n        1: [\"common_user\"],\n        2: [\"enterprise_user\"],\n        3: [\"personal_user\"],\n    }\n    douyin_user_type_help = {\n        0: _(\"不限\"),\n        1: _(\"普通用户\"),\n        2: _(\"企业认证\"),\n        3: _(\"个人认证\"),\n    }\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        keyword: str = ...,\n        channel: int = 0,\n        pages: int = 99999,\n        sort_type: int = 0,\n        publish_time: int = 0,\n        duration: int = 0,\n        search_range: int = 0,\n        content_type: int = 0,\n        douyin_user_fans: int = 0,\n        douyin_user_type: int = 0,\n        offset: int = 0,\n        count: int = 10,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.keyword = keyword\n        self.channel = self.channel_map.get(channel, self.search_params[-1])\n        self.pages = pages\n        self.sort_type = sort_type\n        self.publish_time = publish_time\n        self.duration = self.duration_map.get(duration, \"\")\n        self.content_type = content_type\n        self.search_range = search_range\n        self.douyin_user_fans = self.douyin_user_fans_map.get(douyin_user_fans, [\"\"])\n        self.douyin_user_type = self.douyin_user_type_map.get(douyin_user_type, [\"\"])\n        self.offset = offset\n        self.count = count\n        self.type = self.channel.type\n        self.api = self.channel.api\n        self.key = self.channel.key\n        self.text = f\"{self.channel.note}\"\n        self.filter_selected = self.generate_filter_selected() if channel == 0 else None\n        self.search_filter_value = (\n            self.generate_search_filter_value() if channel == 2 else None\n        )\n        self.search_id = None\n        self.params_func = {\n            0: self._generate_params_general,\n            1: self._generate_params_video,\n            2: self._generate_params_user,\n            3: self._generate_params_live,\n        }.get(channel)\n\n    async def run(self, single_page=False, *args, **kwargs):\n        if not self.api:\n            raise DownloaderError\n        self.set_referer(\n            f\"{self.domain}root/search/{quote(self.keyword)}?type={self.type}\"\n        )\n        match single_page:\n            case True:\n                await self.run_single(\n                    self.channel.key,\n                    params=self.params_func,\n                    *args,\n                    **kwargs,\n                )\n            case False:\n                await self.run_batch(\n                    self.channel.key,\n                    params=self.params_func,\n                    *args,\n                    **kwargs,\n                )\n            case _:\n                raise DownloaderError\n        return self.response\n\n    def generate_filter_selected(\n        self,\n    ) -> str | None:\n        if any(\n            (\n                self.sort_type,\n                self.publish_time,\n                self.duration,\n                self.search_range,\n                self.content_type,\n            )\n        ):\n            return dumps(\n                {\n                    \"sort_type\": f\"{self.sort_type}\",\n                    \"publish_time\": f\"{self.publish_time}\",\n                    \"filter_duration\": f\"{self.duration}\",\n                    \"search_range\": f\"{self.search_range}\",\n                    \"content_type\": f\"{self.content_type}\",\n                },\n                separators=(\",\", \":\"),\n            )\n        return None\n\n    def generate_search_filter_value(\n        self,\n    ) -> str | None:\n        if any(\n            (\n                self.douyin_user_fans,\n                self.douyin_user_type,\n            )\n        ):\n            return dumps(\n                {\n                    \"douyin_user_fans\": self.douyin_user_fans,\n                    \"douyin_user_type\": self.douyin_user_type,\n                },\n                separators=(\",\", \":\"),\n            )\n        return None\n\n    def _generate_params_general(\n        self,\n    ) -> dict:\n        params = self.params | {\n            \"pc_search_top_1_params\": '{\"enable_ai_search_top_1\":1}',\n            \"search_channel\": self.channel.channel,\n            \"enable_history\": \"1\",\n            \"keyword\": self.keyword,\n            \"search_source\": \"switch_tab\",\n            \"query_correct_type\": \"1\",\n            \"is_filter_search\": \"0\",\n            \"from_group_id\": \"\",\n            \"disable_rs\": \"0\",\n            \"offset\": self.offset,\n            \"count\": self.count,\n            \"need_filter_settings\": \"0\",\n            \"list_type\": \"single\",\n            \"version_code\": \"190600\",\n            \"version_name\": \"19.6.0\",\n        }\n        if self.search_id:\n            params |= {\"search_id\": self.search_id}\n        if self.filter_selected:\n            params |= {\n                \"filter_selected\": quote(self.filter_selected),\n                \"is_filter_search\": \"1\",\n            }\n        return params\n\n    def _generate_params_video(\n        self,\n    ) -> dict:\n        params = self.params | {\n            \"pc_search_top_1_params\": '{\"enable_ai_search_top_1\":1}',\n            \"search_channel\": self.channel.channel,\n            \"enable_history\": \"1\",\n            \"keyword\": self.keyword,\n            \"search_source\": \"switch_tab\",\n            \"query_correct_type\": \"1\",\n            \"is_filter_search\": \"0\",\n            \"from_group_id\": \"\",\n            \"disable_rs\": \"0\",\n            \"offset\": self.offset,\n            \"count\": self.count,\n            \"need_filter_settings\": \"0\",\n            \"list_type\": \"single\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n        if self.search_id:\n            params |= {\"search_id\": self.search_id}\n        if self.sort_type:\n            params |= {\n                \"sort_type\": f\"{self.sort_type}\",\n                \"is_filter_search\": \"1\",\n            }\n        if self.publish_time:\n            params |= {\n                \"publish_time\": f\"{self.publish_time}\",\n                \"is_filter_search\": \"1\",\n            }\n        if self.duration:\n            params |= {\n                \"filter_duration\": f\"{self.duration}\",\n                \"is_filter_search\": \"1\",\n            }\n        if self.search_range:\n            params |= {\n                \"search_range\": f\"{self.search_range}\",\n                \"is_filter_search\": \"1\",\n            }\n        return params\n\n    def _generate_params_user(\n        self,\n    ) -> dict:\n        params = self._generate_params_live()\n        if self.search_filter_value:\n            params |= {\n                \"search_filter_value\": quote(self.search_filter_value),\n                \"is_filter_search\": \"1\",\n            }\n        return params\n\n    def _generate_params_live(\n        self,\n    ) -> dict:\n        params = self.params | {\n            \"pc_search_top_1_params\": '{\"enable_ai_search_top_1\":1}',\n            \"search_channel\": self.channel.channel,\n            \"keyword\": self.keyword,\n            \"search_source\": \"switch_tab\",\n            \"query_correct_type\": \"1\",\n            \"is_filter_search\": \"0\",\n            \"from_group_id\": \"\",\n            \"disable_rs\": \"0\",\n            \"offset\": self.offset,\n            \"count\": self.count,\n            \"need_filter_settings\": \"0\",\n            \"list_type\": \"single\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n        if self.search_id:\n            params |= {\"search_id\": self.search_id}\n        return params\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not isinstance(d := data_dict[data_key], list):\n                self.log.warning(error_text)\n                self.finished = True\n            elif len(d) == 0:\n                if not self.response:\n                    self.response.append([])\n                self.finished = True\n            else:\n                self.offset = data_dict[cursor]\n                self.search_id = data_dict[\"log_pb\"][\"impr_id\"]\n                match self.type:\n                    case \"general\" | \"video\" | \"user\":\n                        self.append_response(d)\n                    case \"live\":\n                        self.append_response_video(\n                            d,\n                            \"lives\",\n                        )\n                    case _:\n                        raise DownloaderError\n                self.finished = not data_dict[has_more]\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n            self.finished = True\n\n    def append_response_video(\n        self,\n        data: list[dict],\n        key: str,\n    ) -> None:\n        self.append_response([i[key] for i in data])\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        Search.params[\"uifid\"] = params.uifid\n        Search.params[\"msToken\"] = params.msToken_tiktok\n        i = Search(\n            params,\n            keyword=\"\",\n            channel=3,\n            sort_type=2,\n            publish_time=7,\n            duration=2,\n            douyin_user_fans=5,\n            pages=1,\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/slides.py",
    "content": "# from typing import Callable\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n__all__ = [\"Slides\"]\n\n\nclass Slides(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        slides_id: str | list | tuple = ...,\n    ):\n        super().__init__(params, cookie, proxy)\n        self.slides_id = slides_id\n        self.api = f\"{self.short_domain}web/api/v2/aweme/slidesinfo/\"\n        self.text = _(\"作品\")\n\n    async def run(self, *args, **kwargs):\n        pass\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = Slides(\n            params,\n            slides_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/interface/template.py",
    "content": "from time import time\nfrom typing import TYPE_CHECKING, Callable, Coroutine, Type, Union\nfrom urllib.parse import quote, urlencode\n\nfrom httpx import AsyncClient, get, post\nfrom rich.progress import (\n    BarColumn,\n    Progress,\n    TextColumn,\n    TimeElapsedColumn,\n)\n\nfrom ..custom import PROGRESS, USERAGENT, wait\nfrom ..tools import DownloaderError, FakeProgress, Retry, capture_error_request\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from ..testers import Params\n\n__all__ = [\n    \"API\",\n    \"APITikTok\",\n]\n\n\nclass API:\n    domain = \"https://www.douyin.com/\"\n    short_domain = \"https://www.iesdouyin.com/\"\n    referer = f\"{domain}?recommend=1\"\n    params = {\n        \"device_platform\": \"webapp\",\n        \"aid\": \"6383\",\n        \"channel\": \"channel_pc_web\",\n        \"update_version_code\": \"170400\",\n        \"pc_client_type\": \"1\",\n        \"pc_libra_divert\": \"Windows\",\n        \"support_h265\": \"1\",\n        \"support_dash\": \"1\",\n        \"version_code\": \"290100\",\n        \"version_name\": \"29.1.0\",\n        \"cookie_enabled\": \"true\",\n        \"screen_width\": \"1536\",\n        \"screen_height\": \"864\",\n        \"browser_language\": \"zh-CN\",\n        \"browser_platform\": \"Win32\",\n        \"browser_name\": \"Chrome\",\n        \"browser_version\": \"139.0.0.0\",\n        \"browser_online\": \"true\",\n        \"engine_name\": \"Blink\",\n        \"engine_version\": \"139.0.0.0\",\n        \"os_name\": \"Windows\",\n        \"os_version\": \"10\",\n        \"cpu_core_num\": \"16\",\n        \"device_memory\": \"8\",\n        \"platform\": \"PC\",\n        \"downlink\": \"10\",\n        \"effective_type\": \"4g\",\n        \"round_trip_time\": \"200\",\n        # \"webid\": \"\",\n        \"uifid\": \"\",\n        \"msToken\": \"\",\n    }\n    progress_object: Callable\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        self.headers = params.headers.copy()\n        self.log = params.logger\n        self.ab = params.ab\n        self.console = params.console\n        self.api = \"\"\n        self.proxy = proxy\n        self.max_retry = params.max_retry\n        self.timeout = params.timeout\n        self.cookie = cookie\n        self.client: AsyncClient = params.client\n        self.pages = 99999\n        self.cursor = 0\n        self.response = []\n        self.finished = False\n        self.text = \"\"\n        self.set_temp_cookie(cookie)\n\n    def set_temp_cookie(self, cookie: str = \"\"):\n        if cookie:\n            self.headers[\"Cookie\"] = cookie\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params\n\n    def __generate_params(\n        self,\n    ) -> dict:\n        params = self.generate_params()\n        params[\"msToken\"] = params.pop(\"msToken\")\n        return params\n\n    def generate_data(self, *args, **kwargs) -> dict:\n        return {}\n\n    async def run(\n        self,\n        referer: str = None,\n        single_page=False,\n        data_key: str = \"\",\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        self.set_referer(referer)\n        match single_page:\n            case True:\n                await self.run_single(\n                    data_key,\n                    error_text,\n                    cursor,\n                    has_more,\n                    params,\n                    data,\n                    method,\n                    headers,\n                    *args,\n                    **kwargs,\n                )\n            case False:\n                await self.run_batch(\n                    data_key,\n                    error_text,\n                    cursor,\n                    has_more,\n                    params,\n                    data,\n                    method,\n                    headers,\n                    *args,\n                    **kwargs,\n                )\n            case _:\n                raise DownloaderError\n        return self.response\n\n    async def run_single(\n        self,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        *args,\n        **kwargs,\n    ):\n        if data := await self.request_data(\n            self.api,\n            params=params() or self.__generate_params(),\n            data=data() or self.generate_data(),\n            method=method,\n            headers=headers,\n            finished=True,\n        ):\n            self.check_response(\n                data, data_key, error_text, cursor, has_more, *args, **kwargs\n            )\n        else:\n            self.log.warning(_(\"获取{self_text}数据失败\").format(self_text=self.text))\n\n    async def run_batch(\n        self,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        with self.progress_object() as progress:\n            task_id = progress.add_task(\n                _(\"正在获取{text}数据\").format(text=self.text),\n                total=None,\n            )\n            while not self.finished and self.pages > 0:\n                progress.update(task_id)\n                await self.run_single(\n                    data_key,\n                    error_text,\n                    cursor,\n                    has_more,\n                    params,\n                    data,\n                    method,\n                    headers,\n                    *args,\n                    **kwargs,\n                )\n                self.pages -= 1\n                if callback:\n                    await callback()\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[data_key]):\n                self.log.warning(error_text)\n                self.finished = True\n            else:\n                self.cursor = data_dict[cursor]\n                self.append_response(d)\n                self.finished = not data_dict[has_more]\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n            self.finished = True\n\n    def set_referer(self, url: str = None) -> None:\n        self.headers[\"Referer\"] = url or self.referer\n\n    async def request_data(\n        self,\n        url: str,\n        params: dict = None,\n        data: dict = None,\n        method=\"GET\",\n        headers: dict = None,\n        encryption=\"GET\",\n        finished=False,\n        *args,\n        **kwargs,\n    ):\n        params = self.deal_url_params(\n            params,\n            encryption,\n        )\n        match (method, bool(self.proxy)):\n            case (\"GET\", False):\n                return await self.request_data_get(\n                    url,\n                    params,\n                    headers or self.headers,\n                    finished=finished,\n                    *args,\n                    **kwargs,\n                )\n            case (\"GET\", True):\n                return await self.request_data_get_proxy(\n                    url,\n                    params,\n                    headers or self.headers,\n                    finished=finished,\n                    *args,\n                    **kwargs,\n                )\n            case (\"POST\", False):\n                return await self.request_data_post(\n                    url,\n                    params,\n                    data,\n                    headers or self.headers,\n                    finished=finished,\n                    *args,\n                    **kwargs,\n                )\n            case (\"POST\", True):\n                return await self.request_data_post_proxy(\n                    url,\n                    params,\n                    data,\n                    headers or self.headers,\n                    finished=finished,\n                    *args,\n                    **kwargs,\n                )\n            case _:\n                raise DownloaderError\n\n    @Retry.retry\n    @capture_error_request\n    async def request_data_get(\n        self,\n        url: str,\n        params: str,\n        headers: dict,\n        finished=False,\n        **kwargs,\n    ):\n        self.__record_request_messages(\n            url,\n            params,\n            None,\n            headers,\n            **kwargs,\n        )\n        response = await self.client.get(\n            f\"{url}?{params}\",\n            headers=headers,\n            **kwargs,\n        )\n        return await self.__return_response(response)\n\n    @Retry.retry\n    @capture_error_request\n    async def request_data_get_proxy(\n        self,\n        url: str,\n        params: str,\n        headers: dict,\n        finished=False,\n        **kwargs,\n    ):\n        self.__record_request_messages(\n            url,\n            params,\n            None,\n            headers,\n            **kwargs,\n        )\n        response = get(\n            f\"{url}?{params}\",\n            headers=headers,\n            proxy=self.proxy,\n            follow_redirects=True,\n            verify=False,\n            timeout=self.timeout,\n            **kwargs,\n        )\n        return await self.__return_response(response)\n\n    @Retry.retry\n    @capture_error_request\n    async def request_data_post(\n        self, url: str, params: str, data: dict, headers: dict, finished=False, **kwargs\n    ):\n        self.__record_request_messages(\n            url,\n            params,\n            data,\n            headers,\n            **kwargs,\n        )\n        response = await self.client.post(\n            f\"{url}?{params}\",\n            data=data,\n            headers=headers,\n            **kwargs,\n        )\n        return await self.__return_response(response)\n\n    @Retry.retry\n    @capture_error_request\n    async def request_data_post_proxy(\n        self, url: str, params: str, data: dict, headers: dict, finished=False, **kwargs\n    ):\n        self.__record_request_messages(\n            url,\n            params,\n            data,\n            headers,\n            **kwargs,\n        )\n        response = post(\n            f\"{url}?{params}\",\n            data=data,\n            headers=headers,\n            proxy=self.proxy,\n            follow_redirects=True,\n            verify=False,\n            timeout=self.timeout,\n            **kwargs,\n        )\n        return await self.__return_response(response)\n\n    async def __return_response(self, response):\n        self.log.info(f\"Response URL: {response.url}\", False)\n        self.log.info(f\"Response Code: {response.status_code}\", False)\n        self.log.info(f\"Response Headers: {dict(response.headers)}\", False)\n        # 记录请求体数据会导致日志文件体积过大，仅在必要时记录\n        # self.log.info(f\"Response Content: {response.content}\", False)\n        response.raise_for_status()\n        await wait()\n        # if response.status_code != 200:\n        #     self.log.error(f\"请求 {url} 失败，响应码 {response.status_code}\")\n        #     return\n        return response.json()\n\n    def __record_request_messages(\n        self,\n        url: str,\n        params: str | None,\n        data: dict | None,\n        headers: dict,\n        **kwargs,\n    ):\n        self.log.info(f\"URL: {url}\", False)\n        self.log.info(f\"Params: {params}\", False)\n        self.log.info(f\"Data: {data}\", False)\n        # 请求头脱敏处理，不记录 Cookie\n        desensitize = {k: v for k, v in headers.items() if k != \"Cookie\"}\n        self.log.info(f\"Headers: {desensitize}\", False)\n        self.log.info(f\"Other: {kwargs}\", False)\n\n    def deal_url_params(\n        self,\n        params: dict,\n        method=\"GET\",\n        **kwargs,\n    ) -> str:\n        if params:\n            params = urlencode(\n                params,\n                safe=\"=\",\n                quote_via=quote,\n            )\n            params += f\"&a_bogus={self.ab.get_value(params, method)}\"\n            return params\n        return \"\"\n\n    def summary_works(\n        self,\n    ) -> None:\n        self.log.info(\n            _(\"共获取到 {count} 个{text}\").format(\n                count=len(self.response), text=self.text\n            )\n        )\n\n    @classmethod\n    def init_progress_object(\n        cls,\n        server_mode: bool = False,\n    ) -> None:\n        if server_mode:\n            cls._progress_factory = cls.__fake_progress_object\n        else:\n            cls._progress_factory = cls.__general_progress_object\n\n    def progress_object(self):\n        factory = getattr(self, \"_progress_factory\", self.__general_progress_object)\n        return factory()\n\n    def __general_progress_object(self):\n        return Progress(\n            TextColumn(\n                \"[progress.description]{task.description}\",\n                style=PROGRESS,\n                justify=\"left\",\n            ),\n            \"•\",\n            BarColumn(),\n            \"•\",\n            TimeElapsedColumn(),\n            console=self.console,\n            transient=True,\n            expand=True,\n        )\n\n    @staticmethod\n    def __fake_progress_object(*args, **kwargs):\n        return FakeProgress()\n\n    def append_response(\n        self,\n        data: list[dict],\n        start: int = None,\n        end: int = None,\n        *args,\n        **kwargs,\n    ) -> None:\n        for item in data[start:end]:\n            self.response.append(item)\n        # self.response.extend(data[start:end])\n\n\nclass APITikTok(API):\n    domain = \"https://www.tiktok.com/\"\n    short_domain = \"\"\n    referer = f\"{domain}explore\"\n    params = {\n        \"WebIdLastTime\": int(time()),\n        \"aid\": \"1988\",\n        \"app_language\": \"en\",\n        \"app_name\": \"tiktok_web\",\n        \"browser_language\": \"zh-SG\",\n        \"browser_name\": \"Mozilla\",\n        \"browser_online\": \"true\",\n        \"browser_platform\": \"Win32\",\n        \"browser_version\": \"5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36\",\n        \"channel\": \"tiktok_web\",\n        \"cookie_enabled\": \"true\",\n        \"data_collection_enabled\": \"true\",\n        \"device_id\": \"\",\n        \"device_platform\": \"web_pc\",\n        \"enable_cache\": \"true\",\n        \"focus_state\": \"true\",\n        \"from_page\": \"user\",\n        \"history_len\": \"4\",\n        \"is_fullscreen\": \"false\",\n        \"is_page_visible\": \"true\",\n        \"language\": \"en\",\n        \"os\": \"windows\",\n        \"priority_region\": \"US\",\n        \"referer\": \"\",\n        \"region\": \"US\",\n        \"screen_height\": \"864\",\n        \"screen_width\": \"1536\",\n        \"tz_name\": \"Asia/Shanghai\",\n        \"user_is_login\": \"true\",\n        \"webcast_language\": \"en\",\n        \"msToken\": \"\",\n    }\n\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.xb = params.xb\n        self.xg = params.xg\n        self.headers = params.headers_tiktok.copy()\n        self.cookie = cookie\n        self.client: AsyncClient = params.client_tiktok\n        self.set_temp_cookie(cookie)\n\n    async def request_data(\n        self,\n        url: str,\n        params: dict = None,\n        data: dict = None,\n        method=\"GET\",\n        headers: dict = None,\n        encryption=8,\n        finished=False,\n        *args,\n        **kwargs,\n    ):\n        return await super().request_data(\n            url=url,\n            params=params,\n            data=data,\n            method=method,\n            headers=headers,\n            encryption=encryption,\n            finished=finished,\n            *args,\n            **kwargs,\n        )\n\n    def deal_url_params(\n        self,\n        params: dict,\n        number=8,\n        **kwargs,\n    ) -> str:\n        if params:\n            params = urlencode(\n                params,\n                safe=\"=\",\n                quote_via=quote,\n            )\n            xb = self.xb.get_x_bogus(\n                params, number, self.headers.get(\"User-Agent\", USERAGENT)\n            )\n            xg = self.xg.generate(\n                params, user_agent=self.headers.get(\"User-Agent\", USERAGENT)\n            )\n            params += f\"&X-Bogus={xb}&X-Gnarly={xg}\"\n            return params\n        return \"\"\n"
  },
  {
    "path": "src/interface/user.py",
    "content": "from typing import TYPE_CHECKING, Callable, Type, Coroutine\nfrom typing import Union\n\nfrom src.interface.template import API\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass User(API):\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        cookie: str = \"\",\n        proxy: str = None,\n        sec_user_id: str = ...,\n        *args,\n        **kwargs,\n    ):\n        super().__init__(params, cookie, proxy, *args, **kwargs)\n        self.sec_user_id = sec_user_id\n        self.api = f\"{self.domain}aweme/v1/web/user/profile/other/\"\n        self.text = _(\"账号\")\n\n    async def run(self, *args, **kwargs):\n        return await super().run(\n            single_page=True,\n            data_key=\"user\",\n        )\n\n    async def run_batch(\n        self,\n        data_key: str,\n        error_text=\"\",\n        cursor=\"cursor\",\n        has_more=\"has_more\",\n        params: Callable = lambda: {},\n        data: Callable = lambda: {},\n        method=\"GET\",\n        headers: dict = None,\n        callback: Type[Coroutine] = None,\n        *args,\n        **kwargs,\n    ):\n        pass\n\n    def check_response(\n        self,\n        data_dict: dict,\n        data_key: str,\n        error_text=\"\",\n        *args,\n        **kwargs,\n    ):\n        try:\n            if not (d := data_dict[data_key]):\n                self.log.warning(error_text)\n            else:\n                self.response = d\n        except KeyError:\n            self.log.error(\n                _(\"数据解析失败，请告知作者处理: {data}\").format(data=data_dict)\n            )\n            self.finished = True\n\n    def generate_params(\n        self,\n    ) -> dict:\n        return self.params | {\n            \"publish_video_strategy_type\": \"2\",\n            \"sec_user_id\": self.sec_user_id,\n            \"personal_center_strategy\": \"1\",\n            \"profile_other_record_enable\": \"1\",\n            \"land_to\": \"1\",\n            \"version_code\": \"170400\",\n            \"version_name\": \"17.4.0\",\n        }\n\n\nasync def test():\n    from src.testers import Params\n\n    async with Params() as params:\n        i = User(\n            params,\n            sec_user_id=\"\",\n        )\n        print(await i.run())\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/link/__init__.py",
    "content": "from .extractor import Extractor, ExtractorTikTok\n\n__all__ = [\n    \"Extractor\",\n    \"ExtractorTikTok\",\n]\n"
  },
  {
    "path": "src/link/extractor.py",
    "content": "from re import compile\nfrom typing import TYPE_CHECKING, Union\nfrom urllib.parse import parse_qs, unquote, urlparse\n\nfrom .requester import Requester\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n\n__all__ = [\"Extractor\", \"ExtractorTikTok\"]\n\n\nclass Extractor:\n    WEB_RID = compile(r\"\\\\\\\"webRid\\\\\\\":\\\\\\\"(\\d+?)\\\\\\\"\")\n\n    account_link = compile(\n        r\"\\S*?https://www\\.douyin\\.com/user/([A-Za-z0-9_-]+)(?:\\S*?\\bmodal_id=(\\d{19}))?\"\n    )  # 账号主页链接\n    account_share = compile(\n        r\"\\S*?https://www\\.iesdouyin\\.com/share/user/(\\S*?)\\?\\S*?\"  # 账号主页分享链接\n    )\n\n    detail_id = compile(r\"\\b(\\d{19})\\b\")  # 作品 ID\n    detail_link = compile(\n        r\"\\S*?https://www\\.douyin\\.com/(?:video|note|slides)/([0-9]{19})\\S*?\"\n    )  # 作品链接\n    detail_share = compile(\n        r\"\\S*?https://www\\.iesdouyin\\.com/share/(?:video|note|slides)/([0-9]{19})/\\S*?\"\n    )  # 作品分享链接\n    detail_search = compile(\n        r\"\\S*?https://www\\.douyin\\.com/search/\\S+?modal_id=(\\d{19})\\S*?\"\n    )  # 搜索作品链接\n    detail_discover = compile(\n        r\"\\S*?https://www\\.douyin\\.com/discover\\S*?modal_id=(\\d{19})\\S*?\"\n    )  # 首页作品链接\n\n    mix_link = compile(\n        r\"\\S*?https://www\\.douyin\\.com/collection/(\\d{19})\\S*?\"\n    )  # 合集链接\n    mix_share = compile(\n        r\"\\S*?https://www\\.iesdouyin\\.com/share/mix/detail/(\\d{19})/\\S*?\"\n    )  # 合集分享链接\n\n    live_link = compile(r\"\\S*?https://live\\.douyin\\.com/([0-9]+)\\S*?\")  # 直播链接\n    live_link_self = compile(r\"\\S*?https://www\\.douyin\\.com/follow\\?webRid=(\\d+)\\S*?\")\n    live_link_share = compile(\n        r\"\\S*?https://webcast\\.amemv\\.com/douyin/webcast/reflow/\\S+\"\n    )\n\n    channel_link = compile(\n        r\"\\S*?https://www\\.douyin\\.com/channel/\\d+?\\?modal_id=(\\d{19})\\S*?\"\n    )\n\n    def __init__(\n        self,\n        params: \"Parameter\",\n        tiktok=False,\n    ):\n        self.requester = Requester(\n            params,\n            params.client_tiktok if tiktok else params.client,\n            params.headers_tiktok if tiktok else params.headers,\n        )\n\n    async def run(\n        self,\n        text: str,\n        type_=\"detail\",\n        proxy: str = None,\n    ) -> Union[list[str], tuple[bool, list[str]], str]:\n        text = await self.requester.run(\n            text,\n            proxy,\n        )\n        match type_:\n            case \"detail\":\n                return self.detail(text)\n            case \"user\":\n                return self.user(text)\n            case \"mix\":\n                return self.mix(text)\n            case \"live\":\n                return await self.live(text)\n            case \"\":\n                return text\n        raise ValueError\n\n    async def get_html_data(\n        self,\n        url: str,\n        pattern,\n        index=1,\n    ) -> str:\n        html = await self.requester.request_url(\n            url,\n            \"text\",\n        )\n        data = pattern.search(html or \"\")\n        return data.group(index) if data else \"\"\n\n    def detail(\n        self,\n        urls: str,\n    ) -> list[str]:\n        return self.__extract_detail(urls)\n\n    def user(\n        self,\n        urls: str,\n    ) -> list[str]:\n        link = self.extract_info(self.account_link, urls, 1)\n        share = self.extract_info(self.account_share, urls, 1)\n        return link + share\n\n    def mix(\n        self,\n        urls: str,\n    ) -> tuple[bool, list[str]]:\n        if detail := self.__extract_detail(urls):\n            return False, detail\n        link = self.extract_info(self.mix_link, urls, 1)\n        share = self.extract_info(self.mix_share, urls, 1)\n        return (True, m) if (m := link + share) else (None, [])\n\n    async def live(\n        self,\n        urls: str,\n    ) -> list[str]:\n        live_link = self.extract_info(self.live_link, urls, 1)\n        live_link_self = self.extract_info(self.live_link_self, urls, 1)\n        live_link_share = self.extract_info(self.live_link_share, urls, 0)\n        live_link_share = [\n            await self.get_html_data(i, self.WEB_RID) for i in live_link_share\n        ]\n        return live_link + live_link_self + live_link_share\n\n    def __extract_detail(\n        self,\n        urls: str,\n    ) -> list[str]:\n        link = self.extract_info(self.detail_link, urls, 1)\n        share = self.extract_info(self.detail_share, urls, 1)\n        account = self.extract_info(self.account_link, urls, 2)\n        search = self.extract_info(self.detail_search, urls, 1)\n        discover = self.extract_info(self.detail_discover, urls, 1)\n        channel = self.extract_info(self.channel_link, urls, 1)\n        return link + share + account + search + discover + channel\n\n    @staticmethod\n    def extract_sec_user_id(urls: list[str]) -> list[list]:\n        data = []\n        for url in urls:\n            url = urlparse(url)\n            query_params = parse_qs(url.query)\n            data.append(\n                [url.path.split(\"/\")[-1], query_params.get(\"sec_user_id\", [\"\"])[0]]\n            )\n        return data\n\n    @staticmethod\n    def extract_info(pattern, urls: str, index=1) -> list[str]:\n        result = pattern.finditer(urls)\n        return [i for i in (i.group(index) for i in result) if i] if result else []\n\n\nclass ExtractorTikTok(Extractor):\n    SEC_UID = compile(r'\"verified\":(?:false|true),\"secUid\":\"([a-zA-Z0-9_-]+)\"')\n    ROOD_ID = compile(r'\"roomId\":\"(\\d+)\"')\n    MIX_ID = compile(r'\"canonical\":\"\\S+?(\\d{19})\"')\n\n    account_link = compile(r\"\\S*?(https://www\\.tiktok\\.com/@[^\\s/]+)\\S*?\")\n\n    detail_link = compile(\n        r\"\\S*?https://www\\.tiktok\\.com/@[^\\s/]+/(?!playlist|collection)(?:(?:video|photo)/(\\d{19}))?\\S*?\"\n    )  # 作品链接\n\n    mix_link = compile(\n        r\"\\S*?https://www\\.tiktok\\.com/@\\S+/(?:playlist|collection)/(.+?)-(\\d{19})\\S*?\"\n    )  # 合集链接\n\n    live_link = compile(r\"\\S*?https://www\\.tiktok\\.com/@[^\\s/]+/live\\S*?\")  # 直播链接\n\n    def __init__(self, params: \"Parameter\"):\n        super().__init__(\n            params,\n            True,\n        )\n\n    async def run(\n        self,\n        text: str,\n        type_=\"detail\",\n        proxy: str = None,\n    ) -> Union[\n        list[str],\n        tuple[bool, list[str], list[str | None]],\n        str,\n    ]:\n        text = await self.requester.run(\n            text,\n            proxy,\n        )\n        match type_:\n            case \"detail\":\n                return await self.detail(text)\n            case \"user\":\n                return await self.user(text)\n            case \"mix\":\n                return await self.mix(text)\n            case \"live\":\n                return await self.live(text)\n            case \"\":\n                return text\n        raise ValueError\n\n    async def detail(\n        self,\n        urls: str,\n    ) -> list[str]:\n        return self.__extract_detail(urls)\n\n    async def user(\n        self,\n        urls: str,\n    ) -> list[str]:\n        link = self.extract_info(self.account_link, urls, 1)\n        link = [await self.get_html_data(i, self.SEC_UID) for i in link]\n        return [i for i in link if i]\n\n    def __extract_detail(\n        self,\n        urls: str,\n        index=1,\n    ) -> list[str]:\n        link = self.extract_info(self.detail_link, urls, index)\n        return link\n\n    async def mix(\n        self,\n        urls: str,\n    ) -> tuple[bool, list[str], list[str | None]]:\n        detail = self.__extract_detail(urls, index=0)\n        detail = [await self.get_html_data(i, self.MIX_ID) for i in detail]\n        detail = [i for i in detail if i]\n        mix = self.extract_info(self.mix_link, urls, 2)\n        title = [unquote(i) for i in self.extract_info(self.mix_link, urls, 1)]\n        return True, detail + mix, [None for _ in detail] + title\n\n    async def live(\n        self,\n        urls: str,\n    ) -> list[str]:\n        link = self.extract_info(self.live_link, urls, 0)\n        link = [await self.get_html_data(i, self.ROOD_ID) for i in link]\n        return [i for i in link if i]\n"
  },
  {
    "path": "src/link/requester.py",
    "content": "from re import compile\nfrom typing import TYPE_CHECKING\n\nfrom ..custom import wait\nfrom ..tools import DownloaderError, Retry, capture_error_request\n\nif TYPE_CHECKING:\n    from httpx import AsyncClient, get, head\n\n    from ..config import Parameter\n\n__all__ = [\"Requester\"]\n\n\nclass Requester:\n    URL = compile(r\"(https?://[^\\s\\\"<>\\\\^`{|}，。；！？、【】《》]+)\")\n\n    def __init__(\n        self,\n        params: \"Parameter\",\n        client: \"AsyncClient\",\n        headers: dict[str, str],\n    ):\n        self.client = client\n        self.headers = headers\n        self.log = params.logger\n        self.max_retry = params.max_retry\n        self.timeout = params.timeout\n\n    async def run(\n        self,\n        text: str,\n        proxy: str = None,\n    ) -> str:\n        urls = self.URL.finditer(text)\n        if not urls:\n            return \"\"\n        result = []\n        for i in urls:\n            result.append(\n                await self.request_url(\n                    u := i.group(),\n                    proxy=proxy,\n                )\n                or u\n            )\n            await wait()\n        return \" \".join(i for i in result if i)\n\n    @Retry.retry\n    @capture_error_request\n    async def request_url(\n        self,\n        url: str,\n        content=\"url\",\n        proxy: str = None,\n    ):\n        self.log.info(f\"URL: {url}\", False)\n        match bool(proxy):\n            # case True, True:\n            #     response = self.request_url_head_proxy(\n            #         url,\n            #         proxy,\n            #     )\n            # case True, False:\n            #     response = await self.request_url_head(url)\n            case True:\n                response = self.request_url_get_proxy(\n                    url,\n                    proxy,\n                )\n            case False:\n                response = await self.request_url_get(url)\n            case _:\n                raise DownloaderError\n        self.log.info(f\"Response URL: {response.url}\", False)\n        self.log.info(f\"Response Code: {response.status_code}\", False)\n        # 记录请求体数据会导致日志文件体积过大，仅在必要时记录\n        # self.log.info(f\"Response Content: {response.content}\", False)\n        self.log.info(f\"Response Headers: {dict(response.headers)}\", False)\n        match content:\n            case \"text\":\n                return response.text\n            case \"content\":\n                return response.content\n            case \"json\":\n                return response.json()\n            case \"headers\":\n                return response.headers\n            case \"url\":\n                return str(response.url)\n            case _:\n                raise DownloaderError\n\n    async def request_url_head(\n        self,\n        url: str,\n    ):\n        return await self.client.head(\n            url,\n            headers=self.headers,\n        )\n\n    def request_url_head_proxy(\n        self,\n        url: str,\n        proxy: str,\n    ):\n        return head(\n            url,\n            headers=self.headers,\n            proxy=proxy,\n            follow_redirects=True,\n            verify=False,\n            timeout=self.timeout,\n        )\n\n    async def request_url_get(\n        self,\n        url: str,\n    ):\n        response = await self.client.get(\n            url,\n            headers=self.headers,\n        )\n        response.raise_for_status()\n        return response\n\n    def request_url_get_proxy(\n        self,\n        url: str,\n        proxy: str,\n    ):\n        response = get(\n            url,\n            headers=self.headers,\n            proxy=proxy,\n            follow_redirects=True,\n            verify=False,\n            timeout=self.timeout,\n        )\n        response.raise_for_status()\n        return response\n"
  },
  {
    "path": "src/manager/__init__.py",
    "content": "from .cache import Cache\nfrom .database import Database\nfrom .recorder import DownloadRecorder\n\n__all__ = [\n    \"Cache\",\n    \"DownloadRecorder\",\n    \"Database\",\n]\n"
  },
  {
    "path": "src/manager/cache.py",
    "content": "from pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom ..tools import Retry\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from .database import Database\n\n__all__ = [\"Cache\"]\n\n\nclass Cache:\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n        database: \"Database\",\n        mark: bool,\n        name: bool,\n    ):\n        self.console = parameter.console\n        self.log = parameter.logger  # 日志记录对象\n        self.database = database\n        self.root = parameter.root  # 作品文件保存根目录\n        self.mark = mark\n        self.name = name\n\n    async def update_cache(\n        self,\n        solo_mode: bool,\n        prefix: str,\n        suffix: str,\n        id_: str,\n        name: str,\n        mark: str,\n    ):\n        if d := await self.has_cache(id_):\n            self.__check_file(\n                solo_mode,\n                prefix,\n                suffix,\n                id_,\n                name,\n                mark,\n                d,\n            )\n        data = (\n            id_,\n            name,\n            mark,\n        )\n        await self.database.update_mapping_data(*data)\n        self.log.info(f\"更新缓存数据: {', '.join(data)}\", False)\n\n    async def has_cache(self, id_: str) -> dict:\n        return await self.database.read_mapping_data(id_)\n\n    def __check_file(\n        self,\n        solo_mode: bool,\n        prefix: str,\n        suffix: str,\n        id_: str,\n        name: str,\n        mark: str,\n        data: dict,\n    ):\n        if not (\n            old_folder := self.root.joinpath(\n                f\"{prefix}{id_}_{data['mark'] or data['name']}_{suffix}\"\n            )\n        ).is_dir():\n            self.log.info(f\"{old_folder} 文件夹不存在，自动跳过\", False)\n            return\n        if data[\"mark\"] != mark:\n            self.__rename_folder(old_folder, prefix, suffix, id_, mark)\n            if self.mark:\n                self.__scan_file(\n                    solo_mode,\n                    prefix,\n                    suffix,\n                    id_,\n                    name,\n                    mark,\n                    key=\"mark\",\n                    data=data,\n                )\n        if data[\"name\"] != name and self.name:\n            self.__scan_file(\n                solo_mode,\n                prefix,\n                suffix,\n                id_,\n                name,\n                mark,\n                data=data,\n            )\n\n    def __rename_folder(\n        self,\n        old_folder: Path,\n        prefix: str,\n        suffix: str,\n        id_: str,\n        mark: str,\n    ):\n        new_folder = self.root.joinpath(f\"{prefix}{id_}_{mark}_{suffix}\")\n        self.__rename(\n            old_folder,\n            new_folder,\n            _(\"文件夹\"),\n        )\n        self.log.info(f\"文件夹 {old_folder} 已重命名为 {new_folder}\", False)\n\n    def __rename_works_folder(\n        self,\n        old_: Path,\n        mark: str,\n        name: str,\n        key: str,\n        data: dict,\n    ) -> Path:\n        if (s := data[key]) in old_.name:\n            new_ = old_.parent / old_.name.replace(\n                s, {\"name\": name, \"mark\": mark}[key], 1\n            )\n            self.__rename(\n                old_,\n                new_,\n                _(\"文件夹\"),\n            )\n            self.log.info(f\"文件夹 {old_} 重命名为 {new_}\", False)\n            return new_\n        return old_\n\n    def __scan_file(\n        self,\n        solo_mode: bool,\n        prefix: str,\n        suffix: str,\n        id_: str,\n        name: str,\n        mark: str,\n        data: dict,\n        key=\"name\",\n    ):\n        root = self.root.joinpath(f\"{prefix}{id_}_{mark}_{suffix}\")\n        item_list = root.iterdir()\n        if solo_mode:\n            for f in item_list:\n                if f.is_dir():\n                    f = self.__rename_works_folder(\n                        f,\n                        mark,\n                        name,\n                        key,\n                        data,\n                    )\n                    files = f.iterdir()\n                    self.__batch_rename(\n                        f,\n                        files,\n                        mark,\n                        name,\n                        key,\n                        data,\n                    )\n        else:\n            self.__batch_rename(\n                root,\n                item_list,\n                mark,\n                name,\n                key,\n                data,\n            )\n\n    def __batch_rename(\n        self,\n        root: Path,\n        files,\n        mark: str,\n        name: str,\n        key: str,\n        data: dict,\n    ):\n        for old_file in files:\n            if (s := data[key]) not in old_file.name:\n                break\n            self.__rename_file(root, old_file, s, mark, name, key)\n\n    def __rename_file(\n        self,\n        root: Path,\n        old_file: Path,\n        keywords: str,\n        mark: str,\n        name: str,\n        field: str,\n    ):\n        new_file = root.joinpath(\n            old_file.name.replace(keywords, {\"name\": name, \"mark\": mark}[field], 1)\n        )\n        self.__rename(\n            old_file,\n            new_file,\n            _(\"文件\"),\n        )\n        self.log.info(f\"文件 {old_file} 重命名为 {new_file}\", False)\n        return True\n\n    @Retry.retry_limited\n    def __rename(\n        self,\n        old_: Path,\n        new_: Path,\n        type_=_(\"文件\"),\n    ) -> bool:\n        try:\n            old_.rename(new_)\n            return True\n        except PermissionError as e:\n            self.console.error(\n                _(\"{type} {old}被占用，重命名失败: {error}\").format(\n                    type=type_, old=old_, error=e\n                ),\n            )\n            return False\n        except FileExistsError as e:\n            self.console.error(\n                _(\"{type} {new}名称重复，重命名失败: {error}\").format(\n                    type=type_, new=new_, error=e\n                ),\n            )\n            return False\n        except OSError as e:\n            self.console.error(\n                _(\"处理{type} {old}时发生预期之外的错误: {error}\").format(\n                    type=type_, old=old_, error=e\n                ),\n            )\n            return True\n"
  },
  {
    "path": "src/manager/database.py",
    "content": "from asyncio import CancelledError\nfrom contextlib import suppress\nfrom shutil import move\n\nfrom aiosqlite import Row, connect\n\nfrom ..custom import PROJECT_ROOT\n\n__all__ = [\"Database\"]\n\n\nclass Database:\n    __FILE = \"DouK-Downloader.db\"\n\n    def __init__(\n        self,\n    ):\n        self.file = PROJECT_ROOT.joinpath(self.__FILE)\n        self.database = None\n        self.cursor = None\n\n    async def __connect_database(self):\n        self.database = await connect(self.file)\n        self.database.row_factory = Row\n        self.cursor = await self.database.cursor()\n        await self.__create_table()\n        await self.__write_default_config()\n        await self.__write_default_option()\n        await self.database.commit()\n\n    async def __create_table(self):\n        await self.database.execute(\n            \"\"\"CREATE TABLE IF NOT EXISTS config_data (\n            NAME TEXT PRIMARY KEY,\n            VALUE INTEGER NOT NULL CHECK(VALUE IN (0, 1))\n            );\"\"\"\n        )\n        await self.database.execute(\n            \"CREATE TABLE IF NOT EXISTS download_data (ID TEXT PRIMARY KEY);\"\n        )\n        await self.database.execute(\"\"\"CREATE TABLE IF NOT EXISTS mapping_data (\n        ID TEXT PRIMARY KEY,\n        NAME TEXT NOT NULL,\n        MARK TEXT NOT NULL\n        );\"\"\")\n        await self.database.execute(\"\"\"CREATE TABLE IF NOT EXISTS option_data (\n        NAME TEXT PRIMARY KEY,\n        VALUE TEXT NOT NULL\n        );\"\"\")\n\n    async def __write_default_config(self):\n        await self.database.execute(\"\"\"INSERT OR IGNORE INTO config_data (NAME, VALUE)\n                            VALUES ('Record', 1),\n                            ('Logger', 0),\n                            ('Disclaimer', 0);\"\"\")\n\n    async def __write_default_option(self):\n        await self.database.execute(\"\"\"INSERT OR IGNORE INTO option_data (NAME, VALUE)\n                            VALUES ('Language', 'zh_CN');\"\"\")\n\n    async def read_config_data(self):\n        await self.cursor.execute(\"SELECT * FROM config_data\")\n        return await self.cursor.fetchall()\n\n    async def read_option_data(self):\n        await self.cursor.execute(\"SELECT * FROM option_data\")\n        return await self.cursor.fetchall()\n\n    async def update_config_data(\n        self,\n        name: str,\n        value: int,\n    ):\n        await self.database.execute(\n            \"REPLACE INTO config_data (NAME, VALUE) VALUES (?,?)\", (name, value)\n        )\n        await self.database.commit()\n\n    async def update_option_data(\n        self,\n        name: str,\n        value: str,\n    ):\n        await self.database.execute(\n            \"REPLACE INTO option_data (NAME, VALUE) VALUES (?,?)\", (name, value)\n        )\n        await self.database.commit()\n\n    async def update_mapping_data(self, id_: str, name: str, mark: str):\n        await self.database.execute(\n            \"REPLACE INTO mapping_data (ID, NAME, MARK) VALUES (?,?,?)\",\n            (id_, name, mark),\n        )\n        await self.database.commit()\n\n    async def read_mapping_data(self, id_: str):\n        await self.cursor.execute(\n            \"SELECT NAME, MARK FROM mapping_data WHERE ID=?\", (id_,)\n        )\n        return await self.cursor.fetchone()\n\n    async def has_download_data(self, id_: str) -> bool:\n        await self.cursor.execute(\"SELECT ID FROM download_data WHERE ID=?\", (id_,))\n        return bool(await self.cursor.fetchone())\n\n    async def write_download_data(self, id_: str):\n        await self.database.execute(\n            \"INSERT OR IGNORE INTO download_data (ID) VALUES (?);\", (id_,)\n        )\n        await self.database.commit()\n\n    async def delete_download_data(self, ids: list | tuple | str):\n        if not ids:\n            return\n        if isinstance(ids, str):\n            ids = [ids]\n        [await self.__delete_download_data(i) for i in ids]\n        await self.database.commit()\n\n    async def __delete_download_data(self, id_: str):\n        await self.database.execute(\"DELETE FROM download_data WHERE ID=?\", (id_,))\n\n    async def delete_all_download_data(self):\n        await self.database.execute(\"DELETE FROM download_data\")\n        await self.database.commit()\n\n    async def __aenter__(self):\n        self.compatible()\n        await self.__connect_database()\n        return self\n\n    async def close(self):\n        with suppress(CancelledError):\n            await self.cursor.close()\n        await self.database.close()\n\n    async def __aexit__(self, exc_type, exc_value, traceback):\n        await self.close()\n\n    def compatible(self):\n        if (\n            old := PROJECT_ROOT.parent.joinpath(self.__FILE)\n        ).exists() and not self.file.exists():\n            move(old, self.file)\n"
  },
  {
    "path": "src/manager/recorder.py",
    "content": "from pathlib import Path\nfrom platform import system\nfrom re import compile\nfrom typing import TYPE_CHECKING\n\nfrom ..custom import (\n    ERROR,\n    INFO,\n    WARNING,\n)\n\nif TYPE_CHECKING:\n    from ..tools import ColorfulConsole\n    from .database import Database\n\n__all__ = [\n    \"DownloadRecorder\",\n]\n\n\nclass __DownloadRecorder:\n    encode = \"UTF-8-SIG\" if system() == \"Windows\" else \"UTF-8\"\n    works_id = compile(r\"\\d{19}\")\n\n    def __init__(\n        self, switch: bool, folder: Path, state: bool, console: \"ColorfulConsole\"\n    ):\n        self.switch = switch\n        self.state = state\n        self.backup = folder.joinpath(\"IDRecorder_backup.txt\")\n        self.path = folder.joinpath(\"IDRecorder.txt\")\n        self.file = None\n        self.console = console\n        self.record = self.__get_set()\n\n    def __get_set(self) -> set:\n        return self.__read_file() if self.switch else set()\n\n    def __read_file(self):\n        if not self.path.is_file():\n            blacklist = set()\n        else:\n            with self.path.open(\"r\", encoding=self.encode) as f:\n                blacklist = self.__restore_data({line.strip() for line in f})\n        self.file = self.path.open(\"w\", encoding=self.encode)\n        return blacklist\n\n    def __save_file(self, file):\n        file.write(\"\\n\".join(f\"{i}\" for i in self.record))\n\n    def update_id(self, id_):\n        if self.switch:\n            self.record.add(id_)\n\n    def __extract_ids(self, ids: str) -> list[str]:\n        ids = ids.split()\n        result = []\n        for i in ids:\n            if id_ := self.works_id.search(i):\n                result.append(id_.group())\n        return result\n\n    def delete_ids(self, ids: str) -> None:\n        if ids.upper() == \"ALL\":\n            self.record.clear()\n        else:\n            ids = self.__extract_ids(ids)\n            [self.record.remove(i) for i in ids if i in self.record]\n\n    def backup_file(self):\n        if self.file and self.record:\n            # print(\"Backup IDRecorder\")  # 调试代码\n            with self.backup.open(\"w\", encoding=self.encode) as f:\n                self.__save_file(f)\n\n    def close(self):\n        if self.file:\n            self.__save_file(self.file)\n            self.file.close()\n            self.file = None\n            # print(\"Close IDRecorder\")  # 调试代码\n\n    def __restore_data(self, ids: set) -> set:\n        if self.state:\n            return ids\n        self.console.print(\n            f\"程序检测到上次运行可能没有正常结束，您的作品下载记录数据可能已经丢失！\\n数据文件路径：{\n                self.path.resolve()\n            }\",\n            style=ERROR,\n        )\n        if self.backup.exists():\n            if (\n                self.console.input(\n                    \"检测到 IDRecorder 备份文件，是否恢复最后一次备份的数据(YES/NO): \",\n                    style=WARNING,\n                ).upper()\n                == \"YES\"\n            ):\n                self.path.write_text(self.backup.read_text(encoding=self.encode))\n                self.console.print(\n                    \"IDRecorder 已恢复最后一次备份的数据，请重新运行程序！\", style=INFO\n                )\n                return set(self.backup.read_text(encoding=self.encode).split())\n            else:\n                self.console.print(\n                    \"IDRecorder 数据未恢复，下载任意作品之后，备份数据会被覆盖导致无法恢复！\",\n                    style=ERROR,\n                )\n        else:\n            self.console.print(\n                \"未检测到 IDRecorder 备份文件，您的作品下载记录数据无法恢复！\",\n                style=ERROR,\n            )\n        return set()\n\n\nclass DownloadRecorder:\n    detail = compile(r\"\\d{19}\")\n\n    def __init__(self, database: \"Database\", switch: bool, console: \"ColorfulConsole\"):\n        self.switch = switch\n        self.console = console\n        self.database = database\n\n    async def has_id(self, id_: str) -> bool:\n        return (\n            await self.database.has_download_data(id_) if self.switch and id_ else False\n        )\n\n    async def update_id(self, id_: str):\n        if self.switch and id_:\n            await self.database.write_download_data(id_)\n\n    async def delete_id(self, id_: str) -> None:\n        if self.switch and id_:\n            await self.database.delete_download_data(id_)\n\n    async def delete_ids(self, ids: str) -> None:\n        if ids.upper() == \"ALL\":\n            await self.database.delete_all_download_data()\n        else:\n            ids = self.__extract_ids(ids)\n            await self.database.delete_download_data(ids)\n\n    def __extract_ids(self, ids: str) -> list[str]:\n        ids = ids.split()\n        result = []\n        for i in ids:\n            if id_ := self.detail.search(i):\n                result.append(id_.group())\n        return result\n"
  },
  {
    "path": "src/models/__init__.py",
    "content": "from .response import DataResponse, UrlResponse\nfrom .search import (\n    GeneralSearch,\n    VideoSearch,\n    UserSearch,\n    LiveSearch,\n)\nfrom .settings import Settings\nfrom .share import ShortUrl\nfrom .detail import Detail, DetailTikTok\nfrom .account import Account, AccountTiktok\nfrom .comment import Comment\nfrom .reply import Reply\nfrom .mix import Mix, MixTikTok\nfrom .live import Live, LiveTikTok\n\n__all__ = (\n    \"GeneralSearch\",\n    \"VideoSearch\",\n    \"UserSearch\",\n    \"LiveSearch\",\n    \"DataResponse\",\n    \"Settings\",\n    \"UrlResponse\",\n    \"ShortUrl\",\n    \"Detail\",\n    \"DetailTikTok\",\n    \"Account\",\n    \"AccountTiktok\",\n    \"Comment\",\n    \"Reply\",\n    \"Mix\",\n    \"MixTikTok\",\n    \"Live\",\n    \"LiveTikTok\",\n)\n"
  },
  {
    "path": "src/models/account.py",
    "content": "from pydantic import Field\n\nfrom .base import APIModel\n\n\nclass Account(APIModel):\n    sec_user_id: str\n    tab: str = \"post\"\n    earliest: str | float | int | None = None\n    latest: str | float | int | None = None\n    pages: int | None = None\n    cursor: int = 0\n    count: int = Field(\n        18,\n        gt=0,\n    )\n\n\nclass AccountTiktok(Account):\n    pass\n"
  },
  {
    "path": "src/models/base.py",
    "content": "from pydantic import BaseModel\n\n\nclass APIModel(BaseModel):\n    cookie: str = \"\"\n    proxy: str = \"\"\n    source: bool = False\n"
  },
  {
    "path": "src/models/comment.py",
    "content": "from pydantic import Field\n\nfrom .base import APIModel\n\n\nclass Comment(APIModel):\n    detail_id: str\n    pages: int = Field(\n        1,\n        gt=0,\n    )\n    cursor: int = 0\n    count: int = Field(\n        20,\n        gt=0,\n    )\n    count_reply: int = Field(\n        3,\n        gt=0,\n    )\n    reply: bool = False\n"
  },
  {
    "path": "src/models/detail.py",
    "content": "from .base import APIModel\n\n\nclass Detail(APIModel):\n    detail_id: str\n\n\nclass DetailTikTok(Detail):\n    pass\n"
  },
  {
    "path": "src/models/live.py",
    "content": "from .base import APIModel\n\n\nclass Live(APIModel):\n    web_rid: str | None = None\n    # room_id: str | None = None\n    # sec_user_id: str | None = None\n\n\nclass LiveTikTok(APIModel):\n    room_id: str | None = None\n"
  },
  {
    "path": "src/models/mix.py",
    "content": "from pydantic import Field\n\nfrom .base import APIModel\n\n\nclass Mix(APIModel):\n    mix_id: str | None = None\n    detail_id: str | None = None\n    cursor: int = 0\n    count: int = Field(\n        12,\n        gt=0,\n    )\n\n\nclass MixTikTok(APIModel):\n    mix_id: str | None = None\n    cursor: int = 0\n    count: int = Field(\n        30,\n        gt=0,\n    )\n"
  },
  {
    "path": "src/models/reply.py",
    "content": "from pydantic import Field\n\nfrom .base import APIModel\n\n\nclass Reply(APIModel):\n    detail_id: str\n    comment_id: str\n    pages: int = Field(\n        1,\n        gt=0,\n    )\n    cursor: int = 0\n    count: int = Field(\n        3,\n        gt=0,\n    )\n"
  },
  {
    "path": "src/models/response.py",
    "content": "from datetime import datetime\n\nfrom pydantic import BaseModel, computed_field\n\n\nclass DataResponse(BaseModel):\n    message: str\n    data: dict | list[dict] | None = None\n    params: dict | None\n\n    @computed_field\n    @property\n    def time(self) -> str:\n        \"\"\"格式化后的时间字符串\"\"\"\n        return datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n\n\nclass UrlResponse(BaseModel):\n    message: str\n    url: str | None = None\n    params: dict | None\n\n    @computed_field\n    @property\n    def time(self) -> str:\n        \"\"\"格式化后的时间字符串\"\"\"\n        return datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n"
  },
  {
    "path": "src/models/search.py",
    "content": "from typing import Literal\n\nfrom pydantic import Field, field_validator\n\nfrom src.models.base import APIModel\n\ntry:\n    from src.translation import _\nexcept ImportError:\n\n    def _(x):\n        return x\n\n\nclass BaseSearch(APIModel):\n    keyword: str\n    pages: int = Field(\n        1,\n        gt=0,\n    )\n    offset: int = Field(\n        0,\n        ge=0,\n    )\n    count: int = Field(\n        10,\n        ge=5,\n    )\n\n    @field_validator(\"keyword\", mode=\"before\")\n    @classmethod\n    def keyword_validator(cls, v):\n        if not v:\n            raise ValueError(_(\"keyword 参数无效\"))\n        return v\n\n\nclass GeneralSearch(BaseSearch):\n    channel: Literal[0,] = 0\n    sort_type: Literal[\n        0,\n        1,\n        2,\n    ] = 0\n    publish_time: Literal[\n        0,\n        1,\n        7,\n        180,\n    ] = 0\n    duration: Literal[\n        0,\n        1,\n        2,\n        3,\n    ] = 0\n    search_range: Literal[\n        0,\n        1,\n        2,\n        3,\n    ] = 0\n    content_type: Literal[\n        0,\n        1,\n        2,\n    ] = 0\n\n    @field_validator(\n        \"sort_type\",\n        \"publish_time\",\n        \"duration\",\n        \"search_range\",\n        \"content_type\",\n        mode=\"before\",\n    )\n    @classmethod\n    def val_number(cls, value: str | int) -> int:\n        return int(value) if isinstance(value, str) else value\n\n\nclass VideoSearch(BaseSearch):\n    channel: Literal[1,] = 1\n    sort_type: Literal[\n        0,\n        1,\n        2,\n    ] = 0\n    publish_time: Literal[\n        0,\n        1,\n        7,\n        180,\n    ] = 0\n    duration: Literal[\n        0,\n        1,\n        2,\n        3,\n    ] = 0\n    search_range: Literal[\n        0,\n        1,\n        2,\n        3,\n    ] = 0\n\n    @field_validator(\n        \"sort_type\", \"publish_time\", \"duration\", \"search_range\", mode=\"before\"\n    )\n    @classmethod\n    def val_number(cls, value: str | int) -> int:\n        return int(value) if isinstance(value, str) else value\n\n\nclass UserSearch(BaseSearch):\n    channel: Literal[2,] = 2\n    douyin_user_fans: Literal[\n        0,\n        1,\n        2,\n        3,\n        4,\n        5,\n    ] = 0\n    douyin_user_type: Literal[\n        0,\n        1,\n        2,\n        3,\n    ] = 0\n\n    @field_validator(\"douyin_user_fans\", \"douyin_user_type\", mode=\"before\")\n    @classmethod\n    def val_number(cls, value: str | int) -> int:\n        return int(value) if isinstance(value, str) else value\n\n\nclass LiveSearch(BaseSearch):\n    channel: Literal[3,] = 3\n"
  },
  {
    "path": "src/models/settings.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel, Field\n\n\nclass AccountUrl(BaseModel):\n    mark: str = \"\"\n    url: str\n    tab: str = \"post\"\n    earliest: str | int | float = \"\"\n    latest: str | int | float = \"\"\n    enable: bool = True\n\n\nclass MixUrl(BaseModel):\n    mark: str = \"\"\n    url: str\n    enable: bool = True\n\n\nclass OwnerUrl(BaseModel):\n    mark: str = \"\"\n    url: str\n    uid: str = \"\"\n    sec_uid: str = \"\"\n    nickname: str = \"\"\n\n\nclass BrowserInfo(BaseModel):\n    User_Agent: str = Field(\n        default=\"\",\n        alias=\"User-Agent\",\n    )\n    pc_libra_divert: str = \"\"\n    browser_language: str = \"\"\n    browser_platform: str = \"\"\n    browser_name: str = \"\"\n    browser_version: str = \"\"\n    engine_name: str = \"\"\n    engine_version: str = \"\"\n    os_name: str = \"\"\n    os_version: str = \"\"\n    webid: str = \"\"\n\n\nclass TikTokBrowserInfo(BaseModel):\n    User_Agent: str = Field(\n        \"\",\n        alias=\"User-Agent\",\n    )\n    app_language: str = \"\"\n    browser_language: str = \"\"\n    browser_name: str = \"\"\n    browser_platform: str = \"\"\n    browser_version: str = \"\"\n    language: str = \"\"\n    os: str = \"\"\n    priority_region: str = \"\"\n    region: str = \"\"\n    tz_name: str = \"\"\n    webcast_language: str = \"\"\n    device_id: str = \"\"\n\n\nclass Settings(BaseModel):\n    accounts_urls: List[AccountUrl] = []\n    accounts_urls_tiktok: List[AccountUrl] = []\n    mix_urls: List[MixUrl] = []\n    mix_urls_tiktok: List[MixUrl] = []\n    owner_url: OwnerUrl | dict[str, str] = {}\n    owner_url_tiktok: None = None\n    root: str | None = None\n    folder_name: str | None = None\n    name_format: str | None = None\n    desc_length: int | None = None\n    name_length: int | None = None\n    date_format: str | None = None\n    split: str | None = None\n    folder_mode: bool | None = None\n    music: bool | None = None\n    truncate: int | None = None\n    storage_format: str | None = None\n    cookie: str | dict = \"\"\n    cookie_tiktok: str | dict = \"\"\n    dynamic_cover: bool | None = None\n    static_cover: bool | None = None\n    proxy: str | None = None\n    proxy_tiktok: str | None = None\n    twc_tiktok: str | None = None\n    download: bool | None = None\n    max_size: int | None = None\n    chunk: int | None = None\n    timeout: int | None = None\n    max_retry: int | None = None\n    max_pages: int | None = None\n    run_command: str | None = None\n    ffmpeg: str | None = None\n    live_qualities: str | None = None\n    douyin_platform: bool | None = None\n    tiktok_platform: bool | None = None\n    browser_info: BrowserInfo | None = None\n    browser_info_tiktok: TikTokBrowserInfo | None = None\n\n    class Config:\n        populate_by_name = True\n        arbitrary_types_allowed = True\n        json_encoders = {\n            AccountUrl: lambda v: v.dict(),\n            MixUrl: lambda v: v.dict(),\n            OwnerUrl: lambda v: v.dict(),\n            BrowserInfo: lambda v: v.dict(),\n            TikTokBrowserInfo: lambda v: v.dict(),\n        }\n"
  },
  {
    "path": "src/models/share.py",
    "content": "from pydantic import BaseModel\n\n\nclass ShortUrl(BaseModel):\n    text: str\n    proxy: str = \"\"\n"
  },
  {
    "path": "src/module/__init__.py",
    "content": "from .cookie import Cookie\nfrom .ffmpeg import FFMPEG\nfrom .migrate_folder import MigrateFolder\n\n# from .register import __Register\nfrom .tiktok_unofficial import DetailTikTokExtractor, DetailTikTokUnofficial\n\n__all__ = [\n    \"Cookie\",\n    \"FFMPEG\",\n    # \"__Register\",\n    \"DetailTikTokExtractor\",\n    \"DetailTikTokUnofficial\",\n    \"MigrateFolder\",\n]\n"
  },
  {
    "path": "src/module/cookie.py",
    "content": "from typing import TYPE_CHECKING\nfrom ..tools import cookie_str_to_dict\nfrom ..translation import _\nfrom re import compile\nfrom pyperclip import paste\n\nif TYPE_CHECKING:\n    from ..config import Settings\n    from ..tools import ColorfulConsole\n\n__all__ = [\"Cookie\"]\n\n\nclass Cookie:\n    PATTERN = compile(r\"[!#$%&'*+\\-.^_`|~0-9A-Za-z]+=([^;\\s][^;]*)\")\n    STATE_KEY = \"sessionid_ss\"\n    PLATFORM_KEY = {\n        False: \"cookie\",\n        True: \"cookie_tiktok\",\n    }\n\n    def __init__(self, settings: \"Settings\", console: \"ColorfulConsole\"):\n        self.settings = settings\n        self.console = console\n        self.PLATFORM_NAME = {\n            False: _(\"抖音\"),\n            True: \"TikTok\",\n        }\n\n    def run(\n        self,\n        tiktok=False,\n    ) -> bool:\n        \"\"\"提取 Cookie 并写入配置文件\"\"\"\n        if self.validate_cookie_minimal(cookie := paste()):\n            self.extract(\n                cookie,\n                key=self.PLATFORM_KEY[tiktok],\n                platform=self.PLATFORM_NAME[tiktok],\n            )\n            return True\n        self.console.warning(_(\"当前剪贴板的内容不是有效的 Cookie 内容！\"))\n        return False\n\n    def extract(\n        self,\n        cookie: str,\n        write=True,\n        key=\"cookie\",\n        platform: str = ...,\n    ) -> dict:\n        cookie_dict = cookie_str_to_dict(cookie)\n        self.__check_state(\n            cookie_dict,\n            platform,\n        )\n        if write:\n            self.save_cookie(cookie_dict, key)\n            self.console.print(\n                _(f\"写入 {platform} Cookie 成功！\").format(platform=platform)\n            )\n        return cookie_dict\n\n    def __check_state(self, items: dict, platform: str) -> None:\n        if items.get(self.STATE_KEY):\n            self.console.print(\n                _(f\"当前 {platform} Cookie 已登录\").format(platform=platform)\n            )\n        else:\n            self.console.print(\n                _(f\"当前 {platform} Cookie 未登录\").format(platform=platform)\n            )\n\n    def save_cookie(self, cookie: dict, key=\"cookie\") -> None:\n        data = self.settings.read()\n        data[key] = cookie\n        self.settings.update(data)\n\n    @classmethod\n    def validate_cookie_minimal(cls, cookie_str: str) -> bool:\n        \"\"\"\n        只检查整个字符串中是否存在 key=value 子串，\n        且 key 和 value 都非空。\n        返回 True 或 False。\n        \"\"\"\n        if not isinstance(cookie_str, str):\n            return False\n        return bool(cls.PATTERN.search(cookie_str))\n"
  },
  {
    "path": "src/module/ffmpeg.py",
    "content": "from pathlib import Path\nfrom shutil import which\nfrom platform import system\nfrom subprocess import Popen, run\nfrom textwrap import dedent\n\n__all__ = [\"FFMPEG\"]\n\n\nclass FFMPEG:\n    SYSTEM = system()\n\n    # 常见终端及其执行模板\n    linux_terminal_templates = {\n        # GNOME Terminal (Ubuntu)\n        \"gnome-terminal\": [\"gnome-terminal\", \"--\", \"bash\", \"-c\", \"{cmd}; exec bash\"],\n        # Deepin Terminal\n        \"deepin-terminal\": [\"deepin-terminal\", \"--\", \"bash\", \"-c\", \"{cmd}; exec bash\"],\n        # XFCE4 Terminal (MX Linux 默认)\n        \"xfce4-terminal\": [\n            \"xfce4-terminal\",\n            \"--hold\",\n            \"-e\",\n            'bash -c \"{cmd}; exec bash\"',\n        ],\n        # Konsole (KDE)\n        \"konsole\": [\"konsole\", \"-e\", \"bash\", \"-i\", \"-c\", \"{cmd}; bash\"],\n        # Terminator\n        \"terminator\": [\"terminator\", \"-x\", \"bash\", \"-c\", \"{cmd}; exec bash\"],\n    }\n\n    def __init__(self, path: str):\n        self.path = self.__check_ffmpeg_path(Path(path))\n        self.support = {\n            \"Darwin\": self.generate_command_darwin,\n            \"Linux\": self.generate_command_linux,\n            \"Windows\": self.generate_command_windows,\n        }\n        self.run_command = self.support.get(self.SYSTEM, None)\n        self.state = bool(self.path) if self.run_command else False\n\n    @staticmethod\n    def generate_command_darwin(command: list) -> None:\n        script = dedent(f\"\"\"\n                tell application \"Terminal\"\n                    do script \"{\" \".join(command).replace('\"', '\\\\\"')}\"\n                    activate\n                end tell\n                \"\"\")\n        Popen([\"osascript\", \"-e\", script])\n\n    @staticmethod\n    def generate_command_windows(command: list) -> None:\n        Popen(\n            \" \".join(\n                [\n                    \"start\",\n                    \"cmd\",\n                    \"/k\",\n                ]\n                + command\n            ),\n            shell=True,\n        )\n\n    @classmethod\n    def generate_command_linux(cls, command: list) -> None:\n        # TODO: Linux 系统尚未测试\n        command = \" \".join(command)\n        print(\"ffmpeg command:\", command)\n        for term, template in cls.linux_terminal_templates.items():\n            if which(term):\n                # 填充命令并执行\n                filled = [\n                    part.format(cmd=command) if \"{cmd}\" in part else part\n                    for part in template\n                ]\n                run(\n                    filled,\n                )\n\n    def __check_ffmpeg_path(self, path: Path):\n        return self.__check_system_ffmpeg() or self.__check_system_ffmpeg(path)\n\n    def download(self, data: list[tuple], proxy, user_agent):\n        for u, p in data:\n            command = self.__generate_command(\n                u,\n                p,\n                proxy,\n                user_agent,\n            )\n            self.run_command(command)\n\n    def __generate_command(\n        self,\n        url,\n        file,\n        proxy,\n        user_agent,\n    ) -> list:\n        command = [\n            self.path,\n            \"-hide_banner\",\n            \"-rw_timeout\",\n            f\"{30 * 1000 * 1000}\",\n            \"-loglevel\",\n            \"info\",\n            \"-protocol_whitelist\",\n            \"rtmp,crypto,file,http,https,tcp,tls,udp,rtp,httpproxy\",\n            \"-analyzeduration\",\n            f\"{10 * 1000 * 1000}\",\n            \"-probesize\",\n            f\"{10 * 1000 * 1000}\",\n            \"-fflags\",\n            \"+discardcorrupt\",\n            \"-user_agent\",\n            f'\"{user_agent}\"',\n            \"-i\",\n            f'\"{url}\"',\n            \"-bufsize\",\n            \"10240k\",\n            \"-map\",\n            \"0\",\n            \"-c:v\",\n            \"copy\",\n            \"-c:a\",\n            \"copy\",\n            \"-sn\",\n            \"-dn\",\n            \"-reconnect_delay_max\",\n            \"60\",\n            \"-reconnect_streamed\",\n            \"-reconnect_at_eof\",\n            \"-max_muxing_queue_size\",\n            \"128\",\n            \"-correct_ts_overflow\",\n            \"1\",\n            \"-f\",\n            \"mp4\",\n        ]\n        if proxy:\n            for insert_index, item in enumerate((\"-http_proxy\", proxy), start=2):\n                command.insert(insert_index, item)\n        command.append(f'\"{file}\"')\n        return command\n\n    @staticmethod\n    def __check_system_ffmpeg(path: Path = None):\n        return which(path or \"ffmpeg\")\n"
  },
  {
    "path": "src/module/migrate_folder.py",
    "content": "from shutil import move\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n\n\nclass MigrateFolder:\n    def __init__(\n        self,\n        parameter: \"Parameter\",\n    ):\n        self.ROOT = parameter.ROOT\n        self.root = parameter.root\n        self.folder = parameter.folder_name\n\n    def compatible(self):\n        for i in (\n            \"Music\",\n            \"Data\",\n            \"Live\",\n        ):\n            if (old := self.ROOT.parent.joinpath(i)).exists() and not (\n                new_ := self.ROOT.joinpath(i)\n            ).exists():\n                move(old, new_)\n        if self.ROOT != self.root:\n            return\n        if (old := self.ROOT.parent.joinpath(self.folder)).exists() and not (\n            new_ := self.ROOT.joinpath(self.folder)\n        ).exists():\n            move(old, new_)\n        folders = self.ROOT.parent.iterdir()\n        for i in folders:\n            if not i.is_dir():\n                continue\n            if len(i.name) > 10 and i.name[1:3] == \"ID\":\n                move(i, self.ROOT.joinpath(i.name))\n"
  },
  {
    "path": "src/module/register.py",
    "content": "from platform import system\nfrom subprocess import run\nfrom time import sleep\nfrom typing import TYPE_CHECKING\nfrom urllib.parse import quote\n\nfrom httpx import HTTPError\nfrom qrcode import QRCode\nfrom rich.progress import (\n    BarColumn,\n    Progress,\n    SpinnerColumn,\n    TextColumn,\n    TimeElapsedColumn,\n)\n\nfrom ..custom import ERROR, PROGRESS, QRCODE_HEADERS, WARNING\nfrom ..encrypt import MsToken\n\n# from ..encrypt import VerifyFp\nfrom ..tools import Retry, cookie_str_to_str\n\nif TYPE_CHECKING:\n    from ..config import Parameter, Settings\n\n__all__ = [\"__Register\"]\n\n\nclass __Register:\n    \"\"\"\n    扫码登录功能已过期\n    \"\"\"\n\n    get_url = \"https://sso.douyin.com/get_qrcode/\"\n    check_url = \"https://sso.douyin.com/check_qrconnect/\"\n\n    def __init__(\n        self,\n        params: \"Parameter\",\n        settings: \"Settings\",\n    ):\n        self.ab = params.ab\n        self.xb = params.xb\n        self.client = params.client\n        self.settings = settings\n        self.console = params.console\n        self.log = params.logger\n        self.headers = QRCODE_HEADERS\n        self.proxy = params.proxy\n        # self.verify_fp = None\n        self.cache = params.cache\n        self.url_params = {\n            \"service\": \"https://www.douyin.com\",\n            \"need_logo\": \"false\",\n            \"need_short_url\": \"true\",\n            \"passport_jssdk_version\": \"1.0.22\",\n            \"passport_jssdk_type\": \"pro\",\n            \"aid\": \"6383\",\n            \"language\": \"zh\",\n            \"account_sdk_source\": \"sso\",\n            \"account_sdk_source_info\": \"7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273\"\n            \"f63646976602927756970626c6b76273f5e2755414325536c60726077272927466d776a6860\"\n            \"2555414325536c60726077272927466d776a686c70682555414325536c60726077272927486\"\n            \"c66776a766a637125406162602555414325536c607260772729275260674e6c712567706c69\"\n            \"71286c6b2555414327582927756077686c76766c6a6b76273f5e7e276b646860273f2762606\"\n            \"a696a6664716c6a6b2729277671647160273f2761606b6c60612778297e276b646860273f27\"\n            \"6b6a716c636c6664716c6a6b762729277671647160273f2775776a6875712778297e276b646\"\n            \"860273f27736c61606a5a666475717077602729277671647160273f2761606b6c6061277829\"\n            \"7e276b646860273f276470616c6a5a666475717077602729277671647160273f2761606b6c6\"\n            \"06127785829276c6b6b60774d606c626d71273f32313729276c6b6b6077526c61716d273f34\"\n            \"30363329276a707160774d606c626d71273f3d333129276a70716077526c61716d273f34303\"\n            \"633292767606d64736c6a77273f7e27716a70666d273f63646976602927686a707660273f71\"\n            \"77706029276e607c476a647761273f717770607829277260676269273f7e27736077766c6a6\"\n            \"b273f27526067424925342b35252d4a75606b424925405625372b3525466d776a686c70682c\"\n            \"27292773606b616a77273f275260674e6c7127292777606b6160776077273f275260674e6c7\"\n            \"125526067424927782927776074706076715a6d6a7671273f277272722b616a707c6c6b2b66\"\n            \"6a68272927776074706076715a7564716d6b646860273f272a2778\",\n            \"passport_ztsdk\": \"0\",\n            \"passport_verify\": \"1.0.14\",\n            # \"biz_trace_id\": \"26eba5d6\",\n            \"device_platform\": \"web_app\",\n            \"msToken\": \"\",\n        }\n\n    def __check_progress_object(self):\n        return Progress(\n            TextColumn(\n                \"[progress.description]{task.description}\",\n                style=PROGRESS,\n                justify=\"left\",\n            ),\n            SpinnerColumn(),\n            BarColumn(),\n            \"•\",\n            TimeElapsedColumn(),\n            console=self.console,\n            transient=True,\n            expand=True,\n        )\n\n    def generate_qr_code(self, url: str):\n        qr_code = QRCode()\n        # assert url, \"无效的登录二维码数据\"\n        qr_code.add_data(url)\n        qr_code.make(fit=True)\n        qr_code.print_ascii(invert=True)\n        img = qr_code.make_image()\n        img.save(self.cache)\n        self.console.print(\n            \"请使用抖音 APP 扫描二维码登录，如果二维码无法识别，请尝试更换终端或者选择其他方式写入 Cookie！\"\n        )\n        self._open_qrcode_image()\n\n    def _open_qrcode_image(self):\n        if (s := system()) == \"Darwin\":  # macOS\n            run([\"open\", self.cache])\n        elif s == \"Windows\":  # Windows\n            run([\"start\", self.cache], shell=True)\n        elif s == \"Linux\":  # Linux\n            run([\"xdg-open\", self.cache])\n\n    async def get_qr_code(self):\n        # self.verify_fp = VerifyFp.get_verify_fp()\n        # self.url_params[\"verifyFp\"] = self.verify_fp\n        # self.url_params[\"fp\"] = self.verify_fp\n        await self.__set_ms_token()\n        self.url_params[\"a_bogus\"] = quote(self.ab.get_value(self.url_params), safe=\"\")\n        # self.url_params[\"X-Bogus\"] = self.xb.get_x_bogus(self.url_params)\n        data, _, _ = await self.request_data(\n            url=self.get_url,\n            params=self.url_params,\n        )\n        if not data:\n            return None, None\n        try:\n            url = data[\"data\"][\"qrcode_index_url\"]\n            token = data[\"data\"][\"token\"]\n            return url, token\n        except KeyError:\n            return None, None\n\n    async def __set_ms_token(self):\n        if isinstance(\n            t := await MsToken.get_real_ms_token(\n                self.log,\n                self.headers,\n                **self.proxy,\n            ),\n            dict,\n        ):\n            self.url_params[\"msToken\"] = t[\"msToken\"]\n\n    async def check_register(self, token):\n        self.url_params[\"token\"] = token\n        self.url_params |= {\"is_frontier\": \"false\"}\n        with self.__check_progress_object() as progress:\n            task_id = progress.add_task(\"正在检查登录状态\", total=None)\n            second = 0\n            while second < 30:\n                sleep(1)\n                progress.update(task_id)\n                data, headers, _ = await self.request_data(\n                    url=self.check_url, params=self.url_params\n                )\n                if not data:\n                    self.console.print(\"网络异常，无法获取登录状态！\", style=WARNING)\n                    second = 30\n                    continue\n                # print(response.json())  # 调试使用\n                if data.get(\"error_code\"):\n                    self.console.print(\n                        f\"该账号疑似被风控，建议近期避免扫码登录账号！\\n响应数据: {data}\",\n                        style=WARNING,\n                    )\n                    second = 30\n                elif not (data := data.get(\"data\")):\n                    self.console.print(f\"响应内容异常: {data}\", style=ERROR)\n                    second = 30\n                elif (s := data[\"status\"]) == \"3\":\n                    redirect_url = data[\"redirect_url\"]\n                    cookie = headers.get(\"Set-Cookie\")\n                    break\n                elif s in (\n                    \"4\",\n                    \"5\",\n                ):\n                    second = 30\n                else:\n                    second += 1\n            else:\n                self.console.print(\n                    \"扫码登录失败，请使用其他方式获取 Cookie 并写入配置文件！\",\n                    style=WARNING,\n                )\n                return None, None\n            return redirect_url, cookie\n\n    async def get_cookie(self, url, cookie):\n        self.headers[\"Cookie\"] = cookie_str_to_str(cookie)\n        _, _, history = await self.request_data(False, url=url)\n        if not history or history[0].status_code != 302:\n            return False\n        return cookie_str_to_str(history[1].headers.get(\"Set-Cookie\"))\n\n    @Retry.retry_lite\n    async def request_data(self, json=True, **kwargs):\n        try:\n            response = await self.client.get(headers=self.headers, **kwargs)\n            data = response.json() if json else None\n            headers = response.headers\n            history = response.history\n            return data, headers, history\n        except HTTPError as e:\n            self.console.print(\n                f\"扫码登录发生异常，请向作者反馈，错误信息: {e}\", style=ERROR\n            )\n            return None, None, None\n\n    async def run(\n        self,\n    ):\n        self.cache = str(self.cache.joinpath(\"扫码后请关闭该图片.png\"))\n        url, token = await self.get_qr_code()\n        if not url:\n            return False\n        self.generate_qr_code(url)\n        url, cookie = await self.check_register(token)\n        return await self.get_cookie(url, cookie) if url else False\n"
  },
  {
    "path": "src/module/tiktok_account_index.py",
    "content": "from pathlib import Path\nfrom re import compile\n\nfrom lxml.etree import HTML\n\nfrom src.tools import timestamp\n\n__all__ = []\n\n\nclass __TikTokAccount:\n    urls = '//*[@id=\"main-content-others_homepage\"]/div/div[2]/div[last()]/div/div/div/div/div/a/@href'\n    uid = '//*[@id=\"main-content-others_homepage\"]/div/div[1]/div[1]/div[2]/div/div[2]/a/@href'\n    uid_re = compile(r\".*?u=(\\d+).*?\")\n    nickname = (\n        '//*[@id=\"main-content-others_homepage\"]/div/div[1]/div[1]/div[2]/h2/text()'\n    )\n    works_link_tiktok = compile(\n        r\"\\S*?https://www\\.tiktok\\.com/@\\S+?/video/(\\d{19})\\S*?\"\n    )\n\n    def __init__(self, path: str):\n        self.path = Path(path.replace('\"', \"\"))\n\n    def run(self) -> list:\n        if self.path.is_file() and self.path.suffix == \".html\":\n            return self.__read_html_file([self.path])\n        elif self.path.is_dir():\n            return self.__read_html_file(self.path.glob(\"*.html\"))\n        return []\n\n    def __read_html_file(self, items) -> list:\n        ids = []\n        for i in items:\n            with i.open(\"r\", encoding=\"utf-8\") as f:\n                data = f.read()\n            ids.append(self.__extract_id_data(data))\n        return [i for i in ids if all(i)]\n\n    def __extract_id_data(self, html: str) -> (str, str, list[str]):\n        html_tree = HTML(html)\n        urls = html_tree.xpath(self.urls)\n        uid = self.__extract_uid(html_tree.xpath(self.uid))\n        nickname = self.__extract_nickname(html_tree.xpath(self.nickname))\n        return uid, nickname, self.works_link_tiktok.findall(\" \".join(urls))\n\n    def __extract_uid(self, text: list):\n        if len(text) == 1:\n            return u.group(1) if (u := self.uid_re.search(text[0])) else timestamp()\n        return timestamp()\n\n    @staticmethod\n    def __extract_nickname(text: list):\n        return text[0].strip() or timestamp() if len(text) == 1 else timestamp()\n"
  },
  {
    "path": "src/module/tiktok_unofficial.py",
    "content": "from time import strftime, localtime\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom httpx import get\n\nfrom src.custom import BLANK_HEADERS\nfrom src.custom import wait\nfrom src.extract import Extractor\nfrom src.testers import Params\nfrom src.tools import Retry\nfrom src.tools import capture_error_request\nfrom src.translation import _\n\nif TYPE_CHECKING:\n    from src.config import Parameter\n    from src.testers import Params\n\n\nclass DetailTikTokUnofficial:\n    def __init__(\n        self,\n        params: Union[\"Parameter\", \"Params\"],\n        proxy: str = None,\n        detail_id: str = ...,\n        *args,\n        **kwargs,\n    ):\n        self.headers = BLANK_HEADERS\n        self.log = params.logger\n        self.console = params.console\n        self.api = \"https://www.tikwm.com/api/\"\n        self.proxy = proxy or params.proxy_tiktok\n        self.max_retry = params.max_retry\n        self.timeout = params.timeout\n        self.detail_id = detail_id\n        self.text = _(\"作品\")\n\n    async def run(\n        self,\n    ) -> dict:\n        data = await self.request_data_get()\n        data = self.check_response(data)\n        return data\n\n    @Retry.retry\n    @capture_error_request\n    async def request_data_get(\n        self,\n    ):\n        response = get(\n            self.api,\n            params={\"url\": self.detail_id, \"hd\": \"1\"},\n            headers=self.headers,\n            timeout=self.timeout,\n            follow_redirects=True,\n            verify=False,\n            proxy=self.proxy,\n        )\n        response.raise_for_status()\n        await wait()\n        return response.json()\n\n    def check_response(\n        self,\n        data: dict,\n    ):\n        try:\n            if data[\"msg\"] == \"success\" and data[\"data\"]:\n                return data[\"data\"]\n            raise KeyError\n        except KeyError:\n            self.log.error(_(\"数据解析失败，请告知作者处理: {data}\").format(data=data))\n\n\nclass DetailTikTokExtractor:\n    def __init__(self, params: \"Parameter\"):\n        self.date_format = params.date_format\n        self.cleaner = params.CLEANER\n\n    def __clean_description(self, desc: str) -> str:\n        return self.cleaner.clear_spaces(self.cleaner.filter(desc))\n\n    def __format_date(\n        self,\n        data: int,\n    ) -> str:\n        return strftime(\n            self.date_format,\n            localtime(data or None),\n        )\n\n    def run(self, data: dict) -> dict:\n        item = {}\n        data = Extractor.generate_data_object(data)\n        self.extract_detail_tiktok(item, data)\n        self.extract_music_tiktok(item, data)\n        self.extract_author_tiktok(item, data)\n        self.extract_statistics_tiktok(item, data)\n        return item\n\n    def extract_detail_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        item[\"id\"] = Extractor.safe_extract(data, \"id\")\n        item[\"desc\"] = (\n            self.__clean_description(Extractor.safe_extract(data, \"title\"))\n            or item[\"id\"]\n        )\n        item[\"create_time\"] = self.__format_date(\n            Extractor.safe_extract(data, \"create_time\")\n        )\n        item[\"type\"] = _(\"视频\")\n        item[\"downloads\"] = Extractor.safe_extract(data, \"hdplay\")\n        item[\"dynamic_cover\"] = Extractor.safe_extract(data, \"ai_dynamic_cover\")\n        item[\"static_cover\"] = Extractor.safe_extract(data, \"origin_cover\")\n\n    def extract_author_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        item[\"uid\"] = Extractor.safe_extract(data, \"author.id\")\n        item[\"nickname\"] = Extractor.safe_extract(data, \"author.nickname\")\n        item[\"unique_id\"] = Extractor.safe_extract(data, \"author.unique_id\")\n\n    def extract_music_tiktok(\n        self,\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        item[\"music_author\"] = Extractor.safe_extract(data, \"music_info.author\")\n        item[\"music_title\"] = Extractor.safe_extract(data, \"music_info.title\")\n        item[\"music_url\"] = Extractor.safe_extract(data, \"music\")\n\n    @staticmethod\n    def extract_statistics_tiktok(\n        item: dict,\n        data: SimpleNamespace,\n    ) -> None:\n        for i in Extractor.statistics_keys:\n            item[i] = Extractor.safe_extract(\n                data,\n                i,\n                -1,\n            )\n\n\nasync def test():\n    async with Params() as params:\n        i = DetailTikTokUnofficial(\n            params,\n            detail_id=\"\",\n        )\n        if data := await i.run():\n            print(DetailTikTokExtractor(params).run(data))\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/record/__init__.py",
    "content": "from .base import BaseLogger\nfrom .logger import LoggerManager\n\n__all__ = [\"LoggerManager\", \"BaseLogger\"]\n"
  },
  {
    "path": "src/record/base.py",
    "content": "from pathlib import Path\nfrom time import localtime, strftime\nfrom typing import TYPE_CHECKING\n\nfrom ..custom import (\n    DEBUG,\n    ERROR,\n    GENERAL,\n    INFO,\n    VERSION_BETA,\n    WARNING,\n)\nfrom ..tools import Cleaner\n\nif TYPE_CHECKING:\n    from ..tools import ColorfulConsole\n\n\nclass BaseLogger:\n    \"\"\"不记录日志，空白日志记录器\"\"\"\n\n    DEBUG = VERSION_BETA\n\n    def __init__(\n        self,\n        main_path: Path,\n        console: \"ColorfulConsole\",\n        root=\"\",\n        folder=\"\",\n        name=\"\",\n    ):\n        self.log = None  # 记录器主体\n        self.console = console\n        self._root, self._folder, self._name = self.init_check(\n            main_path=main_path,\n            root=root,\n            folder=folder,\n            name=name,\n        )\n\n    def init_check(\n        self,\n        main_path: Path,\n        root=None,\n        folder=None,\n        name=None,\n    ) -> tuple:\n        root = self.check_root(root, main_path)\n        folder = self.check_folder(folder)\n        name = self.check_name(name)\n        return root, folder, name\n\n    def check_root(self, root: str, default: Path) -> Path:\n        if not root:\n            return default\n        if (r := Path(root)).is_dir():\n            return r\n        self.console.print(\n            f\"日志储存路径 {root} 无效，程序将使用项目根路径作为储存路径\"\n        )\n        return default\n\n    def check_name(self, name: str) -> str:\n        if not name:\n            return \"%Y-%m-%d %H.%M.%S\"\n        try:\n            _ = strftime(name, localtime())\n            return name\n        except ValueError:\n            self.console.print(\n                f\"日志名称格式 {name} 无效，程序将使用默认时间格式：年-月-日 时.分.秒\"\n            )\n            return \"%Y-%m-%d %H.%M.%S\"\n\n    @staticmethod\n    def check_folder(folder: str) -> str:\n        return Cleaner().filter_name(folder, \"Log\")\n\n    def run(self, *args, **kwargs):\n        pass\n\n    def info(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=INFO, **kwargs)\n\n    def warning(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=WARNING, **kwargs)\n\n    def error(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=ERROR, **kwargs)\n\n    def debug(self, text: str, **kwargs):\n        if self.DEBUG:\n            self.console.print(text, style=DEBUG, **kwargs)\n\n    def print(self, text: str, style=GENERAL, **kwargs) -> None:\n        self.console.print(text, style=style, **kwargs)\n"
  },
  {
    "path": "src/record/logger.py",
    "content": "from logging import INFO as INFO_LEVEL\nfrom logging import FileHandler, Formatter, getLogger\nfrom pathlib import Path\nfrom platform import system\nfrom shutil import move\nfrom time import localtime, strftime\nfrom typing import TYPE_CHECKING\n\nfrom ..custom import (\n    DEBUG,\n    ERROR,\n    INFO,\n    WARNING,\n)\nfrom .base import BaseLogger\n\nif TYPE_CHECKING:\n    from ..tools import ColorfulConsole\n\n\nclass LoggerManager(BaseLogger):\n    \"\"\"日志记录\"\"\"\n\n    encode = \"UTF-8-SIG\" if system() == \"Windows\" else \"UTF-8\"\n\n    def __init__(\n        self, main_path: Path, console: \"ColorfulConsole\", root=\"\", folder=\"\", name=\"\"\n    ):\n        super().__init__(main_path, console, root, folder, name)\n\n    def run(\n        self,\n        format_=\"%(asctime)s[%(levelname)s]:  %(message)s\",\n        filename=None,\n    ):\n        dir_ = self._root.joinpath(self._folder)\n        self.compatible(dir_)\n        dir_.mkdir(exist_ok=True)\n        file_handler = FileHandler(\n            dir_.joinpath(\n                f\"{filename}.log\"\n                if filename\n                else f\"{strftime(self._name, localtime())}.log\"\n            ),\n            encoding=self.encode,\n        )\n        formatter = Formatter(format_, datefmt=\"%Y-%m-%d %H:%M:%S\")\n        file_handler.setFormatter(formatter)\n        self.log = getLogger(__name__)\n        self.log.addHandler(file_handler)\n        self.log.setLevel(INFO_LEVEL)\n\n    def info(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=INFO, **kwargs)\n        self.log.info(text.strip())\n\n    def warning(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=WARNING, **kwargs)\n        self.log.warning(text.strip())\n\n    def error(self, text: str, output=True, **kwargs):\n        if output:\n            self.console.print(text, style=ERROR, **kwargs)\n        self.log.error(text.strip())\n\n    def debug(self, text: str, **kwargs):\n        if self.DEBUG:\n            self.console.print(text, style=DEBUG, **kwargs)\n            self.log.debug(text.strip())\n\n    def compatible(\n        self,\n        path: Path,\n    ):\n        if (\n            old := self._root.parent.joinpath(self._folder)\n        ).exists() and not path.exists():\n            move(old, path)\n"
  },
  {
    "path": "src/storage/__init__.py",
    "content": "from .manager import RecordManager\n\n__all__ = [\"RecordManager\"]\n"
  },
  {
    "path": "src/storage/csv.py",
    "content": "from csv import writer\nfrom os.path import getsize\nfrom pathlib import Path\nfrom platform import system\nfrom typing import TYPE_CHECKING\n\nfrom .text import BaseTextLogger\n\nif TYPE_CHECKING:\n    from ..tools import ColorfulConsole\n\n__all__ = [\"CSVLogger\"]\n\n\nclass CSVLogger(BaseTextLogger):\n    \"\"\"CSV 格式保存数据\"\"\"\n\n    __type = \"csv\"\n    encode = \"UTF-8-SIG\" if system() == \"Windows\" else \"UTF-8\"\n\n    def __init__(\n        self,\n        root: Path,\n        title_line: tuple,\n        field_keys: tuple,\n        console: \"ColorfulConsole\",\n        old=None,\n        name=\"Download\",\n        *args,\n        **kwargs,\n    ):\n        super().__init__(*args, **kwargs)\n        self.console = console\n        self.file = None  # 文件对象\n        self.writer = None  # CSV对象\n        self.name = self._rename(root, self.__type, old, name)  # 文件名称\n        self.path = root.joinpath(f\"{self.name}.{self.__type}\")  # 文件路径\n        self.title_line = title_line  # 标题行\n        self.field_keys = field_keys\n\n    async def __aenter__(self):\n        self.file = self.path.open(\"a\", encoding=self.encode, newline=\"\")\n        self.writer = writer(self.file)\n        await self.title()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        self.file.close()\n\n    async def title(self):\n        if getsize(self.path) == 0:\n            # 如果文件没有任何数据，则写入标题行\n            await self.save(self.title_line)\n\n    async def _save(self, data, *args, **kwargs):\n        self.writer.writerow(data)\n"
  },
  {
    "path": "src/storage/manager.py",
    "content": "from shutil import move\nfrom typing import TYPE_CHECKING\n\nfrom .csv import CSVLogger\nfrom .sqlite import SQLLogger\nfrom .text import BaseTextLogger\nfrom .xlsx import XLSXLogger\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    from ..config import Parameter\n\n__all__ = [\"RecordManager\"]\n\n\nclass RecordManager:\n    \"\"\"检查数据储存路径和文件夹\"\"\"\n\n    detail = (\n        (\n            \"type\",\n            \"作品类型\",\n            \"TEXT\",\n        ),\n        (\n            \"collection_time\",\n            \"采集时间\",\n            \"TEXT\",\n        ),\n        (\n            \"uid\",\n            \"UID\",\n            \"TEXT\",\n        ),\n        (\n            \"sec_uid\",\n            \"SEC_UID\",\n            \"TEXT\",\n        ),\n        (\n            \"unique_id\",\n            \"ID\",\n            \"TEXT\",\n        ),\n        # (\"short_id\", \"SHORT_ID\", \"TEXT\",),\n        (\n            \"id\",\n            \"作品ID\",\n            \"TEXT\",\n        ),\n        (\n            \"desc\",\n            \"作品描述\",\n            \"TEXT\",\n        ),\n        (\n            \"text_extra\",\n            \"作品话题\",\n            \"TEXT\",\n        ),\n        (\n            \"duration\",\n            \"视频时长\",\n            \"TEXT\",\n        ),\n        # (\"ratio\", \"视频分辨率\", \"TEXT\",),\n        (\n            \"height\",\n            \"视频高度\",\n            \"INTEGER\",\n        ),\n        (\n            \"width\",\n            \"视频宽度\",\n            \"INTEGER\",\n        ),\n        (\n            \"share_url\",\n            \"作品链接\",\n            \"TEXT\",\n        ),\n        (\n            \"create_time\",\n            \"发布时间\",\n            \"TEXT\",\n        ),\n        (\n            \"uri\",\n            \"视频URI\",\n            \"TEXT\",\n        ),\n        (\n            \"nickname\",\n            \"账号昵称\",\n            \"TEXT\",\n        ),\n        (\n            \"user_age\",\n            \"年龄\",\n            \"INTEGER\",\n        ),\n        (\n            \"signature\",\n            \"账号签名\",\n            \"TEXT\",\n        ),\n        (\n            \"downloads\",\n            \"下载地址\",\n            \"TEXT\",\n        ),\n        (\n            \"music_author\",\n            \"音乐作者\",\n            \"TEXT\",\n        ),\n        (\n            \"music_title\",\n            \"音乐标题\",\n            \"TEXT\",\n        ),\n        (\n            \"music_url\",\n            \"音乐链接\",\n            \"TEXT\",\n        ),\n        (\n            \"static_cover\",\n            \"静态封面\",\n            \"TEXT\",\n        ),\n        (\n            \"dynamic_cover\",\n            \"动态封面\",\n            \"TEXT\",\n        ),\n        (\n            \"tag\",\n            \"隐藏标签\",\n            \"TEXT\",\n        ),\n        (\n            \"digg_count\",\n            \"点赞数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"comment_count\",\n            \"评论数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"collect_count\",\n            \"收藏数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"share_count\",\n            \"分享数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"play_count\",\n            \"播放数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"extra\",\n            \"额外信息\",\n            \"TEXT\",\n        ),\n    )\n    comment = (\n        (\n            \"collection_time\",\n            \"采集时间\",\n            \"TEXT\",\n        ),\n        (\n            \"cid\",\n            \"评论ID\",\n            \"TEXT\",\n        ),\n        (\n            \"create_time\",\n            \"评论时间\",\n            \"TEXT\",\n        ),\n        (\n            \"uid\",\n            \"UID\",\n            \"TEXT\",\n        ),\n        (\n            \"sec_uid\",\n            \"SEC_UID\",\n            \"TEXT\",\n        ),\n        # (\"short_id\", \"SHORT_ID\", \"TEXT\",),\n        # (\"unique_id\", \"抖音号\", \"TEXT\",),\n        (\n            \"nickname\",\n            \"账号昵称\",\n            \"TEXT\",\n        ),\n        (\n            \"signature\",\n            \"账号签名\",\n            \"TEXT\",\n        ),\n        (\n            \"user_age\",\n            \"年龄\",\n            \"INTEGER\",\n        ),\n        (\n            \"ip_label\",\n            \"IP归属地\",\n            \"TEXT\",\n        ),\n        (\n            \"text\",\n            \"评论内容\",\n            \"TEXT\",\n        ),\n        (\n            \"sticker\",\n            \"评论表情\",\n            \"TEXT\",\n        ),\n        (\n            \"image\",\n            \"评论图片\",\n            \"TEXT\",\n        ),\n        (\n            \"digg_count\",\n            \"点赞数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"reply_comment_total\",\n            \"回复数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"reply_id\",\n            \"回复ID\",\n            \"TEXT\",\n        ),\n        (\n            \"reply_to_reply_id\",\n            \"回复对象\",\n            \"TEXT\",\n        ),\n    )\n    user = (\n        (\n            \"collection_time\",\n            \"采集时间\",\n            \"TEXT\",\n        ),\n        (\n            \"nickname\",\n            \"昵称昵称\",\n            \"TEXT\",\n        ),\n        (\n            \"url\",\n            \"账号链接\",\n            \"TEXT\",\n        ),\n        (\n            \"signature\",\n            \"账号签名\",\n            \"TEXT\",\n        ),\n        (\n            \"unique_id\",\n            \"抖音号\",\n            \"TEXT\",\n        ),\n        (\n            \"user_age\",\n            \"年龄\",\n            \"INTEGER\",\n        ),\n        (\n            \"gender\",\n            \"性别\",\n            \"TEXT\",\n        ),\n        (\n            \"country\",\n            \"国家\",\n            \"TEXT\",\n        ),\n        (\n            \"province\",\n            \"省份\",\n            \"TEXT\",\n        ),\n        (\n            \"city\",\n            \"城市\",\n            \"TEXT\",\n        ),\n        (\n            \"district\",\n            \"地区\",\n            \"TEXT\",\n        ),\n        (\n            \"ip_location\",\n            \"IP归属地\",\n            \"TEXT\",\n        ),\n        (\n            \"verify\",\n            \"标签\",\n            \"TEXT\",\n        ),\n        (\n            \"enterprise\",\n            \"企业\",\n            \"TEXT\",\n        ),\n        (\n            \"sec_uid\",\n            \"SEC_UID\",\n            \"TEXT\",\n        ),\n        (\n            \"uid\",\n            \"UID\",\n            \"TEXT\",\n        ),\n        (\n            \"short_id\",\n            \"SHORT_ID\",\n            \"TEXT\",\n        ),\n        (\n            \"avatar\",\n            \"头像链接\",\n            \"TEXT\",\n        ),\n        (\n            \"cover\",\n            \"背景图链接\",\n            \"TEXT\",\n        ),\n        (\n            \"aweme_count\",\n            \"作品数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"total_favorited\",\n            \"获赞数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"favoriting_count\",\n            \"喜欢数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"follower_count\",\n            \"粉丝数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"following_count\",\n            \"关注数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"max_follower_count\",\n            \"粉丝最大值\",\n            \"INTEGER\",\n        ),\n    )\n    search_user = (\n        (\n            \"collection_time\",\n            \"采集时间\",\n            \"TEXT\",\n        ),\n        (\n            \"uid\",\n            \"UID\",\n            \"TEXT\",\n        ),\n        (\n            \"sec_uid\",\n            \"SEC_UID\",\n            \"TEXT\",\n        ),\n        (\n            \"nickname\",\n            \"账号昵称\",\n            \"TEXT\",\n        ),\n        (\n            \"unique_id\",\n            \"抖音号\",\n            \"TEXT\",\n        ),\n        (\n            \"short_id\",\n            \"SHORT_ID\",\n            \"TEXT\",\n        ),\n        (\n            \"avatar\",\n            \"头像链接\",\n            \"TEXT\",\n        ),\n        (\n            \"signature\",\n            \"账号签名\",\n            \"TEXT\",\n        ),\n        (\n            \"verify\",\n            \"标签\",\n            \"TEXT\",\n        ),\n        (\n            \"enterprise\",\n            \"企业\",\n            \"TEXT\",\n        ),\n        (\n            \"follower_count\",\n            \"粉丝数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"total_favorited\",\n            \"获赞数量\",\n            \"INTEGER\",\n        ),\n    )\n    search_live = (\n        (\n            \"collection_time\",\n            \"采集时间\",\n            \"TEXT\",\n        ),\n        (\n            \"room_id\",\n            \"直播ID\",\n            \"TEXT\",\n        ),\n        (\n            \"uid\",\n            \"UID\",\n            \"TEXT\",\n        ),\n        (\n            \"sec_uid\",\n            \"SEC_UID\",\n            \"TEXT\",\n        ),\n        (\n            \"nickname\",\n            \"账号昵称\",\n            \"TEXT\",\n        ),\n        (\n            \"short_id\",\n            \"SHORT_ID\",\n            \"TEXT\",\n        ),\n        (\n            \"avatar\",\n            \"头像链接\",\n            \"TEXT\",\n        ),\n        (\n            \"signature\",\n            \"账号签名\",\n            \"TEXT\",\n        ),\n        (\n            \"verify\",\n            \"标签\",\n            \"TEXT\",\n        ),\n        (\n            \"enterprise\",\n            \"企业\",\n            \"TEXT\",\n        ),\n    )\n    hot = (\n        (\n            \"position\",\n            \"排名\",\n            \"INTEGER\",\n        ),\n        (\n            \"word\",\n            \"内容\",\n            \"TEXT\",\n        ),\n        (\n            \"hot_value\",\n            \"热度\",\n            \"INTEGER\",\n        ),\n        (\n            \"cover\",\n            \"封面\",\n            \"TEXT\",\n        ),\n        (\n            \"event_time\",\n            \"时间\",\n            \"TEXT\",\n        ),\n        (\n            \"view_count\",\n            \"浏览数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"video_count\",\n            \"视频数量\",\n            \"INTEGER\",\n        ),\n        (\n            \"sentence_id\",\n            \"SENTENCE_ID\",\n            \"TEXT\",\n        ),\n    )\n\n    detail_keys = [i[0] for i in detail]\n    detail_name = [i[1] for i in detail]\n    detail_type = [i[2] for i in detail]\n    comment_keys = [i[0] for i in comment]\n    comment_name = [i[1] for i in comment]\n    comment_type = [i[2] for i in comment]\n    user_keys = [i[0] for i in user]\n    user_name = [i[1] for i in user]\n    user_type = [i[2] for i in user]\n    search_user_keys = [i[0] for i in search_user]\n    search_user_name = [i[1] for i in search_user]\n    search_user_type = [i[2] for i in search_user]\n    search_live_keys = [i[0] for i in search_live]\n    search_live_name = [i[1] for i in search_live]\n    search_live_type = [i[2] for i in search_live]\n    hot_keys = [i[0] for i in hot]\n    hot_name = [i[1] for i in hot]\n    hot_type = [i[2] for i in hot]\n\n    LoggerParams = {\n        \"detail\": {\n            \"db_name\": \"DetailData.db\",\n            \"title_line\": detail_name,\n            \"title_type\": detail_type,\n            \"field_keys\": detail_keys,\n        },\n        \"comment\": {\n            \"db_name\": \"CommentData.db\",\n            \"title_line\": comment_name,\n            \"title_type\": comment_type,\n            \"field_keys\": comment_keys,\n        },\n        \"user\": {\n            \"db_name\": \"UserData.db\",\n            \"title_line\": user_name,\n            \"title_type\": user_type,\n            \"field_keys\": user_keys,\n        },\n        \"mix\": {\n            \"db_name\": \"MixData.db\",\n            \"title_line\": detail_name,\n            \"title_type\": detail_type,\n            \"field_keys\": detail_keys,\n        },\n        \"search_general\": {\n            \"db_name\": \"SearchData.db\",\n            \"title_line\": detail_name,\n            \"title_type\": detail_type,\n            \"field_keys\": detail_keys,\n        },\n        \"search_user\": {\n            \"db_name\": \"SearchData.db\",\n            \"title_line\": search_user_name,\n            \"title_type\": search_user_type,\n            \"field_keys\": search_user_keys,\n        },\n        \"search_live\": {\n            \"db_name\": \"SearchData.db\",\n            \"title_line\": search_live_name,\n            \"title_type\": search_live_type,\n            \"field_keys\": search_live_keys,\n        },\n        \"hot\": {\n            \"db_name\": \"BoardData.db\",\n            \"title_line\": hot_name,\n            \"title_type\": hot_type,\n            \"field_keys\": hot_keys,\n        },\n    }\n    DataLogger = {\n        \"csv\": CSVLogger,\n        \"xlsx\": XLSXLogger,\n        \"sql\": SQLLogger,\n        # \"mysql\": BaseTextLogger,\n    }\n\n    def run(\n        self,\n        parameter: \"Parameter\",\n        folder=\"\",\n        type_=\"detail\",\n        blank=False,\n    ):\n        root = parameter.root.joinpath(\n            name := parameter.CLEANER.filter_name(folder, \"Data\")\n        )\n        self.compatible(\n            parameter.root,\n            root,\n            name,\n        )\n        root.mkdir(exist_ok=True)\n        params = self.LoggerParams[type_]\n        logger = (\n            BaseTextLogger\n            if blank\n            else self.DataLogger.get(parameter.storage_format, BaseTextLogger)\n        )\n        return root, params, logger\n\n    @staticmethod\n    def compatible(\n        root: \"Path\",\n        path: \"Path\",\n        name: str,\n    ):\n        if (old := root.parent.joinpath(name)).exists() and not path.exists():\n            move(old, path)\n"
  },
  {
    "path": "src/storage/mysql.py",
    "content": "from .sql import BaseSQLLogger\n\n__all__ = [\"MySQLLogger\"]\n\n\nclass MySQLLogger(BaseSQLLogger):\n    pass\n"
  },
  {
    "path": "src/storage/sql.py",
    "content": "from re import Pattern\nfrom re import compile\n\nfrom .text import BaseTextLogger\n\n__all__ = [\"BaseSQLLogger\"]\n\n\nclass BaseSQLLogger(BaseTextLogger):\n    SHEET_NAME: Pattern = compile(r\"[^\\u4e00-\\u9fffa-zA-Z0-9_]\")\n    CHECK_SQL = \"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?;\"\n    UPDATE_SQL = \"ALTER TABLE {old_name} RENAME TO {new_name};\"\n"
  },
  {
    "path": "src/storage/sqlite.py",
    "content": "from pathlib import Path\nfrom re import sub\n\nfrom aiosqlite import connect\nfrom sqlite3 import OperationalError\n\nfrom rich.text import Text\nfrom rich import print\n\nfrom ..custom import ERROR\nfrom ..translation import _\nfrom .sql import BaseSQLLogger\n\n__all__ = [\"SQLLogger\"]\n\n\nclass SQLLogger(BaseSQLLogger):\n    \"\"\"SQLite 数据库保存数据\"\"\"\n\n    def __init__(\n        self,\n        root: Path,\n        db_name: str,\n        title_line: tuple,\n        title_type: tuple,\n        field_keys: tuple,\n        old=None,\n        name=\"Download\",\n        *args,\n        **kwargs,\n    ):\n        super().__init__(*args, **kwargs)\n        self.db = None  # 数据库\n        self.cursor = None  # 游标对象\n        self.name = (old, name)  # 数据表名称\n        self.file = db_name  # 数据库文件名称\n        self.path = root.joinpath(self.file)\n        self.title_line = title_line  # 数据表列名\n        self.title_type = title_type  # 数据表数据类型\n        self.field_keys = field_keys\n\n    async def __aenter__(self):\n        self.db = await connect(self.path)\n        self.cursor = await self.db.cursor()\n        await self.update_sheet()\n        await self.create()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.db.close()\n\n    async def create(self):\n        create_sql = f\"\"\"CREATE TABLE IF NOT EXISTS {self.name} ({\n            \", \".join([f\"{i} {j}\" for i, j in zip(self.title_line, self.title_type)])\n        });\"\"\"\n        await self.cursor.execute(create_sql)\n        await self.db.commit()\n\n    async def _save(self, data, *args, **kwargs):\n        insert_sql = f\"\"\"REPLACE INTO {self.name} ({\n            \", \".join(self.title_line)\n        }) VALUES ({\", \".join([\"?\" for _ in self.title_line])});\"\"\"\n        await self.cursor.execute(insert_sql, data)\n        await self.db.commit()\n\n    async def update_sheet(self):\n        old_sheet, new_sheet = self.__clean_sheet_name(self.name)\n        mark = new_sheet.split(\"_\", 1)\n        if not old_sheet or mark[-1] == old_sheet:\n            self.name = new_sheet\n            return\n        mark[-1] = old_sheet\n        old_sheet = \"_\".join(mark)\n        if await self.__check_sheet_exists(old_sheet):\n            try:\n                await self.cursor.execute(self.UPDATE_SQL.format(old_name=old_sheet, new_name=new_sheet))\n            except OperationalError as e:\n                print(\n                    Text(\n                        \" \".join(\n                            (\n                                _(\n                                    \"更新数据表名称时发生错误，重命名失败，请向作者反馈以便修复问题！\"\n                                ),\n                                str(e),\n                                old_sheet,\n                                new_sheet,\n                            )\n                        ),\n                        style=ERROR,\n                    )\n                )\n                self.name = old_sheet\n                return\n            await self.db.commit()\n        self.name = new_sheet\n\n    async def __check_sheet_exists(self, sheet: str) -> bool:\n        await self.cursor.execute(self.CHECK_SQL, (sheet,))\n        exists = await self.cursor.fetchone()\n        return exists[0] > 0\n\n    def __clean_sheet_name(self, name: tuple) -> tuple:\n        return self.__clean_characters(name[0]), self.__clean_characters(name[1])\n\n    def __clean_characters(self, text: str | None) -> str | None:\n        if isinstance(text, str):\n            text = self.SHEET_NAME.sub(\"_\", text)\n            text = sub(r\"_+\", \"_\", text)\n        return text\n"
  },
  {
    "path": "src/storage/text.py",
    "content": "from pathlib import Path\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom ..tools import Retry\n\nif TYPE_CHECKING:\n    from typing import Iterable\n\n\ndef convert_to_string(function):\n    async def _convert_to_string(self, data: Union[\"Iterable\", list], *args, **kwargs):\n        for index, value in enumerate(data):\n            if isinstance(value, (int, float)):  # 如果值是数字（整型或浮点型）\n                data[index] = str(value)  # 转换为字符串\n            elif isinstance(value, list):  # 如果值是列表\n                data[index] = \" \".join(value)  # 将列表元素转换为字符串并连接\n        return await function(self, data, *args, **kwargs)\n\n    return _convert_to_string\n\n\nclass BaseTextLogger:\n    def __init__(self, *args, **kwargs):\n        self.field_keys = []\n\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        pass\n\n    @convert_to_string\n    async def save(self, data: \"Iterable\", *args, **kwargs):\n        # 数据保存方法入口\n        return await self._save(data, *args, **kwargs)\n\n    async def _save(self, data: \"Iterable\", *args, **kwargs):\n        # 实际数据保存逻辑\n        pass\n\n    @classmethod\n    def _rename(cls, root: Path, type_: str, old: str, new_: str) -> str:\n        mark = new_.split(\"_\", 1)\n        if not old or mark[-1] == old:\n            return new_\n        mark[-1] = old\n        old_file = root.joinpath(f\"{'_'.join(mark)}.{type_}\")\n        cls.__rename_file(old_file, root.joinpath(f\"{new_}.{type_}\"))\n        return new_\n\n    @staticmethod\n    @Retry.retry_infinite\n    def __rename_file(old_file: Path, new_file: Path) -> bool:\n        if old_file.exists() and not new_file.exists():\n            try:\n                old_file.rename(new_file)\n                return True\n            except PermissionError:\n                return False\n        return True\n"
  },
  {
    "path": "src/storage/xlsx.py",
    "content": "from pathlib import Path\nfrom typing import TYPE_CHECKING\n\nfrom openpyxl import Workbook, load_workbook\nfrom openpyxl.utils.exceptions import IllegalCharacterError\n\nfrom ..translation import _\nfrom .text import BaseTextLogger\n\nif TYPE_CHECKING:\n    from ..tools import ColorfulConsole\n\n__all__ = [\"XLSXLogger\"]\n\n\nclass XLSXLogger(BaseTextLogger):\n    \"\"\"XLSX 格式保存数据\"\"\"\n\n    __type = \"xlsx\"\n\n    def __init__(\n        self,\n        root: Path,\n        title_line: tuple,\n        field_keys: tuple,\n        console: \"ColorfulConsole\",\n        old=None,\n        name=\"Download\",\n        *args,\n        **kwargs,\n    ):\n        super().__init__(*args, **kwargs)\n        self.console = console\n        self.book = None  # XLSX数据簿\n        self.sheet = None  # XLSX数据表\n        self.name = self._rename(root, self.__type, old, name)  # 文件名称\n        self.path = root.joinpath(f\"{self.name}.{self.__type}\")\n        self.title_line = title_line  # 标题行\n        self.field_keys = field_keys\n\n    async def __aenter__(self):\n        self.book = load_workbook(self.path) if self.path.exists() else Workbook()\n        self.sheet = self.book.active\n        self.title()\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        self.book.save(self.path)\n        self.book.close()\n\n    def title(self):\n        if not self.sheet[\"A1\"].value:\n            # 如果文件没有任何数据，则写入标题行\n            for col, value in enumerate(self.title_line, start=1):\n                self.sheet.cell(row=1, column=col, value=value)\n\n    async def _save(self, data, *args, **kwargs):\n        try:\n            self.sheet.append(data)\n        except IllegalCharacterError as e:\n            self.console.warning(\n                _(\"数据包含非法字符，保存数据失败：{error}\").format(error=e)\n            )\n"
  },
  {
    "path": "src/testers/__init__.py",
    "content": "from .logger import Logger\nfrom .params import Params\n"
  },
  {
    "path": "src/testers/logger.py",
    "content": "class Logger:\n    @staticmethod\n    def info(\n        *args,\n    ):\n        print(\n            *args,\n        )\n\n    @staticmethod\n    def warning(\n        *args,\n    ):\n        print(\n            *args,\n        )\n\n    @staticmethod\n    def error(\n        *args,\n    ):\n        print(\n            *args,\n        )\n\n    @staticmethod\n    def debug(\n        *args,\n    ):\n        print(\n            *args,\n        )\n"
  },
  {
    "path": "src/testers/params.py",
    "content": "from configparser import ConfigParser, NoOptionError, NoSectionError\n\nfrom rich.console import Console\n\nfrom src.custom import (\n    DATA_HEADERS,\n    DATA_HEADERS_TIKTOK,\n    DOWNLOAD_HEADERS_TIKTOK,\n    PROJECT_ROOT,\n)\nfrom src.encrypt import ABogus, XBogus, XGnarly\nfrom src.testers.logger import Logger\nfrom src.tools import Cleaner, create_client\n\n\nclass Params:\n    CONFIG = PROJECT_ROOT.joinpath(\"test_cookie.ini\")\n    CLEANER = Cleaner()\n\n    def __init__(self):\n        self.cookie_str = \"\"\n        self.cookie_str_tiktok = \"\"\n        self.uifid = \"\"\n        self.msToken = \"\"\n        self.msToken_tiktok = \"\"\n        self.config = ConfigParser(\n            interpolation=None,\n        )\n        self.read_ini()\n        self.headers = DATA_HEADERS | {\"Cookie\": self.cookie_str}\n        self.headers_tiktok = DATA_HEADERS_TIKTOK | {\n            \"Cookie\": self.cookie_str_tiktok,\n        }\n        self.headers_download = DOWNLOAD_HEADERS_TIKTOK\n        self.logger = Logger()\n        self.ab = ABogus()\n        self.xb = XBogus()\n        self.xg = XGnarly()\n        self.console = Console()\n        self.max_retry = 0\n        self.timeout = 5\n        self.max_pages = 2\n        self.proxy = None\n        self.proxy_tiktok = \"http://127.0.0.1:10808\"\n        self.date_format = \"%Y-%m-%d %H:%M:%S\"\n        self.client = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy,\n        )\n        self.client_tiktok = create_client(\n            timeout=self.timeout,\n            proxy=self.proxy_tiktok,\n        )\n\n    def create_ini(self):\n        self.config[\"dy\"] = {\n            \"cookie\": \"\",\n            \"uifid\": \"\",\n            \"msToken\": \"\",\n        }\n        self.config[\"tk\"] = {\n            \"cookie\": \"\",\n            \"msToken\": \"\",\n        }\n        with self.CONFIG.open(\"w\", encoding=\"utf-8\") as configfile:\n            self.config.write(configfile)\n\n    def read_ini(self):\n        if not self.config.read(self.CONFIG):\n            self.create_ini()\n            return\n        try:\n            self.cookie_str = self.config.get(\n                \"dy\",\n                \"cookie\",\n            )\n            self.uifid = self.config.get(\n                \"dy\",\n                \"uifid\",\n            )\n            self.msToken = self.config.get(\n                \"dy\",\n                \"msToken\",\n            )\n            self.cookie_str_tiktok = self.config.get(\n                \"tk\",\n                \"cookie\",\n            )\n            self.msToken_tiktok = self.config.get(\n                \"tk\",\n                \"msToken\",\n            )\n        except (NoSectionError, NoOptionError) as e:\n            print(f\"读取 Cookie 错误: {e}\")\n\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        await self.client.aclose()\n        await self.client_tiktok.aclose()\n\n\nasync def test():\n    async with Params() as params:\n        print(params.cookie_str)\n        print(params.cookie_str_tiktok)\n\n\nif __name__ == \"__main__\":\n    from asyncio import run\n\n    run(test())\n"
  },
  {
    "path": "src/testers/test_format.py",
    "content": "from http.cookiejar import Cookie, CookieJar\n\nfrom pytest import mark\n\nfrom src.tools import (\n    cookie_dict_to_str,\n    cookie_jar_to_dict,\n    cookie_str_to_dict,\n    cookie_str_to_str,\n    format_size,\n)\n\n\n@mark.parametrize(\n    \"x, y\",\n    [\n        (\n            \"UIFID_V=2; UIFID_TEMP=aaa; fpk1=aaa; fpk2=aaa; tiktok\",\n            {\"UIFID_V\": \"2\", \"UIFID_TEMP\": \"aaa\", \"fpk1\": \"aaa\", \"fpk2\": \"aaa\"},\n        ),\n    ],\n)\ndef test_cookie_str_to_dict(x, y):\n    assert cookie_str_to_dict(x) == y\n\n\n@mark.parametrize(\n    \"x, y\",\n    [\n        (\n            \"ixigua-a-s=1; path=/; secure; httponly\",\n            \"ixigua-a-s=1\",\n        ),\n    ],\n)\ndef test_cookie_str_to_str(x, y):\n    assert cookie_str_to_str(x) == y\n\n\n@mark.parametrize(\n    \"x, y\",\n    [\n        (\n            {\"UIFID_V\": \"2\", \"UIFID_TEMP\": \"aaa\", \"fpk1\": \"aaa\", \"fpk2\": \"aaa\"},\n            \"UIFID_V=2; UIFID_TEMP=aaa; fpk1=aaa; fpk2=aaa\",\n        ),\n        ({\"name\": \"value\"}, \"name=value\"),\n    ],\n)\ndef test_cookie_dict_to_str(x, y):\n    assert cookie_dict_to_str(x) == y\n\n\ndef create_test_cookie_jar():\n    jar = CookieJar()\n    jar.set_cookie(\n        Cookie(\n            version=0,\n            name=\"cookie_name\",\n            value=\"cookie_value\",\n            port=None,\n            port_specified=False,\n            domain=\"example.com\",\n            domain_specified=True,\n            domain_initial_dot=False,\n            path=\"/\",\n            path_specified=True,\n            secure=False,\n            expires=None,\n            discard=False,\n            comment=None,\n            comment_url=None,\n            rest={},\n        )\n    )\n    return jar\n\n\n@mark.parametrize(\n    \"x, y\",\n    [\n        (\n            create_test_cookie_jar(),\n            {\"cookie_name\": \"cookie_value\"},\n        ),\n    ],\n)\ndef test_cookie_jar_to_dict(x, y):\n    assert cookie_jar_to_dict(x) == y\n\n\n@mark.parametrize(\n    \"x, y\",\n    [\n        (1024 * 1024, \"1.00 MB\"),\n        (1024 * 512, \"512.00 KB\"),\n        (1024 * 1024 * 2.25, \"2.25 MB\"),\n    ],\n)\ndef test_format_size(x, y):\n    assert format_size(x) == y\n"
  },
  {
    "path": "src/testers/translate.py",
    "content": "from src.translation import _, switch_language\nfrom src.custom import DISCLAIMER_TEXT\n\nif __name__ == \"__main__\":\n    print(_(DISCLAIMER_TEXT))\n\n    # 切换到英文并打印翻译\n    switch_language(\"en_US\")\n    print(_(DISCLAIMER_TEXT))\n\n    # 切换回中文并打印翻译\n    switch_language(\"zh_CN\")\n    print(_(DISCLAIMER_TEXT))\n"
  },
  {
    "path": "src/tools/__init__.py",
    "content": "from .browser import Browser\nfrom .capture import capture_error_params\nfrom .capture import capture_error_request\nfrom .choose import choose\nfrom .cleaner import Cleaner\nfrom .console import ColorfulConsole\nfrom .error import CacheError\nfrom .error import DownloaderError\nfrom .file_folder import file_switch\nfrom .file_folder import remove_empty_directories\nfrom .format import (\n    cookie_dict_to_str,\n    cookie_str_to_dict,\n    cookie_jar_to_dict,\n    cookie_str_to_str,\n    format_size,\n)\nfrom .list_pop import safe_pop\nfrom .retry import Retry\nfrom .session import (\n    request_params,\n    create_client,\n)\nfrom .temporary import random_string\nfrom .temporary import timestamp\nfrom .timer import run_time\nfrom .truncate import beautify_string\nfrom .truncate import trim_string\nfrom .truncate import truncate_string\nfrom .rename_compatible import RenameCompatible\nfrom .progress import FakeProgress\n"
  },
  {
    "path": "src/tools/browser.py",
    "content": "from contextlib import suppress\nfrom sys import platform\nfrom types import SimpleNamespace\nfrom typing import TYPE_CHECKING\n\nfrom rookiepy import (\n    arc,\n    brave,\n    chrome,\n    chromium,\n    edge,\n    firefox,\n    librewolf,\n    opera,\n    opera_gx,\n    vivaldi,\n)\n\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from ..config import Parameter\n    from ..module import Cookie\n\n__all__ = [\"Browser\"]\n\n\nclass Browser:\n    SUPPORT_BROWSER = {\n        \"Arc\": (arc, \"Linux, macOS, Windows\"),\n        \"Chrome\": (chrome, \"Linux, macOS, Windows\"),\n        \"Chromium\": (chromium, \"Linux, macOS, Windows\"),\n        \"Opera\": (opera, \"Linux, macOS, Windows\"),\n        \"OperaGX\": (opera_gx, \"macOS, Windows\"),\n        \"Brave\": (brave, \"Linux, macOS, Windows\"),\n        \"Edge\": (edge, \"Linux, macOS, Windows\"),\n        \"Vivaldi\": (vivaldi, \"Linux, macOS, Windows\"),\n        \"Firefox\": (firefox, \"Linux, macOS, Windows\"),\n        \"LibreWolf\": (librewolf, \"Linux, macOS, Windows\"),\n    }\n    PLATFORM = {\n        False: SimpleNamespace(\n            name=_(\"抖音\"),\n            domain=[\n                \"douyin.com\",\n            ],\n            key=\"cookie\",\n        ),\n        True: SimpleNamespace(\n            name=\"TikTok\",\n            domain=[\n                \"tiktok.com\",\n            ],\n            key=\"cookie_tiktok\",\n        ),\n    }\n\n    def __init__(self, parameters: \"Parameter\", cookie_object: \"Cookie\"):\n        self.console = parameters.console\n        self.cookie_object = cookie_object\n        self.options = \"\\n\".join(\n            (\n                f\"{i}. {k}: {v[1]}\"\n                for i, (k, v) in enumerate(\n                    self.SUPPORT_BROWSER.items(),\n                    start=1,\n                )\n            )\n        )\n\n    def run(\n        self,\n        tiktok=False,\n        select: str = None,\n    ):\n        if browser := (\n            select\n            or self.console.input(\n                _(\n                    \"读取指定浏览器的 {platform_name} Cookie 并写入配置文件；\\n\"\n                    \"注意：Windows 系统需要以管理员身份运行程序才能读取 Chromium、Chrome、Edge 浏览器 Cookie！\\n\"\n                    \"{options}\\n\"\n                    \"请输入浏览器名称或序号：\"\n                ).format(\n                    platform_name=self.PLATFORM[tiktok].name, options=self.options\n                ),\n            )\n        ):\n            if cookie := self.get(\n                browser,\n                self.PLATFORM[tiktok].domain,\n            ):\n                self.console.info(\n                    _(\"读取 Cookie 成功！\"),\n                )\n                self.__save_cookie(\n                    cookie,\n                    tiktok,\n                )\n            else:\n                self.console.warning(\n                    _(\"Cookie 数据为空！\"),\n                )\n        else:\n            self.console.print(_(\"未选择浏览器！\"))\n\n    def __save_cookie(self, cookie: dict, tiktok: bool):\n        self.cookie_object.save_cookie(cookie, self.PLATFORM[tiktok].key)\n\n    def get(\n        self,\n        browser: str | int,\n        domains: list[str],\n    ) -> dict[str, str]:\n        if not (browser := self.__browser_object(browser)):\n            self.console.warning(\n                _(\"浏览器名称或序号输入错误！\"),\n            )\n            return {}\n        try:\n            cookies = browser(domains=domains)\n            return {i[\"name\"]: i[\"value\"] for i in cookies}\n        except RuntimeError:\n            self.console.warning(\n                _(\"读取 Cookie 失败，未找到 Cookie 数据！\"),\n            )\n        return {}\n\n    @classmethod\n    def __browser_object(cls, browser: str | int):\n        with suppress(ValueError):\n            browser = int(browser) - 1\n        if isinstance(browser, int):\n            try:\n                return list(cls.SUPPORT_BROWSER.values())[browser][0]\n            except IndexError:\n                return None\n        if isinstance(browser, str):\n            try:\n                return cls.__match_browser(browser)\n            except KeyError:\n                return None\n        raise TypeError\n\n    @classmethod\n    def __match_browser(cls, browser: str):\n        for i, j in cls.SUPPORT_BROWSER.items():\n            if i.lower() == browser.lower():\n                return j[0]\n\n\nmatch platform:\n    case \"darwin\":\n        from rookiepy import safari\n\n        Browser.SUPPORT_BROWSER |= {\n            \"Safari\": (safari, \"macOS\"),\n        }\n    case \"linux\":\n        Browser.SUPPORT_BROWSER.pop(\"OperaGX\")\n    case \"win32\":\n        pass\n    case _:\n        print(_(\"从浏览器读取 Cookie 功能不支持当前平台！\"))\n"
  },
  {
    "path": "src/tools/capture.py",
    "content": "from json.decoder import JSONDecodeError\nfrom ssl import SSLError\nfrom typing import TYPE_CHECKING, Union\n\nfrom httpx import HTTPStatusError, NetworkError, RequestError, TimeoutException\n\nfrom ..translation import _\n\nif TYPE_CHECKING:\n    from ..record import BaseLogger, LoggerManager\n\n__all__ = [\n    \"capture_error_params\",\n    \"capture_error_request\",\n]\n\n\ndef capture_error_params(function):\n    async def inner(logger: Union[\"BaseLogger\", \"LoggerManager\"], *args, **kwargs):\n        try:\n            return await function(logger, *args, **kwargs)\n        except (\n            JSONDecodeError,\n            UnicodeDecodeError,\n        ):\n            logger.error(_(\"响应内容不是有效的 JSON 数据\"))\n        except HTTPStatusError as e:\n            logger.error(_(\"响应码异常：{error}\").format(error=e))\n        except NetworkError as e:\n            logger.error(_(\"网络异常：{error}\").format(error=e))\n        except TimeoutException as e:\n            logger.error(_(\"请求超时：{error}\").format(error=e))\n        except (\n            RequestError,\n            SSLError,\n        ) as e:\n            logger.error(_(\"网络异常：{error}\").format(error=e))\n        return None\n\n    return inner\n\n\ndef capture_error_request(function):\n    async def inner(self, *args, **kwargs):\n        try:\n            return await function(self, *args, **kwargs)\n        except (JSONDecodeError, UnicodeDecodeError):\n            self.log.error(_(\"响应内容不是有效的 JSON 数据，请尝试更新 Cookie！\"))\n        except HTTPStatusError as e:\n            self.log.error(_(\"响应码异常：{error}\").format(error=e))\n        except NetworkError as e:\n            self.log.error(_(\"网络异常：{error}\").format(error=e))\n        except TimeoutException as e:\n            self.log.error(_(\"请求超时：{error}\").format(error=e))\n        except (\n            RequestError,\n            SSLError,\n        ) as e:\n            self.log.error(_(\"网络异常：{error}\").format(error=e))\n        return None\n\n    return inner\n"
  },
  {
    "path": "src/tools/choose.py",
    "content": "from typing import TYPE_CHECKING, Union\n\nif TYPE_CHECKING:\n    from rich.console import Console\n\n    from src.tools import ColorfulConsole\n\n__all__ = [\"choose\"]\n\n\ndef choose(\n    title: str,\n    options: tuple | list,\n    console: Union[\"ColorfulConsole\", \"Console\"],\n    separate=None,\n) -> str:\n    screen = f\"{title}:\\n\"\n    for i, j in enumerate(options, start=1):\n        screen += f\"{i: >2d}. {j}\\n\"\n        if separate and i in separate:\n            screen += f\"{'=' * 32}\\n\"\n    return console.input(screen)\n"
  },
  {
    "path": "src/tools/cleaner.py",
    "content": "from platform import system\nfrom re import compile\nfrom string import whitespace\n\nfrom emoji import replace_emoji\n\ntry:\n    from ..translation import _\nexcept ImportError:\n    _ = lambda x: x\n\n__all__ = [\"Cleaner\"]\n\n\nclass Cleaner:\n    CONTROL_CHARACTERS = compile(r\"[\\x00-\\x1F\\x7F]\")\n\n    def __init__(self):\n        \"\"\"\n        替换字符串中包含的非法字符，默认根据系统类型生成对应的非法字符字典，也可以自行设置非法字符字典\n        \"\"\"\n        self.rule = self.default_rule()  # 默认非法字符字典\n\n    @staticmethod\n    def default_rule():\n        \"\"\"根据系统类型生成默认非法字符字典\"\"\"\n        if (s := system()) in (\"Windows\", \"Darwin\"):\n            rule = {\n                \"/\": \"\",\n                \"\\\\\": \"\",\n                \"|\": \"\",\n                \"<\": \"\",\n                \">\": \"\",\n                '\"': \"\",\n                \"?\": \"\",\n                \":\": \"\",\n                \"*\": \"\",\n                \"\\x00\": \"\",\n            }  # Windows 系统和 Mac 系统\n        elif s == \"Linux\":\n            rule = {\n                \"/\": \"\",\n                \"\\x00\": \"\",\n            }  # Linux 系统\n        else:\n            print(_(\"不受支持的操作系统类型，可能无法正常去除非法字符！\"))\n            rule = {}\n        cache = {i: \"\" for i in whitespace[1:]}  # 补充换行符等非法字符\n        return rule | cache\n\n    def set_rule(self, rule: dict[str, str], update=False):\n        \"\"\"\n        设置非法字符字典\n\n        :param rule: 替换规则，字典格式，键为非法字符，值为替换后的内容\n        :param update: 如果是 True，则与原有规则字典合并，否则替换原有规则字典\n        \"\"\"\n        self.rule = {**self.rule, **rule} if update else rule\n\n    def filter(self, text: str) -> str:\n        \"\"\"\n        去除非法字符\n\n        :param text: 待处理的字符串\n        :return: 替换后的字符串，如果替换后字符串为空，则返回 None\n        \"\"\"\n        for i in self.rule:\n            text = text.replace(i, self.rule[i])\n        return text\n\n    def filter_name(\n        self,\n        text: str,\n        default: str = \"\",\n    ) -> str:\n        \"\"\"过滤文件夹名称中的非法字符\"\"\"\n        text = text.replace(\":\", \".\")\n\n        text = self.remove_control_characters(text)\n\n        text = self.filter(text)\n\n        text = replace_emoji(text)\n\n        text = self.clear_spaces(text)\n\n        text = text.strip().strip(\".\")\n\n        return text or default\n\n    @staticmethod\n    def clear_spaces(string: str):\n        \"\"\"将连续的空格转换为单个空格\"\"\"\n        return \" \".join(string.split())\n\n    @classmethod\n    def remove_control_characters(\n        cls,\n        text,\n        replace=\"\",\n    ):\n        # 使用正则表达式匹配所有控制字符\n        return cls.CONTROL_CHARACTERS.sub(\n            replace,\n            text,\n        )\n\n\nif __name__ == \"__main__\":\n    demo = Cleaner()\n    print(demo.rule)\n    print(demo.filter_name(\"\"))\n    print(demo.remove_control_characters(\"hello \\x08world\"))\n"
  },
  {
    "path": "src/tools/console.py",
    "content": "from rich.console import Console\nfrom rich.text import Text\n\nfrom src.custom import (\n    PROMPT,\n    GENERAL,\n    INFO,\n    WARNING,\n    ERROR,\n    DEBUG,\n)\n\n__all__ = [\"ColorfulConsole\"]\n\n\nclass ColorfulConsole(Console):\n    def __init__(self, *args, debug: bool = False, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.debug_mode = debug\n\n    def print(self, *args, style=GENERAL, highlight=False, **kwargs):\n        super().print(*args, style=style, highlight=highlight, **kwargs)\n\n    def info(self, *args, highlight=False, **kwargs):\n        self.print(*args, style=INFO, highlight=highlight, **kwargs)\n\n    def warning(self, *args, highlight=False, **kwargs):\n        self.print(*args, style=WARNING, highlight=highlight, **kwargs)\n\n    def error(self, *args, highlight=False, **kwargs):\n        self.print(*args, style=ERROR, highlight=highlight, **kwargs)\n\n    def debug(self, *args, highlight=False, **kwargs):\n        if self.debug_mode:\n            self.print(*args, style=DEBUG, highlight=highlight, **kwargs)\n\n    def input(self, prompt=\"\", style=PROMPT, *args, **kwargs):\n        try:\n            return super().input(Text(prompt, style=style), *args, **kwargs)\n        except EOFError as e:\n            raise KeyboardInterrupt from e\n"
  },
  {
    "path": "src/tools/error.py",
    "content": "from ..translation import _\n\n\nclass DownloaderError(Exception):\n    def __init__(\n        self,\n        message: str = \"\",\n    ):\n        self.message = message or _(\"项目代码错误\")\n        super().__init__(self.message)\n\n    def __str__(self):\n        return f\"DownloaderError: {self.message}\"\n\n\nclass CacheError(Exception):\n    def __init__(self, message: str):\n        super().__init__(message)\n        self.message = message\n\n    def __str__(self):\n        return self.message\n"
  },
  {
    "path": "src/tools/file_folder.py",
    "content": "from contextlib import suppress\nfrom pathlib import Path\n\n\ndef file_switch(path: Path) -> None:\n    if path.exists():\n        path.unlink()\n    else:\n        path.touch()\n\n\ndef remove_empty_directories(path: Path) -> None:\n    exclude = {\n        \"\\\\.\",\n        \"\\\\_\",\n        \"\\\\__\",\n    }\n    for dir_path, dir_names, file_names in path.walk(\n        top_down=False,\n    ):\n        if any(i in str(dir_path) for i in exclude):\n            continue\n        if not dir_names and not file_names:\n            with suppress(OSError):\n                dir_path.rmdir()\n"
  },
  {
    "path": "src/tools/format.py",
    "content": "from http.cookiejar import CookieJar\nfrom re import compile\n\n\ndef cookie_str_to_dict(cookie_str: str) -> dict:\n    if not cookie_str:\n        return {}\n    cookie = {}\n    pattern = compile(r\"(?P<key>[^=;,]+)=(?P<value>[^;,]+)\")\n    matches = pattern.finditer(cookie_str)\n    for match in matches:\n        key = match.group(\"key\").strip()\n        value = match.group(\"value\").strip()\n        cookie[key] = value\n    return cookie\n\n\ndef cookie_str_to_str(cookie_str: str) -> str:\n    if not cookie_str:\n        return \"\"\n    pattern = compile(r\", (?=\\D)\")\n    return \"; \".join(cookie.split(\"; \")[0] for cookie in pattern.split(cookie_str))\n\n\ndef cookie_dict_to_str(cookie_dict: dict | CookieJar) -> str:\n    if not cookie_dict:\n        return \"\"\n    cookie_pairs = [f\"{key}={value}\" for key, value in cookie_dict.items()]\n    return \"; \".join(cookie_pairs)\n\n\ndef cookie_jar_to_dict(cookie_jar: CookieJar) -> dict:\n    return {i.name: i.value for i in cookie_jar}\n\n\ndef format_size(size_in_bytes: int) -> str:\n    units = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"]\n    index = 0\n    while size_in_bytes >= 1024 and index < len(units) - 1:\n        size_in_bytes /= 1024\n        index += 1\n    return f\"{size_in_bytes:.2f} {units[index]}\"\n\n\nif __name__ == \"__main__\":\n    print(format_size(0))\n"
  },
  {
    "path": "src/tools/list_pop.py",
    "content": "__all__ = [\"safe_pop\"]\n\n\ndef safe_pop(data: list):\n    return data.pop() if data else None\n"
  },
  {
    "path": "src/tools/progress.py",
    "content": "class FakeProgress:\n    def __init__(\n        self,\n        *args,\n        **kwargs,\n    ):\n        pass\n\n    async def __aenter__(self):\n        return self\n\n    def __enter__(self):\n        return self\n\n    async def __aexit__(self, exc_type, exc_val, exc_tb):\n        pass\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        pass\n\n    def add_task(\n        self,\n        *args,\n        **kwargs,\n    ):\n        pass\n\n    def update(\n        self,\n        *args,\n        **kwargs,\n    ):\n        pass\n\n    def remove_task(\n        self,\n        *args,\n        **kwargs,\n    ):\n        pass\n"
  },
  {
    "path": "src/tools/rename_compatible.py",
    "content": "from ..custom import PROJECT_ROOT\nfrom shutil import copy2\n\n\nclass RenameCompatible:\n    OLD_DB_FILE = PROJECT_ROOT.joinpath(\"TikTokDownloader.db\")\n    NEW_DB_FILE = PROJECT_ROOT.joinpath(\"DouK-Downloader.db\")\n\n    @classmethod\n    def migration_file(\n        cls,\n    ):\n        if cls.OLD_DB_FILE.exists() and not cls.NEW_DB_FILE.exists():\n            copy2(cls.OLD_DB_FILE.resolve(), cls.NEW_DB_FILE.resolve())\n"
  },
  {
    "path": "src/tools/retry.py",
    "content": "from ..custom import RETRY, wait\nfrom ..translation import _\n\n__all__ = [\"Retry\"]\n\n\nclass Retry:\n    \"\"\"重试器，仅适用于本项目！\"\"\"\n\n    @staticmethod\n    def retry(function):\n        \"\"\"发生错误时尝试重新执行，装饰的函数需要返回布尔值\"\"\"\n\n        async def inner(self, *args, **kwargs):\n            finished = kwargs.pop(\"finished\", False)\n            for i in range(self.max_retry):\n                if result := await function(self, *args, **kwargs):\n                    return result\n                self.log.warning(_(\"正在进行第 {index} 次重试\").format(index=i + 1))\n                await wait()\n            if not (result := await function(self, *args, **kwargs)) and finished:\n                self.finished = True\n            return result\n\n        return inner\n\n    @staticmethod\n    def retry_lite(function):\n        async def inner(*args, **kwargs):\n            if r := await function(*args, **kwargs):\n                return r\n            for _ in range(RETRY):\n                if r := await function(*args, **kwargs):\n                    return r\n                await wait()\n            return r\n\n        return inner\n\n    @staticmethod\n    def retry_limited(function):\n        def inner(self, *args, **kwargs):\n            while True:\n                if function(self, *args, **kwargs):\n                    return\n                if self.console.input(\n                    _(\n                        \"如需重新尝试处理该对象，请关闭所有正在访问该对象的窗口或程序，然后直接按下回车键！\\n\"\n                        \"如需跳过处理该对象，请输入任意字符后按下回车键！\"\n                    ),\n                ):\n                    return\n\n        return inner\n\n    @staticmethod\n    def retry_infinite(function):\n        def inner(self, *args, **kwargs):\n            while True:\n                if function(self, *args, **kwargs):\n                    return\n                self.console.input(\n                    _(\"请关闭所有正在访问该对象的窗口或程序，然后按下回车键继续处理！\")\n                )\n\n        return inner\n"
  },
  {
    "path": "src/tools/session.py",
    "content": "from typing import TYPE_CHECKING, Union\n\nfrom httpx import AsyncClient, AsyncHTTPTransport, Client, HTTPTransport\n\nfrom ..custom import TIMEOUT, USERAGENT\nfrom ..tools import DownloaderError\nfrom .capture import capture_error_params\nfrom .retry import Retry\n\nif TYPE_CHECKING:\n    from ..record import BaseLogger, LoggerManager\n    from ..testers import Logger\n\n__all__ = [\"request_params\", \"create_client\"]\n\n\ndef create_client(\n    user_agent=USERAGENT,\n    timeout=TIMEOUT,\n    headers: dict = None,\n    proxy: str = None,\n    *args,\n    **kwargs,\n) -> AsyncClient:\n    return AsyncClient(\n        headers=headers\n        or {\n            \"User-Agent\": user_agent,\n        },\n        timeout=timeout,\n        follow_redirects=True,\n        verify=False,\n        mounts={\n            \"http://\": AsyncHTTPTransport(proxy=proxy),\n            \"https://\": AsyncHTTPTransport(proxy=proxy),\n        },\n        *args,\n        **kwargs,\n    )\n\n\nasync def request_params(\n    logger: Union[\n        \"BaseLogger\",\n        \"LoggerManager\",\n        \"Logger\",\n    ],\n    url: str,\n    method: str = \"POST\",\n    params: dict | str = None,\n    data: dict | str = None,\n    useragent=USERAGENT,\n    timeout=TIMEOUT,\n    headers: dict = None,\n    resp=\"headers\",\n    proxy: str = None,\n    **kwargs,\n):\n    with Client(\n        headers=headers\n        or {\n            \"User-Agent\": useragent,\n            \"Content-Type\": \"application/json; charset=utf-8\",\n            # \"Referer\": \"https://www.douyin.com/\"\n        },\n        follow_redirects=True,\n        timeout=timeout,\n        verify=False,\n        mounts={\n            \"http://\": HTTPTransport(proxy=proxy),\n            \"https://\": HTTPTransport(proxy=proxy),\n        },\n    ) as client:\n        return await request(\n            logger,\n            client,\n            method,\n            url,\n            resp,\n            params=params,\n            data=data,\n            **kwargs,\n        )\n\n\n@Retry.retry_lite\n@capture_error_params\nasync def request(\n    logger: Union[\n        \"BaseLogger\",\n        \"LoggerManager\",\n        \"Logger\",\n    ],\n    client: Client,\n    method: str,\n    url: str,\n    resp=\"json\",\n    **kwargs,\n):\n    response = client.request(method, url, **kwargs)\n    response.raise_for_status()\n    match resp:\n        case \"headers\":\n            return response.headers\n        case \"text\":\n            return response.text\n        case \"content\":\n            return response.content\n        case \"json\":\n            return response.json()\n        case \"url\":\n            return str(response.url)\n        case \"response\":\n            return response\n        case _:\n            raise DownloaderError\n"
  },
  {
    "path": "src/tools/temporary.py",
    "content": "from random import choice\nfrom string import (\n    ascii_lowercase,\n    ascii_uppercase,\n    digits,\n)\nfrom time import time\n\nCHARACTER = ascii_lowercase + ascii_uppercase + digits\n\n\ndef timestamp() -> str:\n    return str(time())[:10]\n\n\ndef random_string(length: int = 10) -> str:\n    return \"\".join(choice(CHARACTER) for _ in range(length))\n\n\nif __name__ == \"__main__\":\n    print(timestamp())\n    print(random_string())\n"
  },
  {
    "path": "src/tools/timer.py",
    "content": "from time import time\n\n__all__ = [\"run_time\"]\n\n\ndef run_time(function):\n    def inner(self, *args, **kwargs):\n        start = time()\n        result = function(self, *args, **kwargs)\n        print(f\"{function.__name__}运行耗时: {time() - start}s\")\n        return result\n\n    return inner\n"
  },
  {
    "path": "src/tools/truncate.py",
    "content": "from unicodedata import name\n\n\ndef is_chinese_char(char: str) -> bool:\n    return \"CJK\" in name(char, \"\")\n\n\ndef truncate_string(s: str, length: int = 64) -> str:\n    count = 0\n    result = \"\"\n    for char in s:\n        count += 2 if is_chinese_char(char) else 1\n        if count > length:\n            break\n        result += char\n    return result\n\n\ndef trim_string(s: str, length: int = 64) -> str:\n    length = length // 2 - 2\n    return f\"{s[:length]}...{s[-length:]}\" if len(s) > length else s\n\n\ndef beautify_string(s: str, length: int = 64) -> str:\n    count = 0\n    for char in s:\n        count += 2 if is_chinese_char(char) else 1\n        if count > length:\n            break\n    else:\n        return s\n    length //= 2\n    start = truncate_string(s, length)\n    end = truncate_string(s[::-1], length)[::-1]\n    return f\"{start}...{end}\"\n"
  },
  {
    "path": "src/translation/__init__.py",
    "content": "from .translate import switch_language, _\n"
  },
  {
    "path": "src/translation/static.py",
    "content": "TRANSLATE_MAP = {\n    \"发布作品\": \"Posts\",\n    \"喜欢作品\": \"Liked\",\n    \"收藏作品\": \"Favorites\",\n    \"收藏夹\": \"Collections\",\n    \"收藏夹作品\": \"Collections Works\",\n    \"收藏音乐\": \"Collections Music\",\n    \"收藏合集\": \"Collections Mix\",\n    \"收藏短剧\": \"Collections Series\",\n    \"作品\": \"Works\",\n    \"合集\": \"Mix\",\n    \"合辑\": \"Mix\",\n    \"热榜\": \"HotBoard\",\n    \"实况\": \"LivePhoto\",\n}\n"
  },
  {
    "path": "src/translation/translate.py",
    "content": "from gettext import translation\nfrom locale import getlocale\nfrom pathlib import Path\n\nROOT = Path(__file__).resolve().parent.parent.parent\n\n\nclass TranslationManager:\n    \"\"\"管理gettext翻译的类\"\"\"\n\n    _instance = None  # 单例实例\n\n    def __new__(cls, *args, **kwargs):\n        if not cls._instance:\n            cls._instance = super(TranslationManager, cls).__new__(cls)\n        return cls._instance\n\n    def __init__(self, domain=\"tk\", localedir=None):\n        self.domain = domain\n        if not localedir:\n            localedir = ROOT.joinpath(\"locale\")\n        self.localedir = Path(localedir)\n        self.current_translator = self.setup_translation(\n            self.get_language_code(),\n        )\n\n    @staticmethod\n    def get_language_code() -> str:\n        # 获取当前系统的语言和区域设置\n        language_code, __ = getlocale()\n        if not language_code:\n            return \"en_US\"\n        return (\n            \"zh_CN\"\n            if any(\n                s in language_code.upper()\n                for s in (\n                    \"CHINESE\",\n                    \"ZH\",\n                    \"CHINA\",\n                )\n            )\n            else \"en_US\"\n        )\n\n    def setup_translation(self, language: str = \"zh_CN\"):\n        \"\"\"设置gettext翻译环境\"\"\"\n        try:\n            return translation(\n                self.domain,\n                localedir=self.localedir,\n                languages=[language],\n                fallback=True,\n            )\n        except FileNotFoundError as e:\n            print(\n                f\"Warning: Translation files for '{self.domain}' not found. Error: {e}\"\n            )\n            return translation(self.domain, fallback=True)\n\n    def switch_language(self, language: str = \"en_US\"):\n        \"\"\"切换当前使用的语言\"\"\"\n        self.current_translator = self.setup_translation(language)\n\n    def gettext(self, message):\n        \"\"\"提供gettext方法\"\"\"\n        return self.current_translator.gettext(message)\n\n\n# 初始化TranslationManager单例实例\ntranslation_manager = TranslationManager()\n\n\ndef _translate(message):\n    \"\"\"辅助函数来简化翻译调用\"\"\"\n    return translation_manager.gettext(message)\n\n\ndef switch_language(language: str = \"en_US\"):\n    \"\"\"切换语言并刷新翻译函数\"\"\"\n    global _\n    translation_manager.switch_language(language)\n    _ = translation_manager.gettext\n\n\n# 设置默认翻译函数\n_ = _translate\n"
  },
  {
    "path": "src/tui_edition/__init__.py",
    "content": "from .app import App\n\n__all__ = [\"App\"]\n"
  },
  {
    "path": "src/tui_edition/app.py",
    "content": "__all__ = [\"App\"]\n\n\nclass App:\n    pass\n"
  },
  {
    "path": "src/tui_edition/setting.py",
    "content": "__all__ = [\"Setting\"]\n\n\nclass Setting:\n    pass\n"
  },
  {
    "path": "static/js/X-Bogus.js",
    "content": "var window = null;\n\nfunction _0x5cd844(e) {\n    var b = {\n        exports: {}\n    };\n    return e(b, b.exports), b.exports\n}\n\njsvmp = function (e, b, a) {\n    function f(e, b, a) {\n        return (f = function () {\n            if (\"undefined\" == typeof Reflect || !Reflect.construct || Reflect.construct.sham) return !1;\n            if (\"function\" == typeof Proxy) return !0;\n            try {\n                return Date.prototype.toString.call(Reflect.construct(Date, [], function () {\n                })), !0\n            } catch (e) {\n                return !1\n            }\n        }() ? Reflect.construct : function (e, b, a) {\n            var f = [null];\n            f.push.apply(f, b);\n            var c = new (Function.bind.apply(e, f));\n            return a && function (e, b) {\n                (Object.setPrototypeOf || function (e, b) {\n                    return e.__proto__ = b, e\n                })(e, b)\n            }(c, a.prototype), c\n        }).apply(null, arguments)\n    }\n\n    function c(e) {\n        return function (e) {\n            if (Array.isArray(e)) {\n                for (var b = 0, a = new Array(e.length); b < e.length; b++) a[b] = e[b];\n                return a\n            }\n        }(e) || function (e) {\n            if (Symbol.iterator in Object(e) || \"[object Arguments]\" === Object.prototype.toString.call(e)) return Array.from(e)\n        }(e) || function () {\n            throw new TypeError(\"Invalid attempt to spread non-iterable instance\")\n        }()\n    }\n\n    for (var r = [], t = 0, d = [], i = 0, n = function (e, b) {\n        var a = e[b++],\n            f = e[b],\n            c = parseInt(\"\" + a + f, 16);\n        if (c >> 7 == 0) return [1, c];\n        if (c >> 6 == 2) {\n            var r = parseInt(\"\" + e[++b] + e[++b], 16);\n            return c &= 63, [2, r = (c <<= 8) + r]\n        }\n        if (c >> 6 == 3) {\n            var t = parseInt(\"\" + e[++b] + e[++b], 16),\n                d = parseInt(\"\" + e[++b] + e[++b], 16);\n            return c &= 63, [3, d = (c <<= 16) + (t <<= 8) + d]\n        }\n    }, s = function (e, b) {\n        var a = parseInt(\"\" + e[b] + e[b + 1], 16);\n        return a > 127 ? -256 + a : a\n    }, o = function (e, b) {\n        var a = parseInt(\"\" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16);\n        return a > 32767 ? -65536 + a : a\n    }, l = function (e, b) {\n        var a = parseInt(\"\" + e[b] + e[b + 1] + e[b + 2] + e[b + 3] + e[b + 4] + e[b + 5] + e[b + 6] + e[b + 7], 16);\n        return a > 2147483647 ? 0 + a : a\n    }, _ = function (e, b) {\n        return parseInt(\"\" + e[b] + e[b + 1], 16)\n    }, x = function (e, b) {\n        return parseInt(\"\" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16)\n    }, u = u || this || window, h = (e.length, 0), p = \"\", y = h; y < h + 16; y++) {\n        var v = \"\" + e[y++] + e[y];\n        v = parseInt(v, 16), p += String.fromCharCode(v)\n    }\n    if (\"HNOJ@?RC\" != p) throw new Error(\"error magic number \" + p);\n    parseInt(\"\" + e[h += 16] + e[h + 1], 16), h += 8, t = 0;\n    for (var g = 0; g < 4; g++) {\n        var w = h + 2 * g,\n            A = parseInt(\"\" + e[w++] + e[w], 16);\n        t += (3 & A) << 2 * g\n    }\n    h += 16;\n    var C = parseInt(\"\" + e[h += 8] + e[h + 1] + e[h + 2] + e[h + 3] + e[h + 4] + e[h + 5] + e[h + 6] + e[h + 7], 16),\n        m = C,\n        S = h += 8,\n        z = x(e, h += C);\n    z[1], h += 4, r = {\n        p: [],\n        q: []\n    };\n    for (var B = 0; B < z; B++) {\n        for (var R = n(e, h), q = h += 2 * R[0], I = r.p.length, k = 0; k < R[1]; k++) {\n            var j = n(e, q);\n            r.p.push(j[1]), q += 2 * j[0]\n        }\n        h = q, r.q.push([I, r.p.length])\n    }\n    var O = {\n            5: 1,\n            6: 1,\n            70: 1,\n            22: 1,\n            23: 1,\n            37: 1,\n            73: 1\n        },\n        U = {\n            72: 1\n        },\n        D = {\n            74: 1\n        },\n        N = {\n            11: 1,\n            12: 1,\n            24: 1,\n            26: 1,\n            27: 1,\n            31: 1\n        },\n        J = {\n            10: 1\n        },\n        L = {\n            2: 1,\n            29: 1,\n            30: 1,\n            20: 1\n        },\n        T = [],\n        E = [];\n\n    function M(e, b, a) {\n        for (var f = b; f < b + a;) {\n            var c = _(e, f);\n            T[f] = c, f += 2, U[c] ? (E[f] = s(e, f), f += 2) : O[c] ? (E[f] = o(e, f), f += 4) : D[c] ? (E[f] = l(e, f), f += 8) : N[c] ? (E[f] = _(e, f), f += 2) : J[c] ? (E[f] = x(e, f), f += 4) : L[c] && (E[f] = x(e, f), f += 4)\n        }\n    }\n\n    return F(e, S, m / 2, [], b, a);\n\n    function P(e, b, a, n, h, p, y, v) {\n        null == p && (p = this);\n        var g, w, A, C, m = [],\n            S = 0;\n        y && (w = y);\n        var z, B, R = b,\n            q = R + 2 * a;\n        if (!v)\n            for (; R < q;) {\n                var I = parseInt(\"\" + e[R] + e[R + 1], 16);\n                R += 2;\n                var j = 3 & (z = 13 * I % 241);\n                if (z >>= 2, j < 1)\n                    if (j = 3 & z, z >>= 2, j < 1) {\n                        if ((j = z) < 1) return [1, m[S--]];\n                        j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = o(e, R), (g = function b() {\n                            var a = arguments;\n                            return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)\n                        }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)\n                    } else if (j < 2) (j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));\n                    else if (j < 3) {\n                        if ((j = z) < 9) {\n                            for (w = m[S--], B = x(e, R), j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                            R += 4, m[S--][j] = w\n                        } else if (j < 13) throw m[S--]\n                    } else (j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);\n                else if (j < 2)\n                    if (j = 3 & z, z >>= 2, j < 1)\n                        if ((j = z) < 5) {\n                            B = o(e, R);\n                            try {\n                                if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w\n                            } catch (b) {\n                                if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w\n                            } finally {\n                                if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;\n                                d[i] = 0, i--\n                            }\n                            R += 2 * B - 2\n                        } else j < 7 ? (B = _(e, R), R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)))) : j < 9 && (w = m[S--], m[S] = m[S] & w);\n                    else if (j < 2)\n                        if ((j = z) > 12) m[++S] = s(e, R), R += 2;\n                        else if (j > 10) w = m[S--], m[S] = m[S] << w;\n                        else if (j > 8) {\n                            for (B = x(e, R), j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                            R += 4, m[S] = m[S][j]\n                        } else j > 6 && (A = m[S--], w = delete m[S--][A]);\n                    else if (j < 3) (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w);\n                    else if ((j = z) > 12) m[++S] = p;\n                    else if (j > 5) w = m[S--], m[S] = m[S] !== w;\n                    else if (j > 3) w = m[S--], m[S] = m[S] / w;\n                    else if (j > 1) {\n                        if ((B = o(e, R)) < 0) {\n                            v = 1, M(e, b, 2 * a), R += 2 * B - 2;\n                            break\n                        }\n                        R += 2 * B - 2\n                    } else j > -1 && (m[S] = !m[S]);\n                else if (j < 3)\n                    if (j = 3 & z, z >>= 2, j < 1) (j = z) > 13 ? (m[++S] = o(e, R), R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = _(e, R), R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = x(e, R), R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);\n                    else if (j < 2) (j = z) > 12 ? (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A) : j > 3 ? (w = m[S--], m[S] = m[S] == w) : j > 1 ? (w = m[S--], m[S] = m[S] + w) : j > -1 && (m[++S] = u);\n                    else if (j < 3) {\n                        if ((j = z) > 13) m[++S] = !1;\n                        else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;\n                        else if (j > 4) w = m[S--], m[S] = m[S] % w;\n                        else if (j > 2)\n                            if (m[S--]) R += 4;\n                            else {\n                                if ((B = o(e, R)) < 0) {\n                                    v = 1, M(e, b, 2 * a), R += 2 * B - 2;\n                                    break\n                                }\n                                R += 2 * B - 2\n                            }\n                        else if (j > 0) {\n                            for (B = x(e, R), w = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);\n                            m[++S] = w, R += 4\n                        }\n                    } else (j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = _(e, R), R += 2, m[++S] = h[\"$\" + B]) : j > 3 && (B = o(e, R), d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);\n                else if (j = 3 & z, z >>= 2, j > 2) (j = z) > 13 ? (m[++S] = l(e, R), R += 8) : j > 11 ? (w = m[S--], m[S] = m[S] >>> w) : j > 9 ? m[++S] = !0 : j > 7 ? (B = _(e, R), R += 2, m[S] = m[S][B]) : j > 0 && (w = m[S--], m[S] = m[S] < w);\n                else if (j > 1) (j = z) > 10 ? (B = o(e, R), d[++i] = [\n                    [R + 4, B - 3], 0, 0\n                ], R += 2 * B - 2) : j > 8 ? (w = m[S--], m[S] = m[S] ^ w) : j > 6 && (w = m[S--]);\n                else if (j > 0) {\n                    if ((j = z) > 7) w = m[S--], m[S] = m[S] in w;\n                    else if (j > 5) m[S] = ++m[S];\n                    else if (j > 3) B = _(e, R), R += 2, w = h[B], m[++S] = w;\n                    else if (j > 1) {\n                        var O = 0,\n                            U = m[S].length,\n                            D = m[S];\n                        m[++S] = function () {\n                            var e = O < U;\n                            if (e) {\n                                var b = D[O++];\n                                m[++S] = b\n                            }\n                            m[++S] = e\n                        }\n                    }\n                } else if ((j = z) > 13) w = m[S], m[S] = m[S - 1], m[S - 1] = w;\n                else if (j > 4) w = m[S--], m[S] = m[S] === w;\n                else if (j > 2) w = m[S--], m[S] = m[S] - w;\n                else if (j > 0) {\n                    for (B = x(e, R), j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                    j = +j, R += 4, m[++S] = j\n                }\n            }\n        if (v)\n            for (; R < q;)\n                if (I = T[R], R += 2, j = 3 & (z = 13 * I % 241), z >>= 2, j > 2)\n                    if (j = 3 & z, z >>= 2, j > 2) (j = z) < 2 ? (w = m[S--], m[S] = m[S] < w) : j < 9 ? (B = E[R], R += 2, m[S] = m[S][B]) : j < 11 ? m[++S] = !0 : j < 13 ? (w = m[S--], m[S] = m[S] >>> w) : j < 15 && (m[++S] = E[R], R += 8);\n                    else if (j > 1) (j = z) < 6 || (j < 8 ? w = m[S--] : j < 10 ? (w = m[S--], m[S] = m[S] ^ w) : j < 12 && (B = E[R], d[++i] = [\n                        [R + 4, B - 3], 0, 0\n                    ], R += 2 * B - 2));\n                    else if (j > 0) (j = z) > 7 ? (w = m[S--], m[S] = m[S] in w) : j > 5 ? m[S] = ++m[S] : j > 3 ? (B = E[R], R += 2, w = h[B], m[++S] = w) : j > 1 && (O = 0, U = m[S].length, D = m[S], m[++S] = function () {\n                        var e = O < U;\n                        if (e) {\n                            var b = D[O++];\n                            m[++S] = b\n                        }\n                        m[++S] = e\n                    });\n                    else if ((j = z) < 2) {\n                        for (B = E[R], j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                        j = +j, R += 4, m[++S] = j\n                    } else j < 4 ? (w = m[S--], m[S] = m[S] - w) : j < 6 ? (w = m[S--], m[S] = m[S] === w) : j < 15 && (w = m[S], m[S] = m[S - 1], m[S - 1] = w);\n                else if (j > 1)\n                    if (j = 3 & z, z >>= 2, j < 1) (j = z) > 13 ? (m[++S] = E[R], R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = E[R], R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = E[R], R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);\n                    else if (j < 2) (j = z) < 1 ? m[++S] = u : j < 3 ? (w = m[S--], m[S] = m[S] + w) : j < 5 ? (w = m[S--], m[S] = m[S] == w) : j < 14 && (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A);\n                    else if (j < 3) {\n                        if ((j = z) > 13) m[++S] = !1;\n                        else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;\n                        else if (j > 4) w = m[S--], m[S] = m[S] % w;\n                        else if (j > 2) m[S--] ? R += 4 : R += 2 * (B = E[R]) - 2;\n                        else if (j > 0) {\n                            for (B = E[R], w = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);\n                            m[++S] = w, R += 4\n                        }\n                    } else (j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = E[R], R += 2, m[++S] = h[\"$\" + B]) : j > 3 && (B = E[R], d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);\n                else if (j > 0)\n                    if (j = 3 & z, z >>= 2, j < 1) {\n                        if ((j = z) > 9) ;\n                        else if (j > 7) w = m[S--], m[S] = m[S] & w;\n                        else if (j > 5) B = E[R], R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)));\n                        else if (j > 3) {\n                            B = E[R];\n                            try {\n                                if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w\n                            } catch (b) {\n                                if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w\n                            } finally {\n                                if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;\n                                d[i] = 0, i--\n                            }\n                            R += 2 * B - 2\n                        }\n                    } else if (j < 2)\n                        if ((j = z) < 8) A = m[S--], w = delete m[S--][A];\n                        else if (j < 10) {\n                            for (B = E[R], j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                            R += 4, m[S] = m[S][j]\n                        } else j < 12 ? (w = m[S--], m[S] = m[S] << w) : j < 14 && (m[++S] = E[R], R += 2);\n                    else j < 3 ? (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w) : (j = z) > 12 ? m[++S] = p : j > 5 ? (w = m[S--], m[S] = m[S] !== w) : j > 3 ? (w = m[S--], m[S] = m[S] / w) : j > 1 ? R += 2 * (B = E[R]) - 2 : j > -1 && (m[S] = !m[S]);\n                else if (j = 3 & z, z >>= 2, j < 1) {\n                    if ((j = z) < 1) return [1, m[S--]];\n                    j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = E[R], (g = function b() {\n                        var a = arguments;\n                        return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)\n                    }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)\n                } else if (j < 2) (j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));\n                else if (j < 3) {\n                    if ((j = z) < 9) {\n                        for (w = m[S--], B = E[R], j = \"\", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);\n                        R += 4, m[S--][j] = w\n                    } else if (j < 13) throw m[S--]\n                } else (j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);\n        return [0, null]\n    }\n\n    function F(e, b, a, f, c, r, t, d) {\n        null == r && (r = this), c && !c.d && (c.d = 0, c.$0 = c, c[1] = {});\n        var i, n, s = {},\n            o = s.d = c ? c.d + 1 : 0;\n        for (s[\"$\" + o] = s, n = 0; n < o; n++) s[i = \"$\" + n] = c[i];\n        for (n = 0, o = s.length = f.length; n < o; n++) s[n] = f[n];\n        return d && !T[b] && M(e, b, 2 * a), T[b] ? P(e, b, a, 0, s, r, null, 1)[1] : P(e, b, a, 0, s, r, null, 0)[1]\n    }\n};\nvar _0x397dc7 = \"undefined\" != typeof globalThis ? globalThis : void 0 !== window ? window : \"undefined\" != typeof global ? global : \"undefined\" != typeof self ? self : {},\n    _0x124d1a = _0x5cd844(function (_0x770f81) {\n        !function () {\n            var _0x250d36 = \"input is invalid type\",\n                _0x4cfaee = !1,\n                _0x1702f9 = {},\n                _0x5ccbb3 = !_0x4cfaee && \"object\" == typeof self,\n                _0x54d876 = !_0x1702f9.JS_MD5_NO_NODE_JS && \"object\" == typeof process && process.versions && process.versions.node,\n                _0x185caf;\n            _0x54d876 ? _0x1702f9 = _0x397dc7 : _0x5ccbb3 && (_0x1702f9 = self);\n            var _0x17dcbf = !_0x1702f9.JS_MD5_NO_COMMON_JS && _0x770f81.exports,\n                _0x554fed = !1,\n                _0x2de28f = !_0x1702f9.JS_MD5_NO_ARRAY_BUFFER && \"undefined\" != typeof ArrayBuffer,\n                _0x3a9a1b = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n                _0x465562 = [128, 32768, 8388608, -2147483648],\n                _0x20b37e = [0, 8, 16, 24],\n                _0x323604 = [\"hex\", \"array\", \"digest\", \"buffer\", \"arrayBuffer\", \"base64\"],\n                _0x2c185e = [\"A\", \"B\", \"C\", \"D\", \"E\", \"F\", \"G\", \"H\", \"I\", \"J\", \"K\", \"L\", \"M\", \"N\", \"O\", \"P\", \"Q\", \"R\", \"S\", \"T\", \"U\", \"V\", \"W\", \"X\", \"Y\", \"Z\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\", \"i\", \"j\", \"k\", \"l\", \"m\", \"n\", \"o\", \"p\", \"q\", \"r\", \"s\", \"t\", \"u\", \"v\", \"w\", \"x\", \"y\", \"z\", \"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"+\", \"/\"],\n                _0x4b59e0 = [];\n            if (_0x2de28f) {\n                var _0x395837 = new ArrayBuffer(68);\n                _0x185caf = new Uint8Array(_0x395837), _0x4b59e0 = new Uint32Array(_0x395837)\n            }\n            !_0x1702f9.JS_MD5_NO_NODE_JS && Array.isArray || (Array.isArray = function (e) {\n                return \"[object Array]\" === Object.prototype.toString.call(e)\n            }), _0x2de28f && (_0x1702f9.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView) && (ArrayBuffer.isView = function (e) {\n                return \"object\" == typeof e && e.buffer && e.buffer.constructor === ArrayBuffer\n            });\n            var _0x4e9930 = function (e) {\n                    return function (b) {\n                        return new _0x5887c8(!0).update(b)[e]()\n                    }\n                },\n                _0x38ba77 = function () {\n                    var e = _0x4e9930(\"hex\");\n                    _0x54d876 && (e = _0x474989(e)), e.create = function () {\n                        return new _0x5887c8\n                    }, e.update = function (b) {\n                        return e.create().update(b)\n                    };\n                    for (var b = 0; b < _0x323604.length; ++b) {\n                        var a = _0x323604[b];\n                        e[a] = _0x4e9930(a)\n                    }\n                    return e\n                },\n                _0x474989 = function (_0x57eeaa) {\n                    var _0x114910, _0x226465 = eval(\"require('crypto');\"),\n                        _0x1f6ae0 = eval(\"require('buffer')['Buffer'];\");\n                    return function (e) {\n                        if (\"string\" == typeof e) return _0x226465.createHash(\"md5\").update(e, \"utf8\").digest(\"hex\");\n                        if (null == e) throw _0x250d36;\n                        return e.constructor === ArrayBuffer && (e = new Uint8Array(e)), Array.isArray(e) || ArrayBuffer.isView(e) || e.constructor === _0x1f6ae0 ? _0x226465.createHash(\"md5\").update(new _0x1f6ae0.from(e)).digest(\"hex\") : _0x57eeaa(e)\n                    }\n                };\n\n            function _0x5887c8(e) {\n                if (e) _0x4b59e0[0] = _0x4b59e0[16] = _0x4b59e0[1] = _0x4b59e0[2] = _0x4b59e0[3] = _0x4b59e0[4] = _0x4b59e0[5] = _0x4b59e0[6] = _0x4b59e0[7] = _0x4b59e0[8] = _0x4b59e0[9] = _0x4b59e0[10] = _0x4b59e0[11] = _0x4b59e0[12] = _0x4b59e0[13] = _0x4b59e0[14] = _0x4b59e0[15] = 0, this.blocks = _0x4b59e0, this.buffer8 = _0x185caf;\n                else if (_0x2de28f) {\n                    var b = new ArrayBuffer(68);\n                    this.buffer8 = new Uint8Array(b), this.blocks = new Uint32Array(b)\n                } else this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];\n                this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0, this.finalized = this.hashed = !1, this.first = !0\n            }\n\n            _0x5887c8.prototype.update = function (e) {\n                if (!this.finalized) {\n                    var b, a = typeof e;\n                    if (\"string\" !== a) {\n                        if (\"object\" !== a || null === e) throw _0x250d36;\n                        if (_0x2de28f && e.constructor === ArrayBuffer) e = new Uint8Array(e);\n                        else if (!(Array.isArray(e) || _0x2de28f && ArrayBuffer.isView(e))) throw _0x250d36;\n                        b = !0\n                    }\n                    for (var f, c, r = 0, t = e.length, d = this.blocks, i = this.buffer8; r < t;) {\n                        if (this.hashed && (this.hashed = !1, d[0] = d[16], d[16] = d[1] = d[2] = d[3] = d[4] = d[5] = d[6] = d[7] = d[8] = d[9] = d[10] = d[11] = d[12] = d[13] = d[14] = d[15] = 0), b)\n                            if (_0x2de28f)\n                                for (c = this.start; r < t && c < 64; ++r) i[c++] = e[r];\n                            else\n                                for (c = this.start; r < t && c < 64; ++r) d[c >> 2] |= e[r] << _0x20b37e[3 & c++];\n                        else if (_0x2de28f)\n                            for (c = this.start; r < t && c < 64; ++r) (f = e.charCodeAt(r)) < 128 ? i[c++] = f : f < 2048 ? (i[c++] = 192 | f >> 6, i[c++] = 128 | 63 & f) : f < 55296 || f >= 57344 ? (i[c++] = 224 | f >> 12, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), i[c++] = 240 | f >> 18, i[c++] = 128 | f >> 12 & 63, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f);\n                        else\n                            for (c = this.start; r < t && c < 64; ++r) (f = e.charCodeAt(r)) < 128 ? d[c >> 2] |= f << _0x20b37e[3 & c++] : f < 2048 ? (d[c >> 2] |= (192 | f >> 6) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : f < 55296 || f >= 57344 ? (d[c >> 2] |= (224 | f >> 12) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), d[c >> 2] |= (240 | f >> 18) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 12 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]);\n                        this.lastByteIndex = c, this.bytes += c - this.start, c >= 64 ? (this.start = c - 64, this.hash(), this.hashed = !0) : this.start = c\n                    }\n                    return this.bytes > 4294967295 && (this.hBytes += this.bytes / 4294967296 << 0, this.bytes = this.bytes % 4294967296), this\n                }\n            }, _0x5887c8.prototype.finalize = function () {\n                if (!this.finalized) {\n                    this.finalized = !0;\n                    var e = this.blocks,\n                        b = this.lastByteIndex;\n                    e[b >> 2] |= _0x465562[3 & b], b >= 56 && (this.hashed || this.hash(), e[0] = e[16], e[16] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = e[9] = e[10] = e[11] = e[12] = e[13] = e[14] = e[15] = 0), e[14] = this.bytes << 3, e[15] = this.hBytes << 3 | this.bytes >>> 29, this.hash()\n                }\n            }, _0x5887c8.prototype.hash = function () {\n                var e, b, a, f, c, r, t = this.blocks;\n                this.first ? b = ((b = ((e = ((e = t[0] - 680876937) << 7 | e >>> 25) - 271733879 << 0) ^ (a = ((a = (-271733879 ^ (f = ((f = (-1732584194 ^ 2004318071 & e) + t[1] - 117830708) << 12 | f >>> 20) + e << 0) & (-271733879 ^ e)) + t[2] - 1126478375) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1316259209) << 22 | b >>> 10) + a << 0 : (e = this.h0, b = this.h1, a = this.h2, b = ((b += ((e = ((e += ((f = this.h3) ^ b & (a ^ f)) + t[0] - 680876936) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[1] - 389564586) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[2] + 606105819) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1044525330) << 22 | b >>> 10) + a << 0), b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[4] - 176418897) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[5] + 1200080426) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[6] - 1473231341) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[7] - 45705983) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[8] + 1770035416) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[9] - 1958414417) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[10] - 42063) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[11] - 1990404162) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[12] + 1804603682) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[13] - 40341101) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[14] - 1502002290) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[15] + 1236535329) << 22 | b >>> 10) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[1] - 165796510) << 5 | e >>> 27) + b << 0) ^ b)) + t[6] - 1069501632) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[11] + 643717713) << 14 | a >>> 18) + f << 0) ^ f)) + t[0] - 373897302) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[5] - 701558691) << 5 | e >>> 27) + b << 0) ^ b)) + t[10] + 38016083) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[15] - 660478335) << 14 | a >>> 18) + f << 0) ^ f)) + t[4] - 405537848) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[9] + 568446438) << 5 | e >>> 27) + b << 0) ^ b)) + t[14] - 1019803690) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[3] - 187363961) << 14 | a >>> 18) + f << 0) ^ f)) + t[8] + 1163531501) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[13] - 1444681467) << 5 | e >>> 27) + b << 0) ^ b)) + t[2] - 51403784) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[7] + 1735328473) << 14 | a >>> 18) + f << 0) ^ f)) + t[12] - 1926607734) << 20 | b >>> 12) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[5] - 378558) << 4 | e >>> 28) + b << 0)) + t[8] - 2022574463) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[11] + 1839030562) << 16 | a >>> 16) + f << 0)) + t[14] - 35309556) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[1] - 1530992060) << 4 | e >>> 28) + b << 0)) + t[4] + 1272893353) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[7] - 155497632) << 16 | a >>> 16) + f << 0)) + t[10] - 1094730640) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[13] + 681279174) << 4 | e >>> 28) + b << 0)) + t[0] - 358537222) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[3] - 722521979) << 16 | a >>> 16) + f << 0)) + t[6] + 76029189) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[9] - 640364487) << 4 | e >>> 28) + b << 0)) + t[12] - 421815835) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[15] + 530742520) << 16 | a >>> 16) + f << 0)) + t[2] - 995338651) << 23 | b >>> 9) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[0] - 198630844) << 6 | e >>> 26) + b << 0) | ~a)) + t[7] + 1126891415) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[14] - 1416354905) << 15 | a >>> 17) + f << 0) | ~e)) + t[5] - 57434055) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[12] + 1700485571) << 6 | e >>> 26) + b << 0) | ~a)) + t[3] - 1894986606) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[10] - 1051523) << 15 | a >>> 17) + f << 0) | ~e)) + t[1] - 2054922799) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[8] + 1873313359) << 6 | e >>> 26) + b << 0) | ~a)) + t[15] - 30611744) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[6] - 1560198380) << 15 | a >>> 17) + f << 0) | ~e)) + t[13] + 1309151649) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[4] - 145523070) << 6 | e >>> 26) + b << 0) | ~a)) + t[11] - 1120210379) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[2] + 718787259) << 15 | a >>> 17) + f << 0) | ~e)) + t[9] - 343485551) << 21 | b >>> 11) + a << 0, this.first ? (this.h0 = e + 1732584193 << 0, this.h1 = b - 271733879 << 0, this.h2 = a - 1732584194 << 0, this.h3 = f + 271733878 << 0, this.first = !1) : (this.h0 = this.h0 + e << 0, this.h1 = this.h1 + b << 0, this.h2 = this.h2 + a << 0, this.h3 = this.h3 + f << 0)\n            }, _0x5887c8.prototype.hex = function () {\n                this.finalize();\n                var e = this.h0,\n                    b = this.h1,\n                    a = this.h2,\n                    f = this.h3;\n                return _0x3a9a1b[e >> 4 & 15] + _0x3a9a1b[15 & e] + _0x3a9a1b[e >> 12 & 15] + _0x3a9a1b[e >> 8 & 15] + _0x3a9a1b[e >> 20 & 15] + _0x3a9a1b[e >> 16 & 15] + _0x3a9a1b[e >> 28 & 15] + _0x3a9a1b[e >> 24 & 15] + _0x3a9a1b[b >> 4 & 15] + _0x3a9a1b[15 & b] + _0x3a9a1b[b >> 12 & 15] + _0x3a9a1b[b >> 8 & 15] + _0x3a9a1b[b >> 20 & 15] + _0x3a9a1b[b >> 16 & 15] + _0x3a9a1b[b >> 28 & 15] + _0x3a9a1b[b >> 24 & 15] + _0x3a9a1b[a >> 4 & 15] + _0x3a9a1b[15 & a] + _0x3a9a1b[a >> 12 & 15] + _0x3a9a1b[a >> 8 & 15] + _0x3a9a1b[a >> 20 & 15] + _0x3a9a1b[a >> 16 & 15] + _0x3a9a1b[a >> 28 & 15] + _0x3a9a1b[a >> 24 & 15] + _0x3a9a1b[f >> 4 & 15] + _0x3a9a1b[15 & f] + _0x3a9a1b[f >> 12 & 15] + _0x3a9a1b[f >> 8 & 15] + _0x3a9a1b[f >> 20 & 15] + _0x3a9a1b[f >> 16 & 15] + _0x3a9a1b[f >> 28 & 15] + _0x3a9a1b[f >> 24 & 15]\n            }, _0x5887c8.prototype.toString = _0x5887c8.prototype.hex, _0x5887c8.prototype.digest = function () {\n                this.finalize();\n                var e = this.h0,\n                    b = this.h1,\n                    a = this.h2,\n                    f = this.h3;\n                return [255 & e, e >> 8 & 255, e >> 16 & 255, e >> 24 & 255, 255 & b, b >> 8 & 255, b >> 16 & 255, b >> 24 & 255, 255 & a, a >> 8 & 255, a >> 16 & 255, a >> 24 & 255, 255 & f, f >> 8 & 255, f >> 16 & 255, f >> 24 & 255]\n            }, _0x5887c8.prototype.array = _0x5887c8.prototype.digest, _0x5887c8.prototype.arrayBuffer = function () {\n                this.finalize();\n                var e = new ArrayBuffer(16),\n                    b = new Uint32Array(e);\n                return b[0] = this.h0, b[1] = this.h1, b[2] = this.h2, b[3] = this.h3, e\n            }, _0x5887c8.prototype.buffer = _0x5887c8.prototype.arrayBuffer, _0x5887c8.prototype.base64 = function () {\n                for (var e, b, a, f = \"\", c = this.array(), r = 0; r < 15;) e = c[r++], b = c[r++], a = c[r++], f += _0x2c185e[e >>> 2] + _0x2c185e[63 & (e << 4 | b >>> 4)] + _0x2c185e[63 & (b << 2 | a >>> 6)] + _0x2c185e[63 & a];\n                return f + (_0x2c185e[(e = c[r]) >>> 2] + _0x2c185e[e << 4 & 63] + \"==\")\n            };\n            var _0x4dd781 = _0x38ba77();\n            _0x17dcbf ? _0x770f81.exports = _0x4dd781 : (_0x1702f9.md5 = _0x4dd781, _0x554fed && (void 0)(function () {\n                return _0x4dd781\n            }))\n        }()\n    });\n\nfunction _0x178cef(e) {\n    return jsvmp(\"484e4f4a403f52430038001eab0015840e8ee21a00000000000000621b000200001d000146000306000e271f001b000200021d00010500121b001b000b021b000b04041d0001071b000b0500000003000126207575757575757575757575757575757575757575757575757575757575757575\", [, , void 0 !== _0x124d1a ? _0x124d1a : void 0, _0x178cef, e])\n}\n\nfor (var _0xb55f3e = {\n    boe: !1,\n    aid: 0,\n    dfp: !1,\n    sdi: !1,\n    enablePathList: [],\n    _enablePathListRegex: [],\n    urlRewriteRules: [],\n    _urlRewriteRules: [],\n    initialized: !1,\n    enableTrack: !1,\n    track: {\n        unitTime: 0,\n        unitAmount: 0,\n        fre: 0\n    },\n    triggerUnload: !1,\n    region: \"\",\n    regionConf: {},\n    umode: 0,\n    v: !1,\n    perf: !1,\n    xxbg: !0\n}, _0x3eaf64 = {\n    debug: function (e, b) {\n        let a = !1;\n        a = !1\n    }\n}, _0x233455 = [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\", \"a\", \"b\", \"c\", \"d\", \"e\", \"f\"], _0x2e9f6d = [], _0x511f86 = [], _0x3d35de = 0; _0x3d35de < 256; _0x3d35de++) _0x2e9f6d[_0x3d35de] = _0x233455[_0x3d35de >> 4 & 15] + _0x233455[15 & _0x3d35de], _0x3d35de < 16 && (_0x3d35de < 10 ? _0x511f86[48 + _0x3d35de] = _0x3d35de : _0x511f86[87 + _0x3d35de] = _0x3d35de);\nvar _0x2ce54d = function (e) {\n        for (var b = e.length, a = \"\", f = 0; f < b;) a += _0x2e9f6d[e[f++]];\n        return a\n    },\n    _0x5960a2 = function (e) {\n        for (var b = e.length >> 1, a = b << 1, f = new Uint8Array(b), c = 0, r = 0; r < a;) f[c++] = _0x511f86[e.charCodeAt(r++)] << 4 | _0x511f86[e.charCodeAt(r++)];\n        return f\n    },\n    _0x4e46b6 = {\n        encode: _0x2ce54d,\n        decode: _0x5960a2\n    };\n\nfunction sign(e, b) {\n    return jsvmp(\"484e4f4a403f5243001f240fbf2031ccf317480300000000000007181b0002012e1d00921b000b171b000b02402217000a1c1b000b1726402217000c1c1b000b170200004017002646000306000e271f001b000200021d00920500121b001b000b031b000b17041d0092071b000b041e012f17000d1b000b05260a0000101c1b000b06260a0000101c1b001b000b071e01301d00931b001b000b081e00081d00941b0048021d00951b001b000b1b1d00961b0048401d009e1b001b000b031b000b16041d009f1b001b000b09221e0131241b000b031b000b09221e0131241b000b1e0a000110040a0001101d00d51b001b000b09221e0131241b000b031b000b09221e0131241b000b180a000110040a0001101d00d71b001b000b0a1e00101d00d91b001b000b0b261b000b1a1b000b190a0002101d00db1b001b000b0c261b000b221b000b210a0002101d00dc1b001b000b0d261b000b230200200a0002101d00dd1b001b000b09221e0131241b000b031b000b24040a0001101d00df1b001b000b0e1a00221e00de240a0000104903e82b1d00e31b001b000b0f260a0000101d00e41b001b000b1d1d00e71b001b000b1a4901002b1d00e81b001b000b1a4901002c1d00ea1b001b000b191d00f21b001b000b1f480e191d00f81b001b000b1f480f191d00f91b001b000b20480e191d00fb1b001b000b20480f191d00fe1b001b000b25480e191d01001b001b000b25480f191d01011b001b000b264818344900ff2f1d01031b001b000b264810344900ff2f1d01321b001b000b264808344900ff2f1d01331b001b000b264800344900ff2f1d01341b001b000b274818344900ff2f1d01351b001b000b274810344900ff2f1d01361b001b000b274808344900ff2f1d01371b001b000b274800344900ff2f1d01381b001b000b281b000b29311b000b2a311b000b2b311b000b2c311b000b2d311b000b2e311b000b2f311b000b30311b000b31311b000b32311b000b33311b000b34311b000b35311b000b36311b000b37311b000b38311b000b39311d01391b004900ff1d013a1b001b000b10261b000b281b000b2a1b000b2c1b000b2e1b000b301b000b321b000b341b000b361b000b381b000b3a1b000b291b000b2b1b000b2d1b000b2f1b000b311b000b331b000b351b000b371b000b390a0013101d013b1b001b000b0c261b000b111b000b3b041b000b3c0a0002101d013c1b001b000b12261b000b1c1b000b3b1b000b3d0a0003101d013d1b001b000b13261b000b3e0200240a0002101d013e1b000b3f0000013f000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b726152670f487c717976706733447a7d777c644e08577c70667e767d6712487c7179767067335d72657a7472677c614e057960777c7e10487c7179767067335b7a60677c616a4e07637f66747a7d60084c637b727d677c7e0b70727f7f437b727d677c7e0b4c4c7d7a747b677e726176055266777a7c1850727d65726041767d7776617a7d74507c7d67766b6721570964767177617a657661137476675c647d43617c637661676a5d727e7660097f727d74667274766006707b617c7e760761667d677a7e7607707c7d7d767067144c4c64767177617a6576614c7665727f66726776134c4c60767f767d7a667e4c7665727f667267761b4c4c64767177617a6576614c6070617a63674c75667d70677a7c7d174c4c64767177617a6576614c6070617a63674c75667d70154c4c64767177617a6576614c6070617a63674c757d134c4c756b77617a6576614c7665727f66726776124c4c77617a6576614c667d64617263637677154c4c64767177617a6576614c667d64617263637677114c4c77617a6576614c7665727f66726776144c4c60767f767d7a667e4c667d64617263637677144c4c756b77617a6576614c667d64617263637677094c60767f767d7a667e0c70727f7f40767f767d7a667e164c40767f767d7a667e4c5a57564c4176707c6177766108777c70667e767d670478766a60057e7267707b06417674566b630a4f3748723e694e77704c067072707b764c04607c7e7608707675407b72616308507675407b72616305767c72637a16767c44767151617c64607661577a60637267707b76610f717a7d775c717976706752606a7d700e7a60565c44767151617c646076610120047c63767d0467766067097a7d707c747d7a677c077c7d7661617c6104707c77761242465c47524c564b5056565756574c5641410e607660607a7c7d40677c61727476076076675a67767e10607c7e7658766a5b766176516a6776770a61767e7c65765a67767e097a7d77766b767757510c437c7a7d6776615665767d670e5e40437c7a7d6776615665767d670d706176726776567f767e767d670670727d65726009677c5772677246415f076176637f727076034f603901740a7d72677a6576707c777614487c717976706733437f66747a7d526161726a4e4a4d7b676763602c294f3c4f3c3b48233e2a4e68223f206e3b4f3d48233e2a4e68223f206e3a68206e6f48723e75233e2a4e68223f276e3b2948723e75233e2a4e68223f276e3a68246e3a0127087f7c7072677a7c7d047b61767504757a7f76107b676763293c3c7f7c70727f7b7c606708637f7267757c617e02222102222007647a7d777c646002222703647a7d02222607727d77617c7a77022225057f7a7d666b022224067a637b7c7d7602222b047a63727702222a047a637c77022123037e7270022122097e72707a7d677c607b0c7e72704c637c64766163703a0470617c60036b22220570617a7c6005756b7a7c6004637a787602212102212002212702212602212502212402212b08757a6176757c6b3c067c637661723c05337c63613c05337c63673c07707b617c7e763c0867617a77767d673c047e607a7602212a0220230665767d777c6106547c7c747f760e4c637261727e40647a67707b5c7d0a777a61767067407a747d0a707c7d607a6067767d670660647a67707b03777c7e07637b727d677c7e047b7c7c7840525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e3d03727a77017d01750161096067726167477a7e7601670972717a7f7a677a76600a677a7e766067727e6322137b72617764726176507c7d70666161767d706a0c7776657a70765e767e7c616a087f727d74667274760a6176607c7f66677a7c7d0f7265727a7f4176607c7f66677a7c7d0960706176767d477c630a60706176767d5f767567107776657a7076437a6b767f4172677a7c0a63617c77667067406671077172676776616a016309677c66707b5a7d757c08677a7e76697c7d760a677a7e766067727e6321077463665a7d757c0b7960557c7d67605f7a60670b637f66747a7d605f7a60670a677a7e766067727e63200a76657661507c7c787a760767674c60707a77017e0b606a7d67726b5661617c610c7d72677a65765f767d74677b056167705a43097563457661607a7c7d0b4c4c657661607a7c7d4c4c08707f7a767d675a770a677a7e766067727e63270b766b67767d77557a767f77046366607b03727f7f04677b767d097172607625274c707b0c75617c7e507b7261507c7776067125274c2023022022087172607625274c23022021087172607625274c22022020087172607625274c2102202702202602202507747667477a7e760220240b777c7e5d7c6745727f7a77096066716067617a7d740863617c677c707c7f02202b02202a01230e222323232323232322222323232302272302272207757c616176727f02272104717c776a096067617a7d747a756a02686e0b717c776a45727f216067610a717c776a4c7b72607b2e01350366617f02272005626676616a0a72607c7f774c607a747d096372677b7d727e762e0967674c6476717a772e063566667a772e0227270227260e4c716a6776774c6076704c777a770227250a27212a272a2524212a25097576457661607a7c7d0227240e4c232151274925647c232323232202272b02272a05607f7a7076022623074056505a5d555c037d7c6409677a7e766067727e6305757f7c7c610661727d777c7e0f7476674747447671507c7c787a7660056767647a770867674c6476717a770767674476715a770b67674c6476717a774c65210967674476717a7745210761667d7d7a7d7405757f66607b087e7c65765f7a60670660637f7a70760671765e7c657609707f7a70785f7a6067077176507f7a70780c78766a717c7261775f7a60670a717658766a717c7261770b7270677a657640677267760b647a7d777c6440677267760360477e05676172707808667d7a67477a7e76037270700a667d7a67527e7c667d670871767b72657a7c61077e6074476a637603645a5707727a775f7a60670b63617a6572706a5e7c777606706660677c7e067260607a747d0f4456514c5756455a50564c5a5d555c0479607c7d0a6176747a7c7d507c7d75096176637c616746617f04766b7a67094b3e5e403e404746510c4b3e5e403e43524a5f5c525720232323232323232323232323232323232323232323232323232323232323232320772722772b70772a2b75232371212327762a2b23232a2a2b7670752b272124760165066671707c7776067776707c777602262202262102262002262702262602262502262402262b02262a022523022522022521022520\", [, , void 0, void 0 !== _0x178cef ? _0x178cef : void 0, {\n        boe: !1,\n        aid: 0,\n        dfp: !1,\n        sdi: !1,\n        enablePathList: [],\n        _enablePathListRegex: [/\\/web\\/report/],\n        urlRewriteRules: [],\n        _urlRewriteRules: [],\n        initialized: !1,\n        enableTrack: !1,\n        track: {\n            unitTime: 0,\n            unitAmount: 0,\n            fre: 0\n        },\n        triggerUnload: !1,\n        region: \"\",\n        regionConf: {},\n        umode: 0,\n        v: !1,\n        perf: !1,\n        xxbg: !0\n    }, () => 0, () => \"03v\", {\n        ubcode: 0\n    }, {\n        bogusIndex: 0,\n        msNewTokenList: [],\n        moveList: [],\n        clickList: [],\n        keyboardList: [],\n        activeState: [],\n        aidList: [],\n        envcode: 0,\n        msToken: \"\",\n        msStatus: 0,\n        __ac_testid: \"\",\n        ttwid: \"\",\n        tt_webid: \"\",\n        tt_webid_v2: \"\"\n    }, void 0 !== _0x4e46b6 ? _0x4e46b6 : void 0, {\n        userAgent: b\n    }, (e, b) => {\n        let a = new Uint8Array(3);\n        return a[0] = e / 256, a[1] = e % 256, a[2] = b % 256, String.fromCharCode.apply(null, a)\n    }, (e, b) => {\n        let a, f = [],\n            c = 0,\n            r = \"\";\n        for (let e = 0; e < 256; e++) f[e] = e;\n        for (let b = 0; b < 256; b++) c = (c + f[b] + e.charCodeAt(b % e.length)) % 256, a = f[b], f[b] = f[c], f[c] = a;\n        let t = 0;\n        c = 0;\n        for (let e = 0; e < b.length; e++) c = (c + f[t = (t + 1) % 256]) % 256, a = f[t], f[t] = f[c], f[c] = a, r += String.fromCharCode(b.charCodeAt(e) ^ f[(f[t] + f[c]) % 256]);\n        return r\n    }, (e, b) => jsvmp(\"484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267\", [, , , , e, b]), \"undefined\" != typeof Date ? Date : void 0, () => 0, (e, b, a, f, c, r, t, d, i, n, s, o, l, _, x, u, h, p, y) => {\n        let v = new Uint8Array(19);\n        return v[0] = e, v[1] = s, v[2] = b, v[3] = o, v[4] = a, v[5] = l, v[6] = f, v[7] = _, v[8] = c, v[9] = x, v[10] = r, v[11] = u, v[12] = t, v[13] = h, v[14] = d, v[15] = p, v[16] = i, v[17] = y, v[18] = n, String.fromCharCode.apply(null, v)\n    }, e => String.fromCharCode(e), (e, b, a) => String.fromCharCode(e) + String.fromCharCode(b) + a, (e, b) => jsvmp(\"484e4f4a403f524300281018f7b851f02d296e5b00000000000004a21b0002001d1d001e1b00131e00061a001d001f1b000b070200200200210d1b000b070200220200230d1b000b070200240200250d1b000b070200260200270d1b001b000b071b000b05191d00031b000200001d00281b0048001d00291b000b041e002a1b000b0b4803283b1700f11b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f480833301b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b08221e002d241b000b0a490fc02f4806340a000110281d00281b00220b091b000b08221e002d241b000b0a483f2f0a000110281d002816ff031b000b041e002a1b000b0b294800391700e01b001b000b04221e002b241b001e0029222d1b00241d00290a0001104900ff2f4810331b000b041e002a1b000b0b3917001e1b000b04221e002b241b000b0b0a0001104900ff2f4808331600054800301d002c1b00220b091b000b08221e002d241b000b0a4a00fc00002f4812340a000110281d00281b00220b091b000b08221e002d241b000b0a4a0003f0002f480c340a000110281d00281b00220b091b000b041e002a1b000b0b3917001e1b000b08221e002d241b000b0a490fc02f4806340a0001101600071b000b06281d00281b00220b091b000b06281d00281b000b090000002e000126207575757575757575757575757575757575757575757575757575757575757575012b0e7776757a7d7643617c637661676a027a77065c717976706708777671667474766107767d65707c77760374766707707c7d607c7f7607757a61767166740a7c66677661447a77677b0a7a7d7d7661447a77677b0b7c666776615b767a747b670b7a7d7d76615b767a747b6709666076615274767d670b677c5f7c64766150726076077a7d77766b5c7508767f767067617c7d09667d7776757a7d76770963617c677c676a637608677c4067617a7d740470727f7f0763617c7076606010487c71797670673363617c707660604e067c717976706705677a677f76047d7c7776012e0125012402602341525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a383c2e0260224157787763747b2749586042512b233c5e75656420254b5a22412126384446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e0260214157787763747b2749586042512b233c5e75656420254b5a224121263e4446527f567a245d5f717c624a475c4366697e5579597d616a6b2a5b45547072406750762e02602041525150575655545b5a59585f5e5d5c43424140474645444b4a49727170777675747b7a79787f7e7d7c63626160676665646b6a6923222120272625242b2a3e4c2e012a022222067f767d74677b0a707b7261507c7776526702222306707b72615267\", [, , , , e, b]), , sign, e, void 0])\n}\n\nmodule.exports = {\n    sign\n};\n"
  },
  {
    "path": "static/js/a_bogus.js",
    "content": "// All the content in this article is only for learning and communication use, not for any other purpose, strictly prohibited for commercial use and illegal use, otherwise all the consequences are irrelevant to the author!\nfunction rc4_encrypt(plaintext, key) {\n    var s = [];\n    for (var i = 0; i < 256; i++) {\n        s[i] = i;\n    }\n    var j = 0;\n    for (var i = 0; i < 256; i++) {\n        j = (j + s[i] + key.charCodeAt(i % key.length)) % 256;\n        var temp = s[i];\n        s[i] = s[j];\n        s[j] = temp;\n    }\n\n    var i = 0;\n    var j = 0;\n    var cipher = [];\n    for (var k = 0; k < plaintext.length; k++) {\n        i = (i + 1) % 256;\n        j = (j + s[i]) % 256;\n        var temp = s[i];\n        s[i] = s[j];\n        s[j] = temp;\n        var t = (s[i] + s[j]) % 256;\n        cipher.push(String.fromCharCode(s[t] ^ plaintext.charCodeAt(k)));\n    }\n    return cipher.join('');\n}\n\nfunction le(e, r) {\n    return (e << (r %= 32) | e >>> 32 - r) >>> 0\n}\n\nfunction de(e) {\n    return 0 <= e && e < 16 ? 2043430169 : 16 <= e && e < 64 ? 2055708042 : void console['error'](\"invalid j for constant Tj\")\n}\n\nfunction pe(e, r, t, n) {\n    return 0 <= e && e < 16 ? (r ^ t ^ n) >>> 0 : 16 <= e && e < 64 ? (r & t | r & n | t & n) >>> 0 : (console['error']('invalid j for bool function FF'),\n        0)\n}\n\nfunction he(e, r, t, n) {\n    return 0 <= e && e < 16 ? (r ^ t ^ n) >>> 0 : 16 <= e && e < 64 ? (r & t | ~r & n) >>> 0 : (console['error']('invalid j for bool function GG'),\n        0)\n}\n\nfunction reset() {\n    this.reg[0] = 1937774191,\n        this.reg[1] = 1226093241,\n        this.reg[2] = 388252375,\n        this.reg[3] = 3666478592,\n        this.reg[4] = 2842636476,\n        this.reg[5] = 372324522,\n        this.reg[6] = 3817729613,\n        this.reg[7] = 2969243214,\n        this[\"chunk\"] = [],\n        this[\"size\"] = 0\n}\n\nfunction write(e) {\n    var a = \"string\" == typeof e ? function (e) {\n        n = encodeURIComponent(e)['replace'](/%([0-9A-F]{2})/g, (function (e, r) {\n                return String['fromCharCode'](\"0x\" + r)\n            }\n        ))\n            , a = new Array(n['length']);\n        return Array['prototype']['forEach']['call'](n, (function (e, r) {\n                a[r] = e.charCodeAt(0)\n            }\n        )),\n            a\n    }(e) : e;\n    this.size += a.length;\n    var f = 64 - this['chunk']['length'];\n    if (a['length'] < f)\n        this['chunk'] = this['chunk'].concat(a);\n    else\n        for (this['chunk'] = this['chunk'].concat(a.slice(0, f)); this['chunk'].length >= 64;)\n            this['_compress'](this['chunk']),\n                f < a['length'] ? this['chunk'] = a['slice'](f, Math['min'](f + 64, a['length'])) : this['chunk'] = [],\n                f += 64\n}\n\nfunction sum(e, t) {\n    e && (this['reset'](),\n        this['write'](e)),\n        this['_fill']();\n    for (var f = 0; f < this.chunk['length']; f += 64)\n        this._compress(this['chunk']['slice'](f, f + 64));\n    var i = null;\n    if (t == 'hex') {\n        i = \"\";\n        for (f = 0; f < 8; f++)\n            i += se(this['reg'][f]['toString'](16), 8, \"0\")\n    } else\n        for (i = new Array(32),\n                 f = 0; f < 8; f++) {\n            var c = this.reg[f];\n            i[4 * f + 3] = (255 & c) >>> 0,\n                c >>>= 8,\n                i[4 * f + 2] = (255 & c) >>> 0,\n                c >>>= 8,\n                i[4 * f + 1] = (255 & c) >>> 0,\n                c >>>= 8,\n                i[4 * f] = (255 & c) >>> 0\n        }\n    return this['reset'](),\n        i\n}\n\nfunction _compress(t) {\n    if (t < 64)\n        console.error(\"compress error: not enough data\");\n    else {\n        for (var f = function (e) {\n            for (var r = new Array(132), t = 0; t < 16; t++)\n                r[t] = e[4 * t] << 24,\n                    r[t] |= e[4 * t + 1] << 16,\n                    r[t] |= e[4 * t + 2] << 8,\n                    r[t] |= e[4 * t + 3],\n                    r[t] >>>= 0;\n            for (var n = 16; n < 68; n++) {\n                var a = r[n - 16] ^ r[n - 9] ^ le(r[n - 3], 15);\n                a = a ^ le(a, 15) ^ le(a, 23),\n                    r[n] = (a ^ le(r[n - 13], 7) ^ r[n - 6]) >>> 0\n            }\n            for (n = 0; n < 64; n++)\n                r[n + 68] = (r[n] ^ r[n + 4]) >>> 0;\n            return r\n        }(t), i = this['reg'].slice(0), c = 0; c < 64; c++) {\n            var o = le(i[0], 12) + i[4] + le(de(c), c)\n                , s = ((o = le(o = (4294967295 & o) >>> 0, 7)) ^ le(i[0], 12)) >>> 0\n                , u = pe(c, i[0], i[1], i[2]);\n            u = (4294967295 & (u = u + i[3] + s + f[c + 68])) >>> 0;\n            var b = he(c, i[4], i[5], i[6]);\n            b = (4294967295 & (b = b + i[7] + o + f[c])) >>> 0,\n                i[3] = i[2],\n                i[2] = le(i[1], 9),\n                i[1] = i[0],\n                i[0] = u,\n                i[7] = i[6],\n                i[6] = le(i[5], 19),\n                i[5] = i[4],\n                i[4] = (b ^ le(b, 9) ^ le(b, 17)) >>> 0\n        }\n        for (var l = 0; l < 8; l++)\n            this['reg'][l] = (this['reg'][l] ^ i[l]) >>> 0\n    }\n}\n\nfunction _fill() {\n    var a = 8 * this['size']\n        , f = this['chunk']['push'](128) % 64;\n    for (64 - f < 8 && (f -= 64); f < 56; f++)\n        this.chunk['push'](0);\n    for (var i = 0; i < 4; i++) {\n        var c = Math['floor'](a / 4294967296);\n        this['chunk'].push(c >>> 8 * (3 - i) & 255)\n    }\n    for (i = 0; i < 4; i++)\n        this['chunk']['push'](a >>> 8 * (3 - i) & 255)\n\n}\n\nfunction SM3() {\n    this.reg = [];\n    this.chunk = [];\n    this.size = 0;\n    this.reset()\n}\nSM3.prototype.reset = reset;\nSM3.prototype.write = write;\nSM3.prototype.sum = sum;\nSM3.prototype._compress = _compress;\nSM3.prototype._fill = _fill;\n\nfunction result_encrypt(long_str, num = null) {\n    let s_obj = {\n        \"s0\": \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\",\n        \"s1\": \"Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=\",\n        \"s2\": \"Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=\",\n        \"s3\": \"ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe\",\n        \"s4\": \"Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe\"\n    }\n    let constant = {\n        \"0\": 16515072,\n        \"1\": 258048,\n        \"2\": 4032,\n        \"str\": s_obj[num],\n    }\n\n    let result = \"\";\n    let lound = 0;\n    let long_int = get_long_int(lound, long_str);\n    for (let i = 0; i < long_str.length / 3 * 4; i++) {\n        if (Math.floor(i / 4) !== lound) {\n            lound += 1;\n            long_int = get_long_int(lound, long_str);\n        }\n        let key = i % 4;\n        switch (key) {\n            case 0:\n                temp_int = (long_int & constant[\"0\"]) >> 18;\n                result += constant[\"str\"].charAt(temp_int);\n                break;\n            case 1:\n                temp_int = (long_int & constant[\"1\"]) >> 12;\n                result += constant[\"str\"].charAt(temp_int);\n                break;\n            case 2:\n                temp_int = (long_int & constant[\"2\"]) >> 6;\n                result += constant[\"str\"].charAt(temp_int);\n                break;\n            case 3:\n                temp_int = long_int & 63;\n                result += constant[\"str\"].charAt(temp_int);\n                break;\n            default:\n                break;\n        }\n    }\n    return result;\n}\n\nfunction get_long_int(round, long_str) {\n    round = round * 3;\n    return (long_str.charCodeAt(round) << 16) | (long_str.charCodeAt(round + 1) << 8) | (long_str.charCodeAt(round + 2));\n}\n\nfunction gener_random(random, option) {\n    return [\n        (random & 255 & 170) | option[0] & 85, // 163\n        (random & 255 & 85) | option[0] & 170, //87\n        (random >> 8 & 255 & 170) | option[1] & 85, //37\n        (random >> 8 & 255 & 85) | option[1] & 170, //41\n    ]\n}\n\n//////////////////////////////////////////////\nfunction generate_rc4_bb_str(url_search_params, user_agent, window_env_str, suffix = \"cus\", Arguments = [0, 1, 14]) {\n    let sm3 = new SM3()\n    let start_time = Date.now()\n    /**\n     * 进行3次加密处理\n     * 1: url_search_params两次sm3之的结果\n     * 2: 对后缀两次sm3之的结果\n     * 3: 对ua处理之后的结果\n     */\n        // url_search_params两次sm3之的结果\n    let url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix))\n    // 对后缀两次sm3之的结果\n    let cus = sm3.sum(sm3.sum(suffix))\n    // 对ua处理之后的结果\n    let ua = sm3.sum(result_encrypt(rc4_encrypt(user_agent, String.fromCharCode.apply(null, [0.00390625, 1, 14])), \"s3\"))\n    //\n    let end_time = Date.now()\n    // b\n    let b = {\n        8: 3, // 固定\n        10: end_time, //3次加密结束时间\n        15: {\n            \"aid\": 6383,\n            \"pageId\": 6241,\n            \"boe\": false,\n            \"ddrt\": 7,\n            \"paths\": {\n                \"include\": [\n                    {},\n                    {},\n                    {},\n                    {},\n                    {},\n                    {},\n                    {}\n                ],\n                \"exclude\": []\n            },\n            \"track\": {\n                \"mode\": 0,\n                \"delay\": 300,\n                \"paths\": []\n            },\n            \"dump\": true,\n            \"rpU\": \"\"\n        },\n        16: start_time, //3次加密开始时间\n        18: 44, //固定\n        19: [1, 0, 1, 5],\n    }\n\n    //3次加密开始时间\n    b[20] = (b[16] >> 24) & 255\n    b[21] = (b[16] >> 16) & 255\n    b[22] = (b[16] >> 8) & 255\n    b[23] = b[16] & 255\n    b[24] = (b[16] / 256 / 256 / 256 / 256) >> 0\n    b[25] = (b[16] / 256 / 256 / 256 / 256 / 256) >> 0\n\n    // 参数Arguments [0, 1, 14, ...]\n    // let Arguments = [0, 1, 14]\n    b[26] = (Arguments[0] >> 24) & 255\n    b[27] = (Arguments[0] >> 16) & 255\n    b[28] = (Arguments[0] >> 8) & 255\n    b[29] = Arguments[0] & 255\n\n    b[30] = (Arguments[1] / 256) & 255\n    b[31] = (Arguments[1] % 256) & 255\n    b[32] = (Arguments[1] >> 24) & 255\n    b[33] = (Arguments[1] >> 16) & 255\n\n    b[34] = (Arguments[2] >> 24) & 255\n    b[35] = (Arguments[2] >> 16) & 255\n    b[36] = (Arguments[2] >> 8) & 255\n    b[37] = Arguments[2] & 255\n\n    // (url_search_params + \"cus\") 两次sm3之的结果\n    /**let url_search_params_list = [\n     91, 186,  35,  86, 143, 253,   6,  76,\n     34,  21, 167, 148,   7,  42, 192, 219,\n     188,  20, 182,  85, 213,  74, 213, 147,\n     37, 155,  93, 139,  85, 118, 228, 213\n     ]*/\n    b[38] = url_search_params_list[21]\n    b[39] = url_search_params_list[22]\n\n    // (\"cus\") 对后缀两次sm3之的结果\n    /**\n     * let cus = [\n     136, 101, 114, 147,  58,  77, 207, 201,\n     215, 162, 154,  93, 248,  13, 142, 160,\n     105,  73, 215, 241,  83,  58,  51,  43,\n     255,  38, 168, 141, 216, 194,  35, 236\n     ]*/\n    b[40] = cus[21]\n    b[41] = cus[22]\n\n    // 对ua处理之后的结果\n    /**\n     * let ua = [\n     129, 190,  70, 186,  86, 196, 199,  53,\n     99,  38,  29, 209, 243,  17, 157,  69,\n     147, 104,  53,  23, 114, 126,  66, 228,\n     135,  30, 168, 185, 109, 156, 251,  88\n     ]*/\n    b[42] = ua[23]\n    b[43] = ua[24]\n\n    //3次加密结束时间\n    b[44] = (b[10] >> 24) & 255\n    b[45] = (b[10] >> 16) & 255\n    b[46] = (b[10] >> 8) & 255\n    b[47] = b[10] & 255\n    b[48] = b[8]\n    b[49] = (b[10] / 256 / 256 / 256 / 256) >> 0\n    b[50] = (b[10] / 256 / 256 / 256 / 256 / 256) >> 0\n\n\n    // object配置项\n    b[51] = b[15]['pageId']\n    b[52] = (b[15]['pageId'] >> 24) & 255\n    b[53] = (b[15]['pageId'] >> 16) & 255\n    b[54] = (b[15]['pageId'] >> 8) & 255\n    b[55] = b[15]['pageId'] & 255\n\n    b[56] = b[15]['aid']\n    b[57] = b[15]['aid'] & 255\n    b[58] = (b[15]['aid'] >> 8) & 255\n    b[59] = (b[15]['aid'] >> 16) & 255\n    b[60] = (b[15]['aid'] >> 24) & 255\n\n    // 中间进行了环境检测\n    // 代码索引:  2496 索引值:  17 （索引64关键条件）\n    // '1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32'.charCodeAt()得到65位数组\n    /**\n     * let window_env_list = [49, 53, 51, 54, 124, 55, 52, 55, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 48, 124, 51,\n     * 48, 124, 48, 124, 48, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 49, 53, 51, 54, 124, 56,\n     * 54, 52, 124, 49, 53, 50, 53, 124, 55, 52, 55, 124, 50, 52, 124, 50, 52, 124, 87, 105, 110,\n     * 51, 50]\n     */\n    let window_env_list = [];\n    for (let index = 0; index < window_env_str.length; index++) {\n        window_env_list.push(window_env_str.charCodeAt(index))\n    }\n    b[64] = window_env_list.length\n    b[65] = b[64] & 255\n    b[66] = (b[64] >> 8) & 255\n\n    b[69] = [].length\n    b[70] = b[69] & 255\n    b[71] = (b[69] >> 8) & 255\n\n    b[72] = b[18] ^ b[20] ^ b[26] ^ b[30] ^ b[38] ^ b[40] ^ b[42] ^ b[21] ^ b[27] ^ b[31] ^ b[35] ^ b[39] ^ b[41] ^ b[43] ^ b[22] ^\n        b[28] ^ b[32] ^ b[36] ^ b[23] ^ b[29] ^ b[33] ^ b[37] ^ b[44] ^ b[45] ^ b[46] ^ b[47] ^ b[48] ^ b[49] ^ b[50] ^ b[24] ^\n        b[25] ^ b[52] ^ b[53] ^ b[54] ^ b[55] ^ b[57] ^ b[58] ^ b[59] ^ b[60] ^ b[65] ^ b[66] ^ b[70] ^ b[71]\n    let bb = [\n        b[18], b[20], b[52], b[26], b[30], b[34], b[58], b[38], b[40], b[53], b[42], b[21], b[27], b[54], b[55], b[31],\n        b[35], b[57], b[39], b[41], b[43], b[22], b[28], b[32], b[60], b[36], b[23], b[29], b[33], b[37], b[44], b[45],\n        b[59], b[46], b[47], b[48], b[49], b[50], b[24], b[25], b[65], b[66], b[70], b[71]\n    ]\n    bb = bb.concat(window_env_list).concat(b[72])\n    return rc4_encrypt(String.fromCharCode.apply(null, bb), String.fromCharCode.apply(null, [121]));\n}\n\nfunction generate_random_str() {\n    let random_str_list = []\n    random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [3, 45]))\n    random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 0]))\n    random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 5]))\n    return String.fromCharCode.apply(null, random_str_list)\n}\n\nfunction generate_a_bogus(url_search_params, user_agent) {\n    /**\n     * url_search_params：\"device_platform=webapp&aid=6383&channel=channel_pc_web&update_version_code=170400&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Chrome&browser_version=123.0.0.0&browser_online=true&engine_name=Blink&engine_version=123.0.0.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7362810250930783783&msToken=VkDUvz1y24CppXSl80iFPr6ez-3FiizcwD7fI1OqBt6IICq9RWG7nCvxKb8IVi55mFd-wnqoNkXGnxHrikQb4PuKob5Q-YhDp5Um215JzlBszkUyiEvR\"\n     * user_agent：\"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\"\n     */\n    let result_str = generate_random_str() + generate_rc4_bb_str(\n        url_search_params,\n        user_agent,\n        \"1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32\"\n    );\n    return result_encrypt(result_str, \"s4\") + \"=\";\n}\n\n//测试调用\n// console.log(generate_a_bogus(\n//     \"device_platform=webapp&aid=6383&channel=channel_pc_web&update_version_code=170400&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Chrome&browser_version=123.0.0.0&browser_online=true&engine_name=Blink&engine_version=123.0.0.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7362810250930783783&msToken=VkDUvz1y24CppXSl80iFPr6ez-3FiizcwD7fI1OqBt6IICq9RWG7nCvxKb8IVi55mFd-wnqoNkXGnxHrikQb4PuKob5Q-YhDp5Um215JzlBszkUyiEvR\",\n//     \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36\"\n// ));"
  }
]