[
  {
    "path": ".eslintrc.json",
    "content": "{\n\"env\": {\n  \"browser\": true,\n  \"commonjs\": true,\n  \"es6\": true,\n  \"jquery\": true\n}\n}"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report-cn.md",
    "content": "---\nname: Bug 反馈\nabout: 发起一个Bug反馈\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**请确认你已经做过并了解如下步骤，在 `[]` 中填入 `x` 选中**\n- [ ] 仔细看并搜索过[Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki)\n- [ ] 请确认你的版本比当前[Pre-release](https://github.com/pt-plugins/PT-Plugin-Plus/tags)要新，我们建议使用crx版本作为标准\n- [ ] 搜素过Issue，确认没有相关问题\n- [ ] 不理解、询问问题或者使用上的问题请使用Discuissions\n\n<!--\n为了更快解决您的问题，请提供以下信息，谢谢\n-->\n\n- PT 助手版本:\n- PT 助手安装方式：（市场安装，或zip包安装）\n- 浏览器名称及版本：\n- 浏览器是否安装了其他插件：\n- 停用其他插件后是否正常工作：\n- 问题描述：\n\n\n- 相关截图：\n\n\n- 重现步骤：\n\n<!--\n注意：\n1、上传日志或配置信息前，请先删除个人信息！\n2、请按以上格式填写，否则问题会被机器人直接关闭！\n-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n<!--\nIn order to better solve your problem, please provide the following information, thank you\n-->\n\n- PT Plugin Plus version:\n- PT Plugin Plus installation method: (market installation, or zip package installation)\n- Browser name and version:\n- Whether the browser has other plugins installed:\n- Is it working properly after disabling other plugins:\n- Problem Description:\n\n\n- Related screenshots:\n\n\n- Reproduce steps:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request-cn.md",
    "content": "---\nname: 功能请求\nabout: 发起一个新功能请求\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n<!-- \n注意：不要删除模板内容，删除或更改将会被机器人自动关闭 \n-->\n## 您的功能请求是否与问题有关？ 请描述一下。\n<!-- 简明扼要地描述问题所在。 -->\n\n\n## 描述你想要的解决方案\n<!-- 欢迎提供脑洞 -->\n\n\n## 描述您考虑过的替代方案\n<!-- 如果有参考链接，请在此附上 -->\n\n\n## 其他附加信息\n<!-- 您可以添加屏幕截图等信息 -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new-tracker-request-cn.md",
    "content": "---\nname: 新站点请求\nabout: 发起一个新站点支持请求\ntitle: ''\nlabels: 'New Tracker'\nassignees: ''\n\n---\n\n<!-- \n1、在发起请求前，请先确认是否在已支持的架构内；\n2、如果在已支持的架构内，本主题将会被视为无效并关闭；\n3、如果您是开发者，请按 https://github.com/pt-plugins/PT-Plugin-Plus/wiki/developer 文档适配并提交PR；\n4、如果您无法发送邀请，请不要提交，因为开发人员无法对其适配；\n5、如有考核，请确认是否可免考，因为开发人员无法保证完成考核；\n6、如有多个站点需求，请分别发起；\n-->\n\n- 站点名称：\n- 站点地址：\n- 站点描述：\n\n\n- 资源类型：\n- 开放注册：是/否\n- 是否连坐：是/否\n- 站点规则：\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new-tracker-request.md",
    "content": "---\nname: New Tracker Request\nabout: Initiate a new tracker support request\ntitle: ''\nlabels: 'New Tracker'\nassignees: ''\n\n---\n\n<!-- \n1. Before initiating a request, please confirm whether it is within the supported schema;\n2. If within the supported schema, this topic will be considered invalid and closed;\n3. If you are a developer, please follow the https://github.com/pt-plugins/PT-Plugin-Plus/wiki/developer document to adapt and submit the PR;\n4. If you are unable to send the invitation, please do not submit it because the developer cannot adapt it;\n5. If there is an assessment, please confirm whether the exam can be exempted, because the developer cannot guarantee the completion of the assessment;\n6. A topic only initiates a tracker request;\n-->\n\n- Tracker Name: \n- Tracker Url: \n- Tracker Description: \n\n\n- Resource Type: \n- Open registration: Yes / No\n- Tracker rules: \n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/suggestions-or-comments.md",
    "content": "---\nname: Suggestions or comments\nabout: 其他建议或意见\ntitle: ''\nlabels: Suggestions\nassignees: ''\n\n---\n\n- \n\n<!-- \n👆 👆 👆 请在上面描述您的建议或意见，并且不要删除这段内容。\n👆 👆 👆 Please describe your suggestions or comments above and do not delete this content.\n------------------------------------------\n1、如果是功能或新站点请求，将会被关闭，请按对应请求模板发起；\n2、如果是使用问题，请参考帮助文档：\n\nhttps://github.com/pt-plugins/PT-Plugin-Plus/wiki\n -->"
  },
  {
    "path": ".github/issue-close-app.yml",
    "content": "# Comment that will be sent if an issue is judged to be closed\ncomment: \"This issue is closed because it does not meet our issue template. Please read it.\\n 您好，我是问题处理机器人，因为您没有按规定提交问题，所以被我自动关闭了，请按规定填写，谢谢！\"\nissueConfigs:\n  - content:\n      # bug report\n      - \"PT Plugin Plus version\"\n      - \"PT Plugin Plus installation method\"\n      - \"Problem Description\"\n  - content:\n      # bug report cn\n      - \"PT 助手版本\"\n      - \"PT 助手安装方式\"\n      - \"问题描述\"\n  - content:\n      # feature request\n      - \"Is your feature request related to a problem\"\n      - \"Describe the solution you'd like\"\n      - \"Describe alternatives you've considered\"\n  - content:\n      # feature request cn\n      - \"您的功能请求是否与问题有关\"\n      - \"描述你想要的解决方案\"\n      - \"描述您考虑过的替代方案\"\n  - content:\n      # 建议和意见\n      - \"请在上面描述您的建议或意见，并且不要删除这段内容\"\n      - \"Please describe your suggestions or comments\"\n  - content:\n      # 新站点请求\n      - \"站点名称\"\n      - \"站点地址\"\n      - \"站点描述\"\n      - \"资源类型\"\n      - \"开放注册\"\n      - \"是否连坐\"\n      - \"站点规则\"\n  - content:\n      # 新站点请求\n      - \"Tracker Name\"\n      - \"Tracker Url\"\n      - \"Tracker Description\"\n      - \"Resource Type\"\n      - \"Open registration\"\n      - \"Tracker rules\"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "<!-- \n感谢您提交 PR ，为了更好的进行版本迭代，请将目标分支选择为 `base:dev` ，我们会根据实际情况在后续版本中发布。\n\n## 标题请尽量按以下格式进行描述\n\n`<type>(<scope>): <subject>`\n\n## type 说明\n\n- feat: 添加新功能\n- fix: 修补 bug\n- docs: 文档（documentation）\n- style: 格式（不影响代码运行的变动）\n- refactor: 重构（即不是新增功能，也不是修改 bug 的代码变动）\n- test: 增加测试\n- chore: 构建过程或辅助工具的变动\n\n## 参考文档：http://www.ruanyifeng.com/blog/2016/01/commit_message_change_log.html\n\n## 内容说明\n\n请尽量详细描述本次 PR 的具体作用。\n\n本内容仅供提交前查阅，提交时请务必删除这段内容。\n本内容仅供提交前查阅，提交时请务必删除这段内容。\n本内容仅供提交前查阅，提交时请务必删除这段内容。\n -->"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\n# Number of days of inactivity before an Issue or Pull Request becomes stale\ndaysUntilStale: 90\n\n# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.\n# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.\ndaysUntilClose: 7\n\n# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)\nonlyLabels: []\n\n# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable\nexemptLabels:\n  - pinned\n  - security\n  - enhancement\n  - Development\n  - bug\n\n# Set to true to ignore issues in a project (defaults to false)\nexemptProjects: true\n\n# Set to true to ignore issues in a milestone (defaults to false)\nexemptMilestones: true\n\n# Set to true to ignore issues with an assignee (defaults to false)\nexemptAssignees: true\n\n# Label to use when marking as stale\nstaleLabel: stale\n\n# Comment to post when marking as stale. Set to `false` to disable\nmarkComment: |\n  This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.\n\n  此问题已自动标记为陈旧，因为它近期没有活动。如果没有进一步的活动，它将被自动关闭。感谢您的贡献。\n\n# Comment to post when removing the stale label.\n# unmarkComment: >\n#   Your comment here.\n\n# Comment to post when closing a stale Issue or Pull Request.\ncloseComment: |\n  This issue has been automatically closed due to no further feedback. If you need to continue processing, please submit a new issue. Thank you for your contributions.\n\n  此问题因没有进一步反馈已被自动关闭，如需继续处理，请提交新的 issue ，感谢您的贡献。\n\n# Limit the number of actions per hour, from 1-30. Default is 30\nlimitPerRun: 30\n# Limit to only `issues` or `pulls`\n# only: issues\n\n# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':\n# pulls:\n#   daysUntilStale: 30\n#   markComment: >\n#     This pull request has been automatically marked as stale because it has not had\n#     recent activity. It will be closed if no further activity occurs. Thank you\n#     for your contributions.\n\n# issues:\n#   exemptLabels:\n#     - confirmed\n"
  },
  {
    "path": ".github/workflows/build_action.yml",
    "content": "name: Build Action Release\n\non:\n  push:\n    branches: [ dev ]\n  pull_request:\n    branches: [ dev ]\n  workflow_call:\n    inputs:\n      ref:\n        default: 'dev'\n        type: string\n      auto_update_file:\n        default: 'canary.xml'\n        type: string\n      build_xpi:\n        default: false\n        type: boolean\n    outputs:\n      version:\n        value: ${{ jobs.zip.outputs.version }}\n      buildXPI:\n        value: ${{ jobs.xpi.outputs.exist_xpi_file }}\n    secrets:\n      CRX_PRIVATE_KEY:\n        required: true\n\njobs:\n  build:\n    name: Build dist\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          ref: ${{ inputs.ref }}\n          fetch-depth: 0\n\n      - name: Setup Node\n        uses: actions/setup-node@v4\n        with:\n          node-version: '20'\n          cache: yarn\n\n      - run: yarn\n\n      - run: yarn build\n        continue-on-error: true\n\n      - name: Upload Built to action\n        uses: actions/upload-artifact@v4\n        with:\n          name: build-dist-folder\n          path: dist\n\n  zip:\n    name: Build Zip\n    runs-on: ubuntu-latest\n    needs: build\n    outputs:\n      version: ${{ steps.zip.outputs.extensionVersion }}\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          name: build-dist-folder\n          path: dist\n\n      - id: zip\n        name: Build Zip\n        uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1\n        with:\n          extensionDir: 'dist'\n          zipFilePath: 'artifact/extension.zip'\n\n      - name: Upload Built Zip to action\n        uses: actions/upload-artifact@v4\n        with:\n          name: dev-build-${{ steps.zip.outputs.extensionVersion }}-zip\n          path: artifact/*\n\n  crx:\n    name: Build Crx (Chromium)\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Set name output\n        id: auto_update_file\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != ''  }}\n        run: |\n          USER_INPUT=${{ inputs.auto_update_file }}\n          echo \"value=${USER_INPUT:-\"stable.xml\"}\" >> $GITHUB_OUTPUT\n\n      - uses: actions/download-artifact@v4\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != ''  }}\n        with:\n          name: build-dist-folder\n          path: dist\n\n      - name: Add update_url\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != ''  }}\n        run: echo $(jq '. |= .+ {\"update_url\":\"https://pt-plugins.github.io/PT-Plugin-Plus/update/${{ steps.auto_update_file.outputs.value }}\"}' dist/manifest.json) > dist/manifest.json\n\n      - id: zip\n        name: Build Zip With update_url\n        uses: cardinalby/webext-buildtools-pack-extension-dir-action@v1\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != ''  }}\n        with:\n          extensionDir: 'dist'\n          zipFilePath: 'extension.zip'\n\n      - name: Build Crx\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != ''  }}\n        uses: cardinalby/webext-buildtools-chrome-crx-action@v2\n        with:\n          zipFilePath: 'extension.zip'\n          crxFilePath: 'artifact/extension.crx'\n          privateKey: ${{ secrets.CRX_PRIVATE_KEY }}\n          updateXmlPath: artifact/${{ steps.auto_update_file.outputs.value }}\n          updateXmlCodebaseUrl: https://github.com/pt-plugins/PT-Plugin-Plus/releases/download/v${{ steps.zip.outputs.extensionVersion }}/PT-Plugin-Plus-${{ steps.zip.outputs.extensionVersion }}.crx\n\n      - name: Upload Built Crx to action\n        env:\n          CRX_PRIVATE_KEY: ${{ secrets.CRX_PRIVATE_KEY }}\n        if: ${{ env.CRX_PRIVATE_KEY != '' }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: dev-build-${{ steps.zip.outputs.extensionVersion }}-crx\n          path: artifact/*\n\n  xpi:\n    name: Build Xpi (Firefox)\n    runs-on: ubuntu-latest\n    needs: zip\n    if: ${{ inputs.build_xpi }}\n    outputs:\n      exist_xpi_file: ${{ steps.addonsDeploy.outcome }}\n    steps:\n      - uses: actions/download-artifact@v4\n        with:\n          name: dev-build-${{ needs.zip.outputs.version }}-zip\n\n      - uses: cardinalby/webext-buildtools-firefox-sign-xpi-action@1.0.6\n        id: addonsDeploy\n        if: ${{ env.FF_EXT_UUID != ''  }}\n        env:\n          FF_EXT_UUID: ${{ secrets.FF_EXT_UUID }}\n        continue-on-error: true\n        with:\n          timeoutMs: 600000\n          zipFilePath: extension.zip\n          xpiFilePath: artifact/extension.signed.xpi\n          extensionId: ${{ secrets.FF_EXT_UUID }}\n          jwtIssuer: ${{ secrets.FF_JWT_ISSUER }}\n          jwtSecret: ${{ secrets.FF_JWT_SECRET }}\n\n      - name: Upload Built Xpi to action\n        uses: actions/upload-artifact@v4\n        if: ${{ steps.addonsDeploy.outcome == 'success' }}\n        with:\n          name: dev-build-${{ needs.zip.outputs.version }}-xpi\n          path: artifact/*\n"
  },
  {
    "path": ".github/workflows/build_canary.yml",
    "content": "name: Build Canary Release\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 20 1,15 * *'\n\njobs:\n  action:\n    uses: ./.github/workflows/build_action.yml\n    with:\n      ref: 'dev'\n      build_xpi: true\n    secrets: inherit\n\n  canary:\n    runs-on: ubuntu-latest\n    needs: action\n    permissions:\n      contents: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@master\n        with:\n          ref: 'dev'\n          fetch-depth: 0\n\n      - name: Checkout gh-pages\n        uses: actions/checkout@master\n        with:\n          ref: 'gh-pages'\n          path: 'pages'\n\n      - name: Prepare Release\n        run: mkdir build\n\n      - name: Get And rename Zip build\n        uses: actions/download-artifact@v4\n        with:\n          name: dev-build-${{ needs.action.outputs.version }}-zip\n\n      - run: mv extension.zip build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.zip\n\n      - name: Get And remove Crx Build\n        uses: actions/download-artifact@v4\n        with:\n          name:\n            dev-build-${{ needs.action.outputs.version }}-crx\n\n      - run: |\n          mv canary.xml pages/update/canary.xml -f\n          mv extension.crx build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.crx\n\n      - name: Get And move Xpi Build\n        uses: actions/download-artifact@v4\n        if: ${{ needs.action.outputs.buildXPI == 'success' }}\n        with:\n          name: dev-build-${{ needs.action.outputs.version }}-xpi\n\n      - if: ${{ needs.action.outputs.buildXPI == 'success' }}\n        run: | \n          mv extension.signed.xpi build/PT-Plugin-Plus-${{ needs.action.outputs.version }}.xpi\n          echo $(jq '.addons[].updates += [{\"version\": \"${{ needs.action.outputs.version }}\", \"update_link\": \"https://github.com/pt-plugins/PT-Plugin-Plus/releases/download/v${{ needs.action.outputs.version }}/PT-Plugin-Plus-${{ needs.action.outputs.version }}.xpi\"}]' pages/update/firefox.json) > pages/update/firefox.json\n\n      - name: Deploy update xml and json\n        uses: peaceiris/actions-gh-pages@v4\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          publish_dir: ./pages\n          commit_message: deploy ${{ github.ref }}\n          force_orphan: true\n          user_name: github-actions[bot]\n          user_email: github-actions[bot]@users.noreply.github.com\n\n      - uses: ncipollo/release-action@v1\n        with:\n          name: v${{ needs.action.outputs.version }}\n          tag: v${{ needs.action.outputs.version }}\n          commit: 'dev'\n          generateReleaseNotes: true\n          prerelease: true\n          artifacts: 'build/*'\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw*\ndebug/dist\n\n# Chrome 插件文件\n*.crx\n*.pem\n*.zip\n\n# 发布的二进制文件\n/releases"
  },
  {
    "path": ".nvmrc",
    "content": "16\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 栽培者\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "> [!WARNING]\n> PT-Plugin-Plus 项目已经进入停止维护期（具体说明见： https://github.com/pt-plugins/PT-Plugin-Plus/issues/2235 ）\n> \n> 我们推荐您使用全新的替代方案：  **[PT-depiler](https://github.com/pt-plugins/PT-depiler)** 。\n> PT-depiler 是 PT-Plugin-Plus 的继任者，保留了绝大多数核心功能并进行了全面优化，包括更好的兼容性、更稳定的性能和更丰富的功能支持。建议所有用户迁移至新项目以获得持续的更新和支持。\n\n> [!TIP]\n> 如果你浏览器中使用的PTPP已经被禁用，你可以尝试以下方法 **重新恢复使用** 或 **导出数据以迁移至PT-depiler**：\n> \n> ① [使用浏览器 flags 临时启用](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E6%88%91%E5%B7%B2%E7%BB%8F%E6%97%A0%E6%B3%95%E4%BD%BF%E7%94%A8%E4%BA%86%E5%BA%94%E8%AF%A5%E6%80%8E%E4%B9%88%E5%8A%9E) （需要 chrome < 139）\n> \n> ② [使用临时插件 PTPP Exporter](https://github.com/pt-plugins/PT-Plugin-Plus/discussions/2241) （仅适用于通过 crx 或者 zip 方法安装）\n> \n> ③ [使用低版本 chrome 抢救数据](https://github.com/pt-plugins/PT-Plugin-Plus/discussions/2242)  （推荐！也可用于其他基于 chromium 但未移除 MV2 支持的浏览器）\n\n\n<p align=\"center\">\n<img src=\"https://github.com/pt-plugins/PT-Plugin-Plus/raw/master/public/assets/icon-128.png\"><br/>\n<a href=\"https://github.com/pt-plugins/PT-Plugin-Plus/releases?include_prereleases/latest\" title=\"GitHub Pre-releases\"><img src=\"https://img.shields.io/github/release/pt-plugins/PT-Plugin-Plus.svg?include_prereleases&label=pre-release\"></a>\n<a href=\"https://github.com/pt-plugins/PT-Plugin-Plus/releases\" title=\"GitHub All Releases\"><img alt=\"Releases\" src=\"https://img.shields.io/github/downloads/pt-plugins/PT-Plugin-Plus/total.svg?label=Downloads\"></a>\n<img src=\"https://img.shields.io/badge/Used-TypeScript%20Vue-blue.svg\">\n<a href=\"https://github.com/pt-plugins/PT-Plugin-Plus/LICENSE\" title=\"GitHub license\"><img src=\"https://img.shields.io/github/license/pt-plugins/PT-Plugin-Plus.svg?label=License\" alt=\"GitHub license\"/></a>\n<a href=\"https://t.me/joinchat/NZ9NCxPKXyby8f35rn_QTw\"><img src=\"https://img.shields.io/badge/Telegram-Chat-blue.svg?logo=telegram\" alt=\"Telegram\"/></a>\n</p>\n\n---\n\n## 关于\n\nPT 助手 Plus，是一款浏览器插件（Web Extensions），一个可以提升 PT 站点使用效率的工具。\n\n适用于各 PT 站，可使下载种子等各项操作变化更简单、快捷。配合下载服务器（如 Transmission、µTorrent 等），可一键下载指定的种子。\n\n该版本是对原来的 [PT 助手](https://github.com/ronggang/PT-Plugin) 进行了重构，去掉了繁琐的配置，以获得更好的使用体验；\n\n> ~~注意：`1.0.0` 以下的配置不能直接用于该版本，请勿将 `1.0.0` 以下的版本配置进行导入操作。~~\n\n最新版本请登录后从[Pre-release](https://github.com/pt-plugins/PT-Plugin-Plus/releases?include_prereleases/latest)获取。如不会安装请参看Wiki\n\n**提Issue前请务必检查Dev版本、Pull Request以及之前的Issue**\n\n**M-Team 请于站点控制台 -> 实验室 获取 Token 填入后使用**\n\n## 已支持的浏览器\n- <a href=\"https://chrome.google.com/webstore/detail/abkdiiddckphbigmakaojlnmakpllenb\" title=\"已在 Chrome Web Store 市场上发布的版本\">![Google Chrome](https://img.shields.io/chrome-web-store/v/abkdiiddckphbigmakaojlnmakpllenb.svg?label=Google%20Chrome)</a> （已下架，见[原因](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E5%B7%B2%E8%A2%AB%E4%B8%8B%E6%9E%B6%E7%9A%84%E6%B5%8F%E8%A7%88%E5%99%A8)）\n- <a href=\"https://addons.mozilla.org/zh-CN/firefox/addon/pt-plugin-plus/\" title=\"已在 Mozilla Add-on 上发布的版本\">![Mozilla Firefox](https://img.shields.io/amo/v/pt-plugin-plus.svg?label=Mozilla%20Firefox)</a> （已下架，见[原因](https://github.com/pt-plugins/PT-Plugin-Plus/wiki#%E5%B7%B2%E8%A2%AB%E4%B8%8B%E6%9E%B6%E7%9A%84%E6%B5%8F%E8%A7%88%E5%99%A8)）\n- <a href=\"https://microsoftedge.microsoft.com/addons/detail/ekhingnlcjebipkdcgkkheigmljefepn\" title=\"已在 Microsoft Edge 上发布的版本\">![Microsoft Edge](https://img.shields.io/badge/dynamic/json?label=Edge%20Addons&prefix=v&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2FAddons%2Fgetproductdetailsbycrxid%2Fekhingnlcjebipkdcgkkheigmljefepn)</a>\n- 及其他基于 `Chromium` 内核的浏览器\n\n## 功能\n\n- 一键发送指定的种子到下载服务器，目前已支持：\n  - Transmission\n  - Synology Download Station\n  - µTorrent\n  - Deluge\n  - qBittorrent `v4.1+`\n  - ruTorrent\n  - Flood\n- 比 RSS 更灵活的下载方式：\n  - 针对不同的站点发送到不同的下载服务器；\n  - 针对不同的站点、下载服务器设置不同的保存路径；\n- 批量下载当前页所有种子；\n- 批量复制当前页面所有种子的下载链接（`部分站点需要设置 passkey`）；\n- 显示默认下载服务器当前可用空间，目前已支持：\n  - Transmission\n- 多站聚合搜索相同关键字的种子；\n  - 查看 [已支持的站点列表](https://github.com/pt-plugins/PT-Plugin-Plus/wiki/supported-sites)\n- 根据当前站点显示专属功能，如：\n  - 封面模式浏览种子页面；\n- 保存下载历史记录（默认关闭）；\n- `豆瓣` 电影页面、[Top250](https://movie.douban.com/top250)、[选电影](https://movie.douban.com/explore) 一键搜索 PT 种子支持；\n- `IMDb` 电影页面、[Top250](https://www.imdb.com/chart/top?ref_=nv_mv_250) 一键搜索 PT 种子支持；\n- 更多功能请参考 [Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) ；\n\n## 安装及使用\n\n- 如何安装和使用，请参考 [Wiki](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) 的详细说明；\n- 常见问题可 [点这里](https://github.com/pt-plugins/PT-Plugin-Plus/wiki/frequently-asked-questions) 找到答案；\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/app'\n  ]\n}\n"
  },
  {
    "path": "debug/config/config.json",
    "content": "{\n\t\"port\": 8001,\n\t\"from\": \"../../resource\",\n\t\"to\": \"/\"\n}"
  },
  {
    "path": "debug/data/beforeSearching.json",
    "content": "{\n  \"count\": 8,\n  \"start\": 0,\n  \"total\": 5608,\n  \"subjects\": [\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 9.7,\n        \"stars\": \"50\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u7eaa\\u5f55\\u7247\"\n      ],\n      \"title\": \"\\u521b\\u9020\\u201c\\u54c8\\u5229\\u00b7\\u6ce2\\u7279\\u201d\\u7684\\u4e16\\u754c\\uff1a\\u97f3\\u4e50\",\n      \"casts\": [],\n      \"collect_count\": 1184,\n      \"original_title\": \"Creating the World of Harry Potter, Part 4: Sound and Music\",\n      \"subtype\": \"movie\",\n      \"directors\": [],\n      \"year\": \"2010\",\n      \"images\": {\n        \"small\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2493762829.webp\",\n        \"large\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2493762829.webp\",\n        \"medium\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2493762829.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/6718144\\/\",\n      \"id\": \"6718144\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 7.1,\n        \"stars\": \"35\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u5267\\u60c5\"\n      ],\n      \"title\": \"\\u97f3\\u4e50\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1342515\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/f\\/movie\\/ca527386eb8c4e325611e22dfcb04cc116d6b423\\/pics\\/movie\\/celebrity-default-small.png\",\n            \"large\": \"https://img3.doubanio.com\\/f\\/movie\\/63acc16ca6309ef191f0378faf793d1096a3e606\\/pics\\/movie\\/celebrity-default-large.png\",\n            \"medium\": \"https://img1.doubanio.com\\/f\\/movie\\/8dd0c794499fe925ae2ae89ee30cd225750457b4\\/pics\\/movie\\/celebrity-default-medium.png\"\n          },\n          \"name\": \"\\u9ed2\\u6ca2\\u306e\\u308a\\u5b50\",\n          \"id\": \"1342515\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1323580\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1357565957.64.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1357565957.64.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1357565957.64.webp\"\n          },\n          \"name\": \"\\u7ec6\\u5ddd\\u4fca\\u4e4b\",\n          \"id\": \"1323580\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1039227\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1550212443.35.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1550212443.35.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1550212443.35.webp\"\n          },\n          \"name\": \"\\u9ad8\\u6865\\u957f\\u82f1\",\n          \"id\": \"1039227\"\n        }\n      ],\n      \"collect_count\": 117,\n      \"original_title\": \"\\u97f3\\u697d\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1050508\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p17532.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p17532.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p17532.webp\"\n          },\n          \"name\": \"\\u589e\\u6751\\u4fdd\\u9020\",\n          \"id\": \"1050508\"\n        }\n      ],\n      \"year\": \"1972\",\n      \"images\": {\n        \"small\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2509743913.webp\",\n        \"large\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2509743913.webp\",\n        \"medium\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2509743913.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/3169341\\/\",\n      \"id\": \"3169341\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 7.4,\n        \"stars\": \"40\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u5267\\u60c5\"\n      ],\n      \"title\": \"\\u97f3\\u4e50\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1000594\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p45463.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p45463.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p45463.webp\"\n          },\n          \"name\": \"\\u5fb7\\u83f2\\u56e0\\u00b7\\u585e\\u91cc\\u683c\",\n          \"id\": \"1000594\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1016766\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p14457.webp\",\n            \"large\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p14457.webp\",\n            \"medium\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p14457.webp\"\n          },\n          \"name\": \"\\u7f57\\u8d1d\\u5c14\\u00b7\\u4faf\\u8d5b\\u56e0\",\n          \"id\": \"1016766\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1085159\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1471197409.54.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1471197409.54.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1471197409.54.webp\"\n          },\n          \"name\": \"Julie Dassin\",\n          \"id\": \"1085159\"\n        }\n      ],\n      \"collect_count\": 84,\n      \"original_title\": \"La Musica\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1000758\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p12067.webp\",\n            \"large\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p12067.webp\",\n            \"medium\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p12067.webp\"\n          },\n          \"name\": \"\\u739b\\u683c\\u4e3d\\u7279\\u00b7\\u675c\\u62c9\\u65af\",\n          \"id\": \"1000758\"\n        },\n        {\n          \"alt\": null,\n          \"avatars\": null,\n          \"name\": \"Paul Seban\",\n          \"id\": null\n        }\n      ],\n      \"year\": \"1967\",\n      \"images\": {\n        \"small\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2264840662.webp\",\n        \"large\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2264840662.webp\",\n        \"medium\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2264840662.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/3223707\\/\",\n      \"id\": \"3223707\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 0,\n        \"stars\": \"00\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u559c\\u5267\",\n        \"\\u5267\\u60c5\"\n      ],\n      \"title\": \"\\u97f3\\u4e50\",\n      \"casts\": [],\n      \"collect_count\": 1,\n      \"original_title\": \"Muzika\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1034597\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1391190217.73.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1391190217.73.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1391190217.73.webp\"\n          },\n          \"name\": \"Juraj Nvota\",\n          \"id\": \"1034597\"\n        }\n      ],\n      \"year\": \"2008\",\n      \"images\": {\n        \"small\": \"https://img3.doubanio.com\\/view\\/subject\\/s\\/public\\/s3678106.jpg\",\n        \"large\": \"https://img3.doubanio.com\\/view\\/subject\\/l\\/public\\/s3678106.jpg\",\n        \"medium\": \"https://img3.doubanio.com\\/view\\/subject\\/m\\/public\\/s3678106.jpg\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/3617774\\/\",\n      \"id\": \"3617774\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 8.1,\n        \"stars\": \"40\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u5267\\u60c5\",\n        \"\\u7231\\u60c5\"\n      ],\n      \"title\": \"\\u542c\\u8bf4\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1013782\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1368156632.65.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1368156632.65.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1368156632.65.webp\"\n          },\n          \"name\": \"\\u5f6d\\u4e8e\\u664f\",\n          \"id\": \"1013782\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1274316\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p31663.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p31663.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p31663.webp\"\n          },\n          \"name\": \"\\u9648\\u610f\\u6db5\",\n          \"id\": \"1274316\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1313303\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p37554.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p37554.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p37554.webp\"\n          },\n          \"name\": \"\\u9648\\u598d\\u5e0c\",\n          \"id\": \"1313303\"\n        }\n      ],\n      \"collect_count\": 435902,\n      \"original_title\": \"\\u807d\\u8aaa\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1276077\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p13508.webp\",\n            \"large\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p13508.webp\",\n            \"medium\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p13508.webp\"\n          },\n          \"name\": \"\\u90d1\\u82ac\\u82ac\",\n          \"id\": \"1276077\"\n        }\n      ],\n      \"year\": \"2009\",\n      \"images\": {\n        \"small\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250788664.webp\",\n        \"large\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250788664.webp\",\n        \"medium\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250788664.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/3824672\\/\",\n      \"id\": \"3824672\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 9.0,\n        \"stars\": \"45\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u5267\\u60c5\",\n        \"\\u4f20\\u8bb0\",\n        \"\\u7231\\u60c5\"\n      ],\n      \"title\": \"\\u97f3\\u4e50\\u4e4b\\u58f0\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1041081\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p4777.webp\",\n            \"large\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p4777.webp\",\n            \"medium\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p4777.webp\"\n          },\n          \"name\": \"\\u6731\\u8389\\u00b7\\u5b89\\u5fb7\\u9c81\\u65af\",\n          \"id\": \"1041081\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1036321\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p42033.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p42033.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p42033.webp\"\n          },\n          \"name\": \"\\u514b\\u91cc\\u65af\\u6258\\u5f17\\u00b7\\u666e\\u5362\\u9ed8\",\n          \"id\": \"1036321\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1010671\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1355087417.43.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1355087417.43.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1355087417.43.webp\"\n          },\n          \"name\": \"\\u57c3\\u7433\\u8bfa\\u00b7\\u5e15\\u514b\",\n          \"id\": \"1010671\"\n        }\n      ],\n      \"collect_count\": 453324,\n      \"original_title\": \"The Sound of Music\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1049929\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1485851955.3.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1485851955.3.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1485851955.3.webp\"\n          },\n          \"name\": \"\\u7f57\\u4f2f\\u7279\\u00b7\\u6000\\u65af\",\n          \"id\": \"1049929\"\n        }\n      ],\n      \"year\": \"1965\",\n      \"images\": {\n        \"small\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p453788577.webp\",\n        \"large\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p453788577.webp\",\n        \"medium\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p453788577.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/1294408\\/\",\n      \"id\": \"1294408\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 9.2,\n        \"stars\": \"50\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u8131\\u53e3\\u79c0\"\n      ],\n      \"title\": \"\\u542c\\u8bf4 \\u7b2c\\u4e00\\u5b63\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1350153\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1434704950.63.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1434704950.63.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1434704950.63.webp\"\n          },\n          \"name\": \"\\u9a6c\\u4e16\\u82b3\",\n          \"id\": \"1350153\"\n        }\n      ],\n      \"collect_count\": 3941,\n      \"original_title\": \"\\u542c\\u8bf4 \\u7b2c\\u4e00\\u5b63\",\n      \"subtype\": \"tv\",\n      \"directors\": [\n        {\n          \"alt\": null,\n          \"avatars\": null,\n          \"name\": \"\\u9648\\u6021\\u5206\",\n          \"id\": null\n        }\n      ],\n      \"year\": \"2015\",\n      \"images\": {\n        \"small\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250357437.webp\",\n        \"large\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250357437.webp\",\n        \"medium\": \"https://img1.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250357437.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/26425523\\/\",\n      \"id\": \"26425523\"\n    },\n    {\n      \"rating\": {\n        \"max\": 10,\n        \"average\": 8.5,\n        \"stars\": \"45\",\n        \"min\": 0\n      },\n      \"genres\": [\n        \"\\u559c\\u5267\",\n        \"\\u7231\\u60c5\",\n        \"\\u97f3\\u4e50\"\n      ],\n      \"title\": \"\\u518d\\u6b21\\u51fa\\u53d1\\u4e4b\\u7ebd\\u7ea6\\u9047\\u89c1\\u4f60\",\n      \"casts\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1054448\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p10192.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p10192.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p10192.webp\"\n          },\n          \"name\": \"\\u51ef\\u62c9\\u00b7\\u5948\\u7279\\u8389\",\n          \"id\": \"1054448\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1040505\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p15885.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p15885.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p15885.webp\"\n          },\n          \"name\": \"\\u9a6c\\u514b\\u00b7\\u9c81\\u5f17\\u6d1b\",\n          \"id\": \"1040505\"\n        },\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1174312\\/\",\n          \"avatars\": {\n            \"small\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1380782810.86.webp\",\n            \"large\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1380782810.86.webp\",\n            \"medium\": \"https://img3.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1380782810.86.webp\"\n          },\n          \"name\": \"\\u4e9a\\u5f53\\u00b7\\u83b1\\u6587\",\n          \"id\": \"1174312\"\n        }\n      ],\n      \"collect_count\": 299015,\n      \"original_title\": \"Begin Again\",\n      \"subtype\": \"movie\",\n      \"directors\": [\n        {\n          \"alt\": \"https:\\/\\/movie.douban.com\\/celebrity\\/1280127\\/\",\n          \"avatars\": {\n            \"small\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1470662353.8.webp\",\n            \"large\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1470662353.8.webp\",\n            \"medium\": \"https://img1.doubanio.com\\/view\\/celebrity\\/s_ratio_celebrity\\/public\\/p1470662353.8.webp\"\n          },\n          \"name\": \"\\u7ea6\\u7ff0\\u00b7\\u5361\\u5c3c\",\n          \"id\": \"1280127\"\n        }\n      ],\n      \"year\": \"2013\",\n      \"images\": {\n        \"small\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250287733.webp\",\n        \"large\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250287733.webp\",\n        \"medium\": \"https://img3.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2250287733.webp\"\n      },\n      \"alt\": \"https:\\/\\/movie.douban.com\\/subject\\/6874403\\/\",\n      \"id\": \"6874403\"\n    }\n  ],\n  \"title\": \"\\u641c\\u7d22 \\\"\\u97f3\\u4e50\\\" \\u7684\\u7ed3\\u679c\"\n}"
  },
  {
    "path": "debug/package.json",
    "content": "{\n  \"name\": \"pt-plugin-plus-test\",\n  \"version\": \"1.0.0\",\n  \"description\": \"\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"author\": \"\",\n  \"license\": \"ISC\",\n  \"dependencies\": {\n    \"body-parser\": \"^1.18.3\",\n    \"cors\": \"^2.8.5\",\n    \"express\": \"^4.16.4\",\n    \"lodash\": \"^4.17.19\"\n  },\n  \"devDependencies\": {\n    \"@types/express\": \"^4.17.2\",\n    \"@types/lodash\": \"^4.14.117\",\n    \"@types/node\": \"^10.12.0\",\n    \"ts-node\": \"^7.0.1\",\n    \"typescript\": \"^3.1.3\"\n  }\n}\n"
  },
  {
    "path": "debug/src/App.ts",
    "content": "// 导入基础库\nimport * as Express from \"express\";\nimport * as cors from \"cors\";\nimport * as BodyParser from \"body-parser\";\nimport * as PATH from \"path\";\nimport * as FS from \"fs\";\nimport { BuildPlugin } from \"./BuildPlugin\";\nimport { SearchData } from \"./SearchData\";\n\n/**\n * 默认APP\n */\nclass App {\n  public express = Express();\n  public options;\n  public systemConfig;\n  public i18n;\n\n  constructor(options) {\n    this.options = options || {\n      port: 80,\n      from: \"../resource\",\n      to: \"/\"\n    };\n\n    let buildPlugin = new BuildPlugin(\"../../resource\");\n    this.systemConfig = JSON.stringify(buildPlugin.getSystemConfig());\n    this.i18n = JSON.stringify(buildPlugin.geti18n());\n\n    this.useModules();\n    this.mountRoutes();\n  }\n\n  /**\n   * 使用一些模块\n   */\n  private useModules() {\n    const from = PATH.join(__dirname, this.options.from);\n\n    this.express.use(cors());\n    // 启用静态文件目录\n    this.express.use(this.options.to, Express.static(from));\n    this.express.use(BodyParser.json()); // for parsing application/json\n    this.express.use(BodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded\n  }\n\n  /**\n   * 挂载路由\n   */\n  private mountRoutes(): void {\n    this.express.get(\"/systemConfig.json\", (req, res) => {\n      res.send(this.systemConfig);\n    });\n\n    this.express.get(\"/i18n.json\", (req, res) => {\n      res.send(this.i18n);\n    });\n\n    const config = JSON.parse(this.systemConfig);\n    this.express.get(\"/test/searchData.json\", (req, res) => {\n      res.send(new SearchData(config).generate());\n    });\n\n    this.express.get(\"/test/*.*\", (req, res) => {\n      console.log(req.url);\n      let fileName = (req.url as any).match(/test\\/(.[^\\?]+)/)[1];\n      console.log(fileName);\n      let path = PATH.resolve(__dirname, \"../data/\");\n      let file = PATH.join(path, fileName);\n      if (FS.existsSync(file)) {\n        let content = FS.readFileSync(PATH.join(path, fileName), \"utf-8\");\n        if (fileName.substr(-5) === \".json\") {\n          res.send(JSON.parse(content));\n        } else {\n          console.log(PATH.join(path, fileName));\n          res.sendFile(PATH.join(path, fileName));\n          // res.send(content);\n        }\n      } else {\n        res.send(\"no file.\");\n      }\n    });\n  }\n\n  /**\n   * 启动服务\n   */\n  public start() {\n    this.express.listen(this.options.port, err => {\n      if (err) {\n        return console.log(err);\n      }\n\n      return console.log(`Base Web Service Run at ${this.options.port}`);\n    });\n  }\n}\n\nexport default App;\n"
  },
  {
    "path": "debug/src/BuildPlugin.ts",
    "content": "import * as FS from \"fs\";\nimport * as PATH from \"path\";\n\nexport type Dictionary<T> = { [key: string]: T };\n\n/**\n * 构建过程辅助工具\n */\nexport class BuildPlugin {\n  public resourcePath: string = \"\";\n  private resourceMap = [\"sites\", \"schemas\", \"clients\", \"publicSites\"];\n\n  constructor(rootPaht: string = \"../../dist/resource\") {\n    this.resourcePath = PATH.resolve(__dirname, rootPaht);\n    console.log(this.resourcePath);\n  }\n\n  /**\n   * 创建资源文件列表\n   */\n  public buildResource() {\n    let fileName = PATH.join(this.resourcePath, `systemConfig.json`);\n    FS.writeFileSync(fileName, JSON.stringify(this.getSystemConfig()));\n    fileName = PATH.join(this.resourcePath, `i18n.json`);\n    FS.writeFileSync(fileName, JSON.stringify(this.geti18n()));\n  }\n\n  /**\n   * 获取系统配置信息\n   */\n  public getSystemConfig() {\n    let result = {};\n    this.resourceMap.forEach((name: string) => {\n      result[name] = this.getResourceConfig(name);\n    });\n\n    return result;\n  }\n\n  /**\n   * 获取指定的资源配置信息\n   * @param name\n   */\n  private getResourceConfig(name: string): any {\n    let parentFolder = PATH.join(this.resourcePath, name);\n    let list = FS.readdirSync(parentFolder);\n\n    let results: any[] = [];\n    list.forEach((path: string) => {\n      let _path = PATH.join(parentFolder, path);\n      var stat = FS.statSync(_path);\n      // 仅获取目录\n      if (stat && stat.isDirectory()) {\n        let file = PATH.join(_path, `config.json`);\n        if (FS.existsSync(file)) {\n          let content = JSON.parse(FS.readFileSync(file, \"utf-8\"));\n\n          // 获取解析器\n          let parser = this.getParser(PATH.join(_path, \"parser\"));\n          if (parser) {\n            content[\"parser\"] = parser;\n          }\n          results.push(content);\n        }\n      }\n    });\n\n    return results;\n  }\n\n  /**\n   * 创建架构和站点的解析器\n   */\n  private makeParser(name: string) {\n    let parentFolder = PATH.join(this.resourcePath, name);\n    let list = FS.readdirSync(parentFolder);\n\n    list.forEach((path: string) => {\n      let _path = PATH.join(parentFolder, path);\n      var stat = FS.statSync(_path);\n      // 仅获取目录\n      if (stat && stat.isDirectory()) {\n        let parser = this.getParser(PATH.join(_path, \"parser\"));\n        if (parser) {\n          let fileName = PATH.join(_path, `config.json`);\n          let content = JSON.parse(FS.readFileSync(fileName, \"utf-8\"));\n          content[\"parser\"] = parser;\n\n          FS.writeFileSync(fileName, JSON.stringify(content));\n        }\n      }\n    });\n  }\n\n  /**\n   * 获取解析器\n   * @param parentFolder\n   */\n  private getParser(parentFolder): any {\n    if (!FS.existsSync(parentFolder)) {\n      return null;\n    }\n    let list = FS.readdirSync(parentFolder);\n\n    let results: any = {};\n    list.forEach((path: string) => {\n      let _path = PATH.join(parentFolder, path);\n      var stat = FS.statSync(_path);\n      // 仅获取目录\n      if (stat && stat.isFile() && PATH.extname(_path) == \".js\") {\n        results[PATH.basename(_path, \".js\")] = FS.readFileSync(_path, \"utf-8\");\n      }\n    });\n\n    return results;\n  }\n\n  /**\n   * 获取已支持站点列表\n   */\n  public getSupportedSites() {\n    let schemaFolder = PATH.join(this.resourcePath, \"schemas\");\n    let schemaList = FS.readdirSync(schemaFolder);\n\n    let schemas: any = {};\n    schemaList.forEach((path: string) => {\n      let file = PATH.join(schemaFolder, path);\n      var stat = FS.statSync(file);\n      // 仅获取目录\n      if (stat && stat.isDirectory()) {\n        schemas[path] = [];\n      }\n    });\n\n    schemas[\"其他架构\"] = [];\n\n    let parentFolder = PATH.join(this.resourcePath, \"sites\");\n\n    let list = FS.readdirSync(parentFolder);\n\n    let itemTemplate =\n      \"| $schema$ | $name$ | $search$ | $imdbSearch$ | $userData$ | $sendTorrent$ | $torrentProgress$ | $collaborator$ |\";\n\n    list.forEach((path: string) => {\n      let file = PATH.join(parentFolder, path);\n      var stat = FS.statSync(file);\n      // 仅获取目录\n      if (stat && stat.isDirectory()) {\n        let fileName = PATH.join(file, `config.json`);\n        let content = JSON.parse(FS.readFileSync(fileName, \"utf-8\"));\n        let schema = content.schema;\n        if (!schemas[schema]) {\n          schema = \"其他架构\";\n        }\n\n        let supportedFeatures = {\n          search: true,\n          imdbSearch: true,\n          userData: true,\n          sendTorrent: true\n        };\n\n        if (content.supportedFeatures) {\n          supportedFeatures = Object.assign(\n            supportedFeatures,\n            content.supportedFeatures\n          );\n        }\n\n        // 判断是否有跳过 IMDb 选项，有则定为不支持 IMDb\n        if (content.searchEntryConfig) {\n          if (content.searchEntryConfig.skipIMDbId === true) {\n            supportedFeatures.imdbSearch = false;\n          }\n        }\n\n        let count = schemas[schema].length;\n        let item = this.replaceKeys(itemTemplate, {\n          schema: count == 0 ? schema : \"\",\n          name: content.name,\n          search: supportedFeatures.search === true ? \"√\" : \"\",\n          imdbSearch: supportedFeatures.imdbSearch === true ? \"√\" : \"\",\n          userData:\n            supportedFeatures.userData === true\n              ? \"√\"\n              : supportedFeatures.userData === false\n              ? \"\"\n              : supportedFeatures.userData,\n          sendTorrent: supportedFeatures.sendTorrent === true ? \"√\" : \"\",\n          torrentProgress:\n            content.searchEntryConfig &&\n            content.searchEntryConfig.fieldSelector &&\n            content.searchEntryConfig.fieldSelector.progress\n              ? \"√\"\n              : \"\",\n          collaborator: this.getCollaborator(content.collaborator)\n        });\n        schemas[schema].push(item);\n      }\n    });\n\n    console.log(\"\\n\");\n    for (const key in schemas) {\n      if (schemas.hasOwnProperty(key)) {\n        const items: Array<any> = schemas[key];\n        // console.log(`\\n## ${key}`);\n\n        items.forEach((item: string) => {\n          console.log(item);\n        });\n      }\n    }\n    console.log(\"\\n\");\n\n    // console.log(results);\n  }\n\n  public getCollaborator(source: string | Array<string>): string {\n    if (!source) {\n      return \"\";\n    }\n    if (typeof source == \"string\") {\n      return source;\n    } else if (source.length > 0) {\n      let result: Array<string> = [];\n      source.forEach((item: string) => {\n        result.push(item);\n      });\n\n      return result.join(\", \");\n    }\n    return \"\";\n  }\n\n  /**\n   * 获取语言配置信息\n   */\n  public geti18n() {\n    let parentFolder = PATH.join(this.resourcePath, \"i18n\");\n\n    let list = FS.readdirSync(parentFolder);\n    let results: Array<any> = [];\n\n    list.forEach((path: string) => {\n      let file = PATH.join(parentFolder, path);\n      var stat = FS.statSync(file);\n      // 获取语言配置文件\n      if (stat && stat.isFile() && PATH.extname(file) == \".json\") {\n        let content = JSON.parse(FS.readFileSync(file, \"utf-8\"));\n        if (content && content.code && content.name) {\n          console.log(path, content.name);\n          results.push({\n            name: content.name,\n            code: content.code\n          });\n        }\n      }\n    });\n    return results;\n  }\n\n  /**\n   * 替换指定的字符串列表\n   * @param source\n   * @param keys\n   */\n  replaceKeys(\n    source: string,\n    keys: Dictionary<any>,\n    prefix: string = \"\"\n  ): string {\n    let result: string = source;\n\n    for (const key in keys) {\n      if (keys.hasOwnProperty(key)) {\n        const value = keys[key];\n        let search = \"$\" + key + \"$\";\n        if (prefix) {\n          search = `$${prefix}.${key}$`;\n        }\n        result = result.replace(search, value);\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "debug/src/SearchData.ts",
    "content": "/**\n * 生成搜索测试数据\n */\nexport class SearchData {\n  constructor(public config: any = {}) {}\n\n  public generate() {\n    let results: any[] = [];\n    let count = Math.floor(Math.random() * 100);\n    const status = [1, 2, 255, undefined];\n    for (let i = 0; i < count; i++) {\n      let host = this.getHost();\n      let data = {\n        title: this.getTitle(),\n        subTitle: this.getSubTitle(),\n        link: `https://${host}/details.php?id=${i}`,\n        url: `https://${host}/download.php?id=${i}`,\n        size: this.getSize(),\n        time: Math.floor(\n          new Date().getTime() / 1000 - Math.floor(Math.random() * 10000000)\n        ),\n        author: \"匿名\",\n        seeders: Math.floor(Math.random() * 1000),\n        leechers: Math.floor(Math.random() * 1000),\n        completed: Math.floor(Math.random() * 1000),\n        comments: Math.floor(Math.random() * 1000),\n        host: host,\n        tags: this.getTags(),\n        entryName: \"全部\",\n        progress: Math.floor(Math.random() * 100),\n        status: status[Math.floor(Math.random() * status.length)]\n      };\n\n      results.push(data);\n    }\n\n    return JSON.stringify(results);\n  }\n\n  private getTitle() {\n    let title: string[] = [\"The Shawshank Redemption 1994\"];\n    let datas = [\"BluRay\", \"720p\", \"1080p\", \"x265\", \"10bit\"];\n\n    const count = Math.floor(Math.random() * datas.length) - 1;\n    if (count <= 0) {\n      return title.join(\" \");\n    }\n\n    for (let i = 0; i < count; i++) {\n      let index = Math.floor(Math.random() * datas.length);\n      title.push(datas[index]);\n      datas.splice(index, 1);\n    }\n\n    return title.join(\" \");\n  }\n\n  private getSubTitle() {\n    let title: string[] = [\"肖申克的救赎\"];\n    let datas = [\" / 刺激1995(台)\", \" / 月黑高飞(港)\", \"英简繁特效\"];\n\n    const count = Math.floor(Math.random() * datas.length) - 1;\n    if (count <= 0) {\n      return title.join(\" \");\n    }\n\n    for (let i = 0; i < count; i++) {\n      let index = Math.floor(Math.random() * datas.length);\n      title.push(datas[index]);\n      datas.splice(index, 1);\n    }\n\n    return title.join(\" \");\n  }\n\n  private getHost() {\n    let index = Math.floor(Math.random() * this.config.sites.length);\n\n    return this.config.sites[index].host;\n  }\n\n  private getTags() {\n    let datas = [\n      {\n        name: \"Free\",\n        color: \"blue\"\n      },\n      {\n        name: \"2xFree\",\n        color: \"green\"\n      },\n      {\n        name: \"2xUp\",\n        color: \"lime\"\n      },\n      {\n        name: \"2x50%\",\n        color: \"light-green\"\n      },\n      {\n        name: \"30%\",\n        color: \"indigo\"\n      },\n      {\n        name: \"50%\",\n        color: \"orange\"\n      }\n    ];\n\n    const index = Math.floor(Math.random() * datas.length) - 1;\n    if (index <= 0) {\n      return [];\n    }\n\n    return [datas[index]];\n  }\n\n  private getSize() {\n    const units = [\"MB\", \"GB\"];\n\n    return (\n      (Math.random() * 1000).toFixed(2) +\n      units[Math.floor(Math.random() * units.length)]\n    );\n  }\n}\n"
  },
  {
    "path": "debug/src/buildResource.ts",
    "content": "import { BuildPlugin } from \"./BuildPlugin\";\n\nlet buildPlugin = new BuildPlugin();\nbuildPlugin.buildResource();\nbuildPlugin.getSupportedSites();\nconsole.log(\"编译完成于：%s \\n\", new Date().toLocaleString());\n"
  },
  {
    "path": "debug/src/index.ts",
    "content": "\n\nimport App from \"./App\";\nimport * as config from \"../config/config.json\";\nnew App(config).start();\n"
  },
  {
    "path": "debug/tsconfig.json",
    "content": "// {\n//   \"compilerOptions\": {\n//     /* Basic Options */\n//     \"target\": \"es5\",\n//     /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */\n//     \"module\": \"commonjs\",\n//     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */\n//     // \"lib\": [],                             /* Specify library files to be included in the compilation. */\n//     // \"allowJs\": true,                       /* Allow javascript files to be compiled. */\n//     // \"checkJs\": true,                       /* Report errors in .js files. */\n//     // \"jsx\": \"preserve\",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */\n//     // \"declaration\": true,                   /* Generates corresponding '.d.ts' file. */\n//     // \"sourceMap\": true,                     /* Generates corresponding '.map' file. */\n//     // \"outFile\": \"transmission.js\",\n//     /* Concatenate and emit output to single file. */\n//     // \"outDir\": \"dist\",\n//     /* Redirect output structure to the directory. */\n//     // \"rootDir\": \"./\",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */\n//     // \"removeComments\": true,                /* Do not emit comments to output. */\n//     // \"noEmit\": true,                        /* Do not emit outputs. */\n//     // \"importHelpers\": true,                 /* Import emit helpers from 'tslib'. */\n//     // \"downlevelIteration\": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */\n//     // \"isolatedModules\": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */\n\n//     /* Strict Type-Checking Options */\n//     \"strict\": true,\n//     /* Enable all strict type-checking options. */\n//     // \"noImplicitAny\": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */\n//     // \"strictNullChecks\": true,              /* Enable strict null checks. */\n//     // \"strictFunctionTypes\": true,           /* Enable strict checking of function types. */\n//     // \"strictPropertyInitialization\": true,  /* Enable strict checking of property initialization in classes. */\n//     // \"noImplicitThis\": true,                /* Raise error on 'this' expressions with an implied 'any' type. */\n//     // \"alwaysStrict\": true,                  /* Parse in strict mode and emit \"use strict\" for each source file. */\n\n//     /* Additional Checks */\n//     // \"noUnusedLocals\": true,                /* Report errors on unused locals. */\n//     // \"noUnusedParameters\": true,            /* Report errors on unused parameters. */\n//     // \"noImplicitReturns\": true,             /* Report error when not all code paths in function return a value. */\n//     // \"noFallthroughCasesInSwitch\": true,    /* Report errors for fallthrough cases in switch statement. */\n\n//     /* Module Resolution Options */\n//     // \"moduleResolution\": \"node\",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */\n//     // \"baseUrl\": \"./\",                       /* Base directory to resolve non-absolute module names. */\n//     // \"paths\": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */\n//     // \"rootDirs\": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */\n//     // \"typeRoots\": [],                       /* List of folders to include type definitions from. */\n//     // \"types\": [],                           /* Type declaration files to be included in compilation. */\n//     // \"allowSyntheticDefaultImports\": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */\n//     \"esModuleInterop\": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */\n//     // \"preserveSymlinks\": true,              /* Do not resolve the real path of symlinks. */\n\n//     /* Source Map Options */\n//     // \"sourceRoot\": \"./\",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */\n//     // \"mapRoot\": \"./\",                       /* Specify the location where debugger should locate map files instead of generated locations. */\n//     // \"inlineSourceMap\": true,               /* Emit a single file with source maps instead of having a separate file. */\n//     // \"inlineSources\": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */\n\n//     /* Experimental Options */\n//     // \"experimentalDecorators\": true,        /* Enables experimental support for ES7 decorators. */\n//     // \"emitDecoratorMetadata\": true,         /* Enables experimental support for emitting type metadata for decorators. */\n//   },\n//   \"include\": [\n//     \"src/**/*\"\n//   ],\n//   \"exclude\": [\n//     \"node_modules\",\n//     \"typings/main\",\n//     \"typings/main.d.ts\"\n//   ]\n// }\n\n{\n\t// tsconfig 所在的根目录, 则是一个project\n\t\"compilerOptions\": {\n\t\t \"module\": \"commonjs\", // 模块系统\n\t\t \"target\": \"es2015\",   // 生成目标, 一般选择ES6，因为不是客户端环境，没必要还编译成  ES5\n\t\t \"outDir\": \"dist\",\n\t\t \n\t\t // 一组严苛的编译选项\n\t\t \"noImplicitAny\": false,\n\t\t \"strictNullChecks\": true,\n\t\t \"strict\": true,\n\t\t \"alwaysStrict\": true,\n\t\t \"sourceMap\": false,\n\t\t \"noImplicitReturns\": true,\n\t\t \"noImplicitThis\": true,\n\t\t \"pretty\": true,\n\t\t \n\t\t \"listFiles\": true,  // 包含了哪些库，这个必要的时候还是很有用的\n\t\t \"listEmittedFiles\": true, \n\t\t \"lib\": [            // 要那些 lib，按需选择即可\n\t\t\t  \"es2016\"\n\t\t ],\n\t\t // \"noUnusedLocals\": true,\n\t\t // \"noUnusedParameters\": true,\n\t\t // \"noFallthroughCasesInSwitch\": true,\n\t\t // 指定库的搜索路径，这个比较有用，一般会指定 @types，还可以按需添加\n\t\t \"typeRoots\": [\n\t\t\t  \"./node_modules/@types\"\n\t\t ]\n\t\t // 库搜索路径下, 仅使用哪些库, 一般没啥用\n\t\t // \"types\": [\n\t\t\t  \n\t\t // ]\n\t},\n\t// file include会算出一个交集, 指明哪些是项目的 ts 文件\n\t\"include\": [\n\t\t \"./**/*\"\n\t],\n\t// 排除项目下面不符合要求的文件，这个按需设定即可，可以放心排除乱七八糟的文件\n\t\"exclude\": [\n\t\t \"node_modules\",\n\t\t \"**/*.spec.ts\",\n\t\t \"*.js\"\n\t]\n}\n"
  },
  {
    "path": "debug/typings.d.ts",
    "content": "declare module \"*.json\" {\n\tconst value: any;\n\texport default value;\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"pt-plugin-plus\",\n  \"version\": \"1.6.1\",\n  \"packageManager\": \"yarn@1.19.1\",\n  \"author\": {\n    \"name\": \"ronggang\",\n    \"url\": \"https://github.com/ronggang\"\n  },\n  \"archiverName\": \"PT-Plugin-Plus\",\n  \"displayName\": \"PT 助手 Plus\",\n  \"homepage\": \"https://github.com/pt-plugins/PT-Plugin-Plus\",\n  \"scripts\": {\n    \"serve\": \"set NODE_OPTIONS=--openssl-legacy-provider & vue-cli-service serve --mode=test\",\n    \"build\": \"set NODE_OPTIONS=--openssl-legacy-provider & yarn build:index && yarn build:background && yarn build:content && yarn resource\",\n    \"lint\": \"vue-cli-service lint\",\n    \"background\": \"webpack --config webpack/prod-background.js && webpack --config webpack/prod-content.js\",\n    \"dev\": \"yarn dev:index && yarn dev:background && yarn dev:content && yarn resource\",\n    \"dev-s\": \"cd debug && yarn install && tsc && node ./dist/index.js\",\n    \"dev:index\": \"yarn install && vue-cli-service build --mode=development\",\n    \"dev:background\": \"webpack --config webpack/dev-background.js --progress\",\n    \"dev:content\": \"webpack --config webpack/dev-content.js --progress\",\n    \"dev:bc\": \"yarn dev:background && yarn dev:content\",\n    \"resource\": \"cd debug && yarn install && tsc && node ./dist/buildResource.js\",\n    \"build:index\": \"yarn install && vue-cli-service build\",\n    \"build:background\": \"webpack --config webpack/prod-background.js --progress\",\n    \"build:content\": \"webpack --config webpack/prod-content.js --progress\"\n  },\n  \"dependencies\": {\n    \"@typescript-eslint/eslint-plugin\": \"^4.4.0\",\n    \"@typescript-eslint/parser\": \"^4.4.0\",\n    \"basiccontext\": \"^3.5.1\",\n    \"blueimp-md5\": \"^2.19.0\",\n    \"crypto-js\": \"^3.1.9-1\",\n    \"dayjs\": \"^1.11.5\",\n    \"dom-to-image\": \"^2.6.0\",\n    \"dotenv\": \"^8.2.0\",\n    \"extend\": \"^3.0.2\",\n    \"file-saver\": \"^2.0.5\",\n    \"github-markdown-css\": \"^5.1.0\",\n    \"highcharts\": \"^10.2.1\",\n    \"highcharts-vue\": \"^1.4.0\",\n    \"i18next\": \"^21.9.1\",\n    \"jszip\": \"^3.10.1\",\n    \"marked\": \"^4.2.4\",\n    \"parse-torrent\": \"^7.0.1\",\n    \"ua-parser-js\": \"^1.0.2\",\n    \"url-parse\": \"^1.5.10\",\n    \"vue\": \"~2.6.14\",\n    \"vue-class-component\": \"^6.3.2\",\n    \"vue-i18n\": \"^8.11.2\",\n    \"vue-property-decorator\": \"^7.0.0\",\n    \"vue-router\": \"~3.5.4\",\n    \"vuetify\": \"^1.3.0\",\n    \"vuex\": \"^3.0.1\",\n    \"webdav\": \"^3.6.2\"\n  },\n  \"devDependencies\": {\n    \"@types/blueimp-md5\": \"^2.18.0\",\n    \"@types/chrome\": \"^0.0.75\",\n    \"@types/crypto-js\": \"^3.1.43\",\n    \"@types/dom-to-image\": \"^2.6.4\",\n    \"@types/extend\": \"^3.0.1\",\n    \"@types/file-saver\": \"^2.0.5\",\n    \"@types/jquery\": \"^3.5.14\",\n    \"@types/marked\": \"^4.0.8\",\n    \"@types/parse-torrent\": \"^5.8.4\",\n    \"@types/ua-parser-js\": \"^0.7.36\",\n    \"@types/url-parse\": \"^1.4.8\",\n    \"@vue/cli-plugin-babel\": \"^3.0.5\",\n    \"@vue/cli-plugin-eslint\": \"^5.0.0\",\n    \"@vue/cli-plugin-typescript\": \"^3.2.0\",\n    \"@vue/cli-service\": \"^3.0.5\",\n    \"@vue/eslint-config-typescript\": \"^11.0.0\",\n    \"babel-eslint\": \"^10.0.1\",\n    \"copy-webpack-plugin\": \"^4.6.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-plugin-vue\": \"^9.0.0\",\n    \"git-rev-sync\": \"^3.0.2\",\n    \"sass\": \"^1.54.8\",\n    \"sass-loader\": \"~7.3.1\",\n    \"stylus\": \"^0.54.5\",\n    \"stylus-loader\": \"^3.0.1\",\n    \"terser-webpack-plugin\": \"^2.2.1\",\n    \"ts-loader\": \"^5.3.1\",\n    \"ts-node\": \"^8.5.2\",\n    \"typescript\": \"^3.0.0\",\n    \"uglifyjs-webpack-plugin\": \"~2.1.3\",\n    \"vue-cli-plugin-vuetify\": \"^0.4.6\",\n    \"vue-template-compiler\": \"~2.6.14\",\n    \"vuetify-loader\": \"~1.7.3\",\n    \"webpack\": \"^4.46.0\",\n    \"webpack-cli\": \"^3.3.12\",\n    \"webpack-merge\": \"^4.2.2\"\n  },\n  \"resolutions\": {\n    \"@types/node\": \"~18.11.9\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true\n    },\n    \"extends\": [\n      \"plugin:vue/essential\",\n      \"eslint:recommended\",\n      \"@vue/typescript\"\n    ],\n    \"rules\": {\n      \"no-console\": 0\n    },\n    \"parserOptions\": {\n      \"parser\": \"@typescript-eslint/parser\"\n    }\n  },\n  \"postcss\": {\n    \"plugins\": {\n      \"autoprefixer\": {}\n    }\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ]\n}\n"
  },
  {
    "path": "privacy-statement.md",
    "content": "感谢使用PT助手（下称『助手』），为了让您能够安心的使用助手，特此向您说明助手的隐私权保护政策，以保障您的权益，请您详阅下列内容：\n\n## 一、隐私权保护政策的适用范围\n- 隐私权保护政策仅适用于助手，不适用于助手以外的相关网站；\n\n## 二、个人信息的收集、处理及利用方式\n- 在您使用助手的过程中，助手会使用您的个人信息进行展示，展示内容包括：\n  - 您在已配置站点里的个人信息；\n  - 已保存的个人信息历史记录生成图表；\n- 个人信息仅用于在助手里展示，不会用于其他用途；\n\n## 三、信息存储和交换\n- 在您使用助手的过程中，会产生一些配置数据和历史记录，这些信息全部存储于当前浏览器；\n- 当您已配置了备份服务器，这些信息会由您决定是否上传至这些服务器；\n- 除此之外，助手不会将这些数据上传至任何第三方服务器；\n- 如果您选择了使用备份服务器，我们强烈建议您使用以下方式进行配置：\n  - 使用 `https` 的方式配置服务器地址；\n  - 启用备份数据加密（采用 [AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)）功能后再进行备份操作；\n\n## 四、与第三人共用个人信息之政策\n- 助手不会收集任何相关的个人信息，所以助手绝不会提供、交换、出租或出售任何您的个人信息给其他个人、团体、私人企业或公务机关；\n\n## 五、Cookie之使用\n- 当您进行搜索操作时，助手会访问您已配置的站点，Cookie 由浏览器提供，助手无权访问也不会对内容进行探测和修改，一切内容由浏览器和站点进行相互验证；\n- 当您授权助手访问 Cookie 信息时，这些信息仅用于备份和恢复操作，助手不会在除此之外的任何其他操作中使用它们；\n\n## 六、隐私权保护政策之修正\n- 助手隐私权保护政策将按用户需求变更而随时进行修正，修正后的条款将在本页面显示。\n"
  },
  {
    "path": "public/_locales/en/messages.json",
    "content": "{\r\n\t\"manifest_appName\": {\r\n\t\t\"message\": \"PT Plugin Plus\"\r\n\t},\r\n\t\"manifest_shortName\": {\r\n\t\t\"message\": \"PT Plugin Plus\"\r\n\t},\r\n\t\"manifest_appDescription\": {\r\n\t\t\"message\": \"Just a tools for Private Tracker.\"\r\n\t}\r\n}"
  },
  {
    "path": "public/_locales/zh_CN/messages.json",
    "content": "{\r\n\t\"manifest_appName\": {\r\n\t\t\"message\": \"PT Plugin Plus\"\r\n\t},\r\n\t\"manifest_shortName\": {\r\n\t\t\"message\": \"PTPP\"\r\n\t},\r\n\t\"manifest_appDescription\": {\r\n\t\t\"message\": \"PT 助手，一个可以提升 PT 站点使用效率的工具。\"\r\n\t}\r\n}"
  },
  {
    "path": "public/assets/base.css",
    "content": ".pt-plugin-body {\n  background-color: aliceblue;\n  border-radius: 8px;\n  padding-bottom: 5px;\n  width: 80px;\n  position: fixed;\n  right: 5px;\n  opacity: .3;\n  z-index: 10000;\n  font-size: 12px;\n}\n\n.pt-plugin-body .pt-plugin-drag-title {\n  cursor: move;\n  width: 100%;\n  border-top-left-radius: 5px;\n  border-top-right-radius: 5px;\n  background-color: #ffc107;\n  height: 7px;\n}\n\n.pt-plugin-body:hover,\n.pt-plugin-body-over {\n  opacity: 0.9\n}\n\n.pt-plugin-body hr {\n  height: 2px;\n  border: 0;\n  border-bottom: 1px dotted #ccc;\n  margin: 4px 5px 5px 5px;\n}\n\n.pt-plugin-body .pt-plugin-logo {\n  background-image: url('chrome-extension://__MSG_@@extension_id__/assets/icon-64.png');\n  background-size: cover;\n  height: 32px;\n  width: 32px;\n  margin: 5px auto;\n  opacity: 0.7;\n  cursor: pointer;\n}\n\n@-moz-document url-prefix() {\n  .pt-plugin-body .pt-plugin-logo {\n    background-image: url('moz-extension://__MSG_@@extension_id__/assets/icon-64.png');\n  }\n}\n\n\n.pt-plugin-body .pt-plugin-droper {\n  height: 45px;\n  width: 100%;\n  background-color: #1976d2;\n  opacity: 0.2;\n  border-radius: 5px;\n  position: absolute;\n  top: 0;\n  left: 0;\n  z-index: 100;\n}\n\n.pt-plugin-body .pt-plugin-button {\n  height: 50px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: #1976d2;\n  text-decoration: none;\n}\n\n.pt-plugin-body a.pt-plugin-button {\n  cursor: pointer;\n  text-decoration: none;\n  color: #1976d2;\n}\n\n.pt-plugin-body a.pt-plugin-button:hover {\n  background-color: rgba(77, 154, 231, 0.226)\n}\n\n.pt-plugin-body .pt-plugin-button .pt-plugin-button-inner {\n  text-align: center;\n}\n\n.pt-plugin-body .pt-plugin-onLoading {\n  animation: onLoading 1.9s linear infinite running;\n}\n\n/* 正在执行动画 */\n.pt-plugin-body .pt-plugin-loading {\n  width: 40px;\n  height: 40px;\n  border-radius: 50%;\n  border-width: 5px;\n  border-style: solid;\n  border-color: #1976d2;\n  border-left-style: dashed;\n  border-left-color: rgba(77, 154, 231, 0.226);\n  animation: onLoading 1.9s linear infinite running;\n  display: none;\n}\n\n@keyframes onLoading {\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.action-success {\n  width: 40px;\n  height: 40px;\n  position: relative;\n  /* background: #4caf50; */\n  background-color: transparent;\n  border-radius: 50%;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  display: none;\n}\n\n.action-success-ani {\n  width: 100%;\n  height: 50%;\n  margin: 10% auto;\n  transform: rotate(-45deg);\n  transform: rotate(-45deg);\n  overflow: hidden;\n}\n\n.action-success-ani:before,\n.action-success-ani:after {\n  content: \"\";\n  position: absolute;\n  background: #4caf50;\n  border-radius: 2px\n}\n\n.action-success-ani:before {\n  width: 4px;\n  height: 100%;\n  left: 0;\n  animation: onActionSuccessLeft 0.2s linear 0.2s 1 both;\n  animation: onActionSuccessLeft 0.2s linear 0.2s 1 both\n}\n\n.action-success-ani:after {\n  width: 100%;\n  height: 4px;\n  bottom: 0;\n  animation: onActionSuccessRight 0.2s linear 0.4s 1 both;\n  animation: onActionSuccessRight 0.2s linear 0.4s 1 both\n}\n\n@keyframes onActionSuccessLeft {\n  0% {\n    top: -100%\n  }\n\n  100% {\n    top: 0%\n  }\n}\n\n@keyframes onActionSuccessLeft {\n  0% {\n    top: -100%\n  }\n\n  100% {\n    top: 0%\n  }\n}\n\n@keyframes onActionSuccessRight {\n  0% {\n    left: -100%\n  }\n\n  100% {\n    left: 0%\n  }\n}\n\n@keyframes onActionSuccessRight {\n  0% {\n    left: -100%\n  }\n\n  100% {\n    left: 0%\n  }\n}"
  },
  {
    "path": "public/assets/options.css",
    "content": ".btn-mini {\n  width: 20px !important;\n  height: 20px !important;\n  font-size: 12px !important;\n}\n\n.btn-mini .v-btn__content i {\n  font-size: 12px !important;\n}"
  },
  {
    "path": "public/changelog.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n  <title><%= htmlWebpackPlugin.options.title %></title>\n  <link rel=\"icon\" type=\"image/x-icon\" href=\"./assets/icon-19.png\">\n</head>\n\n<body>\n  <div id=\"app\"></div>\n  <!-- built files will be auto injected -->\n</body>\n\n</html>\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n  <title><%= htmlWebpackPlugin.options.title %></title>\n  <link rel=\"stylesheet\" href=\"./libs/materialIcons/style.css\">\n  <link rel=\"stylesheet\" href=\"./assets/options.css\">\n  <link rel=\"icon\" type=\"image/x-icon\" href=\"./assets/icon-19.png\">\n  <script src=\"./libs/jquery/jquery-3.3.1.min.js\"></script>\n  <script src=\"./libs/Base64.js\"></script>\n</head>\n\n<body>\n  <div id=\"app\"></div>\n  <!-- built files will be auto injected -->\n</body>\n\n</html>"
  },
  {
    "path": "public/libs/Base64.js",
    "content": "/**\n*\n*  Base64 encode / decode\n*\n*  @author haitao.tu\n*  @date   2010-04-26\n*  @email  tuhaitao@foxmail.com\n*\n*/\n \nfunction Base64() {\n \n\t// private property\n\t_keyStr = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=\";\n \n\t// public method for encoding\n\tthis.encode = function (input) {\n\t\tvar output = \"\";\n\t\tvar chr1, chr2, chr3, enc1, enc2, enc3, enc4;\n\t\tvar i = 0;\n\t\tinput = _utf8_encode(input);\n\t\twhile (i < input.length) {\n\t\t\tchr1 = input.charCodeAt(i++);\n\t\t\tchr2 = input.charCodeAt(i++);\n\t\t\tchr3 = input.charCodeAt(i++);\n\t\t\tenc1 = chr1 >> 2;\n\t\t\tenc2 = ((chr1 & 3) << 4) | (chr2 >> 4);\n\t\t\tenc3 = ((chr2 & 15) << 2) | (chr3 >> 6);\n\t\t\tenc4 = chr3 & 63;\n\t\t\tif (isNaN(chr2)) {\n\t\t\t\tenc3 = enc4 = 64;\n\t\t\t} else if (isNaN(chr3)) {\n\t\t\t\tenc4 = 64;\n\t\t\t}\n\t\t\toutput = output +\n\t\t\t_keyStr.charAt(enc1) + _keyStr.charAt(enc2) +\n\t\t\t_keyStr.charAt(enc3) + _keyStr.charAt(enc4);\n\t\t}\n\t\treturn output;\n\t}\n \n\t// public method for decoding\n\tthis.decode = function (input) {\n\t\tvar output = \"\";\n\t\tvar chr1, chr2, chr3;\n\t\tvar enc1, enc2, enc3, enc4;\n\t\tvar i = 0;\n\t\tinput = input.replace(/[^A-Za-z0-9\\+\\/\\=]/g, \"\");\n\t\twhile (i < input.length) {\n\t\t\tenc1 = _keyStr.indexOf(input.charAt(i++));\n\t\t\tenc2 = _keyStr.indexOf(input.charAt(i++));\n\t\t\tenc3 = _keyStr.indexOf(input.charAt(i++));\n\t\t\tenc4 = _keyStr.indexOf(input.charAt(i++));\n\t\t\tchr1 = (enc1 << 2) | (enc2 >> 4);\n\t\t\tchr2 = ((enc2 & 15) << 4) | (enc3 >> 2);\n\t\t\tchr3 = ((enc3 & 3) << 6) | enc4;\n\t\t\toutput = output + String.fromCharCode(chr1);\n\t\t\tif (enc3 != 64) {\n\t\t\t\toutput = output + String.fromCharCode(chr2);\n\t\t\t}\n\t\t\tif (enc4 != 64) {\n\t\t\t\toutput = output + String.fromCharCode(chr3);\n\t\t\t}\n\t\t}\n\t\toutput = _utf8_decode(output);\n\t\treturn output;\n\t}\n \n\t// private method for UTF-8 encoding\n\t_utf8_encode = function (string) {\n\t\tstring = string.replace(/\\r\\n/g,\"\\n\");\n\t\tvar utftext = \"\";\n\t\tfor (var n = 0; n < string.length; n++) {\n\t\t\tvar c = string.charCodeAt(n);\n\t\t\tif (c < 128) {\n\t\t\t\tutftext += String.fromCharCode(c);\n\t\t\t} else if((c > 127) && (c < 2048)) {\n\t\t\t\tutftext += String.fromCharCode((c >> 6) | 192);\n\t\t\t\tutftext += String.fromCharCode((c & 63) | 128);\n\t\t\t} else {\n\t\t\t\tutftext += String.fromCharCode((c >> 12) | 224);\n\t\t\t\tutftext += String.fromCharCode(((c >> 6) & 63) | 128);\n\t\t\t\tutftext += String.fromCharCode((c & 63) | 128);\n\t\t\t}\n \n\t\t}\n\t\treturn utftext;\n\t}\n \n\t// private method for UTF-8 decoding\n\t_utf8_decode = function (utftext) {\n\t\tvar string = \"\";\n\t\tvar i = 0;\n\t\tvar c = c1 = c2 = 0;\n\t\twhile ( i < utftext.length ) {\n\t\t\tc = utftext.charCodeAt(i);\n\t\t\tif (c < 128) {\n\t\t\t\tstring += String.fromCharCode(c);\n\t\t\t\ti++;\n\t\t\t} else if((c > 191) && (c < 224)) {\n\t\t\t\tc2 = utftext.charCodeAt(i+1);\n\t\t\t\tstring += String.fromCharCode(((c & 31) << 6) | (c2 & 63));\n\t\t\t\ti += 2;\n\t\t\t} else {\n\t\t\t\tc2 = utftext.charCodeAt(i+1);\n\t\t\t\tc3 = utftext.charCodeAt(i+2);\n\t\t\t\tstring += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));\n\t\t\t\ti += 3;\n\t\t\t}\n\t\t}\n\t\treturn string;\n\t}\n}"
  },
  {
    "path": "public/libs/drag.js",
    "content": "/**\n * 拖放功能\n * @see http://demo.jb51.net/js/2015/js-mxdx-draw-plug-codes/\n */\nfunction Drag() {\n  // 初始化\n  this.initialize.apply(this, arguments);\n}\nDrag.prototype = {\n  // 参数设置\n  setOptions: function(options) {\n    this.options = {\n      // 事件对象\n      handle: this.drag,\n      // 锁定范围\n      limit: true,\n      // 锁定位置\n      lock: false,\n      // 锁定水平位置\n      lockX: false,\n      // 锁定垂直位置\n      lockY: false,\n      // 指定限制容器\n      maxContainer: document.documentElement || document.body,\n\n      // 开始时回调函数\n      onStart: function() {},\n      // 拖拽时回调函数\n      onMove: function() {},\n      // 停止时回调函数\n      onStop: function() {}\n    };\n    for (var p in options) this.options[p] = options[p];\n  },\n  // 初始化\n  initialize: function(drag, options) {\n    this.drag = this.$(drag);\n    this._x = this._y = this.left = this.top = 0;\n    this._moveDrag = this.bind(this, this.moveDrag);\n    this._stopDrag = this.bind(this, this.stopDrag);\n\n    this.setOptions(options);\n\n    this.handle = this.$(this.options.handle);\n    this.maxContainer = this.$(this.options.maxContainer);\n\n    this.maxTop =\n      Math.max(this.maxContainer.clientHeight, this.maxContainer.scrollHeight) -\n      this.drag.offsetHeight;\n    this.maxLeft =\n      Math.max(this.maxContainer.clientWidth, this.maxContainer.scrollWidth) -\n      this.drag.offsetWidth;\n    this.limit = this.options.limit;\n    this.lockX = this.options.lockX;\n    this.lockY = this.options.lockY;\n    this.lock = this.options.lock;\n    this.onStart = this.options.onStart;\n    this.onMove = this.options.onMove;\n    this.onStop = this.options.onStop;\n    this.handle.style.cursor = \"move\";\n    this.addHandler(this.handle, \"mousedown\", this.bind(this, this.startDrag));\n  },\n  startDrag: function(event) {\n    var event = event || window.event;\n    this._x = event.clientX - this.drag.offsetLeft;\n    this._y = event.clientY - this.drag.offsetTop;\n    this.addHandler(document, \"mousemove\", this._moveDrag);\n    this.addHandler(document, \"mouseup\", this._stopDrag);\n    event.preventDefault && event.preventDefault();\n    this.handle.setCapture && this.handle.setCapture();\n    this.onStart();\n  },\n  moveDrag: function(event) {\n    var event = event || window.event;\n    var iTop = event.clientY - this._y;\n    var iLeft = event.clientX - this._x;\n    if (this.lock) return;\n    this.limit &&\n      (iTop < 0 && (iTop = 0),\n      iLeft < 0 && (iLeft = 0),\n      iTop > this.maxTop && (iTop = this.maxTop),\n      iLeft > this.maxLeft && (iLeft = this.maxLeft));\n    this.lockY || (this.drag.style.top = iTop + \"px\");\n    this.lockX || (this.drag.style.left = iLeft + \"px\");\n    event.preventDefault && event.preventDefault();\n    this.left = iLeft;\n    this.top = iTop;\n    this.onMove();\n  },\n  stopDrag: function() {\n    this.removeHandler(document, \"mousemove\", this._moveDrag);\n    this.removeHandler(document, \"mouseup\", this._stopDrag);\n    this.handle.releaseCapture && this.handle.releaseCapture();\n    this.onStop({\n      left: this.left,\n      top: this.top\n    });\n  },\n\n  // 获取id\n  $: function(id) {\n    return typeof id === \"string\" ? document.getElementById(id) : id;\n  },\n  // 添加绑定事件\n  addHandler: function(oElement, sEventType, fnHandler) {\n    return oElement.addEventListener\n      ? oElement.addEventListener(sEventType, fnHandler, false)\n      : oElement.attachEvent(\"on\" + sEventType, fnHandler);\n  },\n  // 删除绑定事件\n  removeHandler: function(oElement, sEventType, fnHandler) {\n    return oElement.removeEventListener\n      ? oElement.removeEventListener(sEventType, fnHandler, false)\n      : oElement.detachEvent(\"on\" + sEventType, fnHandler);\n  },\n  // 绑定事件到对象\n  bind: function(object, fnHandler) {\n    return function() {\n      return fnHandler.apply(object, arguments);\n    };\n  }\n};\n"
  },
  {
    "path": "public/libs/materialIcons/content_style.css",
    "content": "/* fallback */\n@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: url('chrome-extension://__MSG_@@extension_id__/libs/materialIcons/font.woff2') format('woff2');\n}\n\n/*This will work for firefox*/\n@-moz-document url-prefix() {\n  @font-face {\n    font-family: 'Material Icons';\n    font-style: normal;\n    font-weight: 400;\n    src: url('moz-extension://__MSG_@@extension_id__/libs/materialIcons/font.woff2') format('woff2');\n  }\n}\n\n.material-icons {\n  font-family: 'Material Icons';\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;\n  line-height: 1;\n  letter-spacing: normal;\n  text-transform: none;\n  display: inline-block;\n  white-space: nowrap;\n  word-wrap: normal;\n  direction: ltr;\n  -webkit-font-feature-settings: 'liga';\n  -webkit-font-smoothing: antialiased;\n}"
  },
  {
    "path": "public/libs/materialIcons/style.css",
    "content": "/* fallback */\n@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: url(font.woff2) format('woff2');\n}\n\n.material-icons {\n  font-family: 'Material Icons';\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;\n  line-height: 1;\n  letter-spacing: normal;\n  text-transform: none;\n  display: inline-block;\n  white-space: nowrap;\n  word-wrap: normal;\n  direction: ltr;\n  -webkit-font-smoothing: antialiased;\n}\n"
  },
  {
    "path": "public/libs/notice/notice.js",
    "content": "(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine(\"NoticeJs\", [], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"NoticeJs\"] = factory();\n\telse\n\t\troot[\"NoticeJs\"] = factory();\n})(typeof self !== 'undefined' ? self : this, function() {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\ti: moduleId,\n/******/ \t\t\tl: false,\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.l = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// define getter function for harmony exports\n/******/ \t__webpack_require__.d = function(exports, name, getter) {\n/******/ \t\tif(!__webpack_require__.o(exports, name)) {\n/******/ \t\t\tObject.defineProperty(exports, name, {\n/******/ \t\t\t\tconfigurable: false,\n/******/ \t\t\t\tenumerable: true,\n/******/ \t\t\t\tget: getter\n/******/ \t\t\t});\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t__webpack_require__.n = function(module) {\n/******/ \t\tvar getter = module && module.__esModule ?\n/******/ \t\t\tfunction getDefault() { return module['default']; } :\n/******/ \t\t\tfunction getModuleExports() { return module; };\n/******/ \t\t__webpack_require__.d(getter, 'a', getter);\n/******/ \t\treturn getter;\n/******/ \t};\n/******/\n/******/ \t// Object.prototype.hasOwnProperty.call\n/******/ \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"dist/\";\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(__webpack_require__.s = 2);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nObject.defineProperty(exports, \"__esModule\", {\n    value: true\n});\nvar noticeJsModalClassName = exports.noticeJsModalClassName = 'noticejs-modal';\nvar closeAnimation = exports.closeAnimation = 'noticejs-fadeOut';\n\nvar Defaults = exports.Defaults = {\n    title: '',\n    text: '',\n    type: 'success',\n    position: 'topRight',\n    newestOnTop: false,\n    timeout: 30,\n    progressBar: true,\n    indeterminate: false,\n    closeWith: ['button'],\n    animation: null,\n    modal: false,\n    width: 320,\n    scroll: {\n        maxHeightContent: 300,\n        showOnHover: true\n    },\n    rtl: false,\n    callbacks: {\n        beforeShow: [],\n        onShow: [],\n        afterShow: [],\n        onClose: [],\n        afterClose: [],\n        onClick: [],\n        onHover: [],\n        onTemplate: []\n    }\n};\n\n/***/ }),\n/* 1 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nObject.defineProperty(exports, \"__esModule\", {\n    value: true\n});\nexports.appendNoticeJs = exports.addListener = exports.CloseItem = exports.AddModal = undefined;\nexports.getCallback = getCallback;\n\nvar _api = __webpack_require__(0);\n\nvar API = _interopRequireWildcard(_api);\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nvar options = API.Defaults;\n\n/**\r\n * @param {NoticeJs} ref\r\n * @param {string} eventName\r\n * @return {void}\r\n */\nfunction getCallback(ref, eventName) {\n    if (ref.callbacks.hasOwnProperty(eventName)) {\n        ref.callbacks[eventName].forEach(function (cb) {\n            if (typeof cb === 'function') {\n                cb.apply(ref);\n            }\n        });\n    }\n}\n\nvar AddModal = exports.AddModal = function AddModal() {\n    if (document.getElementsByClassName(API.noticeJsModalClassName).length <= 0) {\n        var element = document.createElement('div');\n        element.classList.add(API.noticeJsModalClassName);\n        element.classList.add('noticejs-modal-open');\n        document.body.appendChild(element);\n        // Remove class noticejs-modal-open\n        setTimeout(function () {\n            element.className = API.noticeJsModalClassName;\n        }, 200);\n    }\n};\n\nvar CloseItem = exports.CloseItem = function CloseItem(item) {\n    getCallback(options, 'onClose');\n\n    // Set animation to close notification item\n    if (options.animation !== null && options.animation.close !== null) {\n        item.className += ' ' + options.animation.close;\n    }\n    setTimeout(function () {\n        item.remove();\n    }, 200);\n\n    // Close modal\n    if (options.modal === true && document.querySelectorAll(\"[noticejs-modal='true']\").length >= 1) {\n        document.querySelector('.noticejs-modal').className += ' noticejs-modal-close';\n        setTimeout(function () {\n            document.querySelector('.noticejs-modal').remove();\n        }, 500);\n    }\n\n    // Remove container\n    var position = '.' + item.closest('.noticejs').className.replace('noticejs', '').trim();\n    setTimeout(function () {\n        if (document.querySelectorAll(position + ' .item').length <= 0) {\n            document.querySelector(position) && document.querySelector(position).remove();\n        }\n    }, 500);\n};\n\nvar addListener = exports.addListener = function addListener(item) {\n    // Add close button Event\n    if (options.closeWith.includes('button')) {\n        item.querySelector('.close').addEventListener('click', function () {\n            CloseItem(item);\n        });\n    }\n\n    // Add close by click Event\n    if (options.closeWith.includes('click')) {\n        item.style.cursor = 'pointer';\n        item.addEventListener('click', function (e) {\n            if (e.target.className !== 'close') {\n                getCallback(options, 'onClick');\n                CloseItem(item);\n            }\n        });\n    } else {\n        item.addEventListener('click', function (e) {\n            if (e.target.className !== 'close') {\n                getCallback(options, 'onClick');\n            }\n        });\n    }\n\n    item.addEventListener('mouseover', function () {\n        getCallback(options, 'onHover');\n    });\n};\n\nvar appendNoticeJs = exports.appendNoticeJs = function appendNoticeJs(noticeJsHeader, noticeJsBody, noticeJsProgressBar) {\n    var target_class = '.noticejs-' + options.position;\n    // Create NoticeJs item\n    var noticeJsItem = document.createElement('div');\n    noticeJsItem.classList.add('item');\n    noticeJsItem.classList.add(options.type);\n    if (options.rtl === true) {\n        noticeJsItem.classList.add('noticejs-rtl');\n    }\n    if (options.width !== '' && Number.isInteger(options.width)) {\n        noticeJsItem.style.width = options.width + 'px';\n    }\n\n    // Add Header\n    if (noticeJsHeader && noticeJsHeader !== '') {\n        noticeJsItem.appendChild(noticeJsHeader);\n    }\n\n    // Add body\n    noticeJsItem.appendChild(noticeJsBody);\n\n    // Add progress bar\n    if (noticeJsProgressBar && noticeJsProgressBar !== '') {\n        noticeJsItem.appendChild(noticeJsProgressBar);\n    }\n\n    // Empty top and bottom container\n    if (['top', 'bottom'].includes(options.position)) {\n        document.querySelector(target_class).innerHTML = '';\n    }\n\n    // Add open animation\n    if (options.animation !== null && options.animation.open !== null) {\n        noticeJsItem.className += ' ' + options.animation.open;\n    }\n\n    // Add Modal\n    if (options.modal === true) {\n        noticeJsItem.setAttribute('noticejs-modal', 'true');\n        AddModal();\n    }\n\n    // Add Listener\n    addListener(noticeJsItem, options.closeWith);\n\n    getCallback(options, 'beforeShow');\n    getCallback(options, 'onShow');\n    if (options.newestOnTop === true) {\n        document.querySelector(target_class).insertAdjacentElement('afterbegin', noticeJsItem);\n    } else {\n        document.querySelector(target_class).appendChild(noticeJsItem);\n    }\n    getCallback(options, 'afterShow');\n\n    return noticeJsItem;\n};\n\n/***/ }),\n/* 2 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nvar _noticejs = __webpack_require__(3);\n\nvar _noticejs2 = _interopRequireDefault(_noticejs);\n\nvar _api = __webpack_require__(0);\n\nvar API = _interopRequireWildcard(_api);\n\nvar _components = __webpack_require__(4);\n\nvar _helpers = __webpack_require__(1);\n\nvar helper = _interopRequireWildcard(_helpers);\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nvar NoticeJs = function () {\n  /**\r\n   * @param {object} options \r\n   * @returns {Noty}\r\n   */\n  function NoticeJs() {\n    var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n    _classCallCheck(this, NoticeJs);\n\n    this.options = Object.assign(API.Defaults, options);\n    this.component = new _components.Components();\n    this.id = \"noticejs-\" + Math.random();\n\n    this.on('beforeShow', this.options.callbacks.beforeShow);\n    this.on('onShow', this.options.callbacks.onShow);\n    this.on('afterShow', this.options.callbacks.afterShow);\n    this.on('onClose', this.options.callbacks.onClose);\n    this.on('afterClose', this.options.callbacks.afterClose);\n    this.on('onClick', this.options.callbacks.onClick);\n    this.on('onHover', this.options.callbacks.onHover);\n\n    return this;\n  }\n\n  /**\r\n   * @returns {NoticeJs}\r\n   */\n\n\n  _createClass(NoticeJs, [{\n    key: 'show',\n    value: function show() {\n      var container = this.component.createContainer();\n      if (document.querySelector('.noticejs-' + this.options.position) === null) {\n        document.body.appendChild(container);\n      }\n\n      var noticeJsHeader = void 0;\n      var noticeJsBody = void 0;\n      var noticeJsProgressBar = void 0;\n\n      // Create NoticeJs header\n      noticeJsHeader = this.component.createHeader(this.options.title, this.options.closeWith);\n\n      // Create NoticeJs body\n      noticeJsBody = this.component.createBody(this.options.text);\n\n      // Create NoticeJs progressBar\n      if (this.options.progressBar === true) {\n        noticeJsProgressBar = this.component.createProgressBar();\n      }\n\n      //Append NoticeJs\n      var noticeJs = helper.appendNoticeJs(noticeJsHeader, noticeJsBody, noticeJsProgressBar);\n      noticeJs.setAttribute(\"id\", this.id);\n      this.dom = noticeJs;\n      return noticeJs;\n    }\n\n    /**\r\n     * @param {string} eventName\r\n     * @param {function} cb\r\n     * @return {NoticeJs}\r\n     */\n\n  }, {\n    key: 'on',\n    value: function on(eventName) {\n      var cb = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {};\n\n      if (typeof cb === 'function' && this.options.callbacks.hasOwnProperty(eventName)) {\n        this.options.callbacks[eventName].push(cb);\n      }\n\n      return this;\n    }\n\n    /**\r\n     * @param {Object} options \r\n     * @return {Notice}\r\n     */\n\n  }, {\n    key: 'close',\n\n\n    /**\r\n     * close\r\n     */\n    value: function close() {\n      helper.CloseItem(this.dom);\n    }\n  }], [{\n    key: 'overrideDefaults',\n    value: function overrideDefaults(options) {\n      this.options = Object.assign(API.Defaults, options);\n      return this;\n    }\n  }]);\n\n  return NoticeJs;\n}();\n\nexports.default = NoticeJs;\nmodule.exports = exports['default'];\n\n/***/ }),\n/* 3 */\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ }),\n/* 4 */\n/***/ (function(module, exports, __webpack_require__) {\n\n\"use strict\";\n\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.Components = undefined;\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nvar _api = __webpack_require__(0);\n\nvar API = _interopRequireWildcard(_api);\n\nvar _helpers = __webpack_require__(1);\n\nvar helper = _interopRequireWildcard(_helpers);\n\nfunction _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nvar options = API.Defaults;\n\nvar Components = exports.Components = function () {\n  function Components() {\n    _classCallCheck(this, Components);\n  }\n\n  _createClass(Components, [{\n    key: 'createContainer',\n    value: function createContainer() {\n      var element_class = 'noticejs-' + options.position;\n      var element = document.createElement('div');\n      element.classList.add('noticejs');\n      element.classList.add(element_class);\n\n      return element;\n    }\n  }, {\n    key: 'createHeader',\n    value: function createHeader() {\n      var element = void 0;\n      if (options.title && options.title !== '') {\n        element = document.createElement('div');\n        element.setAttribute('class', 'noticejs-heading');\n        element.textContent = options.title;\n      }\n\n      // Add close button\n      if (options.closeWith.includes('button')) {\n        var close = document.createElement('div');\n        close.setAttribute('class', 'close');\n        close.innerHTML = '&times;';\n        if (element) {\n          element.appendChild(close);\n        } else {\n          element = close;\n        }\n      }\n\n      return element;\n    }\n  }, {\n    key: 'createBody',\n    value: function createBody() {\n      var element = document.createElement('div');\n      element.setAttribute('class', 'noticejs-body');\n      var content = document.createElement('div');\n      content.setAttribute('class', 'noticejs-content');\n      content.innerHTML = options.text;\n      element.appendChild(content);\n\n      if (options.scroll !== null && options.scroll.maxHeight !== '') {\n        element.style.overflowY = 'auto';\n        element.style.maxHeight = options.scroll.maxHeight + 'px';\n\n        if (options.scroll.showOnHover === true) {\n          element.style.visibility = 'hidden';\n        }\n      }\n      return element;\n    }\n  }, {\n    key: 'createProgressBar',\n    value: function createProgressBar() {\n      var element = document.createElement('div');\n      element.setAttribute('class', 'noticejs-progressbar');\n      var bar = document.createElement('div');\n      bar.setAttribute('class', 'noticejs-bar');\n      element.appendChild(bar);\n\n      // Progress bar animation\n      if (options.progressBar === true && typeof options.timeout !== 'boolean' && options.timeout !== false) {\n        var _frame = function _frame() {\n          if (width <= 0) {\n            clearInterval(id);\n\n            var item = element.closest('div.item');\n            // Add close animation\n            if (options.animation !== null && options.animation.close !== null) {\n\n              // Remove open animation class\n              item.className = item.className.replace(new RegExp('(?:^|\\\\s)' + options.animation.open + '(?:\\\\s|$)'), ' ');\n              // Add close animation class\n              item.className += ' ' + options.animation.close;\n\n              // Close notification after 0.5s + timeout\n              var close_time = parseInt(options.timeout) + 500;\n              setTimeout(function () {\n                helper.CloseItem(item);\n              }, close_time);\n            } else {\n              // Close notification when progress bar completed\n              helper.CloseItem(item);\n            }\n          } else {\n            width--;\n            bar.style.width = width + '%';\n          }\n        };\n\n        var _indeterminateFrame = function _indeterminateFrame() {\n          if (progressDirection === 0) {\n            width--;\n            if (width === 0) {\n              progressDirection = 1;\n            }\n          } else {\n            width++;\n            if (width === 100) {\n              progressDirection = 0;\n            }\n          }\n\n          if (document.getElementById(domId) == null) {\n            clearInterval(id);\n          } else {\n            bar.style.width = width + '%';\n          }\n        };\n\n        var width = 100;\n        var progressDirection = 0;\n        var id = 0;\n        var domId = \"\";\n\n        if (options.indeterminate === true) {\n          id = setInterval(_indeterminateFrame, options.timeout);\n          domId = \"noticejs-progressbar-\" + id;\n          element.setAttribute(\"id\", domId);\n        } else {\n          id = setInterval(_frame, options.timeout);\n        }\n      }\n\n      return element;\n    }\n  }]);\n\n  return Components;\n}();\n\n/***/ })\n/******/ ]);\n});"
  },
  {
    "path": "public/libs/notice/noticejs.css",
    "content": ".noticejs-top{top:0;width:100%!important}.noticejs-top .item{border-radius:0!important;margin:0!important}.noticejs-topRight{top:10px;right:10px}.noticejs-topLeft{top:10px;left:10px}.noticejs-topCenter{top:10px;left:50%;transform:translate(-50%)}.noticejs-middleLeft,.noticejs-middleRight{right:10px;top:50%;transform:translateY(-50%)}.noticejs-middleLeft{left:10px}.noticejs-middleCenter{top:50%;left:50%;transform:translate(-50%,-50%)}.noticejs-bottom{bottom:0;width:100%!important}.noticejs-bottom .item{border-radius:0!important;margin:0!important}.noticejs-bottomRight{bottom:10px;right:10px}.noticejs-bottomLeft{bottom:10px;left:10px}.noticejs-bottomCenter{bottom:10px;left:50%;transform:translate(-50%)}.noticejs{font-family:Helvetica Neue,Helvetica,Arial,sans-serif}.noticejs .item{margin:0 0 10px;border-radius:3px;overflow:hidden}.noticejs .item .close{float:right;font-size:18px;font-weight:700;line-height:1;color:#fff;text-shadow:0 1px 0 #fff;opacity:1;margin-right:7px}.noticejs .item .close:hover{opacity:.5;color:#000}.noticejs .item a{color:#fff;border-bottom:1px dashed #fff}.noticejs .item a,.noticejs .item a:hover{text-decoration:none}.noticejs .success{background-color:#64ce83}.noticejs .success .noticejs-heading{background-color:#3da95c;color:#fff;padding:10px}.noticejs .success .noticejs-body{color:#fff;padding:10px}.noticejs .success .noticejs-body:hover{visibility:visible!important}.noticejs .success .noticejs-content{visibility:visible}.noticejs .info{background-color:#3ea2ff}.noticejs .info .noticejs-heading{background-color:#067cea;color:#fff;padding:10px}.noticejs .info .noticejs-body{color:#fff;padding:10px}.noticejs .info .noticejs-body:hover{visibility:visible!important}.noticejs .info .noticejs-content{visibility:visible}.noticejs .warning{background-color:#ff7f48}.noticejs .warning .noticejs-heading{background-color:#f44e06;color:#fff;padding:10px}.noticejs .warning .noticejs-body{color:#fff;padding:10px}.noticejs .warning .noticejs-body:hover{visibility:visible!important}.noticejs .warning .noticejs-content{visibility:visible}.noticejs .error{background-color:#e74c3c}.noticejs .error .noticejs-heading{background-color:#ba2c1d;color:#fff;padding:10px}.noticejs .error .noticejs-body{color:#fff;padding:10px}.noticejs .error .noticejs-body:hover{visibility:visible!important}.noticejs .error .noticejs-content{visibility:visible}.noticejs .progressbar{width:100%}.noticejs .progressbar .bar{width:1%;height:30px;background-color:#4caf50}.noticejs .success .noticejs-progressbar{width:100%;background-color:#64ce83;margin-top:-1px}.noticejs .success .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#3da95c}.noticejs .info .noticejs-progressbar{width:100%;background-color:#3ea2ff;margin-top:-1px}.noticejs .info .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#067cea}.noticejs .warning .noticejs-progressbar{width:100%;background-color:#ff7f48;margin-top:-1px}.noticejs .warning .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#f44e06}.noticejs .error .noticejs-progressbar{width:100%;background-color:#e74c3c;margin-top:-1px}.noticejs .error .noticejs-progressbar .noticejs-bar{width:100%;height:5px;background:#ba2c1d}@keyframes noticejs-fadeOut{0%{opacity:1}to{opacity:0}}.noticejs-fadeOut{animation-name:noticejs-fadeOut}@keyframes noticejs-modal-in{to{opacity:.3}}@keyframes noticejs-modal-out{to{opacity:0}}.noticejs-rtl .noticejs-heading{direction:rtl}.noticejs-rtl .close{float:left!important;margin-left:7px;margin-right:0!important}.noticejs-rtl .noticejs-content{direction:rtl}.noticejs{position:fixed;z-index:10050}.noticejs ::-webkit-scrollbar{width:8px}.noticejs ::-webkit-scrollbar-button{width:8px;height:5px}.noticejs ::-webkit-scrollbar-track{border-radius:10px}.noticejs ::-webkit-scrollbar-thumb{background:hsla(0,0%,100%,.5);border-radius:10px}.noticejs ::-webkit-scrollbar-thumb:hover{background:#fff}.noticejs-modal{position:fixed;width:100%;height:100%;background-color:#000;z-index:10000;opacity:.3;left:0;top:0}.noticejs-modal-open{opacity:0;animation:noticejs-modal-in .3s ease-out}.noticejs-modal-close{animation:noticejs-modal-out .3s ease-out;animation-fill-mode:forwards}"
  },
  {
    "path": "public/libs/types.expand.js",
    "content": "String.prototype.getQueryString = function(name, split) {\n  if (split == undefined) split = \"&\";\n  var rule =\n    \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"#]*)(\" + split + \"|#|$)\";\n  var reg = new RegExp(rule),\n    r;\n  if ((r = this.match(reg))) return decodeURI(r[2]);\n  return null;\n};\n\n/**\n * @return {number}\n */\nString.prototype.sizeToNumber = function() {\n  let _size_raw_match = this.match(\n    /^(\\d*\\.?\\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB))$/i\n  );\n  if (_size_raw_match) {\n    let _size_num = parseFloat(_size_raw_match[1]);\n    let _size_type = _size_raw_match[3];\n    switch (true) {\n      case /Zi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 70);\n      case /Ei?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 60);\n      case /Pi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 50);\n      case /Ti?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 40);\n      case /Gi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 30);\n      case /Mi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 20);\n      case /Ki?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 10);\n      default:\n        return _size_num;\n    }\n  }\n  return 0;\n};\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\r\n\t\"name\": \"__MSG_manifest_appName__\",\r\n\t\"short_name\": \"__MSG_manifest_shortName__\",\r\n\t\"version\": \"1.6.1\",\r\n\t\"description\": \"__MSG_manifest_appDescription__\",\r\n\t\"manifest_version\": 2,\r\n\t\"default_locale\": \"zh_CN\",\r\n\t\"homepage_url\": \"https://github.com/pt-plugins/PT-Plugin-Plus\",\r\n\t\"browser_action\": {\r\n\t\t\"default_icon\": \"assets/icon-19.png\",\r\n\t\t\"default_title\": \"__MSG_manifest_appName__\"\r\n\t},\r\n\t\"permissions\": [\r\n\t\t\"activeTab\",\r\n\t\t\"clipboardRead\",\r\n\t\t\"clipboardWrite\",\r\n\t\t\"storage\",\r\n\t\t\"contextMenus\",\r\n\t\t\"notifications\",\r\n\t\t\"http://*/*\",\r\n\t\t\"https://*/*\",\r\n\t\t\"unlimitedStorage\"\r\n\t],\r\n\t\"optional_permissions\": [\"downloads\", \"cookies\"],\r\n\t\"icons\": {\r\n\t\t\"16\": \"assets/icon.png\",\r\n\t\t\"19\": \"assets/icon-19.png\",\r\n\t\t\"64\": \"assets/icon-64.png\",\r\n\t\t\"128\": \"assets/icon-128.png\"\r\n\t},\r\n\t\"options_ui\": {\r\n\t\t\"page\": \"index.html\",\r\n\t\t\"open_in_tab\": true\r\n\t},\r\n\t\"background\": {\r\n\t\t\"scripts\": [\r\n\t\t\t\"libs/types.expand.js\",\r\n\t\t\t\"libs/jquery/jquery-3.3.1.min.js\",\r\n\t\t\t\"libs/Base64.js\",\r\n\t\t\t\"js/background/libs.js\",\r\n\t\t\t\"js/background/background.js\"\r\n\t\t]\r\n\t},\r\n\t\"content_scripts\": [{\r\n\t\t\"matches\": [\r\n\t\t\t\"http://*/*\",\r\n\t\t\t\"https://*/*\"\r\n\t\t],\r\n\t\t\"exclude_matches\": [\r\n\t\t\t\"https://fonts.google.com/*\"\r\n\t\t],\r\n\t\t\"css\": [\r\n\t\t\t\"assets/base.css\",\r\n\t\t\t\"libs/materialIcons/content_style.css\",\r\n\t\t\t\"libs/notice/noticejs.css\",\r\n\t\t\t\"libs/basicContext/basicContext.min.css\",\r\n\t\t\t\"libs/basicContext/themes/default.min.css\"\r\n\t\t],\r\n\t\t\"js\": [\r\n\t\t\t\"libs/types.expand.js\",\r\n\t\t\t\"libs/jquery/jquery-3.3.1.min.js\",\r\n\t\t\t\"libs/Base64.js\",\r\n\t\t\t\"libs/notice/notice.js\",\r\n\t\t\t\"libs/basicContext/basicContext.min.js\",\r\n\t\t\t\"libs/drag.js\",\r\n\t\t\t\"js/content/libs.js\",\r\n\t\t\t\"js/content/content.js\"\r\n\t\t]\r\n\t}],\r\n\t\"content_security_policy\": \"script-src 'self' 'unsafe-eval'; object-src 'self'\",\r\n\t\"web_accessible_resources\": [\r\n\t\t\"libs/materialIcons/*.woff2\",\r\n\t\t\"assets/*\",\r\n\t\t\"resource/*\"\r\n\t],\r\n\t\"omnibox\": {\r\n\t\t\"keyword\": \"pt\"\r\n\t},\r\n\t\"minimum_chrome_version\": \"64.0.3242\",\r\n\t\"browser_specific_settings\": {\r\n\t\t\"gecko\": {\r\n\t\t\t\"update_url\": \"https://pt-plugins.github.io/PT-Plugin-Plus/update/firefox.json\"\r\n\t\t}\r\n\t}\r\n}\r\n"
  },
  {
    "path": "public/popup.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"ie=edge\">\n  <title>PT 助手</title>\n  <script type=\"text/javascript\" src=\"./libs/jquery/jquery-3.3.1.min.js\"></script>\n</head>\n\n<body style=\"width: 70px;\">\n  <button id=\"btnConfig\">参数配置</button>\n  <button id=\"btnSystemLog\">查看日志</button>\n</body>\n\n<script type=\"text/javascript\" src=\"./js/popup.js\"></script>\n\n</html>"
  },
  {
    "path": "resource/clients/README.md",
    "content": "# 下载客户端定义说明\n\n## 目录说明\n\n该目录存放所有支持的下载客户端，目录名为架构名称\n\n```\n--目录名\n----config.json\n----init.js\n```\n\n- 目录名为该客户端的类型\n- config.json : 下载客户端定义文件\n- init.js : 下载客户端初始化脚本\n\n## config.json 文件\n\n示例：\n\n```json\n{\n  \"name\": \"qBittorrent\",\n  \"type\": \"qbittorrent\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://www.qbittorrent.org/favicon.ico\",\n  \"scripts\": [\"init.js\"],\n  \"description\": \"当前支持 qBittorrent v4.1+，由于浏览器限制，需要禁用 qBittorrent 的『启用跨站请求伪造(CSRF)保护』功能才能正常使用\",\n  \"warning\": \"注意：由于 qBittorrent 验证机制限制，第一次测试连接成功后，后续测试无论密码正确与否都会提示成功。\",\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"当前目录列表配置是指定硬盘上的绝对路径，如 /volume1/music/ 或 D:\\\\download\\\\music\\\\\"\n}\n```\n\n- `name` : 客户端名称\n- `type` : 客户端类型，必需和目录名相同\n- `ver` : 当前定义的版本号，目前暂无特别用处\n- `icon` : 用于显示客户端的图标\n- `scripts`: <可选>数组，用于执行该客户端执行的脚本列表，目前暂无特别用处\n- `description` : <可选>当前客户端描述\n- `warning` : <可选>用于配置时显示的警告信息，要用于一些特殊提示\n- `allowCustomPath` : <可选>是否允许自定义目录，默认为 false\n- `pathDescription` : <可选>自定义目录说明\n\n## init.js\n\n> 客户端初始化脚本文件\n\n- 脚本最终需要将自身挂载到 `window` 对象下，挂载名称必需和 `type` 相同，且区分大小写！\n  - 如：type='uTorrent'\n  - 那么挂载名称为 `window.uTorrent` = xxx;\n\n* 客户端对象需对外暴露以下公用方法\n  - `init` : 用于初始化客户端，接收一个参数：`options` 表示客户端定义的相关参数\n    - 属性参考 [common.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/interface/common.ts) 的 `DownloadClient`\n  - `call` : 用于方法调用，接收两个参数：`action`, `data`，返回一个 `Promise` 对象\n    - `action` : 字符串，需要执行的方法（动作）名称，方法说明：\n      - `addTorrentFromURL` : 从指定的链接增加种子文件\n      - `testClientConnectivity` : 测试当前客户端是否可连接\n      - 更多方法参考 [common.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/interface/common.ts) 的 `EAction`\n    - `data` : 任意类型，接收的数据，根据 `action` 不同，数据格式也不同\n\n- 在脚本中可用的系统对象\n  - `PTBackgroundService` : 助手后台服务程序，详情参考 [service.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/background/service.ts)\n  - `PTSevriceFilters` : 系统定义的过滤器，详情参考 [filters.ts](https://github.com/pt-plugins/PT-Plugin-Plus/blob/master/src/service/filters.ts)\n"
  },
  {
    "path": "resource/clients/deluge/config.json",
    "content": "{\n  \"name\": \"Deluge\",\n  \"type\": \"deluge\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://www.deluge-torrent.org/images/deluge-icon.png\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"passwordOnly\": true,\n  \"warning\": \"注意：由于 Deluge 验证机制限制，第一次测试连接成功后，后续测试无论密码正确与否都会提示成功。\",\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"当前目录列表配置是指定硬盘上的绝对路径，如 /volume1/music/ 或 D:\\\\download\\\\music\\\\\"\n}"
  },
  {
    "path": "resource/clients/deluge/init.js",
    "content": "/**\n * @see https://deluge.readthedocs.io/en/develop/reference/index.html\n */\n(function ($) {\n  //Deluge\n  // id:1,method:auth.login,params:[url,null]\n  // 380 web.download_torrent_from_url\n  // 2 core.add_torrent_url\n  class Deluge {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * url: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n      this.requestCount = -1;\n\n      if (this.options.address.indexOf(\"/json\") == -1) {\n        let url = PTServiceFilters.parseURL(this.options.address);\n        let address = [url.protocol, \"://\", url.host];\n        if (url.port) {\n          address.push(`:${url.port}`);\n        }\n\n        address.push(url.path);\n        if (url.path.substr(-1) != \"/\") {\n          address.push(\"/\");\n        }\n\n        address.push(\"json\");\n        this.options.address = address.join(\"\");\n      }\n      console.log(\"Deluge.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"Deluge.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, result => {\n              resolve(result);\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.getSessionId()\n              .then(result => {\n                resolve(result != \"\");\n              })\n              .catch(result => {\n                reject(result);\n              });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 获取Session\n     * @param {*} callback\n     */\n    getSessionId(callback) {\n      return new Promise((resolve, reject) => {\n        var data = {\n          id: ++this.requestCount,\n          method: \"auth.login\",\n          params: [this.options.loginPwd]\n        };\n\n        $.ajax({\n          type: \"POST\",\n          url: this.options.address,\n          dataType: \"json\",\n          contentType: \"application/json\",\n          data: JSON.stringify(data),\n          timeout: PTBackgroundService.options.connectClientTimeout\n        })\n          .done((resultData, textStatus) => {\n            this.isInitialized = true;\n            if (callback) {\n              callback(resultData);\n            }\n            resolve(this.token);\n          })\n          .fail((jqXHR, textStatus) => {\n            let result = {\n              status: textStatus || \"error\",\n              code: jqXHR.status,\n              msg: textStatus === \"timeout\" ? i18n.t(\"downloadClient.timeout\") : i18n.t(\"downloadClient.unknownError\") //\"连接超时\" : \"未知错误\"\n            };\n            switch (jqXHR.status) {\n              case 0:\n                result.msg = i18n.t(\"downloadClient.serverIsUnavailable\") //\"服务器不可用或网络错误\"\n                break;\n\n              case 401:\n                result.msg = i18n.t(\"downloadClient.serverConnectionFailed\"); //\"身份验证失败\";\n                break;\n\n              case 404:\n                result.msg = i18n.t(\"downloadClient.notFound\"); //\"指定的地址未找到，服务器返回了 404\";\n                break;\n            }\n            reject(result);\n          });\n      });\n    }\n\n    /**\n     * 调用指定的RPC\n     * @param {*} options\n     * @param {*} callback\n     * @param {*} tags\n     */\n    exec(options, callback, tags) {\n      var data = {\n        id: ++this.requestCount\n      };\n\n      $.extend(data, options);\n\n      var settings = {\n        type: \"POST\",\n        url: this.options.address,\n        data: JSON.stringify(data),\n        contentType: \"application/json\",\n        timeout: PTBackgroundService.options.connectClientTimeout,\n        success: (resultData, textStatus) => {\n          // 未认证\n          if (resultData && resultData.error && resultData.error.code == 1) {\n            this.getSessionId()\n              .then(() => {\n                this.exec(options, callback, tags);\n              })\n              .catch(result => {\n                callback && callback(result);\n              });\n            return;\n          }\n          if (callback) {\n            callback(resultData, tags);\n          }\n        },\n        error: (request, event, page) => {\n          console.log(request);\n          this.getSessionId()\n            .then(() => {\n              this.exec(options, callback, tags);\n            })\n            .catch(result => {\n              callback && callback(result);\n            });\n        }\n      };\n      $.ajax(settings);\n    }\n\n    /**\n     * 添加种子链接\n     * @param {*} data\n     * @param {*} callback\n     */\n    addTorrentFromUrl(data, callback) {\n      let url = data.url;\n\n      // 磁性连接（代码来自原版WEBUI）\n      if (url.startsWith('magnet:')) {\n        this.addTorrent({\n            method: \"core.add_torrent_url\",\n            params: [\n              url,\n              {\n                download_location: data.savePath\n              }\n            ]\n          },\n          callback\n        );\n        return;\n      }\n\n      PTBackgroundService.requestMessage({\n        action: \"getTorrentDataFromURL\",\n        data: url\n      })\n        .then(result => {\n          var fileReader = new FileReader();\n\n          fileReader.onload = e => {\n            var contents = e.target.result;\n            var key = \"base64,\";\n            var index = contents.indexOf(key);\n            if (index == -1) {\n              return;\n            }\n            var metainfo = contents.substring(index + key.length);\n\n            this.addTorrent({\n              method: \"core.add_torrent_file\",\n              params: [\n                \"\",\n                metainfo,\n                {\n                  download_location: data.savePath\n                }\n              ]\n            },\n              callback\n            );\n          };\n          fileReader.readAsDataURL(result);\n        })\n        .catch(result => {\n          callback && callback(result);\n        });\n    }\n\n    addTorrent(options, callback) {\n      this.exec(options, resultData => {\n        if (callback) {\n          var result = resultData;\n          if (!resultData.error && resultData.result) {\n            result.status = \"success\";\n            result.msg = i18n.t(\"downloadClient.addURLSuccess\", {\n              name: this.options.name\n            }); //\"URL已添加至 Deluge 。\";\n          }\n          callback(result);\n        }\n        console.log(resultData);\n      });\n    }\n  }\n\n  window.deluge = Deluge;\n})(jQuery, window);"
  },
  {
    "path": "resource/clients/flood/config.json",
    "content": "{\n  \"name\": \"Flood\",\n  \"type\": \"flood\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://github.com/Flood-UI/flood/raw/master/flood.png\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"description\": \"ruTorrent 的另一款基于Node的Web前端面板，界面美观，加载速度快。\",\n  \"warning\": \"1.仅支持jesec/Flood，不支持原版Flood；2.如果当前已登录Flood面板，请退出登陆后再做连接性测试；3. 目前无法准确获得Flood添加种子是否成功。\",\n  \"allowCustomPath\": true\n}\n"
  },
  {
    "path": "resource/clients/flood/init.js",
    "content": "/**\n * @see https://github.com/jesec/flood/tree/master/server/routes/api\n */\n\n(function ($) {\n  // Flood\n  class Client {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * address: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n\n      // 重写用户给的地址\n      let url = PTServiceFilters.parseURL(this.options.address);\n      let address = [url.protocol, \"://\", url.host];\n      if (url.port) {\n        address.push(`:${url.port}`)\n      }\n      address.push(url.path);\n      if (url.path.substr(-1) !== \"/\") {\n        address.push(\"/\");\n      }\n      this.options.address = address.join(\"\");\n\n\n      console.log(\"Flood.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"Flood.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, (result) => {\n              if (result.status === \"success\") {\n                resolve(result);\n              } else {\n                reject(result);\n              }\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.testClientConnectivity().then(result => {\n              resolve(true);\n            }).catch((code, msg) => {\n              reject({\n                status: \"error\",\n                code,\n                msg\n              });\n            });\n            break;\n        }\n      });\n    }\n\n    authenticate(callback) {\n      // run the login first\n      let options = this.options;\n      $.ajax({\n        type: \"POST\",\n        url: options.address + \"api/auth/authenticate\",\n        contentType: 'application/json',\n        data: JSON.stringify({\n          username: options.loginName,\n          password: options.loginPwd,\n        }),\n        success: function (resultData, textStatus) {\n          console.log(resultData);\n          if (resultData.success) {\n            console.log(resultData.token);\n            if (callback && typeof callback === 'function') {\n              callback(resultData);\n            }\n          }\n        }\n      });\n    }\n\n    /**\n     * 测试可连接性\n     * 接口返回 { isConnected: true } 时说明可连接\n     *\n     * @see https://github.com/Flood-UI/flood/blob/master/server/routes/client.js#L16-L36\n     *\n     * @param callback\n     */\n    testClientConnectivity(callback) {\n      let that = this;\n      return new Promise((resolve, reject) => {\n        this.authenticate(function (authData) {\n          $.ajax({\n            type: 'GET',\n            url: that.options.address + 'api/client/connection-test',  // ping_addr\n            timeout: PTBackgroundService.options.connectClientTimeout\n          }).done((resultData, textStatus, request) => {\n            if (resultData.isConnected) {\n              that.isInitialized = true;\n              if (callback) {\n                callback(resultData);\n              }\n              resolve();\n            }\n          }).fail((jqXHR, textStatus, errorThrown) => {\n            reject(jqXHR.status, textStatus)\n          })\n        });\n      })\n    }\n\n    /**\n     * 添加种子链接\n     *\n     * @see https://github.com/Flood-UI/flood/blob/master/server/routes/client.js#L38-L44\n     *\n     * @param {*} data\n     * @param {*} callback\n     */\n    addTorrentFromUrl(data, callback) {\n      let url = data.url;\n\n      let addTorrentData = {\n        destination: data.savePath || '',\n        /** isBasePath\n         * @see https://github.com/Flood-UI/flood/blob/master/server/models/ClientRequest.js#L143-L149\n         * @see https://rtorrent-docs.readthedocs.io/en/latest/cmd-ref.html\n         */\n        isBasePath: false,\n        start: !data.autoStart,\n        tag: [],\n      }\n\n      // 处理magent链接\n      if (url.startsWith('magnet:')) {\n        addTorrentData.urls = [url];\n\n        this.addTorrentUrl(addTorrentData, callback);\n        return;\n      }\n\n      // 种子文件\n      PTBackgroundService.requestMessage({\n        action: \"getTorrentDataFromURL\",\n        data: url\n      })\n        .then((result) => {\n          var fileReader = new FileReader();\n\n          fileReader.onload = e => {\n            var contents = e.target.result;\n            var key = \"base64,\";\n            var index = contents.indexOf(key);\n            if (index == -1) {\n              return;\n            }\n            var metainfo = contents.substring(index + key.length);\n            addTorrentData.files = [metainfo];\n            this.addTorrentFile(addTorrentData, callback);\n          }\n        })\n        .catch((result) => {\n          callback && callback(result);\n        });\n    }\n\n    addTorrentUrl(data, callback) {\n      this.addTorrent('api/torrents/add-urls', data, callback);\n    }\n\n    addTorrentFile(data, callback) {\n      this.addTorrent('api/torrents/add-files', data, callback);\n    }\n\n    /**\n     *\n     * @param suffix\n     * @param data\n     * @param callback\n     */\n    addTorrent(suffix, data, callback) {\n      let options = this.options;\n      this.authenticate(function () {\n        $.ajax({\n          type: \"POST\",\n          url: options.address + suffix,\n          timeout: PTBackgroundService.options.connectClientTimeout,\n          data: data,\n          contentType: false,\n          processData: false,\n          success: (resultData, textStatus) => {\n            if (callback) {\n              var result = Object.assign({\n                status: \"success\",\n                msg: i18n.t(\"downloadClient.addURLSuccess\", {\n                  name: options.name\n                })\n              }, resultData);\n              callback(result);\n            }\n          },\n        })\n      })\n\n    }\n  }\n\n  // 添加到 window 对象，用于客户页面调用\n  window.flood = Client;\n\n})(jQuery, window);\n"
  },
  {
    "path": "resource/clients/qbittorrent/config.json",
    "content": "{\n  \"name\": \"qBittorrent\",\n  \"type\": \"qbittorrent\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://www.qbittorrent.org/favicon.ico\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"description\": \"当前支持 qBittorrent v4.1+，由于浏览器限制，需要禁用 qBittorrent 的『启用跨站请求伪造(CSRF)保护』功能才能正常使用\",\n  \"warning\": \"注意：由于 qBittorrent 验证机制限制，第一次测试连接成功后，后续测试无论密码正确与否都会提示成功。\",\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"当前目录列表配置是指定硬盘上的绝对路径，如 /volume1/music/ 或 D:\\\\download\\\\music\\\\\"\n}"
  },
  {
    "path": "resource/clients/qbittorrent/init.js",
    "content": "/**\n * @see https://github.com/qbittorrent/qBittorrent/wiki/Web-API-Documentation\n */\n(function($) {\n  //qBittorrent\n  class Client {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * url: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n      this.headers = {};\n      this.sessionId = \"\";\n\n      if (this.options.address.substr(-1) == \"/\") {\n        this.options.address = this.options.address.substr(\n          0,\n          this.options.address.length - 1\n        );\n      }\n\n      console.log(\"qBittorrent.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"qBittorrent.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, result => {\n              if (result.status === \"success\") {\n                resolve(result);\n              } else {\n                reject(result);\n              }\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.getSessionId()\n              .then(result => {\n                resolve(true);\n              })\n              .catch((code, msg) => {\n                reject({\n                  status: \"error\",\n                  code,\n                  msg\n                });\n              });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 获取Session\n     * @param {*} callback\n     */\n    getSessionId(callback) {\n      return new Promise((resolve, reject) => {\n        var data = {\n          username: this.options.loginName,\n          password: this.options.loginPwd\n        };\n\n        // qb 需要禁用『启用跨站请求伪造保护』\n        var settings = {\n          type: \"POST\",\n          url: this.options.address + \"/api/v2/auth/login\",\n          data: data,\n          timeout: PTBackgroundService.options.connectClientTimeout\n        };\n        $.ajax(settings)\n          .done((resultData, textStatus, request) => {\n            this.isInitialized = true;\n            if (callback) {\n              callback(resultData);\n            }\n            resolve();\n            console.log(this.sessionId);\n          })\n          .fail((jqXHR, textStatus, errorThrown) => {\n            reject(jqXHR.status, textStatus);\n          });\n      });\n    }\n\n    /**\n     * 调用指定的RPC\n     * @param {*} options\n     * @param {*} callback\n     * @param {*} tags\n     */\n    exec(options, callback, tags) {\n      var settings = {\n        type: \"POST\",\n        processData: false,\n        contentType: false,\n        method: \"POST\",\n        url: this.options.address + options.method,\n        data: options.params,\n        timeout: PTBackgroundService.options.connectClientTimeout,\n        success: (resultData, textStatus) => {\n          if (callback) {\n            callback(resultData, tags);\n          }\n        },\n        error: (jqXHR, textStatus, errorThrown) => {\n          switch (jqXHR.status) {\n            // Unsupported Media Type\n            case 415:\n              callback({\n                status: \"error\",\n                code: jqXHR.status,\n                msg: i18n.t(\"downloadClient.unsupportedMediaType\") //\"种子文件有误\"\n              });\n              return;\n\n            default:\n              break;\n          }\n          console.log(jqXHR);\n          this.getSessionId()\n            .then(() => {\n              this.exec(options, callback, tags);\n            })\n            .catch((code, msg) => {\n              callback({\n                status: \"error\",\n                code,\n                msg:\n                  msg || code === 0\n                    ? i18n.t(\"downloadClient.serverIsUnavailable\")\n                    : i18n.t(\"downloadClient.unknownError\") //\"服务器不可用或网络错误\" : \"未知错误\"\n              });\n            });\n        }\n      };\n      $.ajax(settings);\n    }\n\n    /**\n     * 添加种子链接\n     * @param {*} data\n     * @param {*} callback\n     */\n    addTorrentFromUrl(data, callback) {\n      let formData = new FormData();\n\n      if (data.savePath) {\n        formData.append(\"savepath\", data.savePath);\n        // 禁用自动管理种子\n        formData.append(\"autoTMM\", false);\n      }\n\n      if (data.autoStart != undefined) {\n        formData.append(\"paused\", !data.autoStart);\n      }\n\n      if (data.imdbId != undefined) {\n        formData.append(\"tags\", data.imdbId);\n      }\n\n      if (data.upLoadLimit && data.upLoadLimit > 0) {\n        formData.append(\"upLimit\", data.upLoadLimit * 1024);\n      }\n\n      let url = data.url;\n\n      // 磁性连接\n      if (url.startsWith('magnet:')) {\n        formData.append('urls', url);\n        this.addTorrent(formData, callback);\n      } else {\n        PTBackgroundService.requestMessage({\n          action: \"getTorrentDataFromURL\",\n          data: url\n        })\n          .then(result => {\n            formData.append(\"torrents\", result, \"file.torrent\");\n            this.addTorrent(formData, callback);\n          })\n          .catch(result => {\n            callback && callback(result);\n          });\n      }\n    }\n\n    addTorrent(params, callback) {\n      this.exec(\n        {\n          method: \"/api/v2/torrents/add\",\n          params: params\n        },\n        resultData => {\n          if (callback) {\n            var result = Object.assign(\n              {\n                status: \"\",\n                msg: \"\"\n              },\n              resultData\n            );\n            if (\n              (!resultData.error && resultData.result) ||\n              resultData == \"Ok.\"\n            ) {\n              result.status = \"success\";\n              result.msg = i18n.t(\"downloadClient.addURLSuccess\", {\n                name: this.options.name\n              }); //\"URL已添加至 qBittorrent 。\";\n            }\n            callback(result);\n          }\n          console.log(resultData);\n        }\n      );\n    }\n  }\n\n  window.qbittorrent = Client;\n})(jQuery, window);\n"
  },
  {
    "path": "resource/clients/ruTorrent/config.json",
    "content": "{\n  \"name\": \"ruTorrent\",\n  \"type\": \"ruTorrent\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://raw.githubusercontent.com/Novik/ruTorrent/master/images/favicon.ico\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"description\": \"ruTorrent\",\n  \"warning\": \"\",\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"当前目录列表配置是指定硬盘上的绝对路径，如 /volume1/music/ 或 D:\\\\download\\\\music\\\\\"\n}"
  },
  {
    "path": "resource/clients/ruTorrent/init.js",
    "content": "/**\n * @see https://github.com/Novik/ruTorrent/blob/master/php/addtorrent.php\n * @see https://github.com/Rhilip/PT-Plugin/blob/master/src/script/client.js#L477_L543\n *\n */\n\n(function($) {\n  // ruTorrent\n  class Client {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * address: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n\n      // 重写用户给的地址\n      let url = PTServiceFilters.parseURL(this.options.address);\n      let address = [url.protocol, \"://\", url.host];\n      if (url.port) {\n        address.push(`:${url.port}`);\n      }\n      address.push(url.path);\n      if (url.path.substr(-1) !== \"/\") {\n        address.push(\"/\");\n      }\n      this.options.address = address.join(\"\");\n\n      console.log(\"ruTorrent.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"ruTorrent.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, result => {\n              if (result.result === \"Success\") {\n                resolve(result);\n              } else {\n                reject(result);\n              }\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.testClientConnectivity()\n              .then(result => {\n                resolve(true);\n              })\n              .catch((code, msg) => {\n                reject({\n                  status: \"error\",\n                  code,\n                  msg\n                });\n              });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 测试可连接性\n     * 鉴于ruTorrent没有相关方法，则考虑请求 `/php/getsettings.php` 页面，如果返回json格式的信息\n     * 则说明可连接\n     *\n     * @see https://github.com/Novik/ruTorrent/blob/master/php/getsettings.php\n     *\n     * @param callback\n     */\n    testClientConnectivity(callback) {\n      return new Promise((resolve, reject) => {\n        $.ajax({\n          type: \"GET\",\n          url: this.options.address + \"php/getsettings.php\", // ping_addr\n          username: this.options.loginName,\n          password: this.options.loginPwd,\n          timeout: PTBackgroundService.options.connectClientTimeout\n        })\n          .done((resultData, textStatus, request) => {\n            this.isInitialized = true;\n            if (callback) {\n              callback(resultData);\n            }\n            resolve();\n          })\n          .fail((jqXHR, textStatus, errorThrown) => {\n            reject(jqXHR.status, textStatus);\n          });\n      });\n    }\n\n    /**\n     * 添加种子链接\n     * @param {*} data\n     * @param {*} callback\n     */\n    addTorrentFromUrl(data, callback) {\n      let url = data.url;\n\n      // 磁性连接\n      if (url.startsWith(\"magnet:\")) {\n        this.addTorrent(\n          {\n            dir_edit: data.savePath,\n            paused: !data.autoStart,\n            url: url,\n            json: 1 // 输出json格式\n          },\n          callback\n        );\n        return;\n      }\n\n      // 种子文件\n      PTBackgroundService.requestMessage({\n        action: \"getTorrentDataFromURL\",\n        data: url\n      })\n        .then(result => {\n          let formData = new FormData();\n          formData.append(\"json\", 1); // 输出json格式\n          // 如果有传值时，则设置路径参数\n          if (data.savePath) {\n            formData.append(\"dir_edit\", data.savePath);\n          }\n\n          formData.append(\"paused\", !data.autoStart);\n          formData.append(\"torrent_file\", result, \"file.torrent\");\n\n          this.addTorrent(formData, callback);\n        })\n        .catch(result => {\n          callback && callback(result);\n        });\n    }\n\n    /**\n     * POST完成后会被302到类似  /php/addtorrent.php?result[]=Success&name[]=file.torrent&json=1\n     * 得到类似 { \"result\" : \"Success\" } 的结果\n     * 其中result取值为 enum(\n     *    'Success',   // 添加成功\n     *    'Failed',    // 添加失败（magnet，不存在种子文件，种子上传失败）\n     *    'FailedURL', // 添加链接失败（ruT获取不到对应种子）\n     *    'FailedFile' // 添加文件失败（rT返回错误）\n     * )\n     *\n     *\n     * @param data\n     * @param callback\n     */\n    addTorrent(data, callback) {\n      $.ajax({\n        type: \"POST\",\n        url: this.options.address + \"php/addtorrent.php\",\n        username: this.options.loginName,\n        password: this.options.loginPwd,\n        timeout: PTBackgroundService.options.connectClientTimeout,\n        data: data,\n        contentType: false,\n        processData: false,\n        dataType: 'json',\n        success: (resultData, textStatus) => {\n          if (callback) {\n            callback(resultData);\n          }\n        }\n      });\n    }\n  }\n\n  // 添加到 window 对象，用于客户页面调用\n  window.ruTorrent = Client;\n})(jQuery, window);\n"
  },
  {
    "path": "resource/clients/synologyDownloadStation/config.json",
    "content": "{\n  \"name\": \"Synology Download Station\",\n  \"type\": \"synologyDownloadStation\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://www.synology.com/img/icon/favicon.png\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"因 Synology Download Station API 接口限制，保存目录依赖于“暂存位置”，并且只允许使用相对路径；<br/>如暂存位置为 /volume1/，期望存储目的地位置为 /volume1/music/，那么请在“目录列表”中填写：<span style='color:red'>music</span>\"\n}"
  },
  {
    "path": "resource/clients/synologyDownloadStation/init.js",
    "content": "/**\n * @see https://global.download.synology.com/download/Document/DeveloperGuide/Synology_Download_Station_Web_API.pdf\n * @backport https://github.com/pt-plugins/PT-Plugin-Plus/blob/48c2d42a1d05c129c0abbbecf653b1b7d88a8a8e/src/resource/btClients/src/clients/synologyDownloadStation.ts\n */\n(function ($, window) {\n  class Client {\n\n    init(options) {\n      this.options = options;\n      this.sessionId = \"\";\n      this.synoToken = \"\";\n      if (this.options.address.substr(-1) == \"/\") {\n        this.options.address = this.options.address.substr(0, this.options.address.length - 1);\n      }\n    }\n\n    /**\n     * 获取 SID\n     */\n    // FIXME 这个方法已经不止获取SID了，CSRFToken也是在此获得，该考虑换个名字了\n    getSessionId() {\n      return new Promise((resolve, reject) => {\n        let url = `${this.options.address}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${encodeURIComponent(this.options.loginName)}&passwd=${encodeURIComponent(this.options.loginPwd)}&session=DownloadStation&format=sid&enable_syno_token=yes`;\n        $.ajax({\n          url,\n          timeout: PTBackgroundService.options.connectClientTimeout,\n          dataType: \"json\"\n        }).done((result) => {\n          if (result && result.success) {\n            this.sessionId = result.data.sid;\n            this.synoToken = result.data.synotoken\n            resolve(this.sessionId)\n          } else {\n            reject({\n              status: \"error\",\n              code: result.error.code,\n              msg: i18n.t(\"downloadClient.permissionDenied\") //\"身份验证失败\"\n            })\n          }\n          /**\n            400 No such account or incorrect password\n            401 Account disabled\n            402 Permission denied\n            403 2-step verification code required\n            404 Failed to authenticate 2-step verification code\n           */\n\n        }).fail(() => {\n          reject()\n        })\n      })\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"synologyDownloadStation.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, (result) => {\n              if (result && result.success) {\n                resolve(result);\n              } else {\n                reject(result)\n              }\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.getSessionId().then(result => {\n              resolve(result != \"\");\n            }).catch(result => {\n              reject(result);\n            })\n            break;\n        }\n      });\n    }\n\n    /**\n     * 添加种子链接\n     * @param {*} options\n     * @param {*} callback\n     */\n    addTorrentFromUrl(options, callback) {\n      if (!this.sessionId) {\n        this.getSessionId().then((result) => {\n          if (result) {\n            this.addTorrentFromUrl(options, callback)\n          } else {\n            callback({\n              status: \"error\",\n              msg: i18n.t(\"downloadClient.serverConnectionFailed\") //\"服务器连接失败\"\n            })\n          }\n        }).catch((result) => {\n          callback(result)\n        })\n        return;\n      }\n\n      let postData = {\n        api: 'SYNO.DownloadStation2.Task',\n        method: 'create',\n        version: 2,\n        create_list: false,\n        _sid: this.sessionId  // fxxk， _sid 参数不能放在第一位，不然会直接 101 报错\n      }\n\n      let headers = {\n        'X-SYNO-TOKEN': this.synoToken\n      }\n\n      // fxxk， 没有 destination 参数也会直接报错\n      let savePath = (options.savePath || \"\") + \"\";\n      if (savePath.substr(-1) === \"/\") {  // 去除路径最后的 / ，以确保可以正常添加目录信息\n        savePath = savePath.substr(0, savePath.length - 1);\n      }\n      postData.destination = `\"${savePath || ''}\"`;\n\n      if (options.url.startsWith('magnet:')) {\n        postData.type = '\"url\"';\n        postData.url = [options.url];\n\n        this.addTorrent(postData, options, callback);\n      } else {\n        postData.type = '\"file\"';\n        postData.file = ['torrent'];\n\n        let formData = new FormData();\n        Object.keys(postData).forEach((k) => {\n          let v = postData[k];\n          if (v !== undefined) {\n            if (Array.isArray(v)) {\n              v = JSON.stringify(v);\n            }\n            formData.append(k, v);\n          }\n        });\n\n\n        PTBackgroundService.requestMessage({\n          action: \"getTorrentDataFromURL\",\n          data: {\n            url: options.url,\n            parseTorrent: true\n          }\n        })\n          .then((result) => {\n            formData.append(\"torrent\", result.content, `${result.torrent.name}.torrent`)\n\n            this.addTorrent(formData, headers, options, callback);\n          })\n          .catch((result) => {\n            callback && callback(result);\n          });\n\n      }\n    }\n\n    addTorrent(formData, headers, options, callback) {\n      $.ajax({\n        url: `${this.options.address}/webapi/entry.cgi`,\n        headers,\n        timeout: PTBackgroundService.options.connectClientTimeout,\n        type: \"POST\",\n        processData: false,\n        contentType: false,\n        data: formData,\n        dataType: \"json\"\n      }).done((result) => {\n        console.log(result)\n        if (result.error) {\n          let errorMap = {\n            400: i18n.t(\"downloadClient.fileUploadFailed\"), // \"文件上传失败\",\n            401: i18n.t(\"downloadClient.maxNumberOfTasksReached\"), //\"达到的最大任务数\",\n            402: i18n.t(\"downloadClient.destinationDenied\", {\n              path: options.savePath\n            }), //`指定的目录[${options.savePath}]不可用或无权限`,\n            403: i18n.t(\"downloadClient.destinationDoesNotExist\", {\n              path: options.savePath\n            }) //`指定的目录[${options.savePath}]不存在`\n          };\n          /**\n           * 400 File upload failed\n              401 Max number of tasks reached\n              402 Destination denied\n              403 Destination does not exist\n              404 Invalid task id\n              405 Invalid task action\n              406 No default destination\n              407 Set destination failed\n              408 File does not exist\n           */\n          if (result.error.code) {\n            result.msg = errorMap[result.error.code];\n          }\n        }\n\n        callback(result)\n      }).fail(() => {\n        callback({\n          status: \"error\",\n          msg: i18n.t(\"downloadClient.serverConnectionFailed\") //\"服务器连接失败\"\n        })\n      })\n    }\n  }\n  // 添加到 window 对象，用于客户页面调用\n  window.synologyDownloadStation = Client;\n})(jQuery, window)\n"
  },
  {
    "path": "resource/clients/transmission/config.json",
    "content": "{\n  \"name\": \"Transmission\",\n  \"type\": \"transmission\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://raw.githubusercontent.com/transmission/transmission/master/web/images/favicon.png\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"当前目录列表配置是指定硬盘上的绝对路径，如 /volume1/music/\",\n  \"description\": \"默认情况下，系统会请求 http://ip:port/transmission/rpc 这个路径，如果无法连接，请确认 `settings.json` 文件的 `rpc-url` 值；详情可参考：https://github.com/pt-plugins/PT-Plugin-Plus/issues/32\"\n}"
  },
  {
    "path": "resource/clients/transmission/init.js",
    "content": "/**\n * @see https://github.com/transmission/transmission/blob/master/extras/rpc-spec.txt\n */\n(function ($, window) {\n  const XHEADER = \"X-Transmission-Session-Id\";\n  class Transmission {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * url: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n      this.headers = [];\n      if (options.loginName && options.loginPwd) {\n        this.headers[\"Authorization\"] = \"Basic \" + (new Base64()).encode(options.loginName + \":\" + options.loginPwd);\n      }\n\n      if (this.options.address.indexOf(\"rpc\") == -1) {\n        let url = PTServiceFilters.parseURL(this.options.address);\n\n        let address = [\n          url.protocol,\n          \"://\",\n          url.host\n        ];\n        if (url.port) {\n          address.push(`:${url.port}`)\n        }\n\n        address.push(url.path);\n        if (url.path.substr(-1) != \"/\") {\n          address.push(\"/\");\n        }\n\n        address.push(\"transmission/rpc\");\n\n        this.options.address = address.join(\"\");\n      }\n      console.log(\"transmission.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"transmission.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从指定的URL添加种子\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data.url, data.savePath, data.autoStart, (result) => {\n              resolve(result);\n            }, data.upLoadLimit);\n            break;\n\n            // 获取可用空间\n          case \"getFreeSpace\":\n            this.getFreeSpace(data.path, (result) => {\n              resolve(result);\n            });\n\n            break;\n\n            // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.sessionStats().then(result => {\n              resolve(result.result == \"success\");\n            }).catch(result => {\n              reject(result);\n            })\n            break;\n\n        }\n      });\n    }\n\n    /**\n     * 调用指定的RPC\n     * @param {*} options\n     * @param {*} callback\n     * @param {*} tags\n     */\n    exec(options, callback, tags) {\n      return new Promise((resolve, reject) => {\n        var data = {\n          method: \"\",\n          arguments: {},\n          tag: \"\"\n        };\n        let result = {};\n\n        $.extend(data, options);\n\n        this.sendRequest({\n          type: \"POST\",\n          url: this.options.address,\n          dataType: 'json',\n          data: JSON.stringify(data),\n          timeout: PTBackgroundService.options.connectClientTimeout,\n          headers: this.headers\n        }, (resultData) => {\n          if (callback) {\n            callback(resultData, tags);\n          }\n          resolve(resultData);\n        }, (request, event, page) => {\n          switch (request.status) {\n            case 0:\n              result = {\n                status: \"error\",\n                code: request.status,\n                msg: i18n.t(\"downloadClient.serverIsUnavailable\") //\"服务器不可用或网络错误\"\n              };\n              reject && reject(result)\n              break;\n\n            case 401:\n              result = {\n                status: \"error\",\n                code: request.status,\n                msg: i18n.t(\"downloadClient.permissionDenied\") //\"身份验证失败\"\n              };\n              reject && reject(result)\n              break;\n\n            default:\n              result = {\n                status: \"error\",\n                code: request.status,\n                msg: event || i18n.t(\"downloadClient.unknownError\") //\"未知错误\"\n              };\n              reject && reject(result)\n              break;\n\n          }\n        });\n      });\n    }\n\n    /**\n     * 发送请求\n     * @param {*} options\n     * @param {*} success\n     * @param {*} error\n     */\n    sendRequest(options, success, error) {\n      $.ajax(options).done((resultData, textStatus) => {\n        success && success(resultData, textStatus);\n      }).fail((request, event, page) => {\n        switch (request.status) {\n          case 409:\n            this.sessionId = request.getResponseHeader(XHEADER);\n            this.headers[XHEADER] = this.sessionId;\n            options.headers = this.headers;\n            this.sendRequest(options, success, error);\n            break;\n\n          default:\n            error && error(request, event, page);\n            break;\n        }\n\n      });\n    }\n\n    sessionStats() {\n      return this.exec({\n        method: \"session-stats\"\n      });\n    }\n\n    /**\n     * 添加种子\n     * @param string url 需要添加的地址\n     * @param string savePath 保存目录，如果不指定则以服务器配置为准\n     * @param bool autoStart 是否自动开始\n     * @param function callback 回调\n     */\n    addTorrentFromUrl(url, savePath, autoStart, callback, uploadLimit = 0) {\n      var options = {\n        method: \"torrent-add\",\n        arguments: {\n          filename: url,\n          paused: (!autoStart)\n        }\n      };\n\n      if (savePath) {\n        options.arguments[\"download-dir\"] = savePath;\n      }\n\n      if (uploadLimit && uploadLimit > 0) {\n        options.arguments[\"uploadLimit\"] = uploadLimit;\n      }\n\n      // 磁性连接\n      if (url.startsWith('magnet:')) {\n        options.arguments[\"filename\"] = url;\n        this.addTorrent(options, callback)\n      } else {\n        PTBackgroundService.requestMessage({\n            action: \"getTorrentDataFromURL\",\n            data: url\n          })\n          .then((result) => {\n            var fileReader = new FileReader();\n\n            fileReader.onload = (e) => {\n              var contents = e.target.result;\n              var key = \"base64,\";\n              var index = contents.indexOf(key);\n              if (index == -1) {\n                return;\n              }\n              var metainfo = contents.substring(index + key.length);\n\n              delete options.arguments[\"filename\"];\n              options.arguments[\"metainfo\"] = metainfo;\n\n              this.addTorrent(options, callback);\n            }\n            fileReader.readAsDataURL(result);\n          })\n          .catch((result) => {\n            callback && callback(result);\n          });\n      }\n    }\n\n    /**\n     * 添加种子\n     * @param {*} options\n     * @param {*} callback\n     */\n    addTorrent(options, callback) {\n      this.exec(options).then((data) => {\n        switch (data.result) {\n          // 添加成功\n          case \"success\":\n            if (callback) {\n              if (data.arguments[\"torrent-added\"]) {\n                callback(data.arguments[\"torrent-added\"]);\n              }\n              // 重复的种子\n              else if (data.arguments[\"torrent-duplicate\"]) {\n                callback({\n                  status: \"duplicate\",\n                  torrent: data.arguments[\"torrent-duplicate\"]\n                });\n              }\n            }\n            break;\n\n            // 重复的种子\n          case \"duplicate torrent\":\n          default:\n            if (callback) {\n              callback(data.result || data);\n            }\n            break;\n\n        }\n      }).catch((result) => {\n        callback && callback(result);\n      });\n    }\n\n\n    /**\n     * 獲取指定目錄的大小\n     * @param string path 需要获取的目录地址\n     * @param function callback 回调\n     */\n    getFreeSpace(path, callback) {\n      this.exec({\n        method: \"free-space\",\n        arguments: {\n          \"path\": path\n        }\n      }).then((result) => {\n        callback && callback(result);\n      }).catch((result) => {\n        callback && callback(result);\n      });\n    }\n  }\n\n  // 添加到 window 对象，用于客户页面调用\n  window.transmission = Transmission;\n})(jQuery, window)\n"
  },
  {
    "path": "resource/clients/utorrent/config.json",
    "content": "{\n  \"name\": \"µTorrent\",\n  \"type\": \"utorrent\",\n  \"ver\": \"0.0.1\",\n  \"icon\": \"https://www.utorrent.com/faviconUT.ico\",\n  \"scripts\": [\n    \"init.js\"\n  ],\n  \"description\": \"由于 µTorrent Web API 接口不统一，当前仅支持 µTorrent Windows 版本，Mac 版本测试不可用，其他系统未知。使用前请确认 WebUI 已安装并开启\",\n  \"allowCustomPath\": true,\n  \"pathDescription\": \"注：目录功能仅支持 µTorrent 3.x.x 及以上版本；<br/><br/>1. 在 µTorrent 的 设置 -> 高级 -> 网页界面 添加一个下载目录，如：D:\\\\download\\\\ <br/>2. 在助手里添加目录列表（仅支持相对路径），如：music\\\\ <br/>3. 最终数据的保存目录为：D:\\\\download\\\\music\\\\\"\n}"
  },
  {
    "path": "resource/clients/utorrent/init.js",
    "content": "/**\n * @see https://github.com/bittorrent/webui/blob/master/webui.js\n */\n(function ($, window) {\n  class uTorrent {\n    /**\n     * 初始化实例\n     * @param {*} options\n     * loginName: 登录名\n     * loginPwd: 登录密码\n     * url: 服务器地址\n     */\n    init(options) {\n      this.options = options;\n      this.headers = [];\n      this.token = \"\";\n      if (options.loginName && options.loginPwd) {\n        this.headers[\"Authorization\"] =\n          \"Basic \" +\n          new Base64().encode(options.loginName + \":\" + options.loginPwd);\n      }\n\n      if (this.options.address.indexOf(\"gui\") == -1) {\n        let url = PTServiceFilters.parseURL(this.options.address);\n        let address = [\n          url.protocol,\n          \"://\",\n          url.host\n        ];\n        if (url.port) {\n          address.push(`:${url.port}`)\n        }\n\n        address.push(url.path);\n        if (url.path.substr(-1) != \"/\") {\n          address.push(\"/\");\n        }\n\n        address.push(\"gui/\");\n        this.options.address = address.join(\"\");\n      }\n      console.log(\"uTorrent.init\", this.options.address);\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      console.log(\"uTorrent.call\", action, data);\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          case \"addTorrentFromURL\":\n            this.addTorrentFromUrl(data, result => {\n              resolve(result);\n            });\n            break;\n\n          // 测试是否可连接\n          case \"testClientConnectivity\":\n            this.getSessionId()\n              .then(result => {\n                resolve(result != \"\");\n              })\n              .catch(result => {\n                reject(result);\n              });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 获取Session\n     * @param {*} callback\n     */\n    getSessionId(callback) {\n      return new Promise((resolve, reject) => {\n        $.ajax({\n          type: \"GET\",\n          url: this.options.address + \"token.html?t=\",\n          headers: this.headers,\n          timeout: PTBackgroundService.options.connectClientTimeout\n        })\n          .done(resultData => {\n            console.log(resultData);\n            this.token = $(resultData).html();\n            this.isInitialized = true;\n            if (callback) {\n              callback(this.token);\n            }\n            resolve(this.token);\n          })\n          .fail((jqXHR, textStatus) => {\n            let result = {\n              status: textStatus || \"error\",\n              code: jqXHR.status,\n              msg: textStatus === \"timeout\" ? i18n.t(\"downloadClient.timeout\") : i18n.t(\"downloadClient.unknownError\") //\"连接超时\" : \"未知错误\"\n            };\n            switch (jqXHR.status) {\n              case 0:\n                result.msg = i18n.t(\"downloadClient.serverIsUnavailable\") //\"服务器不可用或网络错误\"\n                break;\n\n              case 401:\n                result.msg = i18n.t(\"downloadClient.permissionDenied\");//\"身份验证失败\";\n                break;\n\n              case 404:\n                result.msg = i18n.t(\"downloadClient.notFound\");// \"指定的地址未找到，服务器返回了 404\";\n                break;\n            }\n            reject(result);\n          });\n      });\n    }\n\n    /**\n     * 调用指定的RPC\n     * @param {*} options\n     * @param {*} callback\n     * @param {*} tags\n     */\n    exec(options, callback, tags) {\n      if (!this.token) {\n        this.getSessionId().then(() => {\n          this.exec(options, callback, tags);\n        }).catch((result) => {\n          callback && callback(result);\n        });\n        return;\n      }\n      var data = {};\n\n      var _settings = $.extend({\n        method: \"GET\",\n        processData: undefined,\n        contentType: undefined,\n        queryString: \"\"\n      }, options.settings);\n\n      if (options.settings) {\n        delete options.settings;\n      }\n      if (options.formData) {\n        data = options.formData;\n      } else {\n        $.extend(data, options);\n      }\n\n      var settings = {\n        type: _settings.method,\n        url: this.options.address + \"?token=\" + this.token + _settings.queryString,\n        dataType: \"json\",\n        processData: _settings.processData,\n        contentType: _settings.contentType,\n        data: data,\n        timeout: PTBackgroundService.options.connectClientTimeout,\n        success: (resultData, textStatus) => {\n          if (callback) {\n            callback(resultData, tags);\n          }\n        },\n        error: (request, event, page) => {\n          console.log(request);\n          this.getSessionId().then(() => {\n            this.exec(options, callback, tags);\n          }).catch((result) => {\n            callback && callback(result);\n          });\n        },\n        headers: this.headers\n      };\n      $.ajax(settings);\n    }\n\n    /**\n     * 添加种子链接\n     * @param {*} data\n     * @param {*} callback\n     */\n    addTorrentFromUrl(data, callback) {\n      let url = data.url;\n\n      // 磁性连接\n      if (url.startsWith('magnet:')) {\n        this.addTorrent({\n          action: \"add-url\",\n          s: url,\n          download_dir: 0,\n          path: data.savePath ? data.savePath : \"\"\n        }, callback);\n        return;\n      }\n\n      PTBackgroundService.requestMessage({\n        action: \"getTorrentDataFromURL\",\n        data: url\n      })\n        .then((result) => {\n          let formData = new FormData();\n          formData.append(\"torrent_file\", result, \"file.torrent\")\n\n          this.addTorrent({\n            settings: {\n              method: \"POST\",\n              processData: false,\n              contentType: false,\n              queryString: `&action=add-file&download_dir=0&path=` + (data.savePath ? data.savePath : \"\")\n            },\n            formData\n          }, callback);\n        })\n        .catch((result) => {\n          callback && callback(result);\n        });\n\n    }\n\n    addTorrent(options, callback) {\n      this.exec(options,\n        resultData => {\n          if (callback) {\n            var result = resultData;\n            if (resultData.build) {\n              result.status = \"success\";\n              result.msg = result.msg = i18n.t(\"downloadClient.addURLSuccess\", {\n                name: this.options.name\n              });//\"URL已添加至 µTorrent 。\";\n            }\n            callback(result);\n          }\n          console.log(resultData);\n        }\n      );\n    }\n  }\n\n  // 添加到 window 对象，用于客户页面调用\n  window.utorrent = uTorrent;\n})(jQuery, window);"
  },
  {
    "path": "resource/i18n/README.md",
    "content": "# 关于多语言\n> 多语言环境正在构建中，现阶段正在努力将中文翻译为英文，需要各位英文达人参与翻译、复核，待英文文案可用后，将作为其他语言的源文件以供翻译。\n\n## 如何参与英文翻译？\n- 您可以通过以下方式来参与\n  - 通过在线翻译的方式来参与，欢迎加入：https://www.transifex.com/ronggang/pt-plugin-plus-dev\n  - 直接以 `git` 方式更新本目录下的 `en.json` 文件；\n\n## 多语言文案更新流程\n- 中文词汇增加 -> 提交至 `transifex` 的 `dev` 项目 -> 翻译为英文 -> 复核 -> 转为正式文案 -> 翻译为其他语言\n\n## 如何测试已翻译的新语言\n- 更新助手至 `v1.0.9` 之后的版本；\n- 进入助手配置页面；\n- 点击右下角的 `切换语言` -> `临时添加新语言` ；\n- 选择已经翻译好的语言文件，如：`zh-TW.json` ；\n- 如果格式正确，应该就可以看到新的语言内容了；\n- 如果是通过 `transifex` 在线翻译的，可以从 `transifex` 下载已翻译好的文件进行测试；\n- 使用该方式添加的语言文件仅本次生效，浏览器重启后恢复到默认状态；"
  },
  {
    "path": "resource/i18n/en.json",
    "content": "{\n  \"name\": \"English (Beta)\",\n  \"code\": \"en\",\n  \"authors\": [\n    \"ronggang\",\n    \"ylxb2016\",\n    \"xiongqiwei\",\n    \"jackson008\",\n    \"MewX\"\n  ],\n  \"words\": {\n    \"app\": {\n      \"initError\": \"The configuration information failed to be loaded. The system definition information was not obtained. Please try to refresh the current page.\",\n      \"initializing\": \"The data is being prepared, please wait...\",\n      \"author\": \"ronggang\",\n      \"name\": \"PT Plugin Plus\"\n    },\n    \"common\": {\n      \"debugMode\": \"Debug mode active\",\n      \"changeLanguage\": \"Switch language\",\n      \"addLanguage\": \"Add a new language temporarily\",\n      \"version\": \"Version\",\n      \"systemLog\": \"System Log\",\n      \"darkMode\": \"Invert Color\",\n      \"haveNewReleases\": \"Update available\",\n      \"add\": \"Add\",\n      \"edit\": \"Edit\",\n      \"copy\": \"Copy\",\n      \"ok\": \"OK\",\n      \"cancel\": \"Cancel\",\n      \"remove\": \"Remove\",\n      \"clear\": \"Clear\",\n      \"removeConfirm\": \"Are you sure you want to delete this record?\",\n      \"removeSelectedConfirm\": \"Are you sure you want to delete these {count} selected records?\",\n      \"removeConfirmTitle\": \"Delete confirmation\",\n      \"clearConfirm\": \"Are you sure you want to delete all records?\",\n      \"id\": \"ID\",\n      \"readyToStart\": \"Ready to start...\",\n      \"help\": \"Help\",\n      \"export\": \"Export\",\n      \"import\": \"Import\",\n      \"share\": \"Share\",\n      \"actionConfirm\": \"Are you sure you want to do this?\",\n      \"importFailed\": \"Import failed\",\n      \"importSuccess\": \"Import success\",\n      \"all\": \"All\",\n      \"setDefault\": \"Set Default\",\n      \"cancelDefault\": \"Cancel Default\",\n      \"search\": \"Search\",\n      \"color\": \"Color\",\n      \"orderBy\": \"Order By\",\n      \"orderMode\": {\n        \"asc\": \"ASC\",\n        \"desc\": \"DESC\"\n      },\n      \"close\": \"Close\",\n      \"copyed\": \"Copyed\",\n      \"hot\": \"HOT\",\n      \"loading\": \"Loading...\",\n      \"lastUpdate\": \"last updated on {time}\",\n      \"refresh\": \"Refresh\"\n    },\n    \"topbar\": {\n      \"title\": \"@:(app.name)\",\n      \"navBarTip\": \"Click to show\\/hide the navigation bar\",\n      \"help\": \"Wiki\",\n      \"github\": \"Github\",\n      \"showNewTorrents\": \"Get the first page of each tracker\",\n      \"showNewTorrentsTip\": \"Search the first page torrents of each tracker according to the current solution\"\n    },\n    \"navigation\": {\n      \"dashboard\": {\n        \"title\": \"Overview\",\n        \"userData\": \"My Data\",\n        \"searchResults\": \"Search Results\",\n        \"history\": \"Download History\",\n        \"collection\": \"Collection\",\n        \"searchResultSnapshot\": \"Search Snapshot\",\n        \"keepUploadTask\": \"Reseed Task\"\n      },\n      \"settings\": {\n        \"title\": \"Settings\",\n        \"base\": \"General\",\n        \"sites\": \"Sites\",\n        \"downloadClients\": \"Download Server\",\n        \"downloadPaths\": \"Download Paths\",\n        \"searchSolution\": \"Search Solution\",\n        \"backup\": \"Backup & Restore\",\n        \"permissions\": \"Permissions\"\n      },\n      \"thanks\": {\n        \"title\": \"Thanks\",\n        \"reference\": \"Project Reference\",\n        \"specialThanksTo\": \"Special Thanks\"\n      },\n      \"support\": {\n        \"title\": \"Support This Project\",\n        \"bugReport\": \"Bug Report\",\n        \"donate\": \"Donate\",\n        \"debugger\": \"Debugger\"\n      }\n    },\n    \"permissions\": {\n      \"title\": \"Thank you for choosing PT Plugin Plus\",\n      \"subtitle\": \"In order to work properly, please authorize the required permission.\",\n      \"authorize\": \"Authorization\",\n      \"cancel\": \"Cancel\",\n      \"cancelled\": \"Bye!\",\n      \"details\": {\n        \"allSites\": \"Access to all trackers for searching and get torrents data;\",\n        \"tabs\": \"Read permission of the activity tab to display the PT Plugin Plus icon;\",\n        \"downloads\": \"Download permission for batch download of torrents\",\n        \"cookies\": \"Import/Export site cookies\"\n      },\n      \"headers\": {\n        \"title\": \"Permission description\",\n        \"enabled\": \"Authorized\"\n      },\n      \"request\": {\n        \"default\": \"Are you sure granting this permission?\",\n        \"cookies\": \"Are you sure granting cookies permission to read and write?\"\n      }\n    },\n    \"searchBox\": {\n      \"searchTip\": \"Input keyword, IMDb numbers, then press <Enter> to search\",\n      \"default\": \"<Default>\",\n      \"defaultTip\": \"Search only allowed sites\",\n      \"all\": \"<All Sites>\",\n      \"noSearchSolution\": \"No Search Solution, please add a solution\",\n      \"noAllowSearchSites\": \"The site to search is not configured yet. Please configure it before.\",\n      \"searchThisKey\": \"Search “{key}”\",\n      \"doubanTip\": \"The above data comes from the Douban Movie API v2 ; if you do not want to display these results for pre-selection, you can close it in the General Settings\",\n      \"toDouban\": \"View in Douban\"\n    },\n    \"donate\": {\n      \"title\": \"Thanks for the support\"\n    },\n    \"history\": {\n      \"title\": \"Download history\",\n      \"remove\": \"Remove\",\n      \"clear\": \"Clear\",\n      \"removeConfirm\": \"Confirm to delete this record?\",\n      \"removeConfirmTitle\": \"Delete confirmed\",\n      \"clearConfirm\": \"Confirm to delete all download records?\",\n      \"ok\": \"@:(common.ok)\",\n      \"cancel\": \"@:(common.cancel)\",\n      \"download\": \"Download again\",\n      \"fail\": \"Failure\",\n      \"success\": \"Success\",\n      \"unknown\": \"N/A\",\n      \"defaultPath\": \"Default Path\",\n      \"seedingTorrent\": \"Sending torrents to download server...\",\n      \"headers\": {\n        \"site\": \"Source\",\n        \"title\": \"Title\",\n        \"status\": \"Status\",\n        \"time\": \"Download time\",\n        \"action\": \"Action\"\n      }\n    },\n    \"home\": {\n      \"title\": \"My Data\",\n      \"getInfos\": \"Refresh my data\",\n      \"cancelRequest\": \"Cancel request\",\n      \"requesting\": \"Requesting\",\n      \"siteName\": \"Site name\",\n      \"userName\": \"User name\",\n      \"userLevel\": \"User level\",\n      \"levelRequirements\": \"Level requirements\",\n      \"seedingPoints\": \"Seeding Points\",\n      \"showHnR\": \"H&R\",\n      \"selectColumns\": \"Select Columns\",\n      \"week\": \"Expressed in weeks\",\n      \"timeline\": \"Time line\",\n      \"settings\": \"Settings\",\n      \"statistic\": \"Statistic\",\n      \"newMessage\": \"New message\",\n      \"startGetting\": \"Getting user profile...\",\n      \"gettingForSite\": \"Getting {siteName} user profile\",\n      \"requestCompleted\": \"Request completed, time: {second} seconds.\",\n      \"getUserInfoError\": \"An error occurred\",\n      \"getUserInfoAbort\": \"Get user profile request has been canceled. ({siteName})\",\n      \"getUserInfoAbortError\": \"Cancellation failed to get user profile request. ({siteName})\",\n      \"offline\":\"Offline\",\n      \"headers\": {\n        \"date\": \"Date\",\n        \"site\": \"Site\",\n        \"userName\": \"User name\",\n        \"levelName\": \"Level\",\n        \"activitiyData\": \"Activitiy data\",\n        \"ratio\": \"Ratio\",\n        \"seeding\": \"Seeding\",\n        \"seedingSize\": \"Seeding size\",\n        \"bonus\": \"Bonus\",\n        \"seedingPoints\": \"Seeding Points\",\n        \"bonusPerHour\": \"Bonus per hour\",\n        \"joinTime\": \"Join time\",\n        \"lastUpdateTime\": \"Update at\",\n        \"status\": \"Status\",\n        \"comments\": \"Comments\",\n        \"uploads\": \"Uploaded\",\n        \"trueDownloaded\": \"True Downloaded\",\n        \"classPoints\": \"Class Points\",\n        \"unsatisfieds\": \"Unsatisfieds\",\n        \"prewarn\": \"H&R Prewarn\"\n      },\n      \"levelRequirement\": {\n        \"levelRequirements\": \"Level Requirements\",\n        \"date\": \"Date\",\n        \"site\": \"Site\",\n        \"userName\": \"User name\",\n        \"levelName\": \"Level\",\n        \"activitiyData\": \"Activitiy data\",\n        \"ratio\": \"Ratio\",\n        \"seeding\": \"Seeding\",\n        \"seedingSize\": \"Seeding size\",\n        \"bonus\": \"Bonus\",\n        \"seedingPoints\": \"Seeding Points\",\n        \"seedingTime\": \"Seeding Time\",\n        \"bonusPerHour\": \"Bonus per hour\",\n        \"joinTime\": \"Join time\",\n        \"lastUpdateTime\": \"Update at\",\n        \"status\": \"Status\",\n        \"comments\": \"Comments\",\n        \"uploaded\": \"Uploaded\",\n        \"downloaded\":\"Downloaded\",\n        \"uploads\": \"Uploaded\",\n        \"downloads\": \"Downloaded\",\n        \"trueDownloaded\": \"True Downloaded\",\n        \"classPoints\": \"Class Points\",\n        \"uniqueGroups\": \"Unique Groups\",\n        \"perfectFLAC\": \"\\\"Perfect\\\" FLAC\",\n        \"alternative\": \"Alternative\"\n      },\n      \"tip\": \"N/A means no support\",\n      \"nodata\": \"You have not added a site, please go to [Sites] to add a site.\"\n    },\n    \"systemLog\": {\n      \"title\": \"System Log\",\n      \"save\": \"Save log\",\n      \"headers\": {\n        \"module\": \"Module\",\n        \"event\": \"Events\",\n        \"time\": \"Time\",\n        \"msg\": \"Description\",\n        \"action\": \"Action\"\n      }\n    },\n    \"reference\": {\n      \"title\": \"This project uses or references the following items\",\n      \"thanks\": \"The birth of 'PT Plugin Plus' is established on the base of these projects. Thanks all the participants of the project, thanks for your contribution!\",\n      \"headers\": {\n        \"name\": \"Name\",\n        \"ver\": \"Version\",\n        \"url\": \"URL\"\n      }\n    },\n    \"team\": {\n      \"title\": \"Alphabetical Order\",\n      \"contributors\": \"Contributors:\",\n      \"issues\": \"Suggesters:\"\n    },\n    \"timeline\": {\n      \"share\": \"Generate a share image\",\n      \"siteName\": \"Tracker name\",\n      \"blurSiteIcon\": \"Blur site icon\",\n      \"userName\": \"User name\",\n      \"userLevel\": \"User level\",\n      \"userId\": \"User UID\",\n      \"showSites\": \"Show Sites\",\n      \"close\": \"Close\",\n      \"shareMessage\": \"Growth process\",\n      \"time\": {\n        \"year\": \" year(s) \",\n        \"month\": \" month(s) \",\n        \"day\": \" day(s) \",\n        \"hour\": \" hour(s) \",\n        \"mins\": \" minute(s) \",\n        \"week\": \" week(s) \",\n        \"ago\": \" ago\",\n        \"lessThanAWeek\": \"Less than a week\"\n      },\n      \"total\": {\n        \"uploaded\": \"Total uploads: \",\n        \"downloaded\": \"Total download: \",\n        \"seedingSize\": \"Seeding size: \",\n        \"ratio\": \"Total ratio: \",\n        \"years\": \"PT ages：≈ {year} year(s)\"\n      },\n      \"updateat\": \"Update at: \",\n      \"user\": {\n        \"uploaded\": \"Uploaded: \",\n        \"downloaded\": \"Downloaded: \",\n        \"seedingSize\": \"Seeding size: \",\n        \"ratio\": \"Ratio: \",\n        \"bonus\": \"Bonus: \",\n        \"bonusPerHour\": \"Bonus per hour: \"\n      },\n      \"inputDisplayName\": \"Please enter a name to display:\",\n      \"inputShareMessage\": \"Please enter a message to display:\"\n    },\n    \"searchTorrent\": {\n      \"title\": \"Search Result\",\n      \"download\": \"Download\",\n      \"downloadFailed\": \"Re-download failed\",\n      \"sendToClient\": \"Send to server\",\n      \"sendToClientTip\": \"Send torrents to download server\",\n      \"save\": \"Save\",\n      \"saveTip\": \"Save torrents\",\n      \"collection\": \"Collection\",\n      \"searching\": \"Searching, please wait...\",\n      \"cancelSearch\": \"Cancel search\",\n      \"showCheckbox\": \"Multiple selection\",\n      \"noTag\": \"No tag\",\n      \"allSites\": \"All Sites\",\n      \"multiDownloadConfirm\": \"The number of torrents currently downloaded exceeds one, and the browser may prompt multiple times to save. Do you want to continue?\",\n      \"copyToClipboard\": \"Copy Links\",\n      \"copyToClipboardTip\": \"Copy download links to clipboard\",\n      \"reSearch\": \"Re-search\",\n      \"showCategory\": \"Category\",\n      \"filterSearchResults\": \"Filter search results\",\n      \"noResultsSites\": \"Site with 0 result:\",\n      \"failedSites\": \"Failed site:\",\n      \"reSearchFailedSites\": \"Re-search failed site\",\n      \"failUrl\": \"Invalid link\",\n      \"headers\": {\n        \"site\": \"Site\",\n        \"title\": \"Title\",\n        \"category\": \"Category\\/Entrance\",\n        \"size\": \"Size\",\n        \"seeders\": \"S\",\n        \"leechers\": \"L\",\n        \"completed\": \"C\",\n        \"comments\": \"Comments\",\n        \"time\": \"Time(≈)\",\n        \"action\": \"Action\"\n      },\n      \"optionsIsMissing\": \"System parameter is missing\",\n      \"sitesIsMissing\": \"Please set up the site first\",\n      \"optionsIsMissingErrorMsg\": \"System parameters are lost, please re-open this page\",\n      \"doubanIdConversionFailed\": \"Douban ID Conversion Failed\",\n      \"skipSites\": \"Sites that do not support search at this time:\",\n      \"noAllowSearchSites\": \"You have not configured a site that allows search. Please go to [Site Settings] to configure.\",\n      \"searchStartMsg\": \"Ready to start searching, a total of {count} sites\",\n      \"siteIsSearching\": \"[{siteName}] is searching.\",\n      \"siteIsSearchDone\": \"{siteName} Search completed with {count} results.\",\n      \"siteSearchAbort\": \"{host} Search request canceled\",\n      \"siteSearchAbortError\": \"{host} Search request cancellation failed\",\n      \"siteSearchTimeout\": \"{host} Connection timed out\",\n      \"siteSearchError\": \"{host} Network or other error\",\n      \"notLogged\": \"Not logged\",\n      \"searchFinished\": \"Search completed, found {count} results, time: {second} seconds.\",\n      \"searchProgress\": \"{count} results have been received, search is still in progress...\",\n      \"seedingTorrent\": \"Sending torrent to download server...\",\n      \"userCanceled\": \"User Canceled\",\n      \"sendTorrentToClient\": \"Send torrent to the download server\",\n      \"sendTorrentToClientSuccess\": \"Send torrent to download server successfully\",\n      \"sendTorrentToClientError\": \"Send torrent to download server failed\",\n      \"downloadSelectedError\": \"Failed to download torrent file: {name}\",\n      \"copyLinkToClipboardSuccess\": \"Download link has been copied to the clipboard\",\n      \"copyLinkToClipboardError\": \"Copying the download link failed!\",\n      \"copySelectedToClipboardSuccess\": \"{count} download links have been copied to the clipboard\",\n      \"downloadTo\": \"Download to: {path}\",\n      \"noReSearchSites\": \"No sites that need to be re-searched\",\n      \"doubanIdConverting\": \"Trying to convert the douban id, please wait...\",\n      \"invalidDoubanId\": \"Invalid douban id\",\n      \"torrentStatus\": {\n        \"downloading\": \"Downloading\",\n        \"sending\": \"Sending\",\n        \"completed\": \"Completed\",\n        \"inactive\": \"Inactive\"\n      }\n    },\n    \"settings\": {\n      \"backup\": {\n        \"title\": \"Parameter backup and recovery\",\n        \"subTitle\": \"Note: Unless encryption is set, the backup file is in plain text and may contain personal information. Please take care of it.\",\n        \"backup\": \"Backup\",\n        \"restore\": \"Restore\",\n        \"backupToGoogle\": \"Backup To Google\",\n        \"restoreFromGoogle\": \"Restore From Google\",\n        \"restoreConfirm\": \"Are you sure you want to restore the settings from the backup data? This will overwrite all current settings.\",\n        \"restoreSuccess\": \"Parameter has been restored\",\n        \"restoreError\": \"Parameter recovery failed!\",\n        \"loadError\": \"Configuration information failed to load\",\n        \"backupDone\": \"Backup completed\",\n        \"backupError\": \"Backup parameters failed!\",\n        \"errorMessage\": {\n          \"QUOTA_BYTES_PER_ITEM\": \"The size of the content to be saved exceeds the Google limit (8K)\"\n        },\n        \"clearFromGoogle\": \"Clear\",\n        \"clearFromGoogleTip\": \"Clear backed up parameters from Google\",\n        \"clearFromGoogleConfirm\": \"Do you want to clear the backed up parameters from Google?\",\n        \"clearFromGoogleError\": \"Clear failed!\",\n        \"clearFromGoogleSuccess\": \"Content cleared\",\n        \"index\": {\n          \"headers\": {\n            \"name\": \"Service name\",\n            \"type\": \"Type\",\n            \"lastBackupTime\": \"Last backup time\",\n            \"action\": \"Action\"\n          }\n        },\n        \"server\": {\n          \"add\": {\n            \"title\": \"Add Backup Server\"\n          },\n          \"edit\": {\n            \"title\": \"Edit Backup Server\"\n          },\n          \"editor\": {\n            \"type\": \"Server Type\",\n            \"name\": \"Server name\",\n            \"address\": \"Server address\",\n            \"authCode\": \"Authorization Code\",\n            \"applyAuthCode\": \"Apply\",\n            \"loginName\": \"log-in name\",\n            \"loginPwd\": \"login password\",\n            \"digest\": \"Use Digest Authorization\"\n          },\n          \"list\": {\n            \"noData\": \"No backup data yet\",\n            \"backupToServer\": \"Backup to server\",\n            \"loadBackupList\": \"Load backup list\"\n          },\n          \"getFileListError\": \"Failed to get the backup file, please confirm whether the network and backup server are available\",\n          \"owss\": {\n            \"addressTip\": \"The server address contains the port, such as: http:\\/\\/192.168.1.1:8088\\/storage\"\n          }\n        },\n        \"restoreAll\": \"Restore All\",\n        \"restoreCollection\": \"Restore Collection Only\",\n        \"restoreCookies\": \"Restore Cookies Only\",\n        \"restoreSearchResultSnapshot\": \"Restore Search Result Snapshot Only\",\n        \"restoreKeepUploadTask\": \"Restore Reseed Task Only\",\n        \"restoreDownloadHistory\": \"Restore Download History Only\",\n        \"contentNotExist\": {\n          \"cookies\": \"Cookies not exist in the backup file\",\n          \"collection\": \"Collection not exists in the backup file\",\n          \"searchResultSnapshot\": \"Search Result Snapshot not exists in the backup file\",\n          \"keepUploadTask\": \"Reseed Task not exists in the backup file\",\n          \"downloadHistory\": \"Download History not exists in the backup file\"\n        },\n        \"backupItem\": {\n          \"base\": \"General\",\n          \"userDatas\": \"User Datas\",\n          \"collection\": \"Collection\",\n          \"cookies\": \"Cookies\",\n          \"searchResultSnapshot\": \"Search Result Snapshot\",\n          \"keepUploadTask\": \"Reseed Task\",\n          \"downloadHistory\": \"Download History\"\n        },\n        \"restoreErrorType\": {\n          \"needSecretKey\": \"Need Secret Key\",\n          \"errorSecretKey\": \"Error Secret Key\"\n        },\n        \"enterSecretKey\": \"Please enter a Secret key:\",\n        \"restoreCookiesConfirm\": \"Are you sure you want to restore cookies? This will overwrite current cookies.\"\n      },\n      \"base\": {\n        \"title\": \"General\",\n        \"defaultClient\": \"Default download server (required)\",\n        \"autoUpdate\": \"Automatically update official data\",\n        \"save\": \"Save\",\n        \"allowSelectionTextSearch\": \"Enable page content selection search\",\n        \"allowDropToSend\": \"Send a link to the download server when dragging and dropping links to the plugin icon\",\n        \"clearCache\": \"Clear cache\",\n        \"clearCacheConfirm\": \"Are you sure you want to clear the cache? After the cleaning is completed, the system configuration information will be re-downloaded from the official website next time.\",\n        \"needConfirmWhenExceedSize\": \"Confirmation when the total volume of torrent downloaded in batches exceeds the following size\",\n        \"exceedSize\": \"Size\",\n        \"searchResultRows\": \"Number of results returned per site at the time of search\",\n        \"saveDownloadHistory\": \"Enable download history to record torrent information sent with one click at a time\",\n        \"connectClientTimeout\": \"Global timeout (milliseconds, 1000 milliseconds = 1 second), acting on the connection download server, downloading the torrent file, etc.\",\n        \"noClient\": \"The download server has not been configured. Please configure the download service before selecting\",\n        \"cacheIsCleared\": \"The cache has been cleared. If it needs to take effect immediately, please reopen the page.\",\n        \"saved\": \"Parameter saved\",\n        \"autoRefreshUserData\": \"Automatically refresh user data when the browser is open (Beta)\",\n        \"autoRefreshUserDataTip1\": \"Automatically refreshes at\",\n        \"autoRefreshUserDataTip2\": \"every day (if the browser opens after this time, it will be automatically refreshed when the browser is opened)\",\n        \"autoRefreshUserDataTip3\": \"Retry\",\n        \"autoRefreshUserDataTip4\": \"times after failure,\",\n        \"autoRefreshUserDataTip5\": \"minute apart\",\n        \"searchResultOrderBySitePriority\": \"When the search results are clicked on the site header, they are sorted by site priority (effective after refreshing the page after saving)\",\n        \"saveSearchKey\": \"Save historical search keyword records\",\n        \"showMoiveInfoCardOnSearch\": \"Show movie and rating information when searching by IMDb number\",\n        \"getMovieInformationBeforeSearching\": \"When entering a search keyword, load relevant information from Douban for pre-selection\",\n        \"maxMovieInformationCount\": \"Maximum display number of entries (1-20):\",\n        \"searchModeForItem\": \"When clicking on a pre-selected item:\",\n        \"showToolbarOnContentPage\": \"Enable site page plugin icons and toolbars (such as one-click downloads, etc.)\",\n        \"lastUpdate\": \" (last updated on {time})\",\n        \"lastUpdateUnknown\": \" (Update time is unknown)\",\n        \"lastUpdateFailed\": \" (Failed to get update time)\",\n        \"autoRefreshUserDataLastUpdate\": \" (last updated on {time})\",\n        \"beforeSearchingItemSearchMode\": {\n          \"id\": \"Search by IMDb ID to get more accurate content, but it need more time to get IMDb ID\",\n          \"name\": \"Fuzzy search by name to get more content\"\n        },\n        \"downloadFailedRetry\": \"Retry after download failed\",\n        \"downloadFailedRetryTip1\": \"Retry\",\n        \"downloadFailedRetryTip2\": \"times after failure,\",\n        \"downloadFailedRetryTip3\": \"second apart. (0 means retry immediately after failure)\",\n        \"tabs\": {\n          \"base\": \"General\",\n          \"search\": \"Search\",\n          \"download\": \"Download\",\n          \"advanced\": \"Advanced\"\n        },\n        \"apiKey\": {\n          \"omdb\": \"OMDb API Key\",\n          \"douban\": \"Douban API Key\"\n        },\n        \"apiKeyTip\": \"The OMDb API Key is used to obtain the rating information of the movie;\\nThe Douban API Key is used to obtain the basic information of the movie, such as pictures, introductions, etc.\",\n        \"verifyApiKey\": \"Verify Api Key\",\n        \"batchDownloadInterval\": \"Per torrent interval (seconds) when downloading in bulk\",\n        \"enableBackgroundDownload\": \"Enable background download task\",\n        \"position\": {\n          \"label\": \"Displayed on: \",\n          \"left\": \" Left side of page\",\n          \"right\": \"Rigth side of page\"\n        },\n        \"allowBackupCookies\": \"Backup cookies of the configured site. (Backup to Google is not supported)\",\n        \"encryptBackupData\": \"Encrypt backup data. (Backup to Google is not supported)\",\n        \"encryptMode\": \"Encryption: \",\n        \"encryptSecretKey\": \"Secret Key:\",\n        \"encryptTip\": \"Note: The key is only saved in the current browser and will not be backed up. Please save it properly. If the key is lost after encryption, the data will not be recovered.\",\n        \"createSecretKey\": \"Create\",\n        \"allowSaveSnapshot\": \"Allow saving search results snapshots\"\n      },\n      \"downloadClients\": {\n        \"add\": {\n          \"title\": \"Add download server\",\n          \"titleStep1\": \"Select bittorrent client type\",\n          \"titleStep2\": \"Detailed configuration\",\n          \"validMsg\": \"Please select a bittorrent client type\",\n          \"helpMsg\": \"Can't find the bittorrent client type you want? \",\n          \"nextStep\": \"Next\",\n          \"prevStep\": \"Previous\",\n          \"cancel\": \"Cancel\"\n        },\n        \"edit\": {\n          \"title\": \"Edit download server\",\n          \"ok\": \"OK\",\n          \"cancel\": \"Cancel\"\n        },\n        \"editor\": {\n          \"name\": \"Name of server\",\n          \"type\": \"bittorrent client type\",\n          \"address\": \"Server address\",\n          \"addressTip\": \"The server address contains the port, such as: http:\\/\\/192.168.1.1:5000\\/\",\n          \"loginName\": \"log-in name\",\n          \"loginPwd\": \"login password\",\n          \"id\": \"ID\",\n          \"autoStart\": \"Automatically start downloading when sending a torrent\",\n          \"tagIMDb\": \"Add IMDb tag when sending a torrent(Beta)\",\n          \"autoCreate\": \"<Automatically generated after saving>\",\n          \"test\": \"Test if the server can connect\",\n          \"testSuccess\": \"Server can be connected\",\n          \"testConnectionError\": \"Network connection error\",\n          \"testError\": \"Server connection failed\",\n          \"testUnknownError\": \"unknown errors\",\n          \"testOtherError\": \"Other errors, the code returned by the server is: {code}\",\n          \"testAddressError\": \"Server address error\"\n        },\n        \"index\": {\n          \"title\": \"Download server configuration\",\n          \"subTitle\": \"You must to add at least one download server before you start using it.\",\n          \"add\": \"New\",\n          \"remove\": \"Remove\",\n          \"clear\": \"Clear\",\n          \"itemDuplicate\": \"The name already exists\",\n          \"removeConfirm\": \"Are you sure you want to delete this download server?\",\n          \"removeConfirmTitle\": \"Delete confirmation\",\n          \"clearConfirm\": \"Are you sure you want to delete all download servers?\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected download server?\",\n          \"ok\": \"OK\",\n          \"cancel\": \"Cancel\",\n          \"headers\": {\n            \"name\": \"Name\",\n            \"type\": \"Type\",\n            \"address\": \"Address\",\n            \"action\": \"Action\"\n          }\n        }\n      },\n      \"downloadPaths\": {\n        \"add\": {\n          \"title\": \"Add new download Path\",\n          \"path\": \"Paths list\",\n          \"pathTip\": \"Press Enter multiple Paths separated first as the default Path\",\n          \"ok\": \"OK\",\n          \"cancel\": \"Cancel\",\n          \"selectSite\": \"Select a site (not selected means all sites are available)\"\n        },\n        \"edit\": {\n          \"title\": \"Edit download Path definition\",\n          \"site\": \"Site\"\n        },\n        \"index\": {\n          \"title\": \"Download Path settings\",\n          \"selectedClient\": \"Servers need to be set\",\n          \"add\": \"New\",\n          \"remove\": \"Remove\",\n          \"clear\": \"Clear\",\n          \"itemDuplicate\": \"The name already exists\",\n          \"removeConfirm\": \"Are you sure you want to delete this Download Path?\",\n          \"removeConfirmTitle\": \"Delete confirmation\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected Download Path?\",\n          \"ok\": \"OK\",\n          \"cancel\": \"Cancel\",\n          \"notSupport\": \"This server type is not supported at this time\",\n          \"allSite\": \"<All Sites>\",\n          \"headers\": {\n            \"name\": \"Site\",\n            \"path\": \"Download Path\",\n            \"action\": \"Action\"\n          }\n        },\n        \"keyDescription\": {\n          \"allowKeys\": \"The following keywords can be included in the path:\",\n          \"siteName\": \"Will be replaced with the current site name;\",\n          \"siteHost\": \"Will be replaced with the current site domain name;\",\n          \"example\": \"Example: \",\n          \"dynamic\": \"An input box will pop up where the user enters the {key} part of the path;\",\n          \"dynamicExample\": \"Example: \\/volume1\\/<...> ，input: test，result: \\/volume1\\/test\"\n        }\n      },\n      \"searchSolution\": {\n        \"edit\": {\n          \"title\": \"Search Solution Definition\"\n        },\n        \"editor\": {\n          \"name\": \"Solution Name\",\n          \"range\": \"Search range\",\n          \"headers\": {\n            \"name\": \"Site\"\n          }\n        },\n        \"index\": {\n          \"title\": \"Search Solution Definition\",\n          \"itemDuplicate\": \"The name already exists\",\n          \"removeConfirm\": \"Are you sure you want to delete this search solution?\",\n          \"removeConfirmTitle\": \"Delete confirmation\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected search solution?\",\n          \"help\": \"How to use?\",\n          \"headers\": {\n            \"name\": \"Name\",\n            \"range\": \"Range\",\n            \"action\": \"Action\"\n          }\n        }\n      },\n      \"sitePlugins\": {\n        \"add\": {\n          \"title\": \"Add new plugin\"\n        },\n        \"edit\": {\n          \"title\": \"Edit plugin\"\n        },\n        \"editor\": {\n          \"defaultClient\": \"Default download server\",\n          \"name\": \"Plugin name\",\n          \"pages\": \"Applicable page\",\n          \"pagesTip\": \"The page starts with '/' to indicate the root path of the website. After inputting, press Enter to add multiple, which can be a regular expression.\",\n          \"scripts\": \"Script files\",\n          \"scriptsTip\": \"\\/ means to load the script from the resource directory root, you can add multiple\",\n          \"script\": \"Javascript\",\n          \"style\": \"Style\",\n          \"styles\": \"Style files\",\n          \"stylesTip\": \"\\/ means to load the script from the resource directory root, you can add multiple\"\n        },\n        \"index\": {\n          \"title\": \"Site plugin configuration\",\n          \"importAll\": \"Import all\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected plugin?\",\n          \"removeConfirm\": \"Are you sure you want to delete this plugin?\",\n          \"removeTitle\": \"Delete confirmation\",\n          \"headers\": {\n            \"name\": \"Name\",\n            \"pages\": \"Applicable page\",\n            \"enable\": \"Enable\",\n            \"action\": \"Action\"\n          },\n          \"importNameDuplicate\": \"The name [{name}] already exists. Please re-enter the new name:\",\n          \"invalidPlugin\": \"Invalid plugin\"\n        }\n      },\n      \"sites\": {\n        \"add\": {\n          \"title\": \"Add Site\",\n          \"next\": \"Next\",\n          \"prev\": \"Previous\",\n          \"help\": \"Can't find the site you want?\",\n          \"validMsg\": \"Please select a site (support search)\",\n          \"custom\": \"Custom\",\n          \"step1\": \"Select site\",\n          \"step2\": \"Confirm site configuration\"\n        },\n        \"edit\": {\n          \"title\": \"Edit Site\"\n        },\n        \"editor\": {\n          \"defaultClient\": \"Download server (if not selected, the default download server of the basic settings will prevail)\",\n          \"name\": \"Site name\",\n          \"tags\": \"Tags\",\n          \"inputTags\": \"press Enter to add multiple\",\n          \"schema\": \"Site schema\",\n          \"description\": \"Site description\",\n          \"host\": \"Host\",\n          \"url\": \"Full url\",\n          \"urlTip\": \"The full address of the website, such as: https:\\/\\/www.github.com\\/\",\n          \"passkey\": \"Passkey\",\n          \"passkeyTip\": \"The key is only used to copy the download address operation. If you do not need this function, please leave it blank.\",\n          \"allowSearch\": \"Allow Search\",\n          \"allowGetUserInfo\": \"Allow access to user information (Beta)\",\n          \"cdn\": \"Site CDN list\",\n          \"cdnTip\": \"If you use a different URL than the system definition, you can fill in the currently used website address, fill in one address per line, the first one will be used as the address used for the search.\",\n          \"priority\": \"Priority\",\n          \"priorityTip\": \"Can be used for search sorting\",\n          \"offline\": \"Site is offline (downtime\\/shutdown)\",\n          \"timezone\": \"Timezone\",\n          \"upLoadLimit\": \"Upload limit\",\n          \"upLoadLimitTip\": \"Upload limit (KB\\/s), 0 or empty for no limit\",\n          \"disableMessageCount\": \"Display message count\"\n        },\n        \"userinfo\": {\n          \"title\": \"User Info\",\n          \"deleteConfirm\": \"confirm deletion? This operation cannot be restored, please make sure that you have made a corresponding backup.\"\n        },\n        \"index\": {\n          \"importAll\": \"One-click import site\",\n          \"importAllConfirm\": \"Are you sure you want to import site operations? This will import sites that have been logged in to the browser but have not been added.\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected site?\",\n          \"removeConfirm\": \"Are you sure you want to delete this site?\",\n          \"removeTitle\": \"Delete confirmation\",\n          \"plugins\": \"Plugins\",\n          \"showUserInfo\": \"UserInfo\",\n          \"title\": \"Site settings\",\n          \"subTitle\": \"Only the configured site will display the plugin icon and the corresponding function; the offline site will no longer participate in the search and information acquisition;\",\n          \"searchEntry\": \"Search Entry\",\n          \"importedText\": \"Imported successfully\",\n          \"siteDuplicateText\": \"The site already exists\",\n          \"headers\": {\n            \"name\": \"Name\",\n            \"tags\": \"Tags\",\n            \"allowSearch\": \"Allow Search\",\n            \"allowGetUserInfo\": \"User Info\",\n            \"offline\": \"Offline\",\n            \"activeURL\": \"URL\",\n            \"action\": \"Action\"\n          },\n          \"importConfig\": \"Import from config\",\n          \"importConfirm\": \"Are you sure you want to import [{name}] ?\",\n          \"importDuplicateConfirm\": \"The site {name} already exists. Do you need to import search entrys and plugins?\",\n          \"resetFavicons\": \"Reset site favicons\"\n        }\n      },\n      \"siteSearchEntry\": {\n        \"add\": {\n          \"title\": \"New Search Entry\"\n        },\n        \"edit\": {\n          \"title\": \"Edit Search Entry\"\n        },\n        \"editor\": {\n          \"name\": \"Entry name\",\n          \"entry\": \"Entry page (defaults to the system's defined entry page if not filled)\",\n          \"parseScript\": \"Search result parsing script\",\n          \"parseScriptFile\": \"Search result parsing script file\",\n          \"resultSelector\": \"Torrent list selector\",\n          \"category\": \"Category (not selected for all)\",\n          \"queryString\": \"Append query parameters\"\n        },\n        \"index\": {\n          \"title\": \"Site search entry configuration\",\n          \"removeSelectedConfirm\": \"Are you sure you want to delete the selected search entry?\",\n          \"removeConfirm\": \"Are you sure you want to delete this search entry?\",\n          \"removeTitle\": \"Delete confirmation\",\n          \"help\": \"how to use?\",\n          \"headers\": {\n            \"name\": \"Name\",\n            \"categories\": \"Categories\",\n            \"enable\": \"Enable\",\n            \"action\": \"Action\"\n          }\n        }\n      }\n    },\n    \"statistic\": {\n      \"selectSite\": \"Select sites that need statistics\",\n      \"goback\": \"Back\",\n      \"share\": \"Generate a share image\",\n      \"exportRawData\": \"Export original data\",\n      \"allSite\": \"(All Sites)\",\n      \"upload\": \"Upload\",\n      \"download\": \"Download\",\n      \"bonus\": \"Bonus\",\n      \"baseDataTitle\": \"[{userName}@{site}] Basic Data\",\n      \"baseDataSubTitle\": \"Uploaded: {uploaded}, Downloaded: {downloaded}, Bonus: {bonus}\",\n      \"data\": \"Data\",\n      \"seedingDataTitle\": \"[{userName}@{site}] Seeding\",\n      \"seedingDataSubTitle\": \"Seeding size: {seedingSize}, count: {count}\",\n      \"seedingSize\": \"Seeding size\",\n      \"seedingCount\": \"Seeding count\",\n      \"barDataTitle\": \"[{userName}@{site}] Upload Data\",\n      \"size\": \"Size\",\n      \"count\": \"Count\",\n      \"total\": \"Total\",\n      \"percent\": \"Percent\",\n      \"note\": \"Note: <br>1. Chart history data comes from the overview page, manual or automatic update will be recorded; <br>2. Each tracker only saves one per day;\",\n      \"dateRange\": {\n        \"7day\": \"7 days\",\n        \"30day\": \"30 days\",\n        \"60day\": \"60 days\",\n        \"90day\": \"90 days\",\n        \"180day\": \"180 days\",\n        \"all\": \"All days\"\n      }\n    },\n    \"service\": {\n      \"testClientConnectivityFailed\": \"Test client connection failed[{address}]\",\n      \"contextMenus\": {\n        \"history\": \"View download history\",\n        \"systemLog\": \"View plugin log\",\n        \"issues\": \"Use question feedback\",\n        \"searchSelectionText\": \"Search for \\\"%s\\\" related torrents\",\n        \"searchSelectionTextOnThisSite\": \"Search only for this site \\\"%s\\\" related torrents\",\n        \"searchByIMDb\": \"Search current IMDb related torrents\",\n        \"searchByDouban\": \"Search current Douban related torrents\",\n        \"searchByDefault\": \"Search by default\",\n        \"searchInAllSite\": \"Search across all sites\",\n        \"downloadClientPath\": \"{clientName} -> Specified path\",\n        \"userCanceled\": \"User canceled\",\n        \"pluginStatusIsUnknown\": \"The status of the plugin is unknown. The current operation may fail. Please refresh the page and try again.\",\n        \"sendingLink\": \"Sending a link to download server\",\n        \"downloadClientGetFailed\": \"Failed to get the download server.\",\n        \"sendTorrentToClientDone\": \"The download link is sent.\",\n        \"sendTorrentToClientError\": \"Download link failed to send!\",\n        \"sendTorrentToDefaultClient\": \"Sent to the default server {client.name} -> {- client.address}\",\n        \"sendTorrentToClient\": \"Send to other servers\",\n        \"searchInSite\": \"Search in site\",\n        \"searchInSolution\": \"Search in solution\"\n      },\n      \"searcher\": {\n        \"siteSearchConfigEntryIsEmpty\": \"The site [{site.name}] is not configured with a search page. Please configure it first.\",\n        \"siteSearchEntryIsEmpty\": \"The site [{site.name}] does not specify a search entry. Please specify a search entry first.\",\n        \"siteSearchResultParseFailed\": \"[{site.name}] data parsing failed!\",\n        \"siteEvalScriptFailed\": \"[{site.name}] Script execution error!\",\n        \"siteSearchResultError\": \"[{site.name}] No expected data was returned.\",\n        \"siteAbortSearch\": \"Deactivating search request for [{site.host}]\",\n        \"siteAbortSearchError\": \"[{site.host}] Failed to cancel search request!\",\n        \"siteNetworkFailed\": \"[{site.name}]Network request failed! ({msg})\"\n      },\n      \"controller\": {\n        \"invalidAddress\": \"Invalid Address\",\n        \"invalidDownloadServer\": \"Invalid Download Server\",\n        \"downloadTimeout\": \"The download server connection timed out, please check the network settings or adjust the server timeout!\",\n        \"downloadFinished\": \"Download server {name} handles [{action}] command completion\",\n        \"downloadError\": \"Download server {name} processing [{action}] command failed!\",\n        \"torrentAdded\": \"{title} The torrent has been added.\",\n        \"torrentSavePath\": \"<br/>Save to {path}\",\n        \"transmissionSuccess\": \"{data.name} has been sent to Transmission, ID: {data.id}\",\n        \"transmissionDuplicate\": \"The {name} torrent already exists! ID: {id}\",\n        \"transmissionError\": \"The link failed to send. Please check if the download server is available.\",\n        \"invalidTorrent\": \"Invalid Torrent\",\n        \"getUserInfoSiteIsEmpty\": \"No site needs to get user information\",\n        \"invalidImage\": \"Invalid Image\",\n        \"noPermission\": \"No permission, please go to user authorization\",\n        \"downloadTaskIsCreated\": \"Download task has been created ({count} total)\",\n        \"downloadTaskIsCompleted\": \"Batch download task has been sent. Success: {success}, Failed: {failed}.\"\n      },\n      \"omnibox\": {\n        \"search\": \"Search for the relevant seed for \\\"{text}\\\" in the ({solutionName}) solution\"\n      },\n      \"user\": {\n        \"notSupported\": \"Not Supported\",\n        \"notLogged\": \"Not logged\",\n        \"needLogin\": \"Not logged\",\n        \"unknown\": \"Unknown\",\n        \"getUserInfoFailed\": \"Failed to get username and id\",\n        \"abortGetUserInfoFailed\": \"Cancel request to get user information failed\"\n      }\n    },\n    \"contentPage\": {\n      \"backgroundServiceIsStoped\": \"Please refresh the page and try again\",\n      \"actionExecutionFailed\": \"{action} Execution failed, maybe background service is unavailable\",\n      \"pluginTitle\": \"PT Plugin Plus - Click to open the configuration page\",\n      \"callbackFailed\": \"{label} An error has occurred. Please try again.\",\n      \"notSupported\": \"This operation is not supported on the current page\",\n      \"buttons\": {\n        \"downloadAll\": \"Download All\",\n        \"downloadAllTip\": \"Download all torrent from the current page to [{name}]\",\n        \"downloadAllTo\": \"Download to\",\n        \"downloadAllToTip\": \"Download all torrent from the current page to the specified server\",\n        \"copyAllToClipboard\": \"Copy Link\",\n        \"copyAllToClipboardTip\": \"Copy download link to clipboard\",\n        \"needAuthorization\": \"Authorize\",\n        \"needAuthorizationTip\": \"Download all torrent file features require permission, click to go to authorization\",\n        \"saveAllTorrent\": \"All torrents\",\n        \"saveAllTorrentTip\": \"Download all torrent files\",\n        \"freeSpaceTip\": \"Default server free space\\n{path}\",\n        \"downloadTo\": \"Download to\",\n        \"downloadToTip\": \"Download the current torrent to the specified server\",\n        \"downloadToDefault\": \"Download\",\n        \"downloadToDefaultTip\": \"Download the current torrent to [{name}]\",\n        \"copyToClipboard\": \"Copy Link\",\n        \"copyToClipboardTip\": \"Copy download link to clipboard\",\n        \"menuDownloadTo\": \"Download to: {server}\",\n        \"sayThanks\": \"Say Thanks\",\n        \"sayThanksTip\": \"Say thanks to the current torrent\",\n        \"cover\": \"Cover\",\n        \"coverTip\": \"View by cover\",\n        \"addToCollection\": \"Collection\",\n        \"removeFromCollection\": \"Remove Collection\"\n      },\n      \"needPasskey\": \"Please set the site key (Passkey) first.\",\n      \"userCanceled\": \"User Canceled\",\n      \"sendingTorrent\": \"Sending a torrent to the server, please wait...\",\n      \"invalidDownloadServer\": \"Invalid Download Server\",\n      \"invalidURL\": \"Invalid URL\",\n      \"dropInvalidURL\": \"Invalid URL, please drag and drop download link\",\n      \"getDownloadURLisUndefined\": \"'getDownloadURL' is undefined\",\n      \"getDownloadURLsisUndefined\": \"'getDownloadURLs' is undefined\",\n      \"getDownloadURLFailed\": \"Failed to get download link\",\n      \"getDownloadURLsFailed\": \"Failed to get the download link, failed to correctly locate the link\",\n      \"exceedSizeConfirm\": \"The current page seed size is {size} has exceeded {exceedSize} {exceedSizeUnit}, is it sent?\",\n      \"exceedSizeCanceled\": \"Oversized has been cancelled\",\n      \"downloadURLsFinished\": \"{count} links have been sent.\",\n      \"downloadURLsTip\": \"Sending: {text}\",\n      \"search\": {\n        \"needLogin\": \"[{siteName}] needs to log in and search again\",\n        \"noTorrents\": \"[{siteName}] did not find the relevant torrent\",\n        \"torrentTableIsEmpty\": \"[{siteName}] is not targeting the torrent list, or there is no related torrent\",\n        \"parseError\": \"[{siteName}] Error getting seed information: {error}\"\n      },\n      \"dragTitle\": \"Press and hold to drag and drop; double click to reset\"\n    },\n    \"downloadClient\": {\n      \"timeout\": \"Access timed out\",\n      \"unknownError\": \"Unknow Error\",\n      \"notFound\": \"The specified address was not found and the server returned error: 404\",\n      \"addURLSuccess\": \"URL has been added into {name}\",\n      \"unsupportedMediaType\": \"Wrong torrent file\",\n      \"serverIsUnavailable\": \"Server unavailable or network error\",\n      \"permissionDenied\": \"Authentication failure\",\n      \"serverConnectionFailed\": \"Server access failed\",\n      \"destinationDenied\": \"The specified directory [{path}] is unavailable or no access permissions\",\n      \"destinationDoesNotExist\": \"The specified directory [{path}] does not exist\",\n      \"fileUploadFailed\": \"File upload failed\",\n      \"maxNumberOfTasksReached\": \"Maximum number of tasks reached\"\n    },\n    \"footer\": {\n      \"replaceLanguageConfirm\": \"The language already exists. Do you need to replace it?\",\n      \"invalidFile\": \"Invalid language file!\"\n    },\n    \"collection\": {\n      \"title\": \"Collection list\",\n      \"add\": \"Add to collection\",\n      \"remove\": \"Remove from collection\",\n      \"addGroup\": \"Create Group\",\n      \"addToGroup\": \"Add To Group\",\n      \"noGroup\": \"<No Group>\",\n      \"changeGroupName\": \"Change the group name\",\n      \"removeGroupConfirm\": \"Are you sure you want to delete this group?{count} collections will be removed from this group.\",\n      \"setMovieId\": \"Enter IMDb ID (e.g. tt123567) or doubanId (e.g. 12345678):\",\n      \"inputGroupName\": \"Enter the group name:\",\n      \"headers\": {\n        \"site\": \"Site\",\n        \"title\": \"Title\",\n        \"size\": \"Size\",\n        \"time\": \"Add time\",\n        \"action\": \"Action\"\n      }\n    },\n    \"searchResultSnapshot\": {\n      \"title\": \"Search Result Snapshot\",\n      \"show\": \"Show Snapshot\",\n      \"removeConfirmTitle\": \"Delete confirmation\",\n      \"removeConfirm\": \"Are you sure you want to delete this Snapshot?\",\n      \"clearConfirm\": \"Are you sure you want to delete all Snapshot?\",\n      \"create\": \"Create Snapshot\",\n      \"createSuccess\": \"Create Snapshot Success\",\n      \"createError\": \"Create Snapshot Error\",\n      \"snapshotTime\": \"Snapshot Time: {time}\",\n      \"headers\": {\n        \"key\": \"Search Key\",\n        \"time\": \"Time\"\n      }\n    },\n    \"keepUploadTask\": {\n      \"title\": \"Reseed Task\",\n      \"keepUpload\": \"Reseed\",\n      \"removeConfirmTitle\": \"Delete confirmation\",\n      \"removeConfirm\": \"Are you sure you want to delete this Task?\",\n      \"clearConfirm\": \"Are you sure you want to delete all Task?\",\n      \"create\": \"Create Reseed Task\",\n      \"createSuccess\": \"Task creation completed\",\n      \"createError\": \"Task creation failed\",\n      \"noItem\": \"No matching seeds\",\n      \"filterSearchResults\": \"Filter search results\",\n      \"savePath\": \"Save Path: \",\n      \"defaultPath\": \"Default Path\",\n      \"setSavePath\": \"Set Save Path\",\n      \"torrentCount\": \"Reseed count\",\n      \"sendBaseTorrent\": \"Send First Torrent\",\n      \"sendOtherTorrents\": \"Send Other Torrents\",\n      \"sendAllTorrents\": \"Send All Torrents\",\n      \"sendSuccess\": \"Send Success\",\n      \"sendError\": \"Send Error\",\n      \"verification\": \"Verification\",\n      \"baseTorrent\": \"Base Torrent\",\n      \"otherTorrent\": \"Other Torrent\",\n      \"size\": \"Size: \",\n      \"fileCount\": \"Files: \",\n      \"sendConfirm\": \"Are you sure you want to send these {count} torrents?\",\n      \"addToKeepUpload\": \"Add to task list\",\n      \"removeFromKeepUpload\": \"Remove from task list\",\n      \"redownload\" : \"Re-download\",\n      \"addToKeepUploadConfirm\": \"Are you sure want add this torrent to task list?\",\n      \"status\": {\n        \"label\": \"Status: \",\n        \"downloading\": \"Downloading\",\n        \"failed\": \"Verification failed\",\n        \"incorrectOrder\": \"Incorrect Order\",\n        \"missingFiles\": \"Missing Files\",\n        \"success\": \"Verify successful\",\n        \"waiting\": \"Waiting for verification\",\n        \"downloadFailed\": \"Download Failed\"\n      },\n      \"headers\": {\n        \"site\": \"Site\",\n        \"title\": \"Title\",\n        \"size\": \"Size\",\n        \"time\": \"Time\"\n      }\n    },\n    \"movieInfoCard\": {\n      \"alias\": \"Alias: \",\n      \"director\": \"Director: \",\n      \"writer\": \"Writer: \",\n      \"cast\": \"Cast: \",\n      \"type\": \"Type: \",\n      \"pubdate\": \"Pubdate: \",\n      \"duration\": \"Duration: \",\n      \"ratings\": {\n        \"douban\": \"Douban {average} ({numRaters})\",\n        \"imdb\": \"IMDb {average} ({numRaters})\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/i18n/zh-CN.json",
    "content": "{\n  \"name\": \"简体中文 Chinese (Simplified)\",\n  \"code\": \"zh-CN\",\n  \"authors\": [\n    \"栽培者\",\n    \"MewX\"\n  ],\n  \"words\": {\n    \"app\": {\n      \"initError\": \"配置信息加载失败，没有获取到系统定义信息，请尝试刷新当前页面\",\n      \"initializing\": \"程序正在准备一些数据，请稍候……\",\n      \"author\": \"栽培者\",\n      \"name\": \"PT 助手 Plus\"\n    },\n    \"common\": {\n      \"debugMode\": \"当前处于调试模式\",\n      \"changeLanguage\": \"切换语言\",\n      \"addLanguage\": \"临时添加新语言\",\n      \"version\": \"版本\",\n      \"systemLog\": \"系统日志\",\n      \"darkMode\": \"反转颜色\",\n      \"haveNewReleases\": \"有更新可用\",\n      \"add\": \"新增\",\n      \"edit\": \"编辑\",\n      \"copy\": \"复制\",\n      \"ok\": \"确认\",\n      \"cancel\": \"取消\",\n      \"remove\": \"删除\",\n      \"clear\": \"清除\",\n      \"removeConfirm\": \"确认要删除这条记录吗？\",\n      \"removeSelectedConfirm\": \"确认要删除这些已选中的 {count} 条记录吗？\",\n      \"removeConfirmTitle\": \"删除确认\",\n      \"clearConfirm\": \"确认要删除所有记录吗？\",\n      \"id\": \"ID\",\n      \"readyToStart\": \"准备开始……\",\n      \"help\": \"帮助\",\n      \"export\": \"导出\",\n      \"import\": \"导入\",\n      \"share\": \"分享\",\n      \"actionConfirm\": \"确认要进行此操作吗？\",\n      \"importFailed\": \"导入失败\",\n      \"importSuccess\": \"已成功导入\",\n      \"all\": \"全部\",\n      \"setDefault\": \"设为默认\",\n      \"cancelDefault\": \"取消默认\",\n      \"search\": \"搜索\",\n      \"color\": \"颜色\",\n      \"orderBy\": \"排序字段\",\n      \"orderMode\": {\n        \"asc\": \"升序\",\n        \"desc\": \"降序\"\n      },\n      \"close\": \"关闭\",\n      \"copyed\": \"已复制\",\n      \"hot\": \"热门\",\n      \"loading\": \"正在加载……\",\n      \"lastUpdate\": \"数据更新于 {time}\",\n      \"refresh\": \"刷新\"\n    },\n    \"topbar\": {\n      \"title\": \"@:(app.name)\",\n      \"navBarTip\": \"点击显示/隐藏导航栏\",\n      \"help\": \"帮助\",\n      \"github\": \"主页\",\n      \"showNewTorrents\": \"获取各站第一页种子\",\n      \"showNewTorrentsTip\": \"根据当前方案，搜索各站的第一页种子\"\n    },\n    \"navigation\": {\n      \"dashboard\": {\n        \"title\": \"概览\",\n        \"userData\": \"我的数据\",\n        \"searchResults\": \"搜索结果\",\n        \"history\": \"下载历史\",\n        \"collection\": \"我的收藏\",\n        \"searchResultSnapshot\": \"搜索结果快照\",\n        \"keepUploadTask\": \"辅种任务\"\n      },\n      \"settings\": {\n        \"title\": \"参数设置\",\n        \"base\": \"常规设置\",\n        \"sites\": \"站点设置\",\n        \"downloadClients\": \"下载服务器\",\n        \"downloadPaths\": \"下载目录设置\",\n        \"searchSolution\": \"搜索方案\",\n        \"backup\": \"参数备份与恢复\",\n        \"permissions\": \"权限设置\"\n      },\n      \"thanks\": {\n        \"title\": \"鸣谢\",\n        \"reference\": \"项目参考与引用\",\n        \"specialThanksTo\": \"特别感谢\"\n      },\n      \"support\": {\n        \"title\": \"支持本项目\",\n        \"bugReport\": \"Bug反馈\",\n        \"donate\": \"捐赠\",\n        \"debugger\": \"调试信息\"\n      }\n    },\n    \"permissions\": {\n      \"title\": \"感谢您选择 PT 助手\",\n      \"subtitle\": \"为了不影响正常使用，请对需要的功能进行授权：\",\n      \"authorize\": \"授权\",\n      \"cancel\": \"我不用了\",\n      \"cancelled\": \"世界如此之大，期待有缘再相会！\",\n      \"details\": {\n        \"allSites\": \"所有网站的访问权限，用于搜索和读取做种数据；\",\n        \"tabs\": \"所有打开页面的读取权限，用于显示助手图标及各项操作；\",\n        \"downloads\": \"下载权限，用于批量下载种子文件\",\n        \"cookies\": \"Cookies操作权限，用于备份和恢复站点的登录状态等内容\"\n      },\n      \"headers\": {\n        \"title\": \"权限描述\",\n        \"enabled\": \"已授权\"\n      },\n      \"request\": {\n        \"default\": \"是否授于该权限？\",\n        \"cookies\": \"是否授于Cookies读写权限？\"\n      }\n    },\n    \"searchBox\": {\n      \"searchTip\": \"输入种子关键字、IMDb编号，按回车进行搜索\",\n      \"default\": \"<默认>\",\n      \"defaultTip\": \"仅搜索已允许的站点\",\n      \"all\": \"<所有站点>\",\n      \"noSearchSolution\": \"暂无方案，请添加\",\n      \"noAllowSearchSites\": \"暂未配置允许搜索的站点，请先配置\",\n      \"searchThisKey\": \"搜索 “{key}”\",\n      \"doubanTip\": \"以上数据来自©豆瓣电影 API v2 查询接口；如不想显示这些结果进行预选，可在“常规设置”中进行关闭\",\n      \"toDouban\": \"在豆瓣进行查看\"\n    },\n    \"donate\": {\n      \"title\": \"感谢您的关注和支持\"\n    },\n    \"history\": {\n      \"title\": \"下载历史\",\n      \"remove\": \"@:(common.remove)\",\n      \"clear\": \"@:(common.clear)\",\n      \"removeConfirm\": \"确认要删除这条记录吗？\",\n      \"removeConfirmTitle\": \"删除确认\",\n      \"clearConfirm\": \"确认要删除所有下载记录吗？\",\n      \"ok\": \"@:(common.ok)\",\n      \"cancel\": \"@:(common.cancel)\",\n      \"download\": \"重新下载\",\n      \"fail\": \"失败\",\n      \"success\": \"成功\",\n      \"unknown\": \"N/A\",\n      \"defaultPath\": \"默认目录\",\n      \"seedingTorrent\": \"正在发送种子到下载服务器……\",\n      \"headers\": {\n        \"site\": \"来源\",\n        \"title\": \"标题\",\n        \"status\": \"状态\",\n        \"time\": \"下载时间\",\n        \"action\": \"操作\"\n      }\n    },\n    \"home\": {\n      \"title\": \"我的数据\",\n      \"getInfos\": \"刷新我的数据\",\n      \"cancelRequest\": \"取消请求\",\n      \"requesting\": \"正在请求\",\n      \"siteName\": \"网站名称\",\n      \"userName\": \"用户名称\",\n      \"userLevel\": \"用户等级\",\n      \"levelRequirements\": \"等级要求\",\n      \"seedingPoints\": \"保种积分\",\n      \"showHnR\": \"H&R\",\n      \"selectColumns\": \"过滤列\",\n      \"week\": \"时间显示为周数\",\n      \"timeline\": \"时间轴\",\n      \"settings\": \"参数\",\n      \"statistic\": \"数据图表\",\n      \"newMessage\": \"新消息\",\n      \"startGetting\": \"准备开始获取个人数据\",\n      \"gettingForSite\": \"正在获取 {siteName} 个人数据\",\n      \"requestCompleted\": \"请求完成，耗时：{second} 秒。\",\n      \"getUserInfoError\": \"发生错误\",\n      \"getUserInfoAbort\": \"{siteName} 获取用户资料请求已取消\",\n      \"getUserInfoAbortError\": \"{siteName} 获取用户资料请求取消失败\",\n      \"offline\":\"已离线\",\n      \"headers\": {\n        \"date\": \"日期\",\n        \"site\": \"站点\",\n        \"userName\": \"用户名\",\n        \"levelName\": \"等级\",\n        \"activitiyData\": \"数据量\",\n        \"ratio\": \"分享率\",\n        \"seeding\": \"做种数\",\n        \"seedingSize\": \"做种体积\",\n        \"bonus\": \"魔力值/积分\",\n        \"seedingPoints\": \"做种积分\",\n        \"bonusPerHour\": \"时魔\",\n        \"joinTime\": \"入站时间\",\n        \"lastUpdateTime\": \"数据更新于\",\n        \"status\": \"状态\",\n        \"uploads\": \"发布\",\n        \"trueDownloaded\": \"真实下载\",\n        \"classPoints\": \"等级积分\",\n        \"unsatisfieds\": \"H&R考核中\",\n        \"prewarn\": \"H&R预警\"\n      },\n      \"levelRequirement\": {\n        \"levelRequirements\": \"升级要求\",\n        \"date\": \"日期\",\n        \"site\": \"站点\",\n        \"userName\": \"用户名\",\n        \"levelName\": \"等级\",\n        \"activitiyData\": \"数据量\",\n        \"ratio\": \"分享率\",\n        \"seeding\": \"做种数\",\n        \"seedingSize\": \"做种体积\",\n        \"seedingTime\": \"做种时间\",\n        \"bonus\": \"魔力值\",\n        \"seedingPoints\": \"做种积分\",\n        \"bonusPerHour\": \"时魔\",\n        \"joinTime\": \"入站时间\",\n        \"lastUpdateTime\": \"数据更新于\",\n        \"status\": \"状态\",\n        \"uploaded\": \"上传量\",\n        \"downloaded\":\"下载量\",\n        \"uploads\": \"发布\",\n        \"downloads\": \"完成\",\n        \"trueDownloaded\": \"真实下载\",\n        \"classPoints\": \"等级积分\",\n        \"uniqueGroups\": \"独特分组\",\n        \"perfectFLAC\": \"“完美”FLAC\",\n        \"alternative\": \"择一\"\n      },\n      \"tip\": \"N/A 表示暂不支持\",\n      \"nodata\": \"您还没有添加站点，请前往『站点设置』添加站点。\"\n    },\n    \"systemLog\": {\n      \"title\": \"系统日志\",\n      \"save\": \"保存日志\",\n      \"headers\": {\n        \"module\": \"模块\",\n        \"event\": \"事件\",\n        \"time\": \"时间\",\n        \"msg\": \"描述\",\n        \"action\": \"操作\"\n      }\n    },\n    \"reference\": {\n      \"title\": \"本项目使用或参考了以下项目\",\n      \"thanks\": \"PT 助手的诞生是建立在这些项目基础之上，在此感谢所有项目的参与人员，感谢他们的付出！\",\n      \"headers\": {\n        \"name\": \"名称\",\n        \"ver\": \"版本\",\n        \"url\": \"网址\"\n      }\n    },\n    \"team\": {\n      \"title\": \"列表按字母排序\",\n      \"contributors\": \"协作者们:\",\n      \"issues\": \"反馈者们:\"\n    },\n    \"timeline\": {\n      \"share\": \"生成分享图片\",\n      \"siteName\": \"网站名称\",\n      \"blurSiteIcon\": \"模糊站点图标\",\n      \"userName\": \"用户名称\",\n      \"userId\": \"用户UID\",\n      \"userLevel\": \"用户等级\",\n      \"showSites\": \"展示站点\",\n      \"close\": \"关闭\",\n      \"shareMessage\": \"这些年走过的路\",\n      \"time\": {\n        \"year\": \"年\",\n        \"month\": \"月\",\n        \"day\": \"日\",\n        \"hour\": \"时\",\n        \"mins\": \"分\",\n        \"week\": \"周\",\n        \"ago\": \"前\",\n        \"lessThanAWeek\": \"不满一周\"\n      },\n      \"total\": {\n        \"uploaded\": \"上传总量：\",\n        \"downloaded\": \"下载总量：\",\n        \"seedingSize\": \"做种总量：\",\n        \"ratio\": \"总分享率：\",\n        \"years\": \"．．Ｐ龄：≈ {year} 年\"\n      },\n      \"updateat\": \"数据更新于：\",\n      \"user\": {\n        \"uploaded\": \"上传量：\",\n        \"downloaded\": \"下载量：\",\n        \"seedingSize\": \"做种量：\",\n        \"ratio\": \"分享率：\",\n        \"bonus\": \"积分值：\",\n        \"bonusPerHour\": \"时　魔：\"\n      },\n      \"inputDisplayName\": \"请输入需要显示的名称：\",\n      \"inputShareMessage\": \"请输入需要显示的信息：\"\n    },\n    \"searchTorrent\": {\n      \"title\": \"搜索结果\",\n      \"download\": \"下载\",\n      \"downloadFailed\": \"重下失败\",\n      \"sendToClient\": \"推送\",\n      \"sendToClientTip\": \"推送到下载服务器\",\n      \"save\": \"保存\",\n      \"saveTip\": \"下载种子文件到本地\",\n      \"collection\": \"收藏\",\n      \"searching\": \"正在搜索中，请稍候……\",\n      \"cancelSearch\": \"取消搜索\",\n      \"showCheckbox\": \"多选\",\n      \"noTag\": \"无标签\",\n      \"allSites\": \"全部站点\",\n      \"multiDownloadConfirm\": \"当前下载的种子数量超过一个，浏览器可能会多次提示保存，是否继续？\",\n      \"copyToClipboard\": \"复制\",\n      \"copyToClipboardTip\": \"复制下载链接到剪切板\",\n      \"reSearch\": \"重新再搜索\",\n      \"showCategory\": \"分类\",\n      \"filterSearchResults\": \"过滤搜索结果\",\n      \"noResultsSites\": \"无结果站点：\",\n      \"failedSites\": \"失败站点：\",\n      \"reSearchFailedSites\": \"重试失败的站点\",\n      \"failUrl\": \"链接无效\",\n      \"headers\": {\n        \"site\": \"站点\",\n        \"title\": \"标题\",\n        \"category\": \"分类/入口\",\n        \"size\": \"大小\",\n        \"seeders\": \"上传\",\n        \"leechers\": \"下载\",\n        \"completed\": \"完成\",\n        \"comments\": \"评论\",\n        \"time\": \"发布于(≈)\",\n        \"action\": \"操作\"\n      },\n      \"optionsIsMissing\": \"系统参数丢失\",\n      \"sitesIsMissing\": \"请先设置站点\",\n      \"optionsIsMissingErrorMsg\": \"系统参数丢失，多次尝试等待后无效，请重新打开配置页再试\",\n      \"doubanIdConversionFailed\": \"豆瓣ID转换失败\",\n      \"skipSites\": \"暂不支持搜索的站点：\",\n      \"noAllowSearchSites\": \"您还没有配置允许搜索的站点，请先前往【站点设置】进行配置\",\n      \"searchStartMsg\": \"准备开始搜索，共需搜索 {count} 个站点\",\n      \"siteIsSearching\": \"正在搜索 [{siteName}]\",\n      \"siteIsSearchDone\": \"{siteName} 搜索完成，共有 {count} 条结果\",\n      \"siteSearchAbort\": \"{host} 搜索请求已取消\",\n      \"siteSearchAbortError\": \"{host} 搜索请求取消失败\",\n      \"siteSearchTimeout\": \"{host} 连接超时\",\n      \"siteSearchError\": \"{host} 发生网络或其他错误\",\n      \"notLogged\": \"未登录\",\n      \"searchFinished\": \"搜索完成，共找到 {count} 条结果，耗时：{second} 秒。\",\n      \"searchProgress\": \"已接收 {count} 条结果，搜索仍在进行……\",\n      \"seedingTorrent\": \"正在发送种子到下载服务器……\",\n      \"userCanceled\": \"用户已取消\",\n      \"sendTorrentToClient\": \"发送种子到下载服务器\",\n      \"sendTorrentToClientSuccess\": \"发送种子到下载服务器成功\",\n      \"sendTorrentToClientError\": \"发送种子到下载服务器失败\",\n      \"downloadSelectedError\": \"下载种子文件失败：{name}\",\n      \"copyLinkToClipboardSuccess\": \"下载链接已复制到剪切板\",\n      \"copyLinkToClipboardError\": \"复制下载链接失败！\",\n      \"copySelectedToClipboardSuccess\": \"{count} 条下载链接已复制到剪切板\",\n      \"downloadTo\": \"下载到：{path}\",\n      \"noReSearchSites\": \"没有需要重新搜索的站点\",\n      \"doubanIdConverting\": \"正在尝试转换豆瓣编号，请稍候……\",\n      \"invalidDoubanId\": \"无效的豆瓣ID\",\n      \"torrentStatus\": {\n        \"downloading\": \"正在下载\",\n        \"sending\": \"正在做种\",\n        \"completed\": \"已完成，未做种\",\n        \"inactive\": \"未活动\"\n      }\n    },\n    \"settings\": {\n      \"backup\": {\n        \"title\": \"参数备份与恢复\",\n        \"subTitle\": \"注：除非设置加密，否则备份文件为明文，其中可能包含个人信息，请注意保管。\",\n        \"backup\": \"备份\",\n        \"restore\": \"恢复\",\n        \"backupToGoogle\": \"备份到Google\",\n        \"restoreFromGoogle\": \"从Google恢复\",\n        \"restoreConfirm\": \"确认要从备份数据中恢复配置吗？这将覆盖当前设置信息。\",\n        \"restoreSuccess\": \"参数已恢复\",\n        \"restoreError\": \"参数恢复失败！\",\n        \"loadError\": \"配置信息加载失败\",\n        \"backupDone\": \"备份完成\",\n        \"backupError\": \"备份参数失败！\",\n        \"errorMessage\": {\n          \"QUOTA_BYTES_PER_ITEM\": \"要保存的内容大小超出了Google限制（8K）\"\n        },\n        \"clearFromGoogle\": \"清除\",\n        \"clearFromGoogleTip\": \"从Google中清除已备份的参数\",\n        \"clearFromGoogleConfirm\": \"是否要从Google中清除已备份的参数？\",\n        \"clearFromGoogleError\": \"清除失败！\",\n        \"clearFromGoogleSuccess\": \"内容已清除\",\n        \"index\": {\n          \"headers\": {\n            \"name\": \"服务名称\",\n            \"type\": \"类型\",\n            \"lastBackupTime\": \"最近备份时间\",\n            \"action\": \"操作\"\n          }\n        },\n        \"server\": {\n          \"add\": {\n            \"title\": \"添加备份服务器\"\n          },\n          \"edit\": {\n            \"title\": \"编辑备份服务器\"\n          },\n          \"editor\": {\n            \"type\": \"服务器类型\",\n            \"name\": \"服务名称\",\n            \"address\": \"服务器地址\",\n            \"authCode\": \"授权码\",\n            \"applyAuthCode\": \"申请授权码\",\n            \"loginName\": \"登录名\",\n            \"loginPwd\": \"登录密码\",\n            \"digest\": \"启用 Digest 认证\"\n          },\n          \"list\": {\n            \"noData\": \"暂无备份数据\",\n            \"backupToServer\": \"备份到服务器\",\n            \"loadBackupList\": \"历史记录\"\n          },\n          \"getFileListError\": \"获取备份文件失败，请确认网络和备份服务器是否可用\",\n          \"owss\": {\n            \"addressTip\": \"完整的服务器地址，如：http://192.168.1.1:8088/storage\"\n          }\n        },\n        \"restoreAll\": \"恢复所有设置\",\n        \"restoreCollection\": \"仅恢复收藏数据\",\n        \"restoreCookies\": \"仅恢复站点Cookies\",\n        \"restoreSearchResultSnapshot\": \"仅恢复搜索结果快照\",\n        \"restoreKeepUploadTask\": \"仅恢复辅种任务\",\n        \"restoreDownloadHistory\": \"仅恢复下载历史\",\n        \"contentNotExist\": {\n          \"cookies\": \"备份文件中不存在Cookies\",\n          \"collection\": \"备份文件中不存在收藏信息\",\n          \"searchResultSnapshot\": \"备份文件中不存在搜索结果快照\",\n          \"keepUploadTask\": \"备份文件中不存在辅种任务\",\n          \"downloadHistory\": \"备份文件中不存在下载历史\"\n        },\n        \"backupItem\": {\n          \"base\": \"常规设置\",\n          \"userDatas\": \"站点数据\",\n          \"collection\": \"我的收藏\",\n          \"cookies\": \"Cookies\",\n          \"searchResultSnapshot\": \"搜索结果快照\",\n          \"keepUploadTask\": \"辅种任务\",\n          \"downloadHistory\": \"下载历史\"\n        },\n        \"restoreErrorType\": {\n          \"needSecretKey\": \"需要密钥\",\n          \"errorSecretKey\": \"密钥错误\"\n        },\n        \"enterSecretKey\": \"请输入密钥：\",\n        \"restoreCookiesConfirm\": \"备份内容包含 Cookies，是否需要恢复Cookies？此操作可能造成一些意想不到的结果。\"\n      },\n      \"base\": {\n        \"title\": \"常规设置\",\n        \"defaultClient\": \"默认下载服务器\",\n        \"autoUpdate\": \"自动更新官方数据\",\n        \"save\": \"保存\",\n        \"allowSelectionTextSearch\": \"启用页面内容选择搜索\",\n        \"allowDropToSend\": \"启用拖放链接到助手图标时，直接发送链接到下载服务器\",\n        \"clearCache\": \"清除缓存\",\n        \"clearCacheConfirm\": \"确认要清除缓存吗？清除完成后，下次将会从官网中重新下载系统配置信息。\",\n        \"needConfirmWhenExceedSize\": \"当批量下载的种子总体积超过以下大小时需要确认\",\n        \"exceedSize\": \"大小\",\n        \"searchResultRows\": \"搜索时每站点返回结果数量\",\n        \"saveDownloadHistory\": \"启用下载历史，以记录每次一键发送的种子信息\",\n        \"connectClientTimeout\": \"全局超时时间（毫秒，1000毫秒=1秒），作用于连接下载服务器、下载种子文件等操作\",\n        \"noClient\": \"尚未配置下载服务器，请配置下载服务后再选择\",\n        \"cacheIsCleared\": \"缓存已清除，如需立即生效，请重新打开页面\",\n        \"saved\": \"参数已保存\",\n        \"autoRefreshUserData\": \"在浏览器打开的情况下自动刷新用户数据（Beta）\",\n        \"autoRefreshUserDataTip1\": \"每天于\",\n        \"autoRefreshUserDataTip2\": \"自动刷新（如果浏览器在这个时间之后打开，则在浏览器打开时自动刷新）\",\n        \"autoRefreshUserDataTip3\": \"失败后重试\",\n        \"autoRefreshUserDataTip4\": \"次，每次间隔\",\n        \"autoRefreshUserDataTip5\": \"分钟\",\n        \"searchResultOrderBySitePriority\": \"搜索结果点击站点表头时，按站点优先级别排序（保存后需刷新页面后生效）\",\n        \"saveSearchKey\": \"保存搜索关键字\",\n        \"showMoiveInfoCardOnSearch\": \"当以 IMDb 编号搜索时显示电影及评分信息\",\n        \"getMovieInformationBeforeSearching\": \"当输入搜索关键字时，从豆瓣加载相关信息以供预选\",\n        \"maxMovieInformationCount\": \"最多显示条目（1-20）：\",\n        \"searchModeForItem\": \"当点击预选条目时：\",\n        \"showToolbarOnContentPage\": \"启用站点页面助手图标和工具栏（如一键下载等）\",\n        \"lastUpdate\": \"（最后更新于 {time}）\",\n        \"lastUpdateUnknown\": \"（更新时间未知）\",\n        \"lastUpdateFailed\": \"（更新时间获取失败）\",\n        \"autoRefreshUserDataLastUpdate\": \"（最后更新于 {time}）\",\n        \"beforeSearchingItemSearchMode\": {\n          \"id\": \"以ID进行搜索，以获得较精确的内容，但转换ID时需要时间\",\n          \"name\": \"以名称进行模糊搜索，以获得较多的内容\"\n        },\n        \"downloadFailedRetry\": \"当下载失败后进行重试\",\n        \"downloadFailedRetryTip1\": \"重试\",\n        \"downloadFailedRetryTip2\": \"次，每次间隔\",\n        \"downloadFailedRetryTip3\": \"秒（0表示失败后立即重试）\",\n        \"tabs\": {\n          \"base\": \"常规\",\n          \"search\": \"搜索\",\n          \"download\": \"下载\",\n          \"advanced\": \"高级\"\n        },\n        \"apiKey\": {\n          \"omdb\": \"OMDb API Key\",\n          \"douban\": \"豆瓣 API Key\"\n        },\n        \"apiKeyTip\": \"OMDb API Key 用于获取影片的评分信息；\\n豆瓣 API Key 用于获取影片的基本资料，如图片、简介等；\\n助手已内置了一些Key，如出现无法正常获取评分或影片资料，可进行设置；多个Key按回车进行区分。\",\n        \"verifyApiKey\": \"验证 API Key\",\n        \"batchDownloadInterval\": \"批量下载（发送）种子时，每个链接时间间隔（秒）\",\n        \"enableBackgroundDownload\": \"当批量下载（发送）种子时，使用后台任务，完成后再通知我\",\n        \"position\": {\n          \"label\": \"工具栏位于：\",\n          \"left\": \"页面左侧\",\n          \"right\": \"页面右侧\"\n        },\n        \"allowBackupCookies\": \"当进行备份操作时，同时备份已配置站点的 Cookies；（不支持备份到Google）\",\n        \"encryptBackupData\": \"当进行备份时，对备份数据进行加密；（不支持备份到Google）\",\n        \"encryptMode\": \"加密方式：\",\n        \"encryptSecretKey\": \"密钥：\",\n        \"encryptTip\": \"注意：密钥仅保存在当前浏览器，不会备份，请妥善保存；加密后如密钥丢失将无法恢复数据。\",\n        \"createSecretKey\": \"随机生成\",\n        \"allowSaveSnapshot\": \"允许保存搜索结果快照\"\n      },\n      \"downloadClients\": {\n        \"add\": {\n          \"title\": \"新增下载服务器\",\n          \"titleStep1\": \"选择服务器类型\",\n          \"titleStep2\": \"详细配置\",\n          \"validMsg\": \"请选择一个服务器类型\",\n          \"helpMsg\": \"找不到想要的服务器类型？来这里添加吧！\",\n          \"nextStep\": \"下一步\",\n          \"prevStep\": \"上一步\",\n          \"cancel\": \"取消\"\n        },\n        \"edit\": {\n          \"title\": \"编辑下载服务器\",\n          \"ok\": \"确认\",\n          \"cancel\": \"取消\"\n        },\n        \"editor\": {\n          \"name\": \"服务器名称\",\n          \"type\": \"服务器类型\",\n          \"address\": \"服务器地址\",\n          \"addressTip\": \"完整的服务器地址（含端口），如：http://192.168.1.1:5000/\",\n          \"loginName\": \"登录名\",\n          \"loginPwd\": \"登录密码\",\n          \"id\": \"ID\",\n          \"autoStart\": \"发送种子时自动开始下载\",\n          \"tagIMDb\": \"发送种子时自动添加IMDb标签（Beta）\",\n          \"autoCreate\": \"<保存后自动生成>\",\n          \"test\": \"测试服务器是否可连接\",\n          \"testSuccess\": \"服务器可连接\",\n          \"testConnectionError\": \"网络连接错误\",\n          \"testError\": \"服务器连接失败\",\n          \"testUnknownError\": \"未知错误\",\n          \"testOtherError\": \"其他错误，服务器返回的代码为: {code}\",\n          \"testAddressError\": \"服务器地址错误\"\n        },\n        \"index\": {\n          \"title\": \"下载服务器配置\",\n          \"subTitle\": \"在开始使用之前，您至少需要添加一个下载服务器。\",\n          \"add\": \"新增\",\n          \"remove\": \"删除\",\n          \"clear\": \"清除\",\n          \"itemDuplicate\": \"该名称已存在\",\n          \"removeConfirm\": \"确认要删除这个下载服务器吗？\",\n          \"removeConfirmTitle\": \"删除确认\",\n          \"clearConfirm\": \"确认要删除所有下载服务器吗？\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的下载服务器吗？\",\n          \"ok\": \"确认\",\n          \"cancel\": \"取消\",\n          \"headers\": {\n            \"name\": \"名称\",\n            \"type\": \"类型\",\n            \"address\": \"服务器地址\",\n            \"action\": \"操作\"\n          }\n        }\n      },\n      \"downloadPaths\": {\n        \"add\": {\n          \"title\": \"新增下载目录定义\",\n          \"path\": \"目录列表\",\n          \"pathTip\": \"多个目录按回车分隔，第一个为默认目录\",\n          \"ok\": \"确认\",\n          \"cancel\": \"取消\",\n          \"selectSite\": \"选择一个站点（不选表示所有站点都可用）\"\n        },\n        \"edit\": {\n          \"title\": \"编辑下载目录定义\",\n          \"site\": \"站点\"\n        },\n        \"index\": {\n          \"title\": \"下载目录设置\",\n          \"selectedClient\": \"需要设置的服务器\",\n          \"add\": \"新增\",\n          \"remove\": \"删除\",\n          \"clear\": \"清除\",\n          \"itemDuplicate\": \"该名称已存在\",\n          \"removeConfirm\": \"确认要删除这个保存目录吗？\",\n          \"removeConfirmTitle\": \"删除确认\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的保存目录吗？\",\n          \"ok\": \"确认\",\n          \"cancel\": \"取消\",\n          \"notSupport\": \"暂不支持该服务器类型\",\n          \"allSite\": \"<所有站点>\",\n          \"headers\": {\n            \"name\": \"站点\",\n            \"path\": \"保存目录\",\n            \"action\": \"操作\"\n          }\n        },\n        \"keyDescription\": {\n          \"allowKeys\": \"路径中可包含以下关键字：\",\n          \"siteName\": \"将被替换为当前站点名称；\",\n          \"siteHost\": \"将被替换为当前站点域名；\",\n          \"example\": \"例：\",\n          \"dynamic\": \"将会弹出输入框，由用户输入路径中 {key} 部分内容；\",\n          \"dynamicExample\": \"例：/volume1/<...> ，输入：test，结果 /volume1/test\"\n        }\n      },\n      \"searchSolution\": {\n        \"edit\": {\n          \"title\": \"搜索方案定义\"\n        },\n        \"editor\": {\n          \"name\": \"方案名称\",\n          \"range\": \"搜索范围\",\n          \"headers\": {\n            \"name\": \"站点\"\n          }\n        },\n        \"index\": {\n          \"title\": \"搜索方案定义\",\n          \"itemDuplicate\": \"该名称已存在\",\n          \"removeConfirm\": \"确认要删除这个搜索方案吗？\",\n          \"removeConfirmTitle\": \"删除确认\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的搜索方案吗？\",\n          \"help\": \"如何使用？\",\n          \"headers\": {\n            \"name\": \"名称\",\n            \"range\": \"范围\",\n            \"action\": \"操作\"\n          }\n        }\n      },\n      \"sitePlugins\": {\n        \"add\": {\n          \"title\": \"新增插件\"\n        },\n        \"edit\": {\n          \"title\": \"编辑插件\"\n        },\n        \"editor\": {\n          \"defaultClient\": \"默认下载服务器\",\n          \"name\": \"插件名称\",\n          \"pages\": \"适用页面\",\n          \"pagesTip\": \"页面以'/'开始表示网站根目录，输入完成后按回车添加，可添加多个，可以是正则表达式\",\n          \"scripts\": \"附加脚本文件\",\n          \"scriptsTip\": \"/ 表示从资源目录根加载脚本，可添加多个\",\n          \"script\": \"JS脚本\",\n          \"style\": \"附加样式\",\n          \"styles\": \"附加样式文件\",\n          \"stylesTip\": \"/ 表示从资源目录根加载脚本，可添加多个\"\n        },\n        \"index\": {\n          \"title\": \"站点插件配置\",\n          \"importAll\": \"导入所有\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的插件吗？\",\n          \"removeConfirm\": \"确认要删除这个插件吗？\",\n          \"removeTitle\": \"删除确认\",\n          \"headers\": {\n            \"name\": \"名称\",\n            \"pages\": \"适用页面\",\n            \"enable\": \"启用\",\n            \"action\": \"操作\"\n          },\n          \"importNameDuplicate\": \"该名称【{name}】已存在，请重新输入新的名称：\",\n          \"invalidPlugin\": \"无效的插件\"\n        }\n      },\n      \"sites\": {\n        \"add\": {\n          \"title\": \"新增站点\",\n          \"next\": \"下一步\",\n          \"prev\": \"上一步\",\n          \"help\": \"找不到想要的站点？来这里添加吧！\",\n          \"validMsg\": \"请选择一个站点（支持搜索）\",\n          \"custom\": \"自定义\",\n          \"step1\": \"选择站点\",\n          \"step2\": \"确认站点配置\"\n        },\n        \"edit\": {\n          \"title\": \"编辑站点\"\n        },\n        \"editor\": {\n          \"defaultClient\": \"指定下载服务器（如不选择则以基本设置的默认下载服务器为准）\",\n          \"name\": \"站点名称\",\n          \"tags\": \"站点标签\",\n          \"inputTags\": \"标签输入完成后按回车添加，可添加多个\",\n          \"schema\": \"网站架构\",\n          \"description\": \"网站描述\",\n          \"host\": \"域名\",\n          \"url\": \"网站地址\",\n          \"urlTip\": \"网站完整地址，如：https://www.github.com/\",\n          \"passkey\": \"密钥\",\n          \"passkeyTip\": \"密钥仅用于复制下载地址操作，如果不需要用到此功能，请留空\",\n          \"allowSearch\": \"允许搜索\",\n          \"allowGetUserInfo\": \"允许获取用户信息（Beta）\",\n          \"cdn\": \"站点CDN列表\",\n          \"cdnTip\": \"如您使用的网址和系统定义的不同，可在此填写当前使用的网站地址，每行填写一个地址，第一个将做为搜索时使用的地址\",\n          \"priority\": \"优先级\",\n          \"priorityTip\": \"可用于搜索排序\",\n          \"offline\": \"站点已离线（停机/关闭）\",\n          \"timezone\": \"时区\",\n          \"upLoadLimit\": \"上传速度限制\",\n          \"upLoadLimitTip\": \"上传速度限制 (KB/s), 0或不填 不限速\",\n          \"disableMessageCount\": \"关闭消息提醒\"\n        },\n        \"userinfo\": {\n          \"title\": \"用户数据\",\n          \"deleteConfirm\": \"确认删除？这个操作不可恢复，请确认你做好了相应备份。\"\n        },\n        \"index\": {\n          \"importAll\": \"一键导入站点\",\n          \"importAllConfirm\": \"确认要进行导入站点操作吗？此操作会导入已在浏览器上登录过但未添加的站点。\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的站点吗？\",\n          \"removeConfirm\": \"确认要删除这个站点吗？\",\n          \"removeTitle\": \"删除确认\",\n          \"plugins\": \"插件\",\n          \"showUserInfo\": \"用户数据\",\n          \"title\": \"站点设置\",\n          \"subTitle\": \"只有配置过的站点才会显示插件图标及相应的功能；<br/>已离线的站点不再参与搜索和信息获取；\",\n          \"searchEntry\": \"搜索入口\",\n          \"importedText\": \"已成功导入\",\n          \"siteDuplicateText\": \"该站点已存在\",\n          \"headers\": {\n            \"name\": \"名称\",\n            \"tags\": \"标签\",\n            \"allowSearch\": \"允许搜索\",\n            \"allowGetUserInfo\": \"个人信息\",\n            \"offline\": \"已离线\",\n            \"activeURL\": \"URL\",\n            \"action\": \"操作\"\n          },\n          \"importConfig\": \"从文件导入\",\n          \"importConfirm\": \"确认要导入 【{name}】 吗？\",\n          \"importDuplicateConfirm\": \"该站点 【{name}】 已存在，是否需要导入搜索入口和插件？\",\n          \"resetFavicons\": \"重置站点图标缓存\"\n        }\n      },\n      \"siteSearchEntry\": {\n        \"add\": {\n          \"title\": \"新增搜索入口\"\n        },\n        \"edit\": {\n          \"title\": \"编辑搜索入口\"\n        },\n        \"editor\": {\n          \"name\": \"入口名称\",\n          \"entry\": \"入口页面（如果不填写则默认为系统已定义的入口页面）\",\n          \"parseScript\": \"搜索结果解析脚本\",\n          \"parseScriptFile\": \"搜索结果解析脚本文件\",\n          \"resultSelector\": \"种子列表定位选择器\",\n          \"category\": \"资源分类（不选表示所有）\",\n          \"queryString\": \"追加查询参数\"\n        },\n        \"index\": {\n          \"title\": \"站点搜索入口配置\",\n          \"removeSelectedConfirm\": \"确认要删除已选中的搜索入口吗？\",\n          \"removeConfirm\": \"确认要删除这个搜索入口吗？\",\n          \"removeTitle\": \"删除确认\",\n          \"help\": \"如何使用？\",\n          \"headers\": {\n            \"name\": \"名称\",\n            \"categories\": \"已选择分类\",\n            \"enable\": \"启用\",\n            \"action\": \"操作\"\n          }\n        }\n      }\n    },\n    \"statistic\": {\n      \"selectSite\": \"选择需要统计的站点\",\n      \"goback\": \"返回\",\n      \"share\": \"生成分享图片\",\n      \"exportRawData\": \"导出原数据\",\n      \"allSite\": \"<所有站点>\",\n      \"upload\": \"上传\",\n      \"download\": \"下载\",\n      \"bonus\": \"积分\",\n      \"baseDataTitle\": \"[{userName}@{site}] 基本数据\",\n      \"baseDataSubTitle\": \"上传：{uploaded}, 下载：{downloaded}，积分：{bonus}\",\n      \"data\": \"数据\",\n      \"seedingDataTitle\": \"[{userName}@{site}] 保种情况\",\n      \"seedingDataSubTitle\": \"做种体积：{seedingSize}, 数量：{count} 个\",\n      \"seedingSize\": \"做种体积\",\n      \"seedingCount\": \"做种数\",\n      \"barDataTitle\": \"[{userName}@{site}] 上传数据\",\n      \"size\": \"体积\",\n      \"count\": \"数量\",\n      \"total\": \"总和\",\n      \"percent\": \"占比\",\n      \"note\": \"注：<br>1. 图表历史数据来自概览页，手动刷新或自动更新均会记录；<br>2. 助手从 v1.0.1 版开始正式记录每次刷新的数据，每个站每天仅保存一条；\",\n      \"dateRange\": {\n        \"7day\": \"近7天\",\n        \"30day\": \"近30天\",\n        \"60day\": \"近60天\",\n        \"90day\": \"近90天\",\n        \"180day\": \"近180天\",\n        \"all\": \"所有\"\n      }\n    },\n    \"service\": {\n      \"testClientConnectivityFailed\": \"测试客户连接失败[{address}]\",\n      \"contextMenus\": {\n        \"history\": \"查看下载历史\",\n        \"systemLog\": \"查看助手日志\",\n        \"issues\": \"使用问题反馈\",\n        \"searchSelectionText\": \"搜索 \\\"%s\\\" 相关的种子\",\n        \"searchSelectionTextOnThisSite\": \"仅搜索本站 \\\"%s\\\" 相关的种子\",\n        \"searchByIMDb\": \"搜索当前IMDb相关种子\",\n        \"searchByDouban\": \"搜索当前豆瓣链接相关种子\",\n        \"searchByDefault\": \"按默认方式搜索\",\n        \"searchInAllSite\": \"在所有站点中搜索\",\n        \"downloadClientPath\": \"{clientName} -> 指定目录\",\n        \"userCanceled\": \"用户已取消\",\n        \"pluginStatusIsUnknown\": \"插件状态未知，当前操作可能失败，请刷新页面后再试\",\n        \"sendingLink\": \"正在发送链接至下载服务器\",\n        \"downloadClientGetFailed\": \"获取下载服务器失败。\",\n        \"sendTorrentToClientDone\": \"下载链接发送完成。\",\n        \"sendTorrentToClientError\": \"下载链接发送失败！\",\n        \"sendTorrentToDefaultClient\": \"发送到默认服务器 {client.name} -> {- client.address}\",\n        \"sendTorrentToClient\": \"发送到其他服务器\",\n        \"searchInSite\": \"在指定的站点进行搜索\",\n        \"searchInSolution\": \"以指定的方案中搜索\"\n      },\n      \"searcher\": {\n        \"siteSearchConfigEntryIsEmpty\": \"该站点[{site.name}]未配置搜索页面，请先配置\",\n        \"siteSearchEntryIsEmpty\": \"该站点[{site.name}]未指定搜索入口，请先指定一个搜索入口\",\n        \"siteSearchResultParseFailed\": \"[{site.name}]数据解析失败！\",\n        \"siteEvalScriptFailed\": \"[{site.name}]脚本执行出错！\",\n        \"siteSearchResultError\": \"[{site.name}]没有返回预期的数据。\",\n        \"siteAbortSearch\": \"正在取消[{site.host}]的搜索请求\",\n        \"siteAbortSearchError\": \"[{site.host}]取消搜索请求失败！\",\n        \"siteNetworkFailed\": \"[{site.name}]网络请求失败！({msg})\"\n      },\n      \"controller\": {\n        \"invalidAddress\": \"无效的地址\",\n        \"invalidDownloadServer\": \"无效的下载服务器\",\n        \"downloadTimeout\": \"连接下载服务器超时，请检查网络设置或调整服务器超时时间！\",\n        \"downloadFinished\": \"下载服务器{name}处理[{action}]命令完成\",\n        \"downloadError\": \"下载服务器{name}处理[{action}]命令失败！\",\n        \"torrentAdded\": \"{title} 种子已添加完成。\",\n        \"torrentSavePath\": \"<br/>保存至 {path}\",\n        \"transmissionSuccess\": \"{data.name}已发送至 Transmission，编号：{data.id}\",\n        \"transmissionDuplicate\": \"{name}种子已存在！编号：{id}\",\n        \"transmissionError\": \"链接发送失败，请检查下载服务器是否可用。\",\n        \"invalidTorrent\": \"无效的种子文件，请参考 <a href='{link}' target='_blank'>『常见问题』</a>\",\n        \"getUserInfoSiteIsEmpty\": \"没有站点需要获取用户信息\",\n        \"invalidImage\": \"无效的图片文件\",\n        \"noPermission\": \"无权限，请前往用户授权\",\n        \"downloadTaskIsCreated\": \"批量下载任务（共 {count} 条）已创建，任务完成后通知您！\",\n        \"downloadTaskIsCompleted\": \"批量下载任务已发送完成，成功：{success}，失败：{failed}\"\n      },\n      \"omnibox\": {\n        \"search\": \"在「{solutionName}」方案中搜索 “{text}” 的相关种子\"\n      },\n      \"user\": {\n        \"notSupported\": \"暂不支持\",\n        \"notLogged\": \"未登录\",\n        \"needLogin\": \"未登录\",\n        \"unknown\": \"未知错误\",\n        \"getUserInfoFailed\": \"获取用户名和编号失败\",\n        \"abortGetUserInfoFailed\": \"取消获取用户信息请求失败\"\n      }\n    },\n    \"contentPage\": {\n      \"backgroundServiceIsStoped\": \"插件已被禁用或重启过，请刷新页面后再重试\",\n      \"actionExecutionFailed\": \"{action} 执行出错，可能后台服务不可用\",\n      \"pluginTitle\": \"PT助手 - 点击打开配置页\",\n      \"callbackFailed\": \"{label} 发生错误，请重试。\",\n      \"notSupported\": \"当前页面不支持此操作\",\n      \"buttons\": {\n        \"downloadAll\": \"下载所有\",\n        \"downloadAllTip\": \"将当前页面所有种子下载到[{name}]\",\n        \"downloadAllTo\": \"下载到…\",\n        \"downloadAllToTip\": \"将当前页面所有种子下载到指定服务器\",\n        \"copyAllToClipboard\": \"复制链接\",\n        \"copyAllToClipboardTip\": \"复制下载链接到剪切板\",\n        \"needAuthorization\": \"需要授权\",\n        \"needAuthorizationTip\": \"下载所有种子文件功能需要权限，点击前往授权\",\n        \"saveAllTorrent\": \"所有种子\",\n        \"saveAllTorrentTip\": \"下载所有种子文件\",\n        \"freeSpaceTip\": \"默认服务器剩余空间\\n{path}\",\n        \"downloadTo\": \"下载到…\",\n        \"downloadToTip\": \"将当前种子下载到指定的服务器\",\n        \"downloadToDefault\": \"一键下载\",\n        \"downloadToDefaultTip\": \"将当前种子下载到[{name}]\",\n        \"copyToClipboard\": \"复制链接\",\n        \"copyToClipboardTip\": \"复制下载链接到剪切板\",\n        \"menuDownloadTo\": \"下载到：{server}\",\n        \"sayThanks\": \"感谢发布者\",\n        \"sayThanksTip\": \"对当前种子说谢谢\",\n        \"cover\": \"封面模式\",\n        \"coverTip\": \"以封面的方式进行查看\",\n        \"addToCollection\": \"添加到收藏\",\n        \"removeFromCollection\": \"从收藏删除\"\n      },\n      \"needPasskey\": \"请先设置站点密钥（Passkey）。\",\n      \"userCanceled\": \"用户取消操作\",\n      \"sendingTorrent\": \"正在发送下载链接到服务器，请稍候……\",\n      \"invalidDownloadServer\": \"无效的下载服务器\",\n      \"invalidURL\": \"无效的链接\",\n      \"dropInvalidURL\": \"无效的链接，请拖放下载链接\",\n      \"getDownloadURLisUndefined\": \"getDownloadURL 方法未定义\",\n      \"getDownloadURLsisUndefined\": \"getDownloadURLs 方法未定义\",\n      \"getDownloadURLFailed\": \"获取下载链接失败\",\n      \"getDownloadURLsFailed\": \"获取下载链接失败，未能正确定位到链接\",\n      \"exceedSizeConfirm\": \"当前页面种子容量为 {size} 已超过 {exceedSize} {exceedSizeUnit}，是否发送？\",\n      \"exceedSizeCanceled\": \"容量超限，已取消\",\n      \"downloadURLsFinished\": \"{count}条链接已发送完成。\",\n      \"downloadURLsTip\": \"正在发送：{text}\",\n      \"search\": {\n        \"needLogin\": \"[{siteName}]需要登录后再搜索\",\n        \"noTorrents\": \"[{siteName}]没有搜索到相关的种子\",\n        \"torrentTableIsEmpty\": \"[{siteName}]没有定位到种子列表，或没有相关的种子\",\n        \"parseError\": \"[{siteName}]获取种子信息出错: {error}\"\n      },\n      \"dragTitle\": \"按住拖放；双击复位\"\n    },\n    \"downloadClient\": {\n      \"timeout\": \"连接超时\",\n      \"unknownError\": \"未知错误\",\n      \"notFound\": \"指定的地址未找到，服务器返回了 404\",\n      \"addURLSuccess\": \"URL已添加至 {name} 。\",\n      \"unsupportedMediaType\": \"种子文件有误\",\n      \"serverIsUnavailable\": \"服务器不可用或网络错误\",\n      \"permissionDenied\": \"身份验证失败\",\n      \"serverConnectionFailed\": \"服务器连接失败\",\n      \"destinationDenied\": \"指定的目录[{path}]不可用或无权限\",\n      \"destinationDoesNotExist\": \"指定的目录[{path}]不存在\",\n      \"fileUploadFailed\": \"文件上传失败\",\n      \"maxNumberOfTasksReached\": \"达到的最大任务数\"\n    },\n    \"footer\": {\n      \"replaceLanguageConfirm\": \"该语言已存在，是否需要替换？\",\n      \"invalidFile\": \"无效的语言文件！\"\n    },\n    \"collection\": {\n      \"title\": \"收藏列表\",\n      \"add\": \"加入收藏\",\n      \"remove\": \"取消收藏\",\n      \"addGroup\": \"创建分组\",\n      \"addToGroup\": \"添加到分组\",\n      \"noGroup\": \"<未分组>\",\n      \"changeGroupName\": \"修改分组名称\",\n      \"removeGroupConfirm\": \"确认要删除这个分组吗？{count} 个收藏将从这个分组中删除。\",\n      \"setMovieId\": \"设置电影 ID，可以是 IMDB ID（如：tt123456）或豆瓣ID（如：12345678）\",\n      \"inputGroupName\": \"请输入分组名称：\",\n      \"headers\": {\n        \"site\": \"来源\",\n        \"title\": \"标题\",\n        \"size\": \"大小\",\n        \"time\": \"添加时间\",\n        \"action\": \"操作\"\n      }\n    },\n    \"searchResultSnapshot\": {\n      \"title\": \"搜索结果快照\",\n      \"show\": \"查看快照\",\n      \"removeConfirmTitle\": \"删除快照确认\",\n      \"removeConfirm\": \"确认要删除这个快照吗？\",\n      \"clearConfirm\": \"确认要清除所有快照吗？\",\n      \"create\": \"创建快照\",\n      \"createSuccess\": \"快照创建完成\",\n      \"createError\": \"快照创建失败\",\n      \"snapshotTime\": \"快照时间: {time}\",\n      \"headers\": {\n        \"key\": \"搜索内容\",\n        \"time\": \"时间\"\n      }\n    },\n    \"keepUploadTask\": {\n      \"title\": \"辅种任务\",\n      \"keepUpload\": \"辅种\",\n      \"removeConfirmTitle\": \"删除辅种任务确认\",\n      \"removeConfirm\": \"确认要删除这个辅种任务吗？\",\n      \"clearConfirm\": \"确认要清除所有辅种任务吗？\",\n      \"create\": \"生成辅种任务\",\n      \"createSuccess\": \"辅种任务创建完成，请到辅种任务列表中进行后续操作\",\n      \"createError\": \"辅种任务创建失败\",\n      \"noItem\": \"没有符合条件的种子\",\n      \"filterSearchResults\": \"过滤搜索结果\",\n      \"savePath\": \"保存目录：\",\n      \"defaultPath\": \"默认目录\",\n      \"setSavePath\": \"设置保存目录\",\n      \"torrentCount\": \"辅种数量：\",\n      \"sendBaseTorrent\": \"发送基准种子到下载服务器\",\n      \"sendOtherTorrents\": \"发送除基准外的其他种子到下载服务器\",\n      \"sendAllTorrents\": \"发送所有辅种到下载服务器\",\n      \"sendSuccess\": \"任务已发送\",\n      \"sendError\": \"任务发送失败\",\n      \"verification\": \"辅种验证\",\n      \"baseTorrent\": \"基准文件\",\n      \"otherTorrent\": \"辅种文件\",\n      \"size\": \"大小：\",\n      \"fileCount\": \"文件数：\",\n      \"sendConfirm\": \"是否确认要发送这 {count} 个种子？\",\n      \"addToKeepUpload\": \"添加至辅种列表\",\n      \"removeFromKeepUpload\": \"移除种子\",\n      \"redownload\" : \"重新下载\",\n      \"addToKeepUploadConfirm\": \"该种子未能通过初步校验，确认要添加吗？\",\n      \"status\": {\n        \"label\": \"状态：\",\n        \"downloading\": \"正在下载\",\n        \"downloaded\": \"下载完成\",\n        \"failed\": \"校验失败\",\n        \"incorrectOrder\": \"文件顺序错误\",\n        \"missingFiles\": \"缺少文件\",\n        \"success\": \"校验成功\",\n        \"waiting\": \"等待校验\",\n        \"downloadFailed\": \"下载失败\"\n      },\n      \"headers\": {\n        \"site\": \"站点\",\n        \"title\": \"标题\",\n        \"size\": \"大小\",\n        \"time\": \"时间\"\n      }\n    },\n    \"movieInfoCard\": {\n      \"alias\": \"又名：\",\n      \"director\": \"导演：\",\n      \"writer\": \"编剧：\",\n      \"cast\": \"主演：\",\n      \"type\": \"类型：\",\n      \"pubdate\": \"上映：\",\n      \"duration\": \"片长：\",\n      \"ratings\": {\n        \"douban\": \"豆瓣 {average} 共 {numRaters} 人参与评价\",\n        \"imdb\": \"IMDb {average} 共 {numRaters} 人参与评价\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/libs/album/album.js",
    "content": "/**\n * 对数字进行四舍五入操作\n * @param {number} precision \n */\nNumber.prototype.toRound = function (precision) {\n  if (isNaN(precision) || precision == null || precision < 0) {\n    precision = 0;\n  }\n\n  if (window.accounting) {\n    return parseFloat(accounting.toFixed(this, precision));\n  } else {\n    return parseFloat(parseFloat(Math.round(this * Math.pow(10, precision)) / Math.pow(10, precision)).toFixed(precision));\n  }\n};\n// 百分比\nNumber.prototype.toPercent = function (divisor, fix) {\n  if (Math.abs(this) > 0) {\n    if (fix == undefined) {\n      fix = 2;\n    }\n\n    return ((100 - parseFloat((parseFloat(divisor) - parseFloat(this)) / parseFloat(divisor) * 100 + 0.005)).toRound(fix)) + '%';\n  } else {\n    return \"0\";\n  }\n};\n\n/**\n * 相册\n * @author 栽培者\n * @version 0.1.0\n */\nconst _imageCache = [];\nwindow.album = function (options) {\n  return ({\n    options: {\n      // 初始图片列表，每个对象需要有以下属性\n      // url：图片原始地址\n      // thumb：缩略图地址\n      // title：显示标题\n      // key：关键字\n      images: [],\n      url: \"\",\n      fileIds: \"\",\n      active: \"\",\n      resizeRate: 5,\n      listHeight: 160,\n      // 保持缩略图栏\n      keepThumbBar: false,\n      maxSize: 100,\n      minSize: 5,\n      allowContextmenu: false,\n      allowDownload: true,\n      // 是否显示属性栏\n      showPropColumn: false,\n      // 是否显示顶部工具栏\n      showTopBar: true,\n      // 是否显示标签信息栏\n      showLabelBar: true,\n      clickImageToClose: true,\n      // 用户自定义按钮\n      buttons: [],\n      tags: [],\n      defaultTag: \"全部\",\n      activeTag: null,\n      onButtonClick() {},\n      theme: \"\",\n      onClose() {},\n    },\n    // 当前活动对象\n    activeItem: null,\n    // 当前用于显示相册的“窗口”\n    window: null,\n    systemButtons: {\n      left: null,\n      right: null\n    },\n    allowClose: true,\n    shower: null,\n    listBar: null,\n    thumbImagesBar: null,\n    activeImage: null,\n    loadingBar: null,\n    images: [],\n    imageItems: {},\n    listBarOffset: 0,\n    listBarWidth: 0,\n    loadedImageWidth: 0,\n    tags: {},\n    topBarTimer: null,\n    overTopBar: false,\n    theme: \"\",\n    IE: (\"v\" == \"\\v\"),\n    init() {\n      console.log('this\\'s album.js');\n      this.options = $.extend(this.options, options);\n      if (!this.options.parent) {\n        this.options.parent = $(document.body);\n      } else {\n        this.allowClose = false;\n      }\n      if (this.options.theme) {\n        this.theme = `-${this.options.theme}`;\n      }\n      this.parent = this.options.parent;\n      // 主框架\n      this.window = $(\"<div class='album' tabindex='-1'/>\").appendTo(this.parent).focus();\n      // 背景\n      this.background = $(`<div class='background${this.theme}'/>`).appendTo(this.window);\n      // 显示大图片区域\n      this.shower = $(\"<div class='shower'/>\").appendTo(this.window);\n      // 控制图片功能区域\n      this.controlBar = $(\"<div class='controlbar'/>\").appendTo(this.window);\n      // 缩略图显示区域\n      this.listBar = $(`<div class='listbar${this.theme}'/>`).css({\n        height: this.options.listHeight\n      }).appendTo(this.controlBar);\n      // 缩略图列表\n      this.thumbImagesBar = $(\"<div class='thumbimages'/>\").appendTo(this.listBar);\n      // 标签列表\n      this.labelBar = $(\"<div class='labelbar'/>\").appendTo(this.controlBar);\n      // 标签显示容器\n      // this.label = $(\"<span class='label'/>\").appendTo(this.labelBar);\n      this.label = $(\"<a class='label' target='_blank'/>\").appendTo(this.labelBar);\n      // 数量显示容器\n      this.countBar = $(\"<span class='status-count'/>\").appendTo(this.labelBar);\n      // 当前缩放尺寸显示容器\n      this.zoomBar = $(\"<span class='zoomBar'/>\").html(\"100%\").appendTo(this.labelBar);\n      this.systemButtons.zoomIn = $(\"<i class='button-zoom-in' title='放大'/>\").appendTo(this.labelBar);\n      this.systemButtons.zoomOut = $(\"<i class='button-zoom-out' title='缩小'/>\").appendTo(this.labelBar);\n      if (!this.options.showLabelBar) {\n        this.labelBar.hide();\n        this.listBar.css({\n          bottom: 0\n        });\n      }\n\n      // 正在加载显示容器\n      this.loadingBar = $(\"<div class='loading'/>\").hide().appendTo(this.window);\n      $(\"<img/>\").attr({\n        src: \"loading.gif\"\n      }).appendTo(this.loadingBar);\n\n      // 系统按钮\n      this.systemButtons.left = $(\"<div class='button-left' title='← 上一张'/>\").appendTo(this.window).hide();\n      this.systemButtons.right = $(\"<div class='button-right' title='下一张 →'/>\").appendTo(this.window).hide();\n\n      // 标签\n      this.tagsBar = $(\"<div class='album-tags'/>\").hide().appendTo(this.window);\n      // .css({\n      // \tbottom: this.options.listHeight+this.labelBar.height()\n      // })\n\n      this.createTag(this.options.defaultTag, true);\n\n\n      this.listBarOffset = this.parent.width() / 2;\n      this.listBarWidth = this.listBar.width();\n      this.initSystemButtons();\n      this.initCustomButtons();\n      this.initEvents();\n\n      if (this.options.tags.length > 0) {\n        for (let i = 0; i < this.options.tags.length; i++) {\n          this.createTag(this.options.tags[i]);\n        };\n      }\n\n      if (this.options.images.length > 0) {\n        this.initImageList(this.options.images, this.options.active);\n      } else if (this.options.url) {\n        this.load(this.options.url);\n      }\n\n      if (this.options.activeTag) {\n        if (this.tags[this.options.activeTag]) {\n          this.tags[this.options.activeTag].dom.click();\n        }\n      }\n\n      return this;\n    },\n    /**\n     * 初始化系统按钮\n     * @return {[type]} [description]\n     */\n    initSystemButtons() {\n      if (this.options.showTopBar == false) return;\n      // 顶部工具栏\n      this.topBar = $(\"<div class='topbar'/>\").appendTo(this.window).hide();\n      // $(\"<i class='spliter'/>\").appendTo(this.topBar);\n      this.systemButtons.close = $(\"<a class='item close' title='关闭(ESC)'/>\").appendTo(this.topBar);\n    },\n    /**\n     * [initCustomButtons 初始化自定按钮区]\n     * @return {[type]} [description]\n     */\n    initCustomButtons() {\n      if (this.options.buttons.length == 0) return;\n\n      // 用户自定义按钮区\n      this.customButtonBar = $(\"<div class='custom-botton-bar'/>\").appendTo(this.window);\n      const _self = this;\n      engine.create(\"toolbar\", {\n        parent: this.customButtonBar,\n        items: this.options.buttons,\n        buttonType: 1,\n        onitemclick(item) {\n          _self.options.onButtonClick(item, _self.activeItem);\n        }\n      });\n      this.customButtonBar.removeClass(\"toolbar-container\");\n      this.customButtonBar.find(\"table\").css(\"display\", \"inline-block\");\n\n      this.customButtonBar.css({\n        top: this.listBar.offset().top - 30\n      });\n    },\n    /**\n     * 删除当前对象\n     * @return {null}\n     */\n    remove() {\n      if (!this.allowClose) return;\n      clearTimeout(this.topBarTimer);\n      this.window.empty().remove();\n      this.options.onClose && this.options.onClose();\n    },\n    /**\n     * [behind 将当前框架置后]\n     * @return {[type]} [description]\n     */\n    behind(zIndex) {\n      this.isBehind = true;\n      if (!zIndex) {\n        zIndex = 0;\n      }\n\n      this.zIndex = this.window.css(\"zIndex\");\n      this.window.css(\"zIndex\", zIndex);\n    },\n    /**\n     * 置前\n     * @return {[type]} [description]\n     */\n    bringToFront() {\n      if (!this.isBehind) return;\n\n      this.isBehind = false;\n      this.window.css(\"zIndex\", this.zIndex);\n    },\n    /**\n     * 初始化事件\n     * @return {self} \n     */\n    initEvents(self = this) {\n      // 鼠标滚轮放大缩小\n      // DOMMouseScroll 为 FF\n      this.shower.on(\"mousewheel DOMMouseScroll\", event => {\n        // $.log(\"mousewheel\", event);\n        const v = (event.type == \"mousewheel\" ? event.originalEvent.wheelDelta : event.originalEvent.detail);\n        if (v < 0) {\n          self.resize(-self.options.resizeRate);\n        } else {\n          self.resize(self.options.resizeRate);\n        }\n        event.preventDefault();\n        event.stopPropagation();\n      });\n\n      // 键盘事件捕捉\n      this.window.on(\"keydown\", event => {\n        // $.log(\"keydown\", event);\n        // event.preventDefault();\n        switch (event.keyCode) {\n          // 向左\n          case 37:\n            // Page up\n          case 33:\n            self.gotoImage(\"prev\");\n            event.preventDefault();\n            event.stopPropagation();\n            break;\n\n            // 向右\n          case 39:\n            // Page down\n          case 34:\n            self.gotoImage(\"next\");\n            event.preventDefault();\n            event.stopPropagation();\n            break;\n\n            // 向上\n          case 38:\n            self.showThumbsBar();\n            event.preventDefault();\n            event.stopPropagation();\n            break;\n\n            // 向下\n          case 40:\n            self.hideThumbsBar();\n            event.preventDefault();\n            event.stopPropagation();\n            break;\n\n            // ESC\n          case 27:\n            self.remove();\n            event.preventDefault();\n            event.stopPropagation();\n            break;\n\n            // Enter\n          case 13:\n            if (self.activeItem && self.activeItem.link) {\n              self.label.click();\n            }\n            // event.preventDefault();\n            event.stopPropagation();\n            break;\n        }\n      }).on(\"mousemove\", () => {\n        self.showTopBar();\n      });\n\n      if (!this.options.keepThumbBar) {\n        // 鼠标离开和进入图片列表时\n        this.controlBar.on(\"mouseleave\", () => {\n          if(self.hideThumbsBarTimer)\n            clearTimeout(self.hideThumbsBarTimer);\n          // 隐藏图片列表\n          self.hideThumbsBarTimer = setTimeout(() => {\n            self.hideThumbsBar();\n          }, 500);\n        }).on(\"mouseenter\", () => {\n          if(self.hideThumbsBarTimer)\n            clearTimeout(self.hideThumbsBarTimer);\n          self.showThumbsBar();\n          // 鼠标滚轮上一张、下一张\n        }).on(\"mousewheel DOMMouseScroll\", event => {\n          // $.log(\"mousewheel\", event);\n          const v = (event.type == \"mousewheel\" ? event.originalEvent.wheelDelta : event.originalEvent.detail);\n          if (v < 0) {\n            self.gotoImage(\"next\");\n          } else {\n            self.gotoImage(\"prev\");\n          }\n\n          event.preventDefault();\n          event.stopPropagation();\n        });\n\n        // 隐藏图片列表\n        self.hideThumbsBarTimer = setTimeout(() => {\n          self.hideThumbsBar();\n        }, 1000);\n\n      } else {\n        // 鼠标离开和进入图片列表时\n        this.controlBar.on(\"mouseenter\", () => {\n          self.showThumbsBar();\n          // 鼠标滚轮上一张、下一张\n        }).on(\"mousewheel DOMMouseScroll\", event => {\n          // $.log(\"mousewheel\", event);\n          const v = (event.type == \"mousewheel\" ? event.originalEvent.wheelDelta : event.originalEvent.detail);\n          if (v < 0) {\n            self.gotoImage(\"next\");\n          } else {\n            self.gotoImage(\"prev\");\n          }\n\n          event.preventDefault();\n          event.stopPropagation();\n        });\n      }\n\n      // 上一张\n      this.systemButtons.left.click(() => {\n        self.gotoImage(\"prev\");\n      }).on(\"mouseleave\", function () {\n        $(this).animate({\n          opacity: .5\n        });\n      }).on(\"mouseenter\", function () {\n        $(this).animate({\n          opacity: 1\n        });\n      });\n\n      // 下一张\n      this.systemButtons.right.click(() => {\n        self.gotoImage(\"next\");\n      }).on(\"mouseleave\", function () {\n        $(this).animate({\n          opacity: .5\n        });\n      }).on(\"mouseenter\", function () {\n        $(this).animate({\n          opacity: 1\n        });\n      });\n\n      // 标签栏\n      this.tagsBar.on(\"mouseleave\", function () {\n        $(this).animate({\n          opacity: .5\n        });\n      }).on(\"mouseenter\", function () {\n        $(this).animate({\n          opacity: 1\n        });\n      }).animate({\n        opacity: .5\n      });\n\n      // 放大\n      this.systemButtons.zoomIn.click(() => {\n        self.resize(self.options.resizeRate);\n      });\n\n      // 缩小\n      this.systemButtons.zoomOut.click(() => {\n        self.resize(-self.options.resizeRate);\n      });\n\n      if (this.options.showTopBar) {\n        // 关闭事件\n        this.systemButtons.close.click(() => {\n          self.remove();\n        });\n\n        this.topBar.on(\"mouseenter mousemove\", function () {\n          self.overTopBar = true;\n          $(this).addClass(\"over\");\n        }).on(\"mouseleave\", function () {\n          self.overTopBar = false;\n          $(this).removeClass(\"over\");\n        });\n      }\n      return this;\n    },\n    /**\n     * 显示顶部菜单栏\n     * @param  {[type]} self [description]\n     * @return {[type]}      [description]\n     */\n    showTopBar(self) {\n      if (this.options.showTopBar == false) {\n        return;\n      }\n      self = self || this;\n      clearTimeout(this.topBarTimer);\n      this.topBarTimer = null;\n      this.topBar.fadeIn();\n      this.topBarTimer = setTimeout(() => {\n        if (!self.overTopBar)\n          self.topBar.fadeOut();\n      }, 2000);\n    },\n    hideThumbsBar() {\n      const _self = this;\n      this.listBar.stop().animate({\n        height: 30,\n        opacity: .2\n      }, () => {\n        if (!_self.customButtonBar) return;\n        _self.customButtonBar.css({\n          top: _self.listBar.offset().top - 30\n        });\n      });\n\n    },\n    showThumbsBar() {\n      const _self = this;\n      this.listBar.stop().animate({\n        height: this.options.listHeight,\n        opacity: .8\n      }, () => {\n        if (!_self.customButtonBar) return;\n        _self.customButtonBar.css({\n          top: _self.listBar.offset().top - 30\n        });\n      });\n    },\n    /**\n     * 加载指定的地址\n     * @param {string|object} 当指定为一个字符串时，表示链接地址\n     * @return {self}\n     */\n    load() {\n      const self = this;\n      let options = {\n        url: \"\",\n        type: \"POST\",\n        data: null,\n        active: \"\"\n      };\n\n      if (arguments.length == 2) {\n        options.url = arguments[0];\n        options.data = arguments[1];\n      } else if (arguments.length == 1) {\n        if (typeof (arguments[0]) == \"string\") {\n          options.url = arguments[0];\n        } else {\n          options = $.extend(options, arguments[0]);\n        }\n      }\n\n      $.ajax({\n        url: options.url,\n        type: options.type,\n        data: options.data,\n        success(data) {\n          const result = data.result;\n          if (!result) {\n            alert(\"错误\");\n            return;\n          }\n\n          if (result.length == 0) {\n            // alert(\"暂无资料\");\n            return false;\n          }\n          self.initImageList(result, options.active);\n        }\n      });\n      return this;\n    },\n    /**\n     * 添加明细列表\n     * @param {[type]} item [description]\n     */\n    addItem(options, isTagClick) {\n      const self = this;\n      let item = {\n        rate: 100,\n        index: this.images.length,\n        albumObject: this,\n        tags: \"\",\n        fileId: \"\"\n      };\n      if (typeof (options) == \"string\") {\n        item.url = options;\n      } else {\n        delete options.index;\n        options.isloaded = false;\n        item = $.extend(item, options);\n      }\n\n      if (!item.key)\n        item.key = item.url;\n\n      if (!item.thumb) {\n        item.thumb = item.url;\n      }\n\n      if (!isTagClick) {\n        this.tags[this.options.defaultTag].items.push(item);\n        this.tags[this.options.defaultTag].dom.html(`${this.options.defaultTag} (${this.tags[this.options.defaultTag].items.length})`);\n\n        // 是否有标签\n        if (item.tags) {\n          this.tagsBar.show();\n          this.addTag(item);\n        }\n      }\n\n      this.loadImage(item.thumb, img => {\n        self.loadedImageWidth += img.width + 10;\n        if (self.loadedImageWidth >= self.listBarWidth) {\n          self.listBarWidth += img.width + 10;\n          self.listBar.width(self.listBarWidth);\n        }\n      });\n\n      item.image = $(\"<img class='album-item'/>\").attr({\n        src: item.thumb,\n        key: item.key,\n        title: item.title,\n        tags: (Array.isArray(item.tags) ? item.tags.join(\",\") : item.tags)\n      }).css({\n        \"max-height\": (this.options.listHeight - 10)\n      }).click(function () {\n        self.setActive(this.getAttribute(\"key\"));\n      }).appendTo(this.thumbImagesBar);\n\n      this.images.push(item);\n      this.imageItems[item.key] = item;\n    },\n    /**\n     * [addTag 添加标签]\n     * @param {[type]} item [description]\n     */\n    addTag(item) {\n      let texts = [];\n      if (typeof (item.tags) == \"string\") {\n        texts.push(item.tags);\n      } else if (Array.isArray(item.tags)) {\n        texts = item.tags;\n      } else {\n        return;\n      }\n\n      for (const text of texts) {\n        let tag = this.tags[text];\n        if (!tag) {\n          tag = this.createTag(text);\n        }\n\n        tag.items.push(item);\n        tag.dom.html(`${text} (${tag.items.length})`);\n      }\n    },\n    // \n    createTag(tag, setActive) {\n      const _self = this;\n      const result = {\n        name: tag,\n        items: [],\n        dom: $(\"<a class='album-tag'/>\").attr({\n          href: \"javascript:void(0);\",\n          tag\n        }).html(tag).click(function () {\n          if (_self.lastActiveTag) {\n            _self.lastActiveTag.removeClass(\"album-active\");\n          }\n          const tag = this.getAttribute(\"tag\");\n          $(this).addClass(\"album-active\");\n          _self.initImageList(_self.tags[tag].items, null, true);\n          // if (tag==\"全部\")\n          // {\n          // \t_self.thumbImagesBar.find(\".album-item\").show();\n          // }\n          // else\n          // {\n          // \t_self.thumbImagesBar.find(\".album-item\").hide();\n          // \t_self.thumbImagesBar.find(\".album-item[tags*='\"+this.getAttribute(\"tag\")+\"']\").show();\n          // }\n\n          _self.lastActiveTag = $(this);\n\n        }).appendTo(this.tagsBar)\n      };\n\n      if (setActive) {\n        result.dom.addClass(\"album-active\");\n        this.lastActiveTag = result.dom;\n      }\n\n      this.tags[tag] = result;\n      return result;\n    },\n    /**\n     * 初始化小图片列表\n     * @param {string} active 当前活动的键值\n     * @return {self} \n     */\n    initImageList(images, active, isTags) {\n      this.thumbImagesBar.empty();\n      images = images || this.images;\n      this.images = [];\n      this.imageItems = {};\n      this.activeItem = null;\n      if (images.length == 0) return;\n\n      for (let i = 0; i < images.length; i++) {\n        this.addItem(images[i], isTags);\n      };\n\n      if (active) {\n        this.setActive(active, true);\n      } else {\n        this.setActive(this.images[0].key, true);\n      }\n\n      if (images.length == 1) {\n        this.listBar.hide();\n      }\n\n      return this;\n    },\n    /**\n     * 清除图片列表\n     */\n    clear() {\n      this.thumbImagesBar.empty();\n      this.images = [];\n      this.imageItems = {};\n      this.activeItem = null;\n      this.activeImage.hide();\n    },\n    /**\n     * 设置当前活动对象\n     * @param {[type]} key [description]\n     */\n    setActive(key, resize) {\n      this.bringToFront();\n      const item = this.imageItems[key];\n      if (!item) return;\n\n      const self = this;\n      if (this.activeItem) {\n        if (this.activeItem.key == key)\n          return;\n\n        this.activeItem.rate = 100;\n        this.activeItem.image.removeClass(\"active\");\n      }\n\n      if (!this.activeImage) {\n        this.activeImage = $(\"<img class='activeImage'/>\").css({\n          width: 100,\n          position: \"absolute\"\n          // }).draggable({\n          //   cursor: \"move\"\n        }).appendTo(this.shower);\n\n        if (this.options.clickImageToClose) {\n          this.activeImage.click(() => {\n            self.remove();\n          });\n        }\n\n        if (!this.options.allowContextmenu) {\n          this.activeImage.on(\"contextmenu\", event => {\n            event.preventDefault();\n            event.stopPropagation();\n          });\n        }\n      }\n\n      item.image.addClass(\"active\");\n      this.label.html(item.title);\n      if (item.link) {\n        this.label.attr(\"href\", item.link);\n      } else {\n        this.label.removeAttr(\"href\");\n      }\n      this.countBar.html(`${item.index+1}/${this.images.length}`);\n      this.activeItem = item;\n\n      // 设置缩略图列表位置\n      const left = this.listBarOffset - item.image.position().left - item.image.width() / 2;\n\n      // $.log(this.listBarOffset, item.image.position());\n      this.thumbImagesBar.css({\n        left\n      });\n\n      if (!item.isloaded && item.url != item.thumb) {\n        this.loadingBar.show();\n        this.activeImage.hide();\n\n        // 预加载图片\n        this.loadImage(item.url, img => {\n          self.loadingBar.fadeOut();\n          self.activeItem.isloaded = true;\n          self.activeItem.width = img.width;\n          self.activeItem.height = img.height;\n          self.setActiveImage(resize);\n        });\n        // var img = new Image();\n        // // 预加载图片\n        // img.src = item.url;\n        // img.onload = function() {\n\n        // };\n      } else if (item.url == item.thumb) {\n        const _img = new Image();\n        _img.onload = function () {\n          self.loadingBar.fadeOut();\n          self.activeItem.isloaded = true;\n          self.activeItem.width = this.width;\n          self.activeItem.height = this.height;\n          self.setActiveImage(resize);\n        };\n        _img.src = item.url;\n\n        // delete _img;\n      } else {\n        this.setActiveImage(resize);\n      }\n\n    },\n    /**\n     * 设置当前活动的图像\n     * @param {[type]} resize [description]\n     */\n    _setActiveImage(resize) {\n      const item = this.activeItem;\n\n      this.activeImage.hide().attr({\n        src: item.url,\n        fileId: item.fileId\n      });\n\n      // if (!resize) \n      // {\n      // \tthis.activeImage.show();\n      // \treturn;\n      // }\n\n      const parentSize = {\n        width: this.window.width(),\n        height: this.window.height()\n      };\n\n      if (this.options.keepThumbBar) {\n        parentSize.height -= this.options.listHeight;\n      }\n\n      const rateWidth = parseInt(parentSize.width.toPercent(item.width));\n      const rateHeight = parseInt(parentSize.height.toPercent(item.height));\n      let rate = Math.min(rateWidth, rateHeight);\n\n      // $.log(rateWidth, rateHeight, item.width, item.height, parentSize.width, parentSize.height);\n\n      if (rate > this.options.maxSize) {\n        rate = this.options.maxSize;\n      }\n\n      this.activeItem.rate = rate;\n      this.setSize();\n\n      this.systemButtons.left.show();\n      this.systemButtons.right.show();\n\n      if (this.images.length > 1) {\n        if (item.index == 0) {\n          this.systemButtons.left.hide();\n        } else if (item.index == this.images.length - 1) {\n          this.systemButtons.right.hide();\n        }\n      } else {\n        this.systemButtons.left.hide();\n        this.systemButtons.right.hide();\n      }\n    },\n    setActiveImage(resize, _self = this) {\n      if (this.setActiveImageTimer) {\n        clearTimeout(this.setActiveImageTimer);\n      }\n      this.setActiveImageTimer = setTimeout(() => {\n        _self._setActiveImage(resize);\n      }, 260);\n    },\n    /**\n     * 重设当前图片大小\n     * @param  {integer} rate 倍数，正数为放大，负数为缩小\n     * @return {[type]}      [description]\n     */\n    resize(rate) {\n      const min = this.options.minSize;\n      const max = this.options.maxSize;\n\n      if (!this.activeItem.rate) {\n        this.activeItem.rate = 100;\n      }\n      this.activeItem.rate += rate;\n\n      if (this.activeItem.rate < min) {\n        this.activeItem.rate = min;\n      } else if (this.activeItem.rate > max) {\n        this.activeItem.rate = max;\n      }\n\n      if (this.timer) {\n        clearTimeout(this.timer);\n      }\n      const self = this;\n      this.timer = setTimeout(() => {\n        self.setSize();\n      }, 50);\n    },\n    setSize() {\n      const width = this.activeItem.width * (this.activeItem.rate / 100);\n      const height = this.activeItem.height * (this.activeItem.rate / 100);\n      const left = this.listBarOffset - width / 2;\n\n      // if (width<this.window.width())\n      // {\n      // \tleft = this.listBarOffset - width/2;\n      // }\n\n      this.activeImage.animate({\n        width,\n        top: 0,\n        left,\n        marginLeft: 0,\n        marginTop: 0\n      }, 50).show();\n\n      this.zoomBar.html(`${this.activeItem.rate}%`);\n    },\n    /**\n     * 跳转至指定的图片\n     * @param  {[type]} action [description]\n     * @return {[type]}        [description]\n     */\n    gotoImage(action, _self) {\n      if (typeof (action) == \"number\") {\n        var item = this.image[action];\n        if (item) {\n          this.setActive(item.key);\n        }\n        return;\n      }\n      let index = this.activeItem.index;\n      switch (action) {\n        case \"next\":\n          index++;\n          break;\n\n        case \"prev\":\n          index--;\n          break;\n      }\n      if (index < 0) {\n        index = this.images.length - 1;\n      } else if (index >= this.images.length) {\n        index = 0;\n      }\n\n      var item = this.images[index];\n      if (item) {\n        this.setActive(item.key);\n      }\n    },\n    /**\n     * 加载图片\n     * @param  {[type]}   url      [description]\n     * @param  {Function} callback [description]\n     * @param  {[type]}   reload   [description]\n     * @return {[type]}            [description]\n     */\n    loadImage(url, callback, reload) {\n      const cache = _imageCache[url];\n      if (cache && !reload && cache.width > 0) {\n        if (callback) {\n          callback(cache);\n        }\n        return cache;\n      } else {\n        var _img = new Image();\n        // _img.src = url + \"?__t__\" + Math.random();\n        _img.src = url;\n        if (callback) {\n          _img.onload = function () {\n            callback(this);\n          }\n        }\n      }\n      _imageCache[url] = _img;\n      return _img;\n    }\n  }).init();\n};\n"
  },
  {
    "path": "resource/libs/album/style.css",
    "content": ".album {\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tz-index: 30000;\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tleft: 0px;\r\n\tbottom: 0px;\r\n\tright: 0px;\r\n\toverflow: hidden;\r\n}\r\n\r\n.album .background {\r\n\tfilter: alpha(opacity=50);\r\n\topacity: 0.5;\r\n\tbackground-color: #ccc;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tleft: 0px;\r\n\tbottom: 0px;\r\n\tright: 0px;\r\n}\r\n\r\n.album .background-black {\r\n\tbackground-color: #333;\r\n\twidth: 100%;\r\n\theight: 100%;\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tleft: 0px;\r\n\tbottom: 0px;\r\n\tright: 0px;\r\n}\r\n\r\n.album .shower {\r\n\tposition: absolute;\r\n\ttop: 1px;\r\n\tbottom: 190px;\r\n\twidth: 100%;\r\n\ttext-align: center;\r\n}\r\n\r\n.album .topbar {\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tright: 0px;\r\n\topacity: 0.3;\r\n\tfilter: alpha(opacity=30);\r\n\tbackground-color: #000;\r\n\t/*height: 40px;*/\r\n\tpadding: 8px 0 5px 5px;\r\n\twidth: auto;\r\n\toverflow-y: hidden;\r\n}\r\n\r\n.album .topbar.over {\r\n\topacity: 0.8;\r\n\tfilter: alpha(opacity=80);\r\n}\r\n\r\n.album .topbar a.item {\r\n\twidth: 32px;\r\n\theight: 32px;\r\n\tdisplay: inline-block;\r\n\tmargin: 3px 10px;\r\n\tcursor: pointer;\r\n\tfilter: alpha(opacity=60);\r\n\topacity: 0.6;\r\n\tbackground-image: url(\"chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons-32.png\");\r\n\tbackground-repeat: no-repeat;\r\n\toverflow: hidden;\r\n}\r\n\r\n.album .topbar a.item:hover {\r\n\tfilter: alpha(opacity=100);\r\n\topacity: 1;\r\n}\r\n\r\n.album .topbar .spliter {\r\n\twidth: 2px;\r\n\theight: 28px;\r\n\tline-height: 32px;\r\n\tborder-width: 2px;\r\n\tborder-style: solid;\r\n}\r\n\r\n.album .topbar .rotate-left {\r\n\tbackground-position: 0px 0px;\r\n}\r\n\r\n.album .topbar .rotate-right {\r\n\tbackground-position: 0px -32px;\r\n}\r\n\r\n.album .topbar .close {\r\n\tbackground-position: 0px -64px;\r\n}\r\n\r\n.album .topbar .close:hover {\r\n\tbackground-color: #ff3300;\r\n}\r\n\r\n.album .topbar .download {\r\n\tbackground-position: 0px -96px;\r\n}\r\n\r\n.album .controlbar {\r\n\tposition: absolute;\r\n\tbottom: 0px;\r\n\twidth: 100%;\r\n}\r\n\r\n.album .listbar {\r\n\tposition: absolute;\r\n\tbottom: 30px;\r\n\theight: 160px;\r\n\twidth: 100%;\r\n\ttext-align: center;\r\n\tbackground-color: #000;\r\n\tfilter: alpha(opacity=30);\r\n\topacity: 0.3;\r\n\toverflow: hidden;\r\n}\r\n\r\n.album .listbar-black {\r\n\tposition: absolute;\r\n\tbottom: 30px;\r\n\theight: 160px;\r\n\twidth: 100%;\r\n\ttext-align: center;\r\n\tbackground-color: #fff;\r\n\topacity: 0.9;\r\n\toverflow: hidden;\r\n}\r\n\r\n.album .thumbimages {\r\n\theight: 100%;\r\n\tposition: absolute;\r\n\tdisplay: inline-table;\r\n}\r\n\r\n.album .labelbar {\r\n\tposition: absolute;\r\n\ttop: -30px;\r\n\tbottom: 0px;\r\n\tpadding: 8px;\r\n\ttext-align: center;\r\n\twidth: 100%;\r\n\tcolor: #fff;\r\n\tbackground-color: #000;\r\n\t/* background-color: rgba(0, 0, 0, 0.8);\r\n\t*/\r\n\topacity: 0.8;\r\n\tfilter: alpha(opacity=80);\r\n\theight: 30px;\r\n}\r\n\r\n.album .label {\r\n\tcolor: #fff;\r\n}\r\n\r\n.album .closebutton {\r\n\tposition: absolute;\r\n\ttop: 0px;\r\n\tright: 0px;\r\n\twidth: 50px;\r\n\theight: 50px;\r\n\tcursor: pointer;\r\n\ttext-align: center;\r\n\tborder-radius: 0 0 0 50px;\r\n\tfilter: alpha(opacity=70);\r\n\topacity: 0.7;\r\n\tbackground: #ccc url(\"chrome-extension://__MSG_@@extension_id__/resource/libs/album/close.png\") no-repeat 15px 3px;\r\n\t/* background-position: right;\r\n\t*/\r\n}\r\n\r\n.album .album-item {\r\n\tmargin: 2px;\r\n\tborder-style: solid;\r\n\tborder-width: 1px;\r\n\tborder-color: #aabbcc;\r\n\tcursor: pointer;\r\n}\r\n\r\n.album .active {\r\n\tborder-width: 3px;\r\n\tborder-color: #ff6600;\r\n}\r\n\r\n.album .loading {\r\n\tposition: absolute;\r\n\tz-index: 30001;\r\n\tleft: 50%;\r\n\ttop: 50%;\r\n\tbackground-color: rgba(0, 0, 0, 0.4);\r\n\tfilter: alpha(opacity=40);\r\n\tpadding: 15px;\r\n\tborder-radius: 8px;\r\n}\r\n\r\n.album .zoomBar {\r\n\tposition: absolute;\r\n\tdisplay: inline;\r\n\twidth: 40px;\r\n\ttext-align: center;\r\n\tright: 40px;\r\n}\r\n\r\n.album .status-count {\r\n\tposition: absolute;\r\n\tleft: 10px;\r\n}\r\n\r\n.album .button-left {\r\n\tposition: absolute;\r\n\twidth: 48px;\r\n\theight: 48px;\r\n\ttop: 42%;\r\n\tcursor: pointer;\r\n\tfilter: alpha(opacity=50);\r\n\topacity: 0.5;\r\n\tbackground: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png') no-repeat scroll 0px 0px transparent;\r\n}\r\n\r\n.album .button-right {\r\n\tposition: absolute;\r\n\twidth: 48px;\r\n\theight: 48px;\r\n\tcursor: pointer;\r\n\tright: 0px;\r\n\ttop: 42%;\r\n\tfilter: alpha(opacity=50);\r\n\topacity: 0.5;\r\n\tbackground: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png') no-repeat scroll 0px -48px transparent;\r\n}\r\n\r\n.album .button-zoom-in {\r\n\tposition: absolute;\r\n\twidth: 16px;\r\n\theight: 16px;\r\n\tcursor: pointer;\r\n\tright: 80px;\r\n\tmargin-top: -1px;\r\n\tbackground: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png?v=2') no-repeat scroll 0px -112px transparent;\r\n}\r\n\r\n.album .button-zoom-out {\r\n\tposition: absolute;\r\n\twidth: 16px;\r\n\theight: 16px;\r\n\tcursor: pointer;\r\n\tright: 25px;\r\n\tmargin-top: -1px;\r\n\tbackground: url('chrome-extension://__MSG_@@extension_id__/resource/libs/album/icons.png?v=2') no-repeat scroll -16px -112px transparent;\r\n}\r\n\r\n.album .custom-botton-bar {\r\n\tposition: absolute;\r\n\theight: 26px;\r\n\ttext-align: center;\r\n\twidth: 100%;\r\n}\r\n\r\n.album button {\r\n\tbackground-color: #ffb94b;\r\n\tbackground-image: -webkit-gradient(linear, left top, left bottom, from(#fddb6f), to(#ffb94b));\r\n\tbackground-image: -webkit-linear-gradient(top, #fddb6f, #ffb94b);\r\n\tbackground-image: -moz-linear-gradient(top, #fddb6f, #ffb94b);\r\n\tbackground-image: -ms-linear-gradient(top, #fddb6f, #ffb94b);\r\n\tbackground-image: -o-linear-gradient(top, #fddb6f, #ffb94b);\r\n\tbackground-image: linear-gradient(top, #fddb6f, #ffb94b);\r\n\t-moz-border-radius: 3px;\r\n\t-webkit-border-radius: 3px;\r\n\tborder-radius: 3px;\r\n\ttext-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);\r\n\t-moz-box-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset;\r\n\t-webkit-box-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset;\r\n\tbox-shadow: 0 0 1px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.3) inset;\r\n\tborder-width: 1px;\r\n\tborder-style: solid;\r\n\tborder-color: #d69e31 #e3a037 #d5982d #e3a037;\r\n\tfloat: left;\r\n\theight: 24px;\r\n\tpadding: 3px;\r\n\tcursor: pointer;\r\n\tcolor: #8f5a0a;\r\n\tfilter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#fddb6f, endColorstr=#ffb94b, GradientType=0);\r\n}\r\n\r\n.album .album-tags {\r\n\tposition: absolute;\r\n\tleft: 0px;\r\n\tbottom: 30px;\r\n}\r\n\r\n.album .album-tags a.album-tag {\r\n\tdisplay: table;\r\n\tpadding: 5px;\r\n\tbackground-color: #777;\r\n\tcolor: #fff;\r\n\tmargin: 1px;\r\n\ttext-decoration: none;\r\n}\r\n\r\n.album .album-tags a.album-active {\r\n\tbackground-color: #369;\r\n\tborder-right: 4px #09C solid;\r\n}"
  },
  {
    "path": "resource/publicSites/douban.com/common.js",
    "content": "(function ($, window) {\n  class Common {\n    init() {\n      this.initButtons && this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 按指定的关键字进行搜索\n     * @param {*} key \n     * @param {*} button \n     */\n    search(key, button) {\n      PTService.call(PTService.action.openOptions, `search-torrent/${key}`).catch((error) => {\n        console.log(error);\n        button && button.html(error.msg || \"执行失败\");\n      });\n    }\n  };\n  window.DoubanCommon = Common;\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/douban.com/config.json",
    "content": "{\n  \"name\": \"豆瓣\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"电影详情页\",\n    \"pages\": [\"\\/subject\\/\\\\d+\\/\"],\n    \"scripts\": [\"common.js\", \"subject.js\"]\n  }, {\n    \"name\": \"top250\",\n    \"pages\": [\"/top250\"],\n    \"scripts\": [\"common.js\", \"top250.js\"]\n  }, {\n    \"name\": \"explore\",\n    \"pages\": [\"/explore\", \"/tv/\"],\n    \"scripts\": [\"common.js\", \"explore.js\"]\n  }, {\n    \"name\": \"doulist\",\n    \"pages\": [\"\\/doulist\\/\\\\d+\\/\"],\n    \"scripts\": [\"common.js\", \"doulist.js\"]\n  }],\n  \"schema\": \"publicSite\",\n  \"host\": \"movie.douban.com\",\n  \"path\": \"douban.com\",\n  \"cdn\": [\"www.douban.com\"]\n}"
  },
  {
    "path": "resource/publicSites/douban.com/doulist.js",
    "content": "(function ($, window) {\n  class App extends window.DoubanCommon {\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      let items = $(\".doulist-subject\");\n      console.log(items.length);\n      if (items.length > 0) {\n        items.each((index, item) => {\n          let $item = $(item);\n          let $title = $item.find(\".title a\");\n          let link = $title.attr(\"href\");\n          let match = link.match(/subject\\/(\\d+)/);\n          if (match && match.length >= 2) {\n            let title = $title.text().trim();\n            let key = \"\";\n\n            switch (true) {\n              // 电影\n              case link.indexOf(\"movie.douban.com\") > 0:\n                key = `douban${match[1]}`;\n                break;\n\n              // 其他\n              default:\n                key = title;\n                break;\n            }\n            this.createButton($item.find(\".post\"), key, title);\n          }\n        });\n      }\n    }\n\n    createButton(parent, key, title) {\n      if (!key) {\n        return;\n      }\n      let label = \"PT 助手搜索\";\n      parent.css({\n        \"max-height\": \"unset\"\n      });\n      let div = $(\"<div style='padding: 5px;'/>\")\n        .attr(\"title\", `搜索 ${title}`)\n        .appendTo(parent);\n      $(\"<a href='javascript:void(0);' class='lnk-sharing'/>\")\n        .html(label)\n        .on(\"click\", event => {\n          let button = $(event.target);\n          this.search(key, button);\n        })\n        .appendTo(div);\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/publicSites/douban.com/explore.js",
    "content": "(function ($, window) {\n  class App extends window.DoubanCommon {\n    constructor() {\n      super();\n      this.lastId = \"\";\n      this.status = 0;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      // 检测DOM变化\n      $(\".detail-pop\").bind('DOMNodeInserted', (e) => {\n        this.createButton($(e.target))\n      })\n    }\n\n    createButton(parent) {\n      if (this.status == 1) {\n        return;\n      }\n      this.status = 1;\n      let link = $(\"a[href*='movie.douban.com/subject']\", parent);\n      let key = \"\";\n      if (link.length > 0) {\n        let match = link.attr(\"href\").match(/subject\\/(\\d+)/);\n        let title = link.text().split(\" \")[0];\n        if (match && match.length >= 2) {\n          let id = match[1];\n          key = `douban${id}`;\n          if (id != this.lastId) {\n            this.lastId = id;\n            // 预转换\n            PTService.call(PTService.action.getIMDbIdFromDouban, id).catch((error) => {\n              console.log(error);\n            });\n          }\n          key += \"|\" + title;\n        }\n      }\n      if (!key) {\n        this.status = 0;\n        return;\n      }\n      let buttonId = \"pt-plugin-search-button\";\n      $(\"#\" + buttonId, parent).remove();\n      $(\"<a href='javascript:void(0);' id='\" + buttonId + \"' title='用 PT 助手搜索'/>\").html(\"助手搜索\").on(\"click\", (event) => {\n        this.search(key, $(event.target));\n      }).appendTo($(\".collect-area\", parent));\n      this.status = 0;\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/douban.com/subject.js",
    "content": "(function ($, window) {\n  class App extends window.DoubanCommon {\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      let key = this.getIMDbId() || this.getTitle();\n      if (key) {\n        // 搜索\n        PTService.addButton({\n          title: \"搜索当前电影\",\n          icon: \"search\",\n          label: \"搜索\",\n          click: (success, error) => {\n            this.search(key);\n            success();\n          }\n        });\n\n        let recommendButton = $(\"span.rec a[share-id]\");\n        if (recommendButton.length > 0) {\n          $(\"<a href='javascript:void(0);' class='lnk-sharing' style='margin-right: 5px;'/>\").html(\"用 PT 助手搜索\").on(\"click\", (event) => {\n            this.search(key, $(event.target));\n          }).insertBefore(recommendButton);\n        }\n      }\n    }\n\n    /**\n     * 获取 IMDb 编号\n     */\n    getIMDbId() {\n      let link = $(\"a[href*='www.imdb.com/title/']:first\");\n      if (link.length) {\n        return link.text();\n      }\n\n      // 尝试从文本中获取\n      link = $(\"#info\").text().match(/IMDb: (tt\\d+)/);\n\n      if (link) {\n        return link[1];\n      }\n\n      return \"\";\n    }\n\n    getTitle() {\n      return document.title.replace(\" (豆瓣)\", \"\");\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/douban.com/top250.js",
    "content": "(function ($, window) {\n  class App extends window.DoubanCommon {\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      let items = $(\"a[href*='movie.douban.com/subject'] img\");\n      if (items.length > 0) {\n        items.each((index, item) => {\n          let $item = $(item);\n          this.createButton($item.parent(), $item.attr(\"alt\"));\n        });\n      }\n    }\n\n    createButton(parent, key) {\n      if (!key) {\n        return;\n      }\n      let title = \"用 PT 助手搜索\";\n      let div = $(\"<div style='padding: 5px;margin-left: 20px;'/>\").attr(\"title\", `搜索 ${key}`).appendTo(parent);\n      $(\"<a href='javascript:void(0);' class='lnk-sharing'/>\").html(title).on(\"click\", (event) => {\n        let match = parent.attr(\"href\").match(/subject\\/(\\d+)/);\n        if (match && match.length >= 2) {\n          key = `douban${match[1]}`;\n        }\n        let button = $(event.target);\n        this.search(key, button);\n      }).appendTo(div);\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/goodmovieslist.com/best-movies.js",
    "content": "(function($) {\n  console.log(\"this is best-movies.js\");\n  class App extends window.DoubanCommon {\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      let items = $(\"table.list_movies\");\n      if (items.length > 0) {\n        items.each((index, item) => {\n          let $item = $(item);\n          let imdbId = $item.attr(\"id\");\n          if (imdbId) {\n            this.createButton($item.find(\"tr > td:eq(0)\"), imdbId);\n          }\n        });\n      }\n    }\n\n    createButton(parent, key) {\n      if (!key) {\n        return;\n      }\n      let div = $(\"<div style='padding: 5px;text-align:center;'/>\").appendTo(\n        parent\n      );\n      const className = \"gsc-search-button gsc-search-button-v2\";\n      const styles =\n        \"font-size: 12px;color: #fff;padding: 5px;margin-right: 5px;\";\n      $(`<button class='${className}' style='${styles}'/>`)\n        .html(\"助手搜索\")\n        .attr(\"title\", `按当前默认方案直接搜索 ${key}`)\n        .on(\"click\", event => {\n          let button = $(event.target);\n          this.search(key, button);\n        })\n        .appendTo(div);\n\n      $(`<button class='${className}' style='${styles}'/>`)\n        .html(\"按方案搜索\")\n        .attr(\"title\", `按指定方案搜索 ${key}`)\n        .on(\"click\", event => {\n          this.showPopupMenusForSolutions(event, key);\n        })\n        .appendTo(div);\n\n      $(`<button class='${className}' style='${styles}'/>`)\n        .html(\"按站点搜索\")\n        .attr(\"title\", `按指定站点搜索 ${key}`)\n        .on(\"click\", event => {\n          this.showPopupMenus(event, key);\n        })\n        .appendTo(div);\n    }\n\n    showPopupMenus(event, key) {\n      let menus = [];\n      let _this = this;\n\n      function addMenu(item) {\n        menus.push({\n          title: item.title,\n          fn: () => {\n            _this.search(item.key);\n          }\n        });\n      }\n\n      const options = PTService.options;\n\n      if (options.sites && options.sites.length > 0) {\n        // 添加站点\n        options.sites.forEach(site => {\n          if (site.offline) {\n            return;\n          }\n          addMenu({\n            title: `在 ${site.name} - ${site.host} 中搜索`,\n            key: `${key}/${site.host}`\n          });\n        });\n      }\n\n      if (menus.length > 0) {\n        basicContext.show(menus, event);\n        $(\".basicContext\").css({\n          \"font-size\": \"12px\",\n          left: \"-=20px\",\n          top: \"+=10px\"\n        });\n      }\n    }\n\n    showPopupMenusForSolutions(event, key) {\n      let menus = [];\n      let _this = this;\n\n      function addMenu(item) {\n        menus.push({\n          title: item.title,\n          fn: () => {\n            _this.search(item.key);\n          }\n        });\n      }\n\n      const solutions = PTService.options.searchSolutions;\n\n      if (solutions && solutions.length > 0) {\n        // 添加站点\n        solutions.forEach(item => {\n          addMenu({\n            title: `使用 ${item.name} 搜索`,\n            key: `${key}/${item.id}`\n          });\n        });\n      }\n\n      if (menus.length > 0) {\n        menus.push({});\n        addMenu({\n          title: `在 <所有站点> 中搜索`,\n          key: `${key}/all`\n        });\n\n        basicContext.show(menus, event);\n        $(\".basicContext\").css({\n          \"font-size\": \"12px\",\n          left: \"-=20px\",\n          top: \"+=10px\"\n        });\n      }\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/publicSites/goodmovieslist.com/config.json",
    "content": "{\n  \"name\": \"Good Movies List\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"best-movies\",\n    \"pages\": [\"/best-movies/\"],\n    \"scripts\": [\"/publicSites/douban.com/common.js\", \"best-movies.js\"]\n  }],\n  \"schema\": \"publicSite\",\n  \"host\": \"goodmovieslist.com\",\n  \"path\": \"goodmovieslist.com\"\n}"
  },
  {
    "path": "resource/publicSites/imdb.com/config.json",
    "content": "{\n  \"name\": \"IMDb\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"电影详情页\",\n    \"pages\": [\"^\\/title\\/tt\\\\d+\\/?$\"],\n    \"scripts\": [\"subject.js\"]\n  }, {\n    \"name\": \"Top\",\n    \"pages\": [\"\\/chart\\/top\"],\n    \"scripts\": [\"top.js\"]\n  }],\n  \"schema\": \"publicSite\",\n  \"host\": \"www.imdb.com\",\n  \"path\": \"imdb.com\"\n}"
  },
  {
    "path": "resource/publicSites/imdb.com/subject.js",
    "content": "(function ($, window) {\n  class App {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n\n      let IMDbId = this.getIMDbId();\n      if (IMDbId) {\n        // 搜索\n        PTService.addButton({\n          title: \"搜索当前电影\",\n          icon: \"search\",\n          label: \"搜索\",\n          click: (success, error) => {\n            PTService.call(PTService.action.openOptions, `search-torrent/${IMDbId}`);\n            success();\n          }\n        });\n      }\n    }\n\n    /**\n     * 获取 IMDb 编号\n     */\n    getIMDbId() {\n      let link = location.pathname.match(/title\\/(tt\\d+)/);\n      if (link.length > 1) {\n        return link[1];\n      }\n\n      return \"\";\n    }\n\n\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/imdb.com/top.js",
    "content": "(function ($, window) {\n  class App {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      let items = $(\"td.titleColumn a\");\n      console.log(items);\n      if (items.length > 0) {\n        items.each((index, item) => {\n          let $item = $(item);\n          let title = $item.text();\n          let link = item.href.match(/title\\/(tt\\d+)/);\n          if (link.length > 1) {\n            this.createButton($item.parent(), link[1], title);\n          }\n        });\n      }\n    }\n\n    createButton(parent, key, title) {\n      if (!key) {\n        return;\n      }\n      let div = $(\"<div style='text-align:right;'/>\").attr(\"title\", `搜索 ${title}`).appendTo(parent);\n      $(\"<a href='javascript:void(0);'/>\").html(\"用 PT 助手搜索\").on(\"click\", () => {\n        PTService.call(PTService.action.openOptions, `search-torrent/${key}`);\n      }).appendTo(div);\n    }\n\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/publicSites/reseed.tongyifan.me/config.json",
    "content": "{\n  \"name\": \"Reseed\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"辅种页面\",\n    \"pages\": [\"/\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"reseed.js\"]\n  }],\n  \"schema\": \"publicSite\",\n  \"host\": \"reseed.tongyifan.me\",\n  \"path\": \"reseed.tongyifan.me\"\n}"
  },
  {
    "path": "resource/publicSites/reseed.tongyifan.me/reseed.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.options = PTService.options;\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let text = $(\"textarea:first\").val();\n\n      if (!text) {\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let links = text.split(\"\\n\");\n\n      if (links.length == 0) {\n        //  \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = [];\n\n      links.forEach(link => {\n        if (this.checkURL(link)) {\n          urls.push(link);\n        }\n      });\n\n      if (urls.length == 0) {\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      return urls;\n    }\n\n    checkURL(url) {\n      const sites = this.options.sites;\n      if (!sites) {\n        return false;\n      }\n      const URL = PTService.filters.parseURL(url);\n      const index = sites.findIndex(site => {\n        return site.host === URL.host;\n      });\n\n      return index !== -1;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/Common/common.js",
    "content": "(function($, window) {\n  class Common {\n    constructor() {\n      this.siteContentMenus = {};\n      this.clientContentMenus = [];\n      this.defaultPath = PTService.getSiteDefaultPath();\n      this.downloadClientType = PTService.downloadClientType;\n      this.defaultClientOptions = PTService.getClientOptions();\n      this.currentURL = location.href;\n    }\n\n    /**\n     * 获取指定key的当前语言内容\n     * @param {*} key\n     * @param {*} options\n     */\n    t(key, options) {\n      return PTService.i18n.t(key, options);\n    }\n\n    /**\n     * 初始化当前默认服务器可用空间\n     */\n    initFreeSpaceButton() {\n      if (!this.defaultPath) {\n        return;\n      }\n      PTService.call(PTService.action.getFreeSpace, {\n        path: this.defaultPath,\n        clientId: PTService.site.defaultClientId\n      })\n        .then(result => {\n          console.log(\"命令执行完成\", result);\n          if (result && result.arguments) {\n            // console.log(PTService.filters.formatSize(result.arguments[\"size-bytes\"]));\n\n            PTService.addButton({\n              title: this.t(\"buttons.freeSpaceTip\", {\n                path: this.defaultPath,\n                interpolation: { escapeValue: false }\n              }), // \"默认服务器剩余空间\\n\" + this.defaultPath,\n              icon: \"filter_drama\",\n              label: PTService.filters.formatSize(\n                result.arguments[\"size-bytes\"]\n              )\n            });\n          }\n          // success();\n        })\n        .catch(() => {\n          // error()\n        });\n    }\n\n    /**\n     * 初始化种子详情页面按钮\n     */\n    initDetailButtons() {\n      // 添加下载按钮\n      this.addSendTorrentToDefaultClientButton();\n\n      // 添加下载到按钮\n      this.addSendTorrentToClientButton();\n\n      // 添加复制下载链接按钮\n      this.addCopyTextToClipboardButton();\n\n      // 初始化可用空间按钮\n      this.initFreeSpaceButton();\n\n      // 初始化收藏按钮\n      this.initCollectionButton();\n\n      // 初始化说谢谢按钮\n      this.initSayThanksButton();\n    }\n\n    /**\n     * 初始化种子列表页面按钮\n     */\n    initListButtons(checkPasskey = false) {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title: this.t(\"buttons.downloadAllTip\", {\n            name: this.defaultClientOptions.name\n          }), //`将当前页面所有种子下载到[${this.defaultClientOptions.name}]`,\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadAll\"), //\"下载所有\",\n          click: (success, error) => {\n            if (checkPasskey && !PTService.site.passkey) {\n              error(\"请先设置站点密钥（Passkey）。\");\n              return;\n            }\n            this.startDownloadURLs(success, error);\n          }\n        });\n\n      // 添加下载到按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadAllToTip\"), //`将当前页面所有种子下载到指定服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadAllTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            // \"请先设置站点密钥（Passkey）。\"\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          this.showAllContentMenus(event.originalEvent, success, error);\n        },\n        onDrop: (data, event, success, error) => {\n          console.log(data);\n          let url = this.getDroperURL(data.url);\n          console.log(url);\n          this.showContentMenusForUrl(\n            {\n              url,\n              title: data.title,\n              link: data.url\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyAllToClipboardTip\"), // \"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyAllToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, urls.join(\"\\n\"))\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(() => {\n              error();\n            });\n        },\n        onDrop: (data, event, success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let url = this.getDroperURL(data.url);\n          url &&\n            PTService.call(PTService.action.copyTextToClipboard, url)\n              .then(result => {\n                console.log(\"命令执行完成\", result);\n                success();\n              })\n              .catch(() => {\n                error();\n              });\n        }\n      });\n\n      // 检查是否有下载管理权限\n      this.checkPermissions([\"downloads\"])\n        .then(() => {\n          this.addSaveAllTorrentFilesButton(checkPasskey);\n        })\n        .catch(() => {\n          PTService.addButton({\n            title: this.t(\"buttons.needAuthorizationTip\"), //\"下载所有种子文件功能需要权限，点击前往授权\",\n            icon: \"verified_user\",\n            key: \"requestPermissions\",\n            label: this.t(\"buttons.needAuthorization\"), //\"需要授权\",\n            click: (success, error) => {\n              PTService.call(PTService.action.openOptions, \"set-permissions\");\n              success();\n            }\n          });\n        });\n    }\n\n    /**\n     * 添加下载所有种子文件按钮\n     * @param {*} checkPasskey\n     */\n    addSaveAllTorrentFilesButton(checkPasskey) {\n      // 批量下载当前页种子文件\n      PTService.addButton({\n        title: this.t(\"buttons.saveAllTorrentTip\"), //\"下载所有种子文件\",\n        icon: \"save\",\n        label: this.t(\"buttons.saveAllTorrent\"), //\"所有种子\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          let downloads = [];\n          urls.forEach(url => {\n            downloads.push({\n              url,\n              method: PTService.site.downloadMethod\n            });\n          });\n\n          console.log(downloads);\n\n          PTService.call(PTService.action.addBrowserDownloads, downloads)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(e => {\n              console.log(e);\n              error(e);\n            });\n        }\n      });\n    }\n\n    checkPermissions(permissions) {\n      return PTService.call(PTService.action.checkPermissions, permissions);\n    }\n\n    /**\n     * 发送种子到默认下载服务器\n     * @param {string} url\n     */\n    sendTorrentToDefaultClient(option, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof option === \"string\") {\n          option = {\n            url: option,\n            title: \"\"\n          };\n        }\n\n        let savePath = PTService.pathHandler.getSavePath(\n          this.defaultPath,\n          PTService.site\n        );\n\n        if (savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToDefaultClient, {\n          url: option.url,\n          title: option.title,\n          savePath: savePath,\n          autoStart: this.defaultClientOptions.autoStart,\n          tagIMDb: this.defaultClientOptions.tagIMDb,\n          link: option.link,\n          imdbId: option.imdbId\n        })\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 隐藏指定的 notice\n     * @param notice\n     */\n    hideNotice(notice) {\n      if (!notice) return;\n      if (notice.id && notice.close) {\n        notice.close();\n      } else if (notice.hide) {\n        notice.hide();\n      }\n    }\n\n    /**\n     * 发送种子到指定下载服务器\n     * @param {string} url\n     */\n    sendTorrentToClient(options, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof options === \"string\") {\n          options = {\n            url: options,\n            title: \"\"\n          };\n        }\n\n        if (!options.clientId) {\n          // \"无效的下载服务器\"\n          reject(this.t(\"invalidDownloadServer\"));\n          return;\n        }\n\n        options.savePath = PTService.pathHandler.getSavePath(\n          options.savePath,\n          PTService.site\n        );\n        if (options.savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToClient, options)\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\",\n          link: data\n        };\n      }\n\n      data.url = this.getDroperURL(data.url);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    isNexusPHP() {\n      return PTService.site.schema == \"NexusPHP\";\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (url && url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url && url.substr(0, 4) !== \"http\") {\n        if (url.substr(0, 1) == \"/\") {\n          url = url.substr(1);\n        }\n        url = `${siteURL}${url}`;\n      }\n\n      return url;\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 添加下载到指定下载服务器按钮\n     */\n    addSendTorrentToClientButton() {\n      // 添加下载按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadToTip\"), //`将当前种子下载到指定的服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = document.title;\n          }\n\n          this.showContentMenusForUrl(\n            {\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n    }\n\n    /**\n     * 添加一键下载按钮\n     */\n    addSendTorrentToDefaultClientButton() {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title:\n            this.t(\"buttons.downloadToDefaultTip\", {\n              name: this.defaultClientOptions.name\n            }) + (this.defaultPath ? \"\\n\" + this.defaultPath : \"\"), //`将当前种子下载到[${this.defaultClientOptions.name}]` +\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadToDefault\"), //\"一键下载\",\n          /**\n           * 单击事件\n           * @param success 成功回调事件\n           * @param error 失败回调事件\n           *\n           * 两个事件必需执行一个，可以传递一个参数\n           */\n          click: (success, error) => {\n            // getDownloadURL 方法由继承者提供\n            if (!this.getDownloadURL) {\n              // \"getDownloadURL 方法未定义\"\n              error(this.t(\"getDownloadURLisUndefined\"));\n              return;\n            }\n\n            let url = this.getDownloadURL();\n\n            if (!url) {\n              // \"获取下载链接失败\"\n              error(this.t(\"getDownloadURLFailed\"));\n              return;\n            }\n\n            let title = \"\";\n\n            if (this.getTitle) {\n              title = this.getTitle();\n            } else {\n              title = document.title;\n            }\n\n            this.sendTorrentToDefaultClient({\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            })\n              .then(() => {\n                success();\n              })\n              .catch(result => {\n                error(result);\n              });\n          }\n        });\n    }\n\n    /**\n     * 添加复制下载链接按钮\n     */\n    addCopyTextToClipboardButton() {\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyToClipboardTip\"), //\"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          console.log(PTService.site, this.defaultPath);\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, url)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(result => {\n              error(result);\n            });\n        }\n      });\n    }\n\n    /**\n     * 初始化收藏按钮\n     */\n    initCollectionButton() {\n      // 获取收藏情况\n      PTService.call(PTService.action.getTorrentCollention, location.href)\n        .then(result => {\n          this.addRemoveCollectionButton(result);\n        })\n        .catch(() => {\n          this.addToCollectionButton();\n        });\n    }\n\n    /**\n     * 添加收藏按钮\n     */\n    addToCollectionButton() {\n      PTService.removeButton(\"removeFromCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.addToCollection\"),\n        icon: \"favorite_border\",\n        label: this.t(\"buttons.addToCollection\"),\n        key: \"addToCollection\",\n        click: (success, error) => {\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = PTService.getFieldValue(\"title\");\n          }\n\n          if (!title) {\n            title = $(\"title:first\").text();\n          }\n\n          let imdbId = PTService.getFieldValue(\"imdbId\");\n\n          if (!imdbId) {\n            const link = $(\"a[href*='www.imdb.com/title/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n              if (match && match.length >= 2) {\n                imdbId = match[1];\n              }\n            }\n          }\n\n          let doubanId = PTService.getFieldValue(\"doubanId\");\n\n          if (!doubanId) {\n            const link = $(\"a[href*='movie.douban.com/subject/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/subject\\/(\\d+)/);\n\n              if (match && match.length >= 2) {\n                doubanId = match[1];\n              }\n            }\n          }\n\n          const data = {\n            title: title,\n            url: this.getDownloadURL(),\n            link: location.href,\n            host: location.host,\n            size: PTService.getFieldValue(\"size\"),\n            subTitle: PTService.getFieldValue(\"subTitle\"),\n            movieInfo: {\n              imdbId: imdbId,\n              doubanId: doubanId\n            }\n          };\n\n          PTService.call(PTService.action.addTorrentToCollection, data)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addRemoveCollectionButton(data);\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 添加移除收藏按钮\n     */\n    addRemoveCollectionButton(item) {\n      PTService.removeButton(\"addToCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.removeFromCollection\"),\n        icon: \"favorite\",\n        label: this.t(\"buttons.removeFromCollection\"),\n        key: \"removeFromCollection\",\n        click: (success, error) => {\n          PTService.call(PTService.action.deleteTorrentFromCollention, item)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addToCollectionButton();\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 根据指定的URL获取可用的下载目录及客户端信息\n     * @param url\n     */\n    getContentMenusForUrl(url) {\n      let urlParser = PTService.filters.parseURL(url);\n      if (!urlParser.host) {\n        return [];\n      }\n      let results = [];\n      let clients = [];\n      let site = PTService.getSiteFromHost(urlParser.host);\n      if (!site) {\n        return [];\n      }\n      let host = site.host;\n\n      if (this.siteContentMenus[host]) {\n        return this.siteContentMenus[host];\n      }\n\n      /**\n       * 增加下载目录\n       * @param paths\n       * @param client\n       */\n      function pushPath(paths, client) {\n        paths.forEach(path => {\n          results.push({\n            client: client,\n            path: path,\n            host: host\n          });\n        });\n      }\n\n      PTService.options.clients.forEach(client => {\n        clients.push({\n          client: client,\n          path: \"\",\n          host: host\n        });\n\n        if (client.paths) {\n          // 根据已定义的路径创建菜单\n          for (const _host in client.paths) {\n            let paths = client.paths[host];\n\n            if (_host !== host) {\n              continue;\n            }\n\n            pushPath(paths, client);\n          }\n\n          // 最后添加当前客户端适用于所有站点的目录\n          let publicPaths = client.paths[PTService.allSiteKey];\n          if (publicPaths) {\n            if (results.length > 0) {\n              results.push({});\n            }\n\n            pushPath(publicPaths, client);\n          }\n        }\n      });\n\n      if (results.length > 0) {\n        clients.splice(0, 0, {});\n      }\n\n      results = results.concat(clients);\n\n      this.siteContentMenus[host] = results;\n\n      return results;\n    }\n\n    /**\n     * 显示指定链接的下载服务器及目录菜单\n     * @param options\n     * @param event\n     */\n    showContentMenusForUrl(options, event, success, error) {\n      let items = this.getContentMenusForUrl(options.url);\n      let menus = [];\n\n      items.forEach(item => {\n        if (item.client && item.client.name) {\n          menus.push({\n            title:\n              this.t(\"buttons.menuDownloadTo\", {\n                server: `${item.client.name} -> ${item.client.address}`\n              }) + //`下载到：${item.client.name} -> ${item.client.address}` +\n              (item.path\n                ? ` -> ${PTService.pathHandler.replacePathKey(\n                    item.path,\n                    PTService.site\n                  )}`\n                : \"\"),\n            fn: () => {\n              if (options.url) {\n                // console.log(options, item);\n                this.sendTorrentToClient({\n                  clientId: item.client.id,\n                  url: options.url,\n                  title: options.title,\n                  savePath: item.path,\n                  autoStart: item.client.autoStart,\n                  tagIMDb: item.client.tagIMDb,\n                  link: options.link,\n                  imdbId: options.imdbId\n                })\n                  .then(result => {\n                    success();\n                  })\n                  .catch(result => {\n                    error(result);\n                  });\n              }\n            }\n          });\n        } else {\n          menus.push({});\n        }\n      });\n\n      console.log(items, menus);\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 验证指定元素的大小信息\n     * @param {*} doms\n     */\n    checkSize(doms) {\n      if (!PTService.options.needConfirmWhenExceedSize) {\n        return true;\n      }\n      // 获取所有种子的大小信息\n      let size = this.getTotalSize(doms);\n\n      let exceedSize = 0;\n      switch (PTService.options.exceedSizeUnit) {\n        //\n        case PTService.sizeUnit.MiB:\n          exceedSize = PTService.options.exceedSize * 1048576;\n          break;\n\n        case PTService.sizeUnit.GiB:\n          exceedSize = PTService.options.exceedSize * 1073741824;\n          break;\n\n        case \"T\":\n        case PTService.sizeUnit.TiB:\n          exceedSize = PTService.options.exceedSize * 1099511627776;\n          break;\n      }\n\n      return size >= exceedSize ? PTService.filters.formatSize(size) : true;\n    }\n\n    /**\n     *\n     * @param {*} source\n     */\n    getTotalSize(source) {\n      let total = 0;\n\n      $.each(source, (index, item) => {\n        total += this.getSize($(item).text());\n      });\n\n      return total;\n    }\n\n    /**\n     * @return {number}\n     */\n    getSize(size) {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size.match(\n        /^(\\d*\\.?\\d+)(.*[^TGMK])?([TGMK](B|iB){0,1})$/i\n      );\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Ti?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    }\n\n    /**\n     * 种子大小超限时确认\n     */\n    confirmSize(doms) {\n      let size = this.checkSize(doms);\n\n      if (size !== true) {\n        let content = this.t(\"exceedSizeConfirm\", {\n          size,\n          exceedSize: PTService.options.exceedSize,\n          exceedSizeUnit: PTService.options.exceedSizeUnit\n        });\n        if (!confirm(content)) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    /**\n     * 准备开始批量下载\n     * @param {*} success\n     * @param {*} error\n     * @param {*} downloadOptions\n     */\n    startDownloadURLs(success, error, downloadOptions) {\n      if (this.confirmWhenExceedSize) {\n        if (!this.confirmWhenExceedSize()) {\n          // \"容量超限，已取消\"\n          error(this.t(\"exceedSizeCanceled\"));\n          return;\n        }\n      }\n\n      if (!this.getDownloadURLs) {\n        // \"getDownloadURLs 方法未定义\"\n        error(this.t(\"getDownloadURLsisUndefined\"));\n        return;\n      }\n\n      let urls = this.getDownloadURLs();\n      if (!urls.length || typeof urls == \"string\") {\n        error(urls);\n        return;\n      }\n\n      // 是否启用后台下载任务\n      if (PTService.options.enableBackgroundDownload) {\n        this.downloadURLsInBackground(\n          urls,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      } else {\n        this.downloadURLs(\n          urls,\n          urls.length,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      }\n    }\n\n    downloadURLsInBackground(urls, callback, downloadOptions) {\n      const items = [];\n\n      const savePath = downloadOptions\n        ? PTService.pathHandler.getSavePath(\n            downloadOptions.savePath || downloadOptions.path,\n            PTService.site\n          )\n        : \"\";\n\n      urls.forEach(url => {\n        if (downloadOptions) {\n          items.push({\n            clientId: downloadOptions.client.id,\n            url,\n            savePath,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb\n          });\n        } else {\n          items.push({\n            url\n          });\n        }\n      });\n\n      PTService.call(PTService.action.sendTorrentsInBackground, items)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 批量下载指定的URL\n     * @param {*} urls\n     * @param {*} count\n     * @param {*} callback\n     * @param {*} downloadOptions 下载选项，如不指定，则发送至默认下载服务器\n     */\n    downloadURLs(urls, count, callback, downloadOptions) {\n      let index = count - urls.length;\n      let url = urls.shift();\n      if (!url) {\n        $(this.statusBar).remove();\n        this.statusBar = null;\n        // count + \"条链接已发送完成。\"\n        callback(\n          this.t(\"downloadURLsFinished\", {\n            count\n          })\n        );\n        return;\n      }\n\n      this.showStatusMessage(\n        this.t(\"downloadURLsTip\", {\n          text:\n            url.replace(PTService.site.passkey, \"***\") +\n            \"(\" +\n            (count - index) +\n            \"/\" +\n            count +\n            \")\"\n        }),\n        0\n      );\n\n      if (!downloadOptions) {\n        this.sendTorrentToDefaultClient(url, false)\n          .then(result => {\n            this.downloadURLs(urls, count, callback);\n          })\n          .catch(result => {\n            this.downloadURLs(urls, count, callback);\n          });\n      } else {\n        this.sendTorrentToClient(\n          {\n            clientId: downloadOptions.client.id,\n            url: url,\n            title: \"\",\n            savePath: downloadOptions.path,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb,\n            imdbId: downloadOptions.imdbId\n          },\n          false\n        )\n          .finally(() => {\n            // 是否设置了时间间隔\n            if (PTService.options.batchDownloadInterval > 0) {\n              setTimeout(() => {\n                this.downloadURLs(urls, count, callback, downloadOptions);\n              }, PTService.options.batchDownloadInterval * 1000);\n            } else {\n              this.downloadURLs(urls, count, callback, downloadOptions);\n            }\n          })\n          .catch(error => {\n            console.log(error);\n          });\n      }\n    }\n\n    showStatusMessage(msg) {\n      if (!this.statusBar) {\n        this.statusBar = PTService.showNotice({\n          text: msg,\n          type: \"info\",\n          width: 600,\n          progressBar: false\n        });\n      } else {\n        this.statusBar.find(\".noticejs-content\").html(msg);\n      }\n    }\n\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */\n    clone(source) {\n      return JSON.parse(JSON.stringify(source));\n    }\n\n    /**\n     * 显示批量下载时可用下载服务器菜单\n     * @param event\n     */\n    showAllContentMenus(event, success, error) {\n      let clients = [];\n      let menus = [];\n      let _this = this;\n\n      function addMenu(item) {\n        let title = _this.t(\"buttons.menuDownloadTo\", {\n          server: `${item.client.name} -> ${item.client.address}`\n        }); //`下载到：${item.client.name} -> ${item.client.address}`;\n        if (item.path) {\n          title += ` -> ${PTService.pathHandler.replacePathKey(\n            item.path,\n            PTService.site\n          )}`;\n        }\n        menus.push({\n          title: title,\n          fn: () => {\n            // 克隆是为了多次选择时，不覆盖原来的值\n            let _item = PPF.clone(item);\n            console.log(item);\n            let savePath = PTService.pathHandler.getSavePath(\n              _item.path,\n              PTService.site\n            );\n            if (savePath === false) {\n              // \"用户取消操作\"\n              error(_this.t(\"userCanceled\"));\n              return;\n            }\n            _item.path = savePath;\n            _this.startDownloadURLs(success, error, _item);\n          }\n        });\n      }\n\n      if (this.clientContentMenus.length == 0) {\n        PTService.options.clients.forEach(client => {\n          clients.push({\n            client: client,\n            path: \"\"\n          });\n        });\n        clients.forEach(item => {\n          if (item.client && item.client.name) {\n            addMenu(item);\n\n            if (item.client.paths) {\n              // 添加适用于所有站点的目录\n              let publicPaths = item.client.paths[PTService.allSiteKey];\n              if (publicPaths) {\n                publicPaths.forEach(path => {\n                  let _item = this.clone(item);\n                  _item.path = path;\n                  addMenu(_item);\n                });\n              }\n            }\n          } else {\n            menus.push({});\n          }\n        });\n        this.clientContentMenus = menus;\n      } else {\n        menus = this.clientContentMenus;\n      }\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      if (!url) {\n        return \"\";\n      }\n      if (url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 初始化说谢谢按钮\n     */\n    initSayThanksButton() {\n      let sayThanksButton = PTService.getFieldValue(\"sayThanksButton\");\n      console.log(\"sayThanksButton\");\n      if (sayThanksButton && sayThanksButton.length) {\n        // 说谢谢\n        PTService.addButton({\n          title: this.t(\"buttons.sayThanksTip\"),\n          icon: \"thumb_up\",\n          label: this.t(\"buttons.sayThanks\"),\n          key: \"sayThanks\",\n          click: (success, error) => {\n\t        sayThanksButton[0].click();\n            success();\n            setTimeout(() => {\n\t          \n              PTService.removeButton(\"sayThanks\");\n            }, 1000);\n          }\n        });\n      }\n    }\n  }\n\n  window.NexusPHPCommon = Common;\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/Common/config.json",
    "content": "{\n  \"name\": \"Common\",\n  \"ver\": \"0.0.1\",\n  \"searchEntryConfig\": {\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/schemas/Common/getSearchResult.js\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }]\n}"
  },
  {
    "path": "resource/schemas/Common/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let url = PTService.getFieldValue(\"downloadURL\");\n\n      return this.getFullURL(url);\n    }\n    \n    showTorrentSize() {\n      let size = PTService.filters.formatSize(PTService.getFieldValue(\"size\"));\n      PTService.addButton({\n       title: \"当前种子大小\",\n        icon: \"attachment\",\n        label: size\n      });\n    }\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return PTService.getFieldValue(\"title\") || $(\"title\").text();\n    }\n\n    /**\n     * 获取当前种子IMDb Id\n     */\n    getIMDbId() {\n      try\n      {\n        let imdbId = PTService.getFieldValue(\"imdbId\");\n        console.log(imdbId);\n        if (imdbId)\n          return imdbId;\n        else {\n          const link = $(\"a[href*='www.imdb.com/title/']:first\");\n          if (link.length > 0) {\n            let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n            if (match && match.length >= 2)\n              return imdbId = match[1];\n\n          }\n        }\n      } catch {\n      }\n      return null;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/Common/getSearchResult.js",
    "content": "/**\n * 通用搜索解析脚本\n */\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      // 判断是否已登录\n      if (\n        options.entry.loggedRegex &&\n        !new RegExp(options.entry.loggedRegex, \"\").test(options.responseText)\n      ) {\n        // 需要登录后再搜索\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n\t   console.log(\"Common schemas search js\");\n      if (!this.haveData) {\n        return [];\n      }\n      let selector = options.resultSelector;\n      let dataRowSelector = options.entry.dataRowSelector || \"> tbody > tr\";\n      selector = selector.replace(dataRowSelector, \"\");\n      let cellSelector = options.entry.dataCellSelector || \">td\";\n      // 获取数据表格\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(dataRowSelector);\n      if (rows.length == 0) {\n        // 没有定位到种子列表，或没有相关的种子\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\n        return [];\n      }\n      let results = [];\n      let beginRowIndex = options.entry.firstDataRowIndex || 0;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = options.entry.fieldIndex || {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: -1,\n        // 分类\n        category: -1\n      };\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(cellSelector);\n\n          let title = this.getTitle(row, cells, fieldIndex);\n\n          // 没有获取标题时，继续下一个\n          if (!title) {\n            continue;\n          }\n          let link = this.getFieldValue(row, cells, fieldIndex, \"link\");\n\n          // 获取下载链接\n          let url = this.getFieldValue(row, cells, fieldIndex, \"url\");\n\n          if (!url || !link) {\n            continue;\n          }\n\n          let data = {\n            title: title,\n            subTitle: this.getFieldValue(row, cells, fieldIndex, \"subTitle\"),\n            link: this.getFullURL(link),\n            url: this.getFullURL(url),\n            size: this.getFieldValue(row, cells, fieldIndex, \"size\") || 0,\n            time: this.getFieldValue(row, cells, fieldIndex, \"time\"),\n            author: this.getFieldValue(row, cells, fieldIndex, \"author\") || \"\",\n            seeders: this.getFieldValue(row, cells, fieldIndex, \"seeders\") || 0,\n            leechers:\n              this.getFieldValue(row, cells, fieldIndex, \"leechers\") || 0,\n            completed:\n              this.getFieldValue(row, cells, fieldIndex, \"completed\") || 0,\n            comments:\n              this.getFieldValue(row, cells, fieldIndex, \"comments\") || 0,\n            site: this.site,\n            tags: Searcher.getRowTags(this.site, row),\n            entryName: options.entry.name,\n            category: this.getFieldValue(row, cells, fieldIndex, \"category\"),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        // 获取种子信息出错\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n\n      // 没有搜索到相关的种子\n      if (results.length == 0 && !options.errorMsg) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取指定字段内容\n     * @param {*} row\n     * @param {*} cells\n     * @param {*} fieldIndex\n     * @param {*} fieldName\n     */\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(this.site, parent, fieldName);\n\n      if (!result && cell && result !== 0) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      let URL = PTServiceFilters.parseURL(this.site.url);\n      if (url.substr(0, 2) === \"//\") {\n        url = `${URL.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${URL.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${URL.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 获取标题\n     */\n    getTitle(row, cells, fieldIndex) {\n      let title = this.getFieldValue(row, cells, fieldIndex, \"title\", true);\n\n      if (!title) {\n        return \"\";\n      }\n\n      if (typeof title === \"string\") {\n        return title;\n      }\n\n      // 对title进行处理，防止出现cf的email protect\n      let cfemail = title.find(\"span.__cf_email__\");\n      if (cfemail.length > 0) {\n        cfemail.each((index, el) => {\n          $(el).replaceWith(Searcher.cfDecodeEmail($(el).data(\"cfemail\")));\n        });\n      }\n\n      return title.text();\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/schemas/Common/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = PTService.getFieldValue(\"downloadURLs\");\n\n      if (links.length == 0) {\n        //  \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n      if (typeof(links[0])!=\"string\"){\n        let urls = $.map(links, item => {\n          let url = $(item).attr(\"href\");\n          return this.getFullURL(url);\n        });\n        return urls;\n      }\n      return links\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(PTService.getFieldValue(\"confirmSize\"));\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/Discuz/config.json",
    "content": "{\n  \"name\": \"Discuz\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/forum.php?mod=viewthread\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/forum.php?mod=torrents\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/forum.php\",\n    \"queryString\": \"mod=torrents&search=$key$\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/schemas/Discuz/getSearchResult.js\",\n    \"resultSelector\": \"table.torrents:last\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img.sp_4\"\n  }, {\n    \"name\": \"35%\",\n    \"selector\": \"img.sp_3\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img.sp_2\"\n  }]\n}"
  },
  {
    "path": "resource/schemas/Discuz/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      if (this.getDownloadURL()) {\n        this.initDetailButtons();\n      }\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='passkey'][href*='download.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      } else {\n        query = $(\"a[href*='passkey']\");\n        if (query.length > 0) {\n          url = query.attr(\"href\");\n        }\n      }\n\n      if (!url) {\n        url = $(\"a[href*='download'][href*='?id']:first\").attr(\"href\") || $(\"a[href*='download.php?']:first\").attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      if (url.substr(0, 2) === '//') { // 首先尝试适配HUDBT、WHU这样以相对链接开头\n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n\n      if (url.indexOf(\"https=1\") === -1) {\n        url += \"&https=1\"\n      }\n\n      return url;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"title\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/schemas/Discuz/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/\\/login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector = options.resultSelector;\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: header.length - 1,\n        // 分类\n        category: 0\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n\n        // 评论数\n        if (cell.find(\"img.comments\").length) {\n          fieldIndex.comments = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 发布时间\n        if (cell.find(\"img.time\").length) {\n          fieldIndex.time = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"img.size\").length) {\n          fieldIndex.size = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"img.seeders\").length) {\n          fieldIndex.seeders = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"img.leechers\").length) {\n          fieldIndex.leechers = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"img.snatched\").length) {\n          fieldIndex.completed = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 分类\n        if (/(cat|类型|類型|分类|分類|Тип)/gi.test(text)) {\n          fieldIndex.category = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          // 跳过字幕文件\n          if (row.find(\"a[href*='download.php?type=ass']\").length > 0) {\n            continue;\n          }\n\n          let title = row.find(\"a[href*='/forum.php?mod=viewthread']:first\");\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"img.download\").parent();\n\n          if (url.length) {\n            if (url.get(0).tagName !== \"A\") {\n              let id = link.getQueryString(\"id\");\n              url = `download.php?id=${id}`;\n            } else {\n              url = url.attr(\"href\");\n            }\n          } else {\n            let id = link.getQueryString(\"id\");\n            url = `download.php?id=${id}`;\n          }\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time:\n              fieldIndex.time == -1\n                ? \"\"\n                : this.getTime(cells.eq(fieldIndex.time)),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category)),\n            progress: Searcher.getFieldValue(site, row, \"progress\"),\n            status: Searcher.getFieldValue(site, row, \"status\")\n          };\n          results.push(data);\n        }\n\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取时间\n     * @param {*} cell\n     */\n    getTime(cell) {\n      let time = cell.find(\"span[title],time[title]\").attr(\"title\");\n      if (!time) {\n        time = $(\"<span>\")\n          .html(cell.html().replace(\"<br>\", \" \"))\n          .text();\n      }\n      return time || \"\";\n    }\n\n    /**\n     * 获取副标题\n     * @param {*} title\n     * @param {*} row\n     */\n    getSubTitle(title, row) {\n      return \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      if (link.length) {\n        result.link = link.attr(\"href\");\n        if (result.link.substr(0, 4) !== \"http\") {\n          result.link = options.site.url + result.link;\n        }\n      }\n\n      if (img.length) {\n        result.name = img.attr(\"title\") || img.attr(\"alt\");\n      } else {\n        result.name = link.text();\n      }\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/schemas/Discuz/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href*='download']\").toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let url =\n          $(item).attr(\"href\") +\n          (PTService.site.passkey ? \"&passkey=\" + PTService.site.passkey : \"\");\n        if (url) {\n          if (url.substr(0, 1) === \"/\") {\n            url = url.substr(1);\n          }\n          url = siteURL + url;\n\n          if (\n            url &&\n            url.indexOf(\"https=1\") === -1 &&\n            !PTService.site.disableHttps\n          ) {\n            url += \"&https=1\";\n          }\n        }\n        return url;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\".torrents\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/Gazelle/config.json",
    "content": "{\n  \"name\": \"Gazelle\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"securityKeyFields\": [\"authkey\", \"torrent_pass\"],\n  \"searchEntry\": [{\n    \"entry\": \"/torrents.php?searchstr=$key$&searchsubmit=1\",\n    \"name\": \"all\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/schemas/Gazelle/getSearchResult.js\",\n    \"resultSelector\": \"table.torrent_table:last > tbody > tr\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a.username[href*='user.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a.username[href*='user.php']:first\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.alert-bar > a[href*='inbox.php']\", \"div.alertbar > a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": \"div:contains('Stats') + ul.stats > li:contains('Uploaded')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Uploaded.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": \"div:contains('Stats') + ul.stats > li:contains('Downloaded')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Downloaded.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"ratio\": {\n          \"selector\": \"div:contains('Stats') + ul.stats > li:contains('Ratio:')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Ratio.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"levelName\": {\n          \"selector\": \"div:contains('Personal') + ul.stats > li:contains('Class:')\",\n          \"filters\": [\"query.text().match(/Class:.+?(.+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div:contains('Stats') + ul.stats > li:contains('Bonus Points:')\", \"div:contains('Stats') + ul.stats > li:contains('SeedBonus:')\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"query.match(/Bonus Points.+?([\\\\d.]+)/)||query.match(/SeedBonus.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"div:contains('Stats') + ul.stats > li:contains('Joined:') > span\"],\n          \"filters\": [\"query.attr('title')||query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/schemas/Gazelle/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table.torrent_table:last > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: -1,\n        size: -1,\n        seeders: -1,\n        leechers: -1,\n        completed: -1,\n        comments: -1,\n        author: -1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        const cell = header.eq(index);\n\n        // 发布时间\n        if (cell.find(\"a[href*='order_by=time']\").length) {\n          fieldIndex.time = index;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"a[href*='order_by=size']\").length) {\n          fieldIndex.size = index;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[href*='order_by=seeders']\").length) {\n          fieldIndex.seeders = index;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[href*='order_by=leechers']\").length) {\n          fieldIndex.leechers = index;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[href*='order_by=snatched']\").length) {\n          fieldIndex.completed = index;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a[href*='torrents.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row\n            .find(\"a[href*='torrents.php?action=download'][title='Download']\")\n            .first();\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          url = url.attr(\"href\");\n\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells\n                  .eq(fieldIndex.time)\n                  .find(\"span[title],time[title]\")\n                  .attr(\"title\") ||\n                cells.eq(fieldIndex.time).text() ||\n                \"\";\n          if (time) {\n            time += \":00\";\n          }\n\n          let data = {\n            title: title.text(),\n            link,\n            url: url,\n            size: cells.eq(fieldIndex.size).html() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name,\n            category: this.getCategory(cells.find(\"a[href*='filter_cat']\"))\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} link 当前列\n     */\n    getCategory(link) {\n      if (link.length == 0) {\n        return null;\n      }\n\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n\n      result.link = link.attr(\"href\");\n      let id = result.link.match(/filter_cat\\[(\\d+)\\]/)[1];\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = link.text().trim();\n\n      if (!result.name) {\n        result.name = this.getCategoryName(id);\n      }\n      return result;\n    }\n\n    getCategoryName(id) {\n      if ($.isEmptyObject(this.categories)) {\n        let cells = options.page.find(\".cat_list:first\").find(\"td\");\n        cells.each((i, dom) => {\n          let id = $(dom)\n            .find(\"input\")\n            .attr(\"id\")\n            .replace(\"cat_\", \"\");\n          let name = $(dom)\n            .find(\"label\")\n            .text();\n          if (id) {\n            this.categories[id] = name;\n          }\n        });\n      }\n\n      return this.categories ? this.categories[id] : \"\";\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/schemas/Gazelle/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[title='Download']\").toArray();\n\n      if (links.length == 0) {\n        // 排除使用免费令牌的链接,id是autofeed的a连接会带，会造成干扰\n        links = $(\n          \"a[href*='torrents.php?action=download']:not([href*='usetoken']):not([id])\"\n        ).toArray();\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table, .torrent_table tr.basic-movie-list__torrent-row\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB'),td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      let authkey = data.url.getQueryString(\"authkey\");\n      let torrent_pass = data.url.getQueryString(\"torrent_pass\");\n      // authkey=&torrent_pass\n      if (!authkey && !torrent_pass) {\n        PTService.showNotice({\n          msg: this.t(\"dropInvalidURL\") //\"无效的链接，请拖放下载链接\"\n        });\n        callback();\n        return;\n      }\n\n      data.url = this.getFullURL(data.url);\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/GazelleJSONAPI/config.json",
    "content": "{\n  \"name\": \"GazelleJSONAPI\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Gazelle/torrents.js\"]\n  }],\n  \"securityKeyFields\": [\"authkey\", \"torrent_pass\"],\n  \"searchEntryConfig\": {\n    \"page\": \"/ajax.php\",\n    \"resultType\": \"json\",\n    \"parseScriptFile\": \"/schemas/GazelleJSONAPI/getSearchResult.js\",\n    \"asyncParse\": true,\n    \"queryString\": \"action=browse&searchstr=$key$\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/ajax.php?action=index\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"response.id\"]\n        },\n        \"name\": {\n          \"selector\": [\"response.username\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"response.notifications.messages\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"response.userstats.uploaded\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"response.userstats.downloaded\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"response.userstats.ratio\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"response.userstats.class\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"response.userstats.bonusPointsPerHour\", \"response.userstats.seedingBonusPointsPerHour\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/ajax.php?action=user&id=$user.id$\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"response.stats.joinedDate\"],\n          \"filters\": [\"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"response.community.seeding\"]\n        },\n        \"uploads\": {\n          \"selector\": [\"response.community.uploaded\"]\n        },\n        \"downloads\": {\n          \"selector\": [\"response.community.snatched\"]\n        },\n        \"uniqueGroups\": {\n          \"selector\": [\"response.community.groups\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/torrents.php?type=seeding&userid=$user.id$\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"tr.torrent_row > td.number_column.nobr\"],\n          \"filters\": [\"jQuery.map(query, (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/schemas/GazelleJSONAPI/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n      this.authkey = \"\";\n      this.passkey = \"\";\n    }\n\n    start() {\n      this.getAuthKey()\n        .then(() => {\n          options.resolve(this.getResult());\n        })\n        .catch(() => {\n          options.reject({\n            success: false,\n            msg: options.searcher.getErrorMessage(\n              options.site,\n              ESearchResultParseStatus.parseError,\n              options.errorMsg\n            ),\n            data: {\n              site: options.site,\n              isLogged: options.isLogged\n            }\n          });\n        });\n    }\n\n    /**\n     * 获取搜索结果\n     */\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let groups = options.page.response.results;\n      if (groups.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return [];\n      }\n      let results = [];\n      let authkey = this.authkey;\n      let passkey = this.passkey;\n      //console.log(\"groups.length\", groups.length);\n      try {\n        groups.forEach(group => {\n          if (group.hasOwnProperty(\"torrents\")) {\n            let torrents = group.torrents;\n            torrents.forEach(torrent => {\n              let data = {\n                title:\n                  (group.artist ? group.artist + \" - \" : \"\") +\n                  group.groupName +\n                  \" / \" +\n                  group.groupYear +\n                  \" / \" +\n                  torrent.media +\n                  (group.releaseType ? \" / \" + group.releaseType : \"\") + \n                  (torrent.format ? \" / \" + torrent.format : \" / \" + torrent.codec) + \n                  (torrent.encoding ? \" / \" + torrent.encoding : \" / \" + torrent.resolution),\n                  \n                subTitle:\n                  (torrent.container ? torrent.container: \"\") + \n                  (torrent.hasLog ? `Log(${torrent.logScore})` : \"\") +\n                  (torrent.hasCue ? \" / Cue\" : \"\") +\n                  (torrent.remastered ? ` / Remaster / ${torrent.remasterYear} / ${torrent.remasterTitle}` : \"\") +\n                  (torrent.scene ? \" / Scene\" : \"\"),\n                link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${torrent.torrentId}`,\n                url: `${site.url}torrents.php?action=download&id=${torrent.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n                size: parseFloat(torrent.size),\n                time: torrent.time,\n                seeders: torrent.seeders,\n                leechers: torrent.leechers,\n                completed: torrent.snatches,\n                site: site,\n                tags: (torrent.isFreeleech || torrent.isPersonalFreeleech) ? [{name: \"Free\",color: \"blue\"}] : torrent.isNeutralLeech ? [{name: \"Neutral\",color: \"purple\"}] : [],\n                entryName: options.entry.name,\n                category: group.releaseType,\n                imdbId: group.imdbId,\n              };\n              results.push(data);\n            });\n          } else {\n            let data = {\n              title: group.groupName,\n              link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${group.torrentId}`,\n              url: `${site.url}torrents.php?action=download&id=${group.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n              size: parseFloat(group.size),\n              time: group.groupTime,\n              author: \"\",\n              seeders: group.seeders,\n              leechers: group.leechers,\n              completed: group.snatches,\n              comments: 0,\n              site: site,\n              tags: group.tags,\n              entryName: options.entry.name,\n              category: group.category,\n              imdbId: group.imdbId,\n            };\n            results.push(data);\n          }\n        });\n        console.log(\"results.length\", results.length);\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n\n    /**\n     * 获取 AuthKey ，用于组合完整的下载链接\n     */\n    getAuthKey() {\n      const url = (options.site.activeURL + \"/ajax.php?action=index\")\n        .replace(\"://\", \"****\")\n        .replace(/\\/\\//g, \"/\")\n        .replace(\"****\", \"://\");\n\n      return new Promise((resolve, reject) => {\n        $.get(url)\n          .done(result => {\n            if (result && result.status === \"success\" && result.response) {\n              this.authkey = result.response.authkey;\n              this.passkey = result.response.passkey;\n              resolve();\n            } else {\n              reject();\n            }\n          })\n          .fail(() => {\n            reject();\n          });\n      });\n    }\n  }\n\n  let parser = new Parser(options);\n  parser.start();\n})(options);\n"
  },
  {
    "path": "resource/schemas/NexusPHP/common.js",
    "content": "(function($, window) {\n  class Common {\n    constructor() {\n      this.siteContentMenus = {};\n      this.clientContentMenus = [];\n      this.defaultPath = PTService.getSiteDefaultPath();\n      this.downloadClientType = PTService.downloadClientType;\n      this.defaultClientOptions = PTService.getClientOptions();\n      this.currentURL = location.href;\n    }\n\n    /**\n     * 获取指定key的当前语言内容\n     * @param {*} key\n     * @param {*} options\n     */\n    t(key, options) {\n      return PTService.i18n.t(key, options);\n    }\n\n    /**\n     * 初始化当前默认服务器可用空间\n     */\n    initFreeSpaceButton() {\n      if (!this.defaultPath) {\n        return;\n      }\n      PTService.call(PTService.action.getFreeSpace, {\n        path: this.defaultPath,\n        clientId: PTService.site.defaultClientId\n      })\n        .then(result => {\n          console.log(\"命令执行完成\", result);\n          if (result && result.arguments) {\n            // console.log(PTService.filters.formatSize(result.arguments[\"size-bytes\"]));\n\n            PTService.addButton({\n              title: this.t(\"buttons.freeSpaceTip\", {\n                path: this.defaultPath,\n                interpolation: { escapeValue: false }\n              }), // \"默认服务器剩余空间\\n\" + this.defaultPath,\n              icon: \"filter_drama\",\n              label: PTService.filters.formatSize(\n                result.arguments[\"size-bytes\"]\n              )\n            });\n          }\n          // success();\n        })\n        .catch(() => {\n          // error()\n        });\n    }\n\n    /**\n     * 初始化种子详情页面按钮\n     */\n    initDetailButtons() {\n      // 添加下载按钮\n      this.addSendTorrentToDefaultClientButton();\n\n      // 添加下载到按钮\n      this.addSendTorrentToClientButton();\n\n      // 添加复制下载链接按钮\n      this.addCopyTextToClipboardButton();\n\n      // 初始化可用空间按钮\n      this.initFreeSpaceButton();\n\n      // 初始化收藏按钮\n      this.initCollectionButton();\n\n      // 初始化说谢谢按钮\n      this.initSayThanksButton();\n      if(document.domain.match(\"keepfrds.com\")){$(\".pt-plugin-body\").css(\"z-index\",\"39\")}\n    }\n\n    /**\n     * 初始化种子列表页面按钮\n     */\n    initListButtons(checkPasskey = false) {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title: this.t(\"buttons.downloadAllTip\", {\n            name: this.defaultClientOptions.name\n          }), //`将当前页面所有种子下载到[${this.defaultClientOptions.name}]`,\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadAll\"), //\"下载所有\",\n          click: (success, error) => {\n            if (checkPasskey && !PTService.site.passkey) {\n              error(\"请先设置站点密钥（Passkey）。\");\n              return;\n            }\n            this.startDownloadURLs(success, error);\n          }\n        });\n\n      // 添加下载到按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadAllToTip\"), //`将当前页面所有种子下载到指定服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadAllTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            // \"请先设置站点密钥（Passkey）。\"\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          this.showAllContentMenus(event.originalEvent, success, error);\n        },\n        onDrop: (data, event, success, error) => {\n          console.log(data);\n          let url = this.getDroperURL(data.url);\n          console.log(url);\n          this.showContentMenusForUrl(\n            {\n              url,\n              title: data.title,\n              link: data.url\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyAllToClipboardTip\"), // \"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyAllToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, urls.join(\"\\n\"))\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(() => {\n              error();\n            });\n        },\n        onDrop: (data, event, success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let url = this.getDroperURL(data.url);\n          url &&\n            PTService.call(PTService.action.copyTextToClipboard, url)\n              .then(result => {\n                console.log(\"命令执行完成\", result);\n                success();\n              })\n              .catch(() => {\n                error();\n              });\n        }\n      });\n\n      // 检查是否有下载管理权限\n      this.checkPermissions([\"downloads\"])\n        .then(() => {\n          this.addSaveAllTorrentFilesButton(checkPasskey);\n        })\n        .catch(() => {\n          PTService.addButton({\n            title: this.t(\"buttons.needAuthorizationTip\"), //\"下载所有种子文件功能需要权限，点击前往授权\",\n            icon: \"verified_user\",\n            key: \"requestPermissions\",\n            label: this.t(\"buttons.needAuthorization\"), //\"需要授权\",\n            click: (success, error) => {\n              PTService.call(PTService.action.openOptions, \"set-permissions\");\n              success();\n            }\n          });\n        });\n    }\n\n    /**\n     * 添加下载所有种子文件按钮\n     * @param {*} checkPasskey\n     */\n    addSaveAllTorrentFilesButton(checkPasskey) {\n      // 批量下载当前页种子文件\n      PTService.addButton({\n        title: this.t(\"buttons.saveAllTorrentTip\"), //\"下载所有种子文件\",\n        icon: \"save\",\n        label: this.t(\"buttons.saveAllTorrent\"), //\"所有种子\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          let downloads = [];\n          urls.forEach(url => {\n            downloads.push({\n              url,\n              method: PTService.site.downloadMethod\n            });\n          });\n\n          console.log(downloads);\n\n          PTService.call(PTService.action.addBrowserDownloads, downloads)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(e => {\n              console.log(e);\n              error(e);\n            });\n        }\n      });\n    }\n\n    checkPermissions(permissions) {\n      return PTService.call(PTService.action.checkPermissions, permissions);\n    }\n\n    /**\n     * 发送种子到默认下载服务器\n     * @param {string} url\n     */\n    sendTorrentToDefaultClient(option, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof option === \"string\") {\n          option = {\n            url: option,\n            title: \"\"\n          };\n        }\n\n        let savePath = PTService.pathHandler.getSavePath(\n          this.defaultPath,\n          PTService.site\n        );\n\n        if (savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToDefaultClient, {\n          url: option.url,\n          title: option.title,\n          savePath: savePath,\n          autoStart: this.defaultClientOptions.autoStart,\n          tagIMDb: this.defaultClientOptions.tagIMDb,\n          link: option.link,\n          imdbId: option.imdbId\n        })\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 隐藏指定的 notice\n     * @param notice\n     */\n    hideNotice(notice) {\n      if (!notice) return;\n      if (notice.id && notice.close) {\n        notice.close();\n      } else if (notice.hide) {\n        notice.hide();\n      }\n    }\n\n    /**\n     * 发送种子到指定下载服务器\n     * @param {string} url\n     */\n    sendTorrentToClient(options, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof options === \"string\") {\n          options = {\n            url: options,\n            title: \"\"\n          };\n        }\n\n        if (!options.clientId) {\n          // \"无效的下载服务器\"\n          reject(this.t(\"invalidDownloadServer\"));\n          return;\n        }\n\n        options.savePath = PTService.pathHandler.getSavePath(\n          options.savePath,\n          PTService.site\n        );\n        if (options.savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToClient, options)\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\",\n          link: data\n        };\n      }\n\n      data.url = this.getDroperURL(data.url);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    isNexusPHP() {\n      return PTService.site.schema == \"NexusPHP\";\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (url && url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url && url.substr(0, 4) !== \"http\") {\n        if (url.substr(0, 1) == \"/\") {\n          url = url.substr(1);\n        }\n        url = `${siteURL}${url}`;\n      }\n\n      return url;\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 添加下载到指定下载服务器按钮\n     */\n    addSendTorrentToClientButton() {\n      // 添加下载按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadToTip\"), //`将当前种子下载到指定的服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = document.title;\n          }\n\n          this.showContentMenusForUrl(\n            {\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n    }\n\n    /**\n     * 添加一键下载按钮\n     */\n    addSendTorrentToDefaultClientButton() {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title:\n            this.t(\"buttons.downloadToDefaultTip\", {\n              name: this.defaultClientOptions.name\n            }) + (this.defaultPath ? \"\\n\" + this.defaultPath : \"\"), //`将当前种子下载到[${this.defaultClientOptions.name}]` +\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadToDefault\"), //\"一键下载\",\n          /**\n           * 单击事件\n           * @param success 成功回调事件\n           * @param error 失败回调事件\n           *\n           * 两个事件必需执行一个，可以传递一个参数\n           */\n          click: (success, error) => {\n            // getDownloadURL 方法由继承者提供\n            if (!this.getDownloadURL) {\n              // \"getDownloadURL 方法未定义\"\n              error(this.t(\"getDownloadURLisUndefined\"));\n              return;\n            }\n\n            let url = this.getDownloadURL();\n\n            if (!url) {\n              // \"获取下载链接失败\"\n              error(this.t(\"getDownloadURLFailed\"));\n              return;\n            }\n\n            let title = \"\";\n\n            if (this.getTitle) {\n              title = this.getTitle();\n            } else {\n              title = document.title;\n            }\n\n            this.sendTorrentToDefaultClient({\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            })\n              .then(() => {\n                success();\n              })\n              .catch(result => {\n                error(result);\n              });\n          }\n        });\n    }\n\n    /**\n     * 添加复制下载链接按钮\n     */\n    addCopyTextToClipboardButton() {\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyToClipboardTip\"), //\"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          console.log(PTService.site, this.defaultPath);\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, url)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(result => {\n              error(result);\n            });\n        }\n      });\n    }\n\n    /**\n     * 初始化收藏按钮\n     */\n    initCollectionButton() {\n      // 获取收藏情况\n      PTService.call(PTService.action.getTorrentCollention, location.href)\n        .then(result => {\n          this.addRemoveCollectionButton(result);\n        })\n        .catch(() => {\n          this.addToCollectionButton();\n        });\n    }\n\n    /**\n     * 添加收藏按钮\n     */\n    addToCollectionButton() {\n      PTService.removeButton(\"removeFromCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.addToCollection\"),\n        icon: \"favorite_border\",\n        label: this.t(\"buttons.addToCollection\"),\n        key: \"addToCollection\",\n        click: (success, error) => {\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = PTService.getFieldValue(\"title\");\n          }\n\n          if (!title) {\n            title = $(\"title:first\").text();\n          }\n\n          let imdbId = PTService.getFieldValue(\"imdbId\");\n\n          if (!imdbId) {\n            const link = $(\"a[href*='www.imdb.com/title/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n              if (match && match.length >= 2) {\n                imdbId = match[1];\n              }\n            }\n          }\n\n          let doubanId = PTService.getFieldValue(\"doubanId\");\n\n          if (!doubanId) {\n            const link = $(\"a[href*='movie.douban.com/subject/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/subject\\/(\\d+)/);\n\n              if (match && match.length >= 2) {\n                doubanId = match[1];\n              }\n            }\n          }\n\n          const data = {\n            title: title,\n            url: this.getDownloadURL(),\n            link: location.href,\n            host: location.host,\n            size: PTService.getFieldValue(\"size\"),\n            subTitle: PTService.getFieldValue(\"subTitle\"),\n            movieInfo: {\n              imdbId: imdbId,\n              doubanId: doubanId\n            }\n          };\n\n          PTService.call(PTService.action.addTorrentToCollection, data)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addRemoveCollectionButton(data);\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 添加移除收藏按钮\n     */\n    addRemoveCollectionButton(item) {\n      PTService.removeButton(\"addToCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.removeFromCollection\"),\n        icon: \"favorite\",\n        label: this.t(\"buttons.removeFromCollection\"),\n        key: \"removeFromCollection\",\n        click: (success, error) => {\n          PTService.call(PTService.action.deleteTorrentFromCollention, item)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addToCollectionButton();\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 根据指定的URL获取可用的下载目录及客户端信息\n     * @param url\n     */\n    getContentMenusForUrl(url) {\n      let urlParser = PTService.filters.parseURL(url);\n      if (!urlParser.host) {\n        return [];\n      }\n      let results = [];\n      let clients = [];\n      let site = PTService.getSiteFromHost(urlParser.host);\n      if (!site) {\n        return [];\n      }\n      let host = site.host;\n\n      if (this.siteContentMenus[host]) {\n        return this.siteContentMenus[host];\n      }\n\n      /**\n       * 增加下载目录\n       * @param paths\n       * @param client\n       */\n      function pushPath(paths, client) {\n        paths.forEach(path => {\n          results.push({\n            client: client,\n            path: path,\n            host: host\n          });\n        });\n      }\n\n      PTService.options.clients.forEach(client => {\n        clients.push({\n          client: client,\n          path: \"\",\n          host: host\n        });\n\n        if (client.paths) {\n          // 根据已定义的路径创建菜单\n          for (const _host in client.paths) {\n            let paths = client.paths[host];\n\n            if (_host !== host) {\n              continue;\n            }\n\n            pushPath(paths, client);\n          }\n\n          // 最后添加当前客户端适用于所有站点的目录\n          let publicPaths = client.paths[PTService.allSiteKey];\n          if (publicPaths) {\n            if (results.length > 0) {\n              results.push({});\n            }\n\n            pushPath(publicPaths, client);\n          }\n        }\n      });\n\n      if (results.length > 0) {\n        clients.splice(0, 0, {});\n      }\n\n      results = results.concat(clients);\n\n      this.siteContentMenus[host] = results;\n\n      return results;\n    }\n\n    /**\n     * 显示指定链接的下载服务器及目录菜单\n     * @param options\n     * @param event\n     */\n    showContentMenusForUrl(options, event, success, error) {\n      let items = this.getContentMenusForUrl(options.url);\n      let menus = [];\n\n      items.forEach(item => {\n        if (item.client && item.client.name) {\n          menus.push({\n            title:\n              this.t(\"buttons.menuDownloadTo\", {\n                server: `${item.client.name} -> ${item.client.address}`\n              }) + //`下载到：${item.client.name} -> ${item.client.address}` +\n              (item.path\n                ? ` -> ${PTService.pathHandler.replacePathKey(\n                    item.path,\n                    PTService.site\n                  )}`\n                : \"\"),\n            fn: () => {\n              if (options.url) {\n                // console.log(options, item);\n                this.sendTorrentToClient({\n                  clientId: item.client.id,\n                  url: options.url,\n                  title: options.title,\n                  savePath: item.path,\n                  autoStart: item.client.autoStart,\n                  tagIMDb: item.client.tagIMDb,\n                  link: options.link,\n                  imdbId: options.imdbId\n                })\n                  .then(result => {\n                    success();\n                  })\n                  .catch(result => {\n                    error(result);\n                  });\n              }\n            }\n          });\n        } else {\n          menus.push({});\n        }\n      });\n\n      console.log(items, menus);\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 验证指定元素的大小信息\n     * @param {*} doms\n     */\n    checkSize(doms) {\n      if (!PTService.options.needConfirmWhenExceedSize) {\n        return true;\n      }\n      // 获取所有种子的大小信息\n      let size = this.getTotalSize(doms);\n\n      let exceedSize = 0;\n      switch (PTService.options.exceedSizeUnit) {\n        //\n        case PTService.sizeUnit.MiB:\n          exceedSize = PTService.options.exceedSize * 1048576;\n          break;\n\n        case PTService.sizeUnit.GiB:\n          exceedSize = PTService.options.exceedSize * 1073741824;\n          break;\n\n        case \"T\":\n        case PTService.sizeUnit.TiB:\n          exceedSize = PTService.options.exceedSize * 1099511627776;\n          break;\n      }\n\n      return size >= exceedSize ? PTService.filters.formatSize(size) : true;\n    }\n\n    /**\n     *\n     * @param {*} source\n     */\n    getTotalSize(source) {\n      let total = 0;\n\n      $.each(source, (index, item) => {\n        total += this.getSize($(item).text());\n      });\n\n      return total;\n    }\n\n    /**\n     * @return {number}\n     */\n    getSize(size) {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size.match(\n        /^(\\d*\\.?\\d+)(.*[^TGMK])?([TGMK](B|iB){0,1})$/i\n      );\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Ti?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    }\n\n    /**\n     * 种子大小超限时确认\n     */\n    confirmSize(doms) {\n      let size = this.checkSize(doms);\n\n      if (size !== true) {\n        let content = this.t(\"exceedSizeConfirm\", {\n          size,\n          exceedSize: PTService.options.exceedSize,\n          exceedSizeUnit: PTService.options.exceedSizeUnit\n        });\n        if (!confirm(content)) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    /**\n     * 准备开始批量下载\n     * @param {*} success\n     * @param {*} error\n     * @param {*} downloadOptions\n     */\n    startDownloadURLs(success, error, downloadOptions) {\n      if (this.confirmWhenExceedSize) {\n        if (!this.confirmWhenExceedSize()) {\n          // \"容量超限，已取消\"\n          error(this.t(\"exceedSizeCanceled\"));\n          return;\n        }\n      }\n\n      if (!this.getDownloadURLs) {\n        // \"getDownloadURLs 方法未定义\"\n        error(this.t(\"getDownloadURLsisUndefined\"));\n        return;\n      }\n\n      let urls = this.getDownloadURLs();\n      if (!urls.length || typeof urls == \"string\") {\n        error(urls);\n        return;\n      }\n\n      // 是否启用后台下载任务\n      if (PTService.options.enableBackgroundDownload) {\n        this.downloadURLsInBackground(\n          urls,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      } else {\n        this.downloadURLs(\n          urls,\n          urls.length,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      }\n    }\n\n    downloadURLsInBackground(urls, callback, downloadOptions) {\n      const items = [];\n\n      const savePath = downloadOptions\n        ? PTService.pathHandler.getSavePath(\n            downloadOptions.savePath || downloadOptions.path,\n            PTService.site\n          )\n        : \"\";\n\n      urls.forEach(url => {\n        if (downloadOptions) {\n          items.push({\n            clientId: downloadOptions.client.id,\n            url,\n            savePath,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb\n          });\n        } else {\n          items.push({\n            url\n          });\n        }\n      });\n\n      PTService.call(PTService.action.sendTorrentsInBackground, items)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 批量下载指定的URL\n     * @param {*} urls\n     * @param {*} count\n     * @param {*} callback\n     * @param {*} downloadOptions 下载选项，如不指定，则发送至默认下载服务器\n     */\n    downloadURLs(urls, count, callback, downloadOptions) {\n      let index = count - urls.length;\n      let url = urls.shift();\n      if (!url) {\n        $(this.statusBar).remove();\n        this.statusBar = null;\n        // count + \"条链接已发送完成。\"\n        callback(\n          this.t(\"downloadURLsFinished\", {\n            count\n          })\n        );\n        return;\n      }\n\n      this.showStatusMessage(\n        this.t(\"downloadURLsTip\", {\n          text:\n            url.replace(PTService.site.passkey, \"***\") +\n            \"(\" +\n            (count - index) +\n            \"/\" +\n            count +\n            \")\"\n        }),\n        0\n      );\n\n      if (!downloadOptions) {\n        this.sendTorrentToDefaultClient(url, false)\n          .then(result => {\n            this.downloadURLs(urls, count, callback);\n          })\n          .catch(result => {\n            this.downloadURLs(urls, count, callback);\n          });\n      } else {\n        this.sendTorrentToClient(\n          {\n            clientId: downloadOptions.client.id,\n            url: url,\n            title: \"\",\n            savePath: downloadOptions.path,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb,\n            imdbId: downloadOptions.imdbId\n          },\n          false\n        )\n          .finally(() => {\n            // 是否设置了时间间隔\n            if (PTService.options.batchDownloadInterval > 0) {\n              setTimeout(() => {\n                this.downloadURLs(urls, count, callback, downloadOptions);\n              }, PTService.options.batchDownloadInterval * 1000);\n            } else {\n              this.downloadURLs(urls, count, callback, downloadOptions);\n            }\n          })\n          .catch(error => {\n            console.log(error);\n          });\n      }\n    }\n\n    showStatusMessage(msg) {\n      if (!this.statusBar) {\n        this.statusBar = PTService.showNotice({\n          text: msg,\n          type: \"info\",\n          width: 600,\n          progressBar: false\n        });\n      } else {\n        this.statusBar.find(\".noticejs-content\").html(msg);\n      }\n    }\n\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */\n    clone(source) {\n      return JSON.parse(JSON.stringify(source));\n    }\n\n    /**\n     * 显示批量下载时可用下载服务器菜单\n     * @param event\n     */\n    showAllContentMenus(event, success, error) {\n      let clients = [];\n      let menus = [];\n      let _this = this;\n\n      function addMenu(item) {\n        let title = _this.t(\"buttons.menuDownloadTo\", {\n          server: `${item.client.name} -> ${item.client.address}`\n        }); //`下载到：${item.client.name} -> ${item.client.address}`;\n        if (item.path) {\n          title += ` -> ${PTService.pathHandler.replacePathKey(\n            item.path,\n            PTService.site\n          )}`;\n        }\n        menus.push({\n          title: title,\n          fn: () => {\n            // 克隆是为了多次选择时，不覆盖原来的值\n            let _item = PPF.clone(item);\n            console.log(item);\n            let savePath = PTService.pathHandler.getSavePath(\n              _item.path,\n              PTService.site\n            );\n            if (savePath === false) {\n              // \"用户取消操作\"\n              error(_this.t(\"userCanceled\"));\n              return;\n            }\n            _item.path = savePath;\n            _this.startDownloadURLs(success, error, _item);\n          }\n        });\n      }\n\n      if (this.clientContentMenus.length == 0) {\n        PTService.options.clients.forEach(client => {\n          clients.push({\n            client: client,\n            path: \"\"\n          });\n        });\n        clients.forEach(item => {\n          if (item.client && item.client.name) {\n            addMenu(item);\n\n            if (item.client.paths) {\n              // 添加适用于所有站点的目录\n              let publicPaths = item.client.paths[PTService.allSiteKey];\n              if (publicPaths) {\n                publicPaths.forEach(path => {\n                  let _item = this.clone(item);\n                  _item.path = path;\n                  addMenu(_item);\n                });\n              }\n            }\n          } else {\n            menus.push({});\n          }\n        });\n        this.clientContentMenus = menus;\n      } else {\n        menus = this.clientContentMenus;\n      }\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      if (!url) {\n        return \"\";\n      }\n      if (url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 初始化说谢谢按钮\n     */\n    initSayThanksButton() {\n      let sayThanksButton = PTService.getFieldValue(\"sayThanksButton\");\n      console.log(\"sayThanksButton\");\n      if (sayThanksButton && sayThanksButton.length) {\n        // 说谢谢\n        PTService.addButton({\n          title: this.t(\"buttons.sayThanksTip\"),\n          icon: \"thumb_up\",\n          label: this.t(\"buttons.sayThanks\"),\n          key: \"sayThanks\",\n          click: (success, error) => {\n            sayThanksButton.click();\n            success();\n            setTimeout(() => {\n              PTService.removeButton(\"sayThanks\");\n            }, 1000);\n          }\n        });\n      }\n    }\n  }\n\n  window.NexusPHPCommon = Common;\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/NexusPHP/config.json",
    "content": "{\n  \"name\": \"NexusPHP\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\", \"/plugin_details.php\", \"\\/t-\\\\d+\"],\n    \"scripts\": [\"common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\", \"/music.php\", \"/movie.php\"],\n    \"scripts\": [\"common.js\", \"torrents.js\"]\n  }],\n  \"securityKeyFields\": [\"passkey\"],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"queryString\": \"search=$key$&notnewword=1\",\n    \"area\": [{\n      \"name\": \"标题\",\n      \"appendQueryString\": \"&search_area=0\"\n    }, {\n      \"name\": \"简介\",\n      \"appendQueryString\": \"&search_area=1\"\n    }, {\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"appendQueryString\": \"&search_area=4\"\n    }],\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n    \"resultSelector\": \"table.torrents:last\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"checker\": {\n    \"isLogin\": {\n      \"page\": \"/usercp.php\",\n      \"contains\": \"logout.php\"\n    }\n  },\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img.pro_free, .free_bg, font.free\"\n  }, {\n    \"name\": \"2xFree\",\n    \"selector\": \"img.pro_free2up, .twoupfree_bg, font.twoupfree\"\n  }, {\n    \"name\": \"2xUp\",\n    \"selector\": \"img.pro_2up, .twoup_bg, font.twoup\"\n  }, {\n    \"name\": \"2x50%\",\n    \"selector\": \"img.pro_50pctdown2up, .twouphalfdown_bg, font.twouphalfdown\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"img.pro_30pctdown, .thirtypercentdown_bg, font.thirtypercent\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img.pro_50pctdown, .halfdown_bg, font.halfdown\"\n  }, {\n    \"name\": \"⛔️\",\n    \"selector\": \"span.tags.tjz, span.tag.tag-dz, span.tag.tag-limited\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='userdetails.php'][class*='Name']:first\", \"a[href*='userdetails.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails.php'][class*='Name']:first\", \"a[href*='userdetails.php']:first\"],\n          \"filters\": [\"query && query.attr('href').getQueryString('id') > 0 ? query.text(): ''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='usercp.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background: red'] a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\", \"td.rowfollow:contains('分享率')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(上[传傳]量|Uploaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==3)?(query[2]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\", \"td.rowfollow:contains('分享率')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(下[载載]量|Downloaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==3)?(query[2]).sizeToNumber():0\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('等级')\", \"td.rowhead:contains('等級')\", \"td.rowhead:contains('Class')\"],\n          \"filters\": [\"query.next().find('img').attr('title')\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('魔力') + td\", \"td.rowhead:contains('Karma'):contains('Points') + td\", \"td.rowhead:contains('麦粒') + td\", \"td.rowfollow:contains('魔力值')\", \"td.rowhead:contains('bonus') + td\"],\n          \"filters\": [\"query.is(\\\":contains('魔力值:')\\\")||query.is(\\\":contains('Bonus Points:')\\\")?query.text().replace(/,/g,'').match(/(?:魔力值|Bonus Points).+?([\\\\d.]+)/)[1]:query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        },\n        \"seedingPoints\": {\n          \"selector\": [\"td.rowhead:contains('做种积分') + td\", \"td.rowhead:contains('Seeding Points') + td\", \"td.rowhead:contains('做種積分') + td\", \"td.rowhead:contains('保种积分') + td\", \"td.rowfollow:contains('做种积分')\", \"td.rowfollow:contains('Seeding Points')\", \"td.rowfollow:contains('做種積分')\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'')\",\n            \"query.includes('做种积分') || query.includes('做種積分') || query.includes('Seeding Points') ? query.match(/(做种积分|做種積分|Seeding Points).+?[\\\\d.]+/g)[0] : query\",\n            \"query ? parseFloat(query.match(/[\\\\d.]+/)[0]) : null\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('加入日期')\", \"td.rowhead:contains('Join'):contains('date')\"],\n          \"filters\": [\"query.next().text().split(' (')[0].trim()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/mybonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"div:contains('你当前每小时能获取'):last\", \"div:contains('You are currently getting'):last\", \"div:contains('你當前每小時能獲取'):last\"],\n          \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"prerequisites\": \"!user.seeding\",\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"query.find('td.rowfollow:eq(2)').length != 0 ? query.find('td.rowfollow:eq(2)').length : query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"query.find('td.rowfollow:eq(2)').length != 0 ? jQuery.map(query.find('td.rowfollow:eq(2)'), (item)=>{return $(item).text();}) : jQuery.map(query.find('td:eq(2)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"b:contains('大小'):first\"],\n          \"filters\": [\"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"imdbId\": {\n          \"selector\": [\"a[href*='www.imdb.com/title/']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query.match(/(tt\\\\d+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"input#saythanks:not(:disabled)\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    },\n    \"/plugin_details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"td.rowtitle:contains('大小：'):first\"],\n          \"filters\": [\"query.next().text()\", \"(query && query.length>1)?query.sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"input#saythanks:not(:disabled)\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/schemas/NexusPHP/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 通过尝试分析 href 获取真正下载链接\n     */\n\n    _getDownloadUrlByPossibleHrefs() {\n      const possibleHrefs = [\n        // pthome\n        \"a[href*='downhash'][href*='https'][class!='forward_a']\",\n        // hdchina\n        \"a[href*='hash'][href*='https'][class!='forward_a']\",\n        // misc\n        \"a[href*='passkey'][href*='https'][class!='forward_a']\",\n        \"a[href*='passkey'][class!='forward_a']\"\n      ];\n\n      for (const href of possibleHrefs) {\n        const query = $(href);\n        if (query.length) {\n          return query.attr(\"href\");\n        }\n      }\n      return null;\n    }\n\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let url = PTService.getFieldValue(\"downloadURL\");\n      if (!url) {\n\n        url = this._getDownloadUrlByPossibleHrefs();\n\n        if (!url) {\n          url =\n            $(\"td.rowfollow:contains('&passkey='):last\").text() ||\n            $(\"a[href*='download'][href*='?id']:first\").attr(\"href\") ||\n            $(\"a[href*='download.php?']:first\").attr(\"href\");\n        }\n\n        // 如果链接地址中不包含passkey，且站点已配置 passkey 信息\n        // 则尝试 passkey 来生成下载链接\n        if (!(url + \"\").getQueryString(\"passkey\") && PTService.site.passkey) {\n          let id = location.href.getQueryString(\"id\");\n          if (id) {\n            // 如果站点没有配置禁用https，则默认添加https链接\n            return (\n              location.origin +\n              \"/download.php?id=\" +\n              id +\n              \"&passkey=\" +\n              PTService.site.passkey +\n              (PTService.site.disableHttps ? \"\" : \"&https=1\")\n            );\n          }\n        }\n\n        if (!url) {\n          return \"\";\n        }\n      }\n\n      return this.getFullURL(url);\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"title\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n    \n    /**\n     * 获取当前种子IMDb Id\n     */\n    getIMDbId() {\n      try\n      {\n        let imdbId = PTService.getFieldValue(\"imdbId\");\n        console.log(imdbId);\n        if (imdbId)\n          return imdbId;\n        else {\n          const link = $(\"a[href*='www.imdb.com/title/']:first\");\n          if (link.length > 0) {\n            let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n            if (match && match.length >= 2)\n              return imdbId = match[1];\n\n          }\n        }\n      } catch {\n      }\n      return null;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/NexusPHP/getSearchResult.js",
    "content": "/**\r\n * NexusPHP 默认搜索结果解析类\r\n */\r\n(function (options, Searcher) {\r\n  class Parser {\r\n    constructor() {\r\n      this.haveData = false;\r\n      if (/takelogin\\.php|<form action=\"\\?returnto=/.test(options.responseText)) {\r\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\r\n        return;\r\n      }\r\n\r\n      options.isLogged = true;\r\n\r\n      if (\r\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\r\n          options.responseText\r\n        )\r\n      ) {\r\n        options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\r\n        return;\r\n      }\r\n\r\n      this.haveData = true;\r\n      this.site = options.site;\r\n    }\r\n\r\n    /**\r\n     * 获取搜索结果\r\n     */\r\n    getResult() {\r\n      if (!this.haveData) {\r\n        return [];\r\n      }\r\n      let site = options.site;\r\n      let site_url_help = PTServiceFilters.parseURL(site.url);\r\n      let selector = options.resultSelector || \"table.torrents:last\";\r\n      selector = selector.replace(\"> tbody > tr\", \"\");\r\n      let table = options.page.find(selector);\r\n      // 获取种子列表行\r\n      let rows = table.find(\"> tbody > tr\");\r\n      if (rows.length == 0) {\r\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\r\n        return [];\r\n      }\r\n      let results = [];\r\n      // 获取表头\r\n      let header = table.find(\"> thead > tr > th\");\r\n      let beginRowIndex = 0;\r\n      if (header.length == 0) {\r\n        beginRowIndex = 1;\r\n        header = rows.eq(0).find(\"th,td\");\r\n      }\r\n\r\n      // 用于定位每个字段所列的位置\r\n      let fieldIndex = {\r\n        // 发布时间\r\n        time: -1,\r\n        // 大小\r\n        size: -1,\r\n        // 上传数量\r\n        seeders: -1,\r\n        // 下载数量\r\n        leechers: -1,\r\n        // 完成数量\r\n        completed: -1,\r\n        // 评论数量\r\n        comments: -1,\r\n        // 发布人\r\n        author: header.length - 1,\r\n        // 分类\r\n        category: -1\r\n      };\r\n\r\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\r\n        site.url += \"/\";\r\n      }\r\n      //2023.5.10 fix byr.pt 不显示数据，下列div.icons.*是为了单独适配\r\n      // 获取字段所在的列\r\n      for (let index = 0; index < header.length; index++) {\r\n        let cell = header.eq(index);\r\n        let text = cell.text();\r\n\r\n        // 评论数\r\n        if (cell.find(\".comments\").length) {\r\n          fieldIndex.comments = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 发布时间\r\n        if (cell.find(\"img.time,div.date,div.icons.time\").length) {\r\n          fieldIndex.time = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 大小\r\n        if (cell.find(\"img.size,div[alt='size'],div.icons.size\").length) {\r\n          fieldIndex.size = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 种子数\r\n        if (cell.find(\"img.seeders,div[alt='seeders'],div.icons.seeders\").length) {\r\n          fieldIndex.seeders = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 下载数\r\n        if (cell.find(\"img.leechers,div[alt='leechers'],div.icons.leechers\").length) {\r\n          fieldIndex.leechers = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 完成数\r\n        if (cell.find(\"img.snatched,div[alt='snatched'],div.icons.snatched\").length) {\r\n          fieldIndex.completed = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 分类\r\n        if (/(cat|类型|類型|分类|分類|Тип)/gi.test(text)) {\r\n          fieldIndex.category = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n      }\r\n\r\n      if (options.entry.fieldIndex) {\r\n        fieldIndex = Object.assign(fieldIndex, options.entry.fieldIndex);\r\n      }\r\n\r\n      try {\r\n        // 遍历数据行\r\n        for (let index = beginRowIndex; index < rows.length; index++) {\r\n          const row = rows.eq(index);\r\n\r\n          // FIX https://github.com/pt-plugins/PT-Plugin-Plus/issues/347\r\n          row.attr('id') === 'zhiding' && row.removeAttr('id');\r\n\r\n          let cells = row.find(\">td\");\r\n\r\n          let title = this.getTitle(row, cells, fieldIndex);\r\n\r\n          // 没有获取标题时，继续下一个\r\n          if (title.length == 0) {\r\n            continue;\r\n          }\r\n          let link = title.attr(\"href\");\r\n          if (link && link.substr(0, 2) === \"//\") {\r\n            // 适配HUDBT、WHU这样以相对链接开头\r\n            link = `${site_url_help.protocol}://${link}`;\r\n          } else if (link && link.substr(0, 4) !== \"http\") {\r\n            link = `${site.url}${link}`;\r\n          }\r\n\r\n          // 获取下载链接\r\n          let url = this.getDownloadLink(row, link);\r\n          if (url && url.substr(0, 2) === \"//\") {\r\n            // 适配HUDBT、WHU这样以相对链接开头\r\n            url = `${site_url_help.protocol}://${url}`;\r\n          } else if (url && url.substr(0, 4) !== \"http\") {\r\n            url = `${site.url}${url}`;\r\n          }\r\n\r\n          if (!url) {\r\n            continue;\r\n          }\r\n\r\n          url = url +\r\n            (site && site.passkey ? \"&passkey=\" + site.passkey : \"\");\r\n\r\n          let data = {\r\n            title: title.attr(\"title\") || title.text(),\r\n            subTitle: this.getSubTitle(title, row),\r\n            link,\r\n            url,\r\n            size: this.getFieldValue(row, cells, fieldIndex, \"size\") || 0,\r\n            time:\r\n              fieldIndex.time == -1\r\n                ? \"\"\r\n                : this.getTime(cells.eq(fieldIndex.time)),\r\n            author: this.getFieldValue(row, cells, fieldIndex, \"author\") || \"\",\r\n            seeders: this.getFieldValue(row, cells, fieldIndex, \"seeders\") || 0,\r\n            leechers:\r\n              this.getFieldValue(row, cells, fieldIndex, \"leechers\") || 0,\r\n            completed:\r\n              this.getFieldValue(row, cells, fieldIndex, \"completed\") || 0,\r\n            comments:\r\n              this.getFieldValue(row, cells, fieldIndex, \"comments\") || 0,\r\n            site: site,\r\n            tags: Searcher.getRowTags(this.site, row),\r\n            entryName: options.entry.name,\r\n            category:\r\n              fieldIndex.category == -1\r\n                ? null\r\n                : this.getFieldValue(row, cells, fieldIndex, \"category\") ||\r\n                this.getCategory(cells.eq(fieldIndex.category)),\r\n            progress: Searcher.getFieldValue(site, row, \"progress\"),\r\n            status: Searcher.getFieldValue(site, row, \"status\"),\r\n            imdbId: this.getIMDbId(row)\r\n          };\r\n\r\n          results.push(data);\r\n        }\r\n      } catch (error) {\r\n        options.status = ESearchResultParseStatus.parseError;\r\n        options.errorMsg = error.stack;\r\n        //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\r\n      }\r\n\r\n      return results;\r\n    }\r\n\r\n    /**\r\n     * 获取指定字段内容\r\n     * @param {*} row\r\n     * @param {*} cells\r\n     * @param {*} fieldIndex\r\n     * @param {*} fieldName\r\n     */\r\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\r\n      let parent = row;\r\n      let cell = null;\r\n      if (\r\n        cells &&\r\n        fieldIndex &&\r\n        fieldIndex[fieldName] !== undefined &&\r\n        fieldIndex[fieldName] !== -1\r\n      ) {\r\n        cell = cells.eq(fieldIndex[fieldName]);\r\n        parent = cell || row;\r\n      }\r\n\r\n      let result = Searcher.getFieldValue(this.site, parent, fieldName);\r\n\r\n      if (!result && cell) {\r\n        if (returnCell) {\r\n          return cell;\r\n        }\r\n        result = cell.text();\r\n      }\r\n\r\n      return result;\r\n    }\r\n\r\n    /**\r\n     * 获取时间\r\n     * @param {*} cell\r\n     */\r\n    getTime(cell) {\r\n      let time = cell.find(\"span[title],time[title]\").attr(\"title\");\r\n      if (!time) {\r\n        time = $(\"<span>\")\r\n          .html(cell.html().replace(\"<br>\", \" \"))\r\n          .text();\r\n      }\r\n      if (options.site.host === \"pt.sjtu.edu.cn\") {\r\n        if (time.match(/\\d+[分时天月年]/g)) {\r\n          time = Date.now() - this._parseTime(time)\r\n          time = new Date(time).toLocaleString(\"zh-CN\", { hour12: false }).replace(/\\//g, '-')\r\n        }\r\n      }\r\n      return time || \"\";\r\n    }\r\n\r\n    _parseTime(timeString) {\r\n      const timeMatch = timeString.match(/\\d+[分时天月年]/g)\r\n      let length = 0\r\n      timeMatch.forEach(time => {\r\n        const timeMatch = time.match(/(\\d+)([分时天月年])/)\r\n        const number = parseInt(timeMatch[1])\r\n        const unit = timeMatch[2]\r\n        switch (true) {\r\n          case unit === '分':\r\n            length += number\r\n            break\r\n          case unit === '时':\r\n            length += number * 60\r\n            break\r\n          case unit === '天':\r\n            length += number * 60 * 24\r\n            break\r\n          case unit === '月':\r\n            length += number * 60 * 24 * 30\r\n            break\r\n          case unit === '年':\r\n            length += number * 60 * 24 * 365\r\n            break\r\n          default:\r\n        }\r\n      })\r\n      return length * 60 * 1000\r\n    }\r\n\r\n    /**\r\n     * 获取标题\r\n     */\r\n    getTitle(row, cells, fieldIndex) {\r\n      let title =\r\n        this.getFieldValue(row, cells, fieldIndex, \"title\", true) ||\r\n        row.find(\"a[href*='hit'][title]:not(a[href*='comment'])\").first();\r\n\r\n      if (typeof title === \"string\") {\r\n        return title;\r\n      }\r\n\r\n      if (title.length == 0) {\r\n        title = row.find(\"a[href*='hit']:has(b)\").first();\r\n      }\r\n\r\n      if (title.length == 0) {\r\n        // 特殊情况处理\r\n        switch (options.site.host) {\r\n          case \"u2.dmhy.org\":\r\n            title = row.find(\"a.tooltip[href*='hit']\").first();\r\n            break;\r\n        }\r\n      }\r\n\r\n      // 对title进行处理，防止出现cf的email protect\r\n      let cfemail = title.find(\"span.__cf_email__\");\r\n      if (cfemail.length > 0) {\r\n        cfemail.each((index, el) => {\r\n          $(el).replaceWith(Searcher.cfDecodeEmail($(el).data(\"cfemail\")));\r\n        });\r\n      }\r\n\r\n      return title;\r\n    }\r\n\r\n    /**\r\n     * 获取IMDbId\r\n     * @param {*} row\r\n     */\r\n    getIMDbId(row)\r\n    {\r\n      let imdbId = Searcher.getFieldValue(this.site, row, \"imdbId\");\r\n      if (imdbId) {\r\n        return imdbId;\r\n      }\r\n\r\n      try {\r\n        let link = row.find(\"a[href*='imdb.com/title/tt']\").first().attr(\"href\");\r\n        if (link)\r\n        {\r\n          imdbId = link.match(/(tt\\d+)/);\r\n          if (imdbId)\r\n            return imdbId[0];\r\n        }\r\n      } catch (error){\r\n        console.log(error)\r\n        return null;\r\n      }\r\n      return null;\r\n    }\r\n\r\n    /**\r\n     * 获取副标题\r\n     * @param {*} title\r\n     * @param {*} row\r\n     */\r\n    getSubTitle(title, row) {\r\n      let subTitle = Searcher.getFieldValue(this.site, row, \"subTitle\");\r\n      if (subTitle) {\r\n        return subTitle;\r\n      }\r\n\r\n      try {\r\n        subTitle = title\r\n          .parent()\r\n          .html()\r\n          .split(\"<br>\");\r\n        if (subTitle && subTitle.length > 1) {\r\n          subTitle = $(\"<span>\")\r\n            .html(subTitle[subTitle.length - 1])\r\n            .text();\r\n        } else {\r\n          // 特殊情况处理\r\n          switch (options.site.host) {\r\n            case \"hdchina.org\":\r\n              if (\r\n                title\r\n                  .parent()\r\n                  .next()\r\n                  .is(\"h4\")\r\n              ) {\r\n                subTitle = title\r\n                  .parent()\r\n                  .next()\r\n                  .text();\r\n              }\r\n              break;\r\n\r\n            case \"tp.m-team.cc\":\r\n            case \"pt.m-team.cc\":\r\n            case \"kp.m-team.cc\":\r\n              title = row.find(\"a[href*='hit'][title]\").last();\r\n              subTitle = title\r\n                .parent()\r\n                .html()\r\n                .split(\"<br>\");\r\n              subTitle = $(\"<span>\")\r\n                .html(subTitle[subTitle.length - 1])\r\n                .text();\r\n              break;\r\n\r\n            case \"u2.dmhy.org\":\r\n              subTitle = $(\".torrentname > tbody > tr:eq(1)\", row)\r\n                .find(\".tooltip\")\r\n                .text();\r\n              break;\r\n\r\n            case \"whu.pt\":\r\n            case \"hudbt.hust.edu.cn\":\r\n              subTitle = $(\"h3\", row).text();\r\n              break;\r\n\r\n            default:\r\n              subTitle = \"\";\r\n              break;\r\n          }\r\n        }\r\n\r\n        return subTitle || \"\";\r\n      } catch (error) {\r\n        return \"\";\r\n      }\r\n    }\r\n\r\n    // 很\r\n    getDownloadLink(row, link) {\r\n      let url;\r\n      switch (options.site.host) {\r\n        case 'hdsky.me': {\r\n          let url_another = row.find('form[action*=\"download.php\"]:eq(0)')\r\n          if (url_another.length > 0) {\r\n            url = url_another.attr('action')\r\n            break;\r\n          }\r\n\r\n        }\r\n\r\n        default: {\r\n          let url_another = row.find(\"img.download\").parent();\r\n\r\n          if (url_another.length) {\r\n            if (url_another.get(0).tagName !== \"A\") {\r\n              let id = link.getQueryString(\"id\");\r\n              url = `download.php?id=${id}`;\r\n            } else {\r\n              url = url_another.attr(\"href\");\r\n            }\r\n          } else {\r\n            let id = link.getQueryString(\"id\");\r\n            url = `download.php?id=${id}`;\r\n          }\r\n          url = url + \"&https=1\"\r\n        }\r\n      }\r\n\r\n      return url;\r\n    }\r\n\r\n    /**\r\n     * 获取分类\r\n     * @param {*} cell 当前列\r\n     */\r\n    getCategory(cell) {\r\n      let result = {\r\n        name: \"\",\r\n        link: \"\"\r\n      };\r\n      let link = cell.find(\"a:first\");\r\n      let img = link.find(\"img:first\");\r\n\r\n      if (link.length) {\r\n        result.link = link.attr(\"href\");\r\n        if (result.link.substr(0, 4) !== \"http\") {\r\n          result.link = options.site.url + result.link;\r\n        }\r\n      }\r\n\r\n      if (img.length) {\r\n        result.name = img.attr(\"title\") || img.attr(\"alt\");\r\n      } else {\r\n        result.name = link.text();\r\n      }\r\n      return result;\r\n    }\r\n  }\r\n\r\n  let parser = new Parser(options);\r\n  options.results = parser.getResult();\r\n  console.log(options.results);\r\n})(options, options.searcher);\r\n"
  },
  {
    "path": "resource/schemas/NexusPHP/parser/downloadURL.js",
    "content": "(function (options) {\n  if (options.url && options.url.query && options.url.href.getQueryString) {\n    let url = options.url.href;\n    let passkey = options.site.passkey || options.url.href.getQueryString(\"passkey\");\n    if (url.indexOf(\"download.php\") == -1) {\n      let id = url.getQueryString(\"id\");\n      if (id) {\n        // 如果站点没有配置禁用https，则默认添加https链接\n        url = options.url.origin + \"/download.php?id=\" + id + (passkey ? \"&passkey=\" + passkey : \"\") + (options.site.disableHttps ? \"\" : \"&https=1\");\n      } else {\n        url = \"\";\n      }\n    }\n\n    if (!url) {\n      options.error = {\n        success: false,\n        msg: \"无效的下载地址\"\n      }\n      return;\n    }\n\n    options.result = url;\n  }\n})(options)"
  },
  {
    "path": "resource/schemas/NexusPHP/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let urlParser = PTService.filters.parseURL(location.href);\n      let site = PTService.getSiteFromHost(urlParser.host);\n      \n      let urls = PTService.getFieldValue(\"downloadURLs\");\n      if (!urls) {\n        let links = $(\"a[href*='download.php']\").toArray();\n\n        if (links.length === 0) {\n          links = $(\".torrentname a[href*='details.php']\").toArray();\n        }\n\n        if (links.length === 0) {\n          //  \"获取下载链接失败，未能正确定位到链接\";\n          return this.t(\"getDownloadURLsFailed\");\n        }\n\n        urls = $.map(links, item => {\n          let url = $(item)\n            .attr(\"href\")\n            .replace(/details\\.php/gi, \"download.php\");\n          if (url) {\n            if (url.indexOf(\"passkey=\") === -1 && PTService.site.passkey) {\n              url += \"&passkey=\" + PTService.site.passkey;\n            }\n\n            if (\n              url &&\n              url.indexOf(\"https=1\") === -1 &&\n              !PTService.site.disableHttps\n            ) {\n              url += \"&https=1\";\n            }\n\n            try\n            {\n              if (site) {\n                switch (site.name) {\n                  case 'HDChina': \n                    url += `&uid=${site.user.id}` \n                    break;\n                  default:\n                    break;\n                }\n              }\n            } catch {}\n          }\n          return url;\n        });\n      }\n\n      if (urls) {\n        urls = urls.map((x) => {\n          return this.getFullURL(x)\n        })\n      }\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\".torrents\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB'),td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (!url.getQueryString) {\n        PTService.showNotice({\n          msg:\n            \"系统依赖函数（getQueryString）未正确加载，请尝试刷新页面或重新启用插件。\"\n        });\n        return null;\n      }\n\n      if (url.indexOf(\"download.php\") === -1) {\n        let id = url.getQueryString(\"id\");\n        if (id) {\n          // 如果站点没有配置禁用https，则默认添加https链接\n          url =\n            siteURL +\n            \"download.php?id=\" +\n            id +\n            (PTService.site.passkey\n              ? \"&passkey=\" + PTService.site.passkey\n              : \"\") +\n            (PTService.site.disableHttps ? \"\" : \"&https=1\");\n        } else {\n          url = \"\";\n        }\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/README.md",
    "content": "# 网站架构定义\n\n> 网站架构是指当前网站使用了什么样的架构搭建的，国内大部分的 `PT` 网站都使用了 `NexusPHP` 这个架构\n\n## 目录说明\n\n该目录存放所有支持的网站架构，目录名为架构名称\n\n```\n--目录名\n----parser\n-------xxxx.js\n----config.json\n----xxxx.js\n```\n\n- parser : （可选）解析器目录，会在打包时自动将该目录下的所有 js 文件内容生成到 config.js 文件中的 `parser` 字段中\n- config.json : 架构的定义\n- xxxx.js : 页面对应的脚本文件\n\n### config.json 文件\n\n```json\n{\n  \"name\": \"NexusPHP\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [\"/details.php\", \"/plugin_details.php\"],\n      \"scripts\": [\"details.js\"]\n    },\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\"/torrents.php\", \"/music.php\", \"/movie.php\", \"/adult.php\"],\n      \"scripts\": [\"torrents.js\"]\n    }\n  ],\n  \"searchEntry\": [\n    {\n      \"entry\": \"/torrents.php?search=$key$\",\n      \"name\": \"全部\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\"\n    }\n  ],\n  \"patterns\": {\n    \"torrentLinks\": [\"*://*/*\"]\n  },\n  \"parser\": {\n    \"downloadURL\": \"解析脚本内容\"\n  }\n}\n```\n\n- `name` : 架构名称；\n- `ver` : 当前配置版本；\n- `plugins` : 支持的插件列表，是一个数组\n  - `name` : 插件名称；\n  - `pages` : 表示该插件在哪些页面加载；\n  - `scripts` : 插件对应的脚本文件，`JavaScript` 文件\n- `searchEntry` : 搜索入口配置，需定义为数组\n  - `entry` : 入口文件\n  - `name` : 自定义入口的名称\n  - `resultType` : 搜索返回的原始结果类型：html, json, xml\n  - `parseScriptFile` : 解析原始结果的脚本文件\n  - `resultSelector` : 定位种子列表的 `jQuery` 查询表达式\n- `patterns` : （可选）页面匹配规则\n  - `torrentLinks` : 用于匹配有效的种子链接，作用于右键菜单，如果不指定，则匹配所有链接；\n- `parser` : （可选）解析器\n  - `downloadURL` : 解析下载链接，用于解析和生成点击右键下载时的链接\n\n### 关于脚本及其他资源文件路径说明\n\n- 如果在第一个位置指定了 `/` ，则路径会被指向到：\n  - `https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/`\n- 如果第一个位置不是 `/` ，则表示当前路径为该架构所在目录，如 `NexusPHP` 的指向目录为：\n  - `https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/schemas/NexusPHP/`\n\n## 脚本中可用的全局对象\n\n- `PTService` : 该对象由 `PT 助手` 挂载到 `window` 对象中的全局对象；目前提供以下方法：\n\n  - `addButton`: 添加命令按钮方法\n\n  ```js\n  PTService.addButton({\n    title: \"按钮的提示信息\",\n    icon: \"图标\",\n    label: \"按钮标签\",\n    /**\n     * 单击事件\n     * @param success 成功回调事件\n     * @param error 失败回调事件\n     *\n     * 两个事件必需执行一个，可以传递一个参数\n     */\n    click: (success, error) => {}\n  });\n  ```\n\n  - `call`: 调用指定的命令\n\n- `jQuery` : jQuery 对象\n\n## NexusPHP 获取站点分类信息\n\n```js\nJSON.stringify(\n  jQuery.map(jQuery(\"#ksearchboxmain\").find(\"a[href*='cat']\"), function(n) {\n    return {\n      id: parseInt(\n        jQuery(n)\n          .attr(\"href\")\n          .replace(\"?cat=\", \"\")\n      ),\n      name:\n        jQuery(n)\n          .find(\"img\")\n          .attr(\"title\") || jQuery(n).text()\n    };\n  })\n);\n```\n"
  },
  {
    "path": "resource/schemas/TNode/common.js",
    "content": "(function($, window) {\n  class Common {\n    constructor() {\n      this.siteContentMenus = {};\n      this.clientContentMenus = [];\n      this.defaultPath = PTService.getSiteDefaultPath();\n      this.downloadClientType = PTService.downloadClientType;\n      this.defaultClientOptions = PTService.getClientOptions();\n      this.currentURL = location.href;\n    }\n\n    /**\n     * 获取指定key的当前语言内容\n     * @param {*} key\n     * @param {*} options\n     */\n    t(key, options) {\n      return PTService.i18n.t(key, options);\n    }\n\n    /**\n     * 初始化当前默认服务器可用空间\n     */\n    initFreeSpaceButton() {\n      if (!this.defaultPath) {\n        return;\n      }\n      PTService.call(PTService.action.getFreeSpace, {\n        path: this.defaultPath,\n        clientId: PTService.site.defaultClientId\n      })\n        .then(result => {\n          console.log(\"命令执行完成\", result);\n          if (result && result.arguments) {\n            // console.log(PTService.filters.formatSize(result.arguments[\"size-bytes\"]));\n\n            PTService.addButton({\n              title: this.t(\"buttons.freeSpaceTip\", {\n                path: this.defaultPath,\n                interpolation: { escapeValue: false }\n              }), // \"默认服务器剩余空间\\n\" + this.defaultPath,\n              icon: \"filter_drama\",\n              label: PTService.filters.formatSize(\n                result.arguments[\"size-bytes\"]\n              )\n            });\n          }\n          // success();\n        })\n        .catch(() => {\n          // error()\n        });\n    }\n\n    /**\n     * 初始化种子详情页面按钮\n     */\n    initDetailButtons() {\n      // 添加下载按钮\n      this.addSendTorrentToDefaultClientButton();\n\n      // 添加下载到按钮\n      this.addSendTorrentToClientButton();\n\n      // 添加复制下载链接按钮\n      this.addCopyTextToClipboardButton();\n\n      // 初始化可用空间按钮\n      this.initFreeSpaceButton();\n\n      // 初始化收藏按钮\n      this.initCollectionButton();\n\n      // 初始化说谢谢按钮\n      this.initSayThanksButton();\n      if(document.domain.match(\"keepfrds.com\")){$(\".pt-plugin-body\").css(\"z-index\",\"39\")}\n    }\n\n    /**\n     * 初始化种子列表页面按钮\n     */\n    initListButtons(checkPasskey = false) {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title: this.t(\"buttons.downloadAllTip\", {\n            name: this.defaultClientOptions.name\n          }), //`将当前页面所有种子下载到[${this.defaultClientOptions.name}]`,\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadAll\"), //\"下载所有\",\n          click: (success, error) => {\n            if (checkPasskey && !PTService.site.passkey) {\n              error(\"请先设置站点密钥（Passkey）。\");\n              return;\n            }\n            this.startDownloadURLs(success, error);\n          }\n        });\n\n      // 添加下载到按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadAllToTip\"), //`将当前页面所有种子下载到指定服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadAllTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            // \"请先设置站点密钥（Passkey）。\"\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          this.showAllContentMenus(event.originalEvent, success, error);\n        },\n        onDrop: (data, event, success, error) => {\n          console.log(data);\n          let url = this.getDroperURL(data.url);\n          console.log(url);\n          this.showContentMenusForUrl(\n            {\n              url,\n              title: data.title,\n              link: data.url\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyAllToClipboardTip\"), // \"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyAllToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, urls.join(\"\\n\"))\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(() => {\n              error();\n            });\n        },\n        onDrop: (data, event, success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let url = this.getDroperURL(data.url);\n          url &&\n            PTService.call(PTService.action.copyTextToClipboard, url)\n              .then(result => {\n                console.log(\"命令执行完成\", result);\n                success();\n              })\n              .catch(() => {\n                error();\n              });\n        }\n      });\n\n      // 检查是否有下载管理权限\n      this.checkPermissions([\"downloads\"])\n        .then(() => {\n          this.addSaveAllTorrentFilesButton(checkPasskey);\n        })\n        .catch(() => {\n          PTService.addButton({\n            title: this.t(\"buttons.needAuthorizationTip\"), //\"下载所有种子文件功能需要权限，点击前往授权\",\n            icon: \"verified_user\",\n            key: \"requestPermissions\",\n            label: this.t(\"buttons.needAuthorization\"), //\"需要授权\",\n            click: (success, error) => {\n              PTService.call(PTService.action.openOptions, \"set-permissions\");\n              success();\n            }\n          });\n        });\n    }\n\n    /**\n     * 添加下载所有种子文件按钮\n     * @param {*} checkPasskey\n     */\n    addSaveAllTorrentFilesButton(checkPasskey) {\n      // 批量下载当前页种子文件\n      PTService.addButton({\n        title: this.t(\"buttons.saveAllTorrentTip\"), //\"下载所有种子文件\",\n        icon: \"save\",\n        label: this.t(\"buttons.saveAllTorrent\"), //\"所有种子\",\n        click: (success, error) => {\n          if (checkPasskey && !PTService.site.passkey) {\n            error(this.t(\"needPasskey\"));\n            return;\n          }\n          let urls = this.getDownloadURLs();\n\n          if (!urls.length || typeof urls == \"string\") {\n            error(urls);\n            return;\n          }\n\n          let downloads = [];\n          urls.forEach(url => {\n            downloads.push({\n              url,\n              method: PTService.site.downloadMethod\n            });\n          });\n\n          console.log(downloads);\n\n          PTService.call(PTService.action.addBrowserDownloads, downloads)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(e => {\n              console.log(e);\n              error(e);\n            });\n        }\n      });\n    }\n\n    checkPermissions(permissions) {\n      return PTService.call(PTService.action.checkPermissions, permissions);\n    }\n\n    /**\n     * 发送种子到默认下载服务器\n     * @param {string} url\n     */\n    sendTorrentToDefaultClient(option, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof option === \"string\") {\n          option = {\n            url: option,\n            title: \"\"\n          };\n        }\n\n        let savePath = PTService.pathHandler.getSavePath(\n          this.defaultPath,\n          PTService.site\n        );\n\n        if (savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToDefaultClient, {\n          url: option.url,\n          title: option.title,\n          savePath: savePath,\n          autoStart: this.defaultClientOptions.autoStart,\n          tagIMDb: this.defaultClientOptions.tagIMDb,\n          link: option.link,\n          imdbId: option.imdbId\n        })\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 隐藏指定的 notice\n     * @param notice\n     */\n    hideNotice(notice) {\n      if (!notice) return;\n      if (notice.id && notice.close) {\n        notice.close();\n      } else if (notice.hide) {\n        notice.hide();\n      }\n    }\n\n    /**\n     * 发送种子到指定下载服务器\n     * @param {string} url\n     */\n    sendTorrentToClient(options, showNotice = true) {\n      return new Promise((resolve, reject) => {\n        if (typeof options === \"string\") {\n          options = {\n            url: options,\n            title: \"\"\n          };\n        }\n\n        if (!options.clientId) {\n          // \"无效的下载服务器\"\n          reject(this.t(\"invalidDownloadServer\"));\n          return;\n        }\n\n        options.savePath = PTService.pathHandler.getSavePath(\n          options.savePath,\n          PTService.site\n        );\n        if (options.savePath === false) {\n          // \"用户取消操作\"\n          reject(this.t(\"userCanceled\"));\n          return;\n        }\n\n        let notice = null;\n        if (showNotice) {\n          notice = PTService.showNotice({\n            type: \"info\",\n            timeout: 2,\n            indeterminate: true,\n            msg: this.t(\"sendingTorrent\") //\"正在发送下载链接到服务器，请稍候……\"\n          });\n        }\n\n        PTService.call(PTService.action.sendTorrentToClient, options)\n          .then(result => {\n            console.log(\"命令执行完成\", result);\n            if (showNotice) {\n              PTService.showNotice(result);\n            }\n            resolve(result);\n          })\n          .catch(result => {\n            // PTService.showNotice({\n            //   msg: (result && result.msg) || result\n            // });\n            reject(result);\n          })\n          .finally(() => {\n            this.hideNotice(notice);\n          });\n      });\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\",\n          link: data\n        };\n      }\n\n      data.url = this.getDroperURL(data.url);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    isNexusPHP() {\n      return PTService.site.schema == \"NexusPHP\";\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (url && url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url && url.substr(0, 4) !== \"http\") {\n        if (url.substr(0, 1) == \"/\") {\n          url = url.substr(1);\n        }\n        url = `${siteURL}${url}`;\n      }\n\n      return url;\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 添加下载到指定下载服务器按钮\n     */\n    addSendTorrentToClientButton() {\n      // 添加下载按钮\n      PTService.addButton({\n        title: this.t(\"buttons.downloadToTip\"), //`将当前种子下载到指定的服务器`,\n        icon: \"save_alt\",\n        type: PTService.buttonType.popup,\n        label: this.t(\"buttons.downloadTo\"), //\"下载到…\",\n        /**\n         * 单击事件\n         * @param success 成功回调事件\n         * @param error 失败回调事件\n         * @param event 当前按钮事件\n         *\n         * 两个事件必需执行一个，可以传递一个参数\n         */\n        click: (success, error, event) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = document.title;\n          }\n\n          this.showContentMenusForUrl(\n            {\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            },\n            event.originalEvent,\n            success,\n            error\n          );\n        }\n      });\n    }\n\n    /**\n     * 添加一键下载按钮\n     */\n    addSendTorrentToDefaultClientButton() {\n      // 添加下载按钮\n      this.defaultClientOptions &&\n        PTService.addButton({\n          title:\n            this.t(\"buttons.downloadToDefaultTip\", {\n              name: this.defaultClientOptions.name\n            }) + (this.defaultPath ? \"\\n\" + this.defaultPath : \"\"), //`将当前种子下载到[${this.defaultClientOptions.name}]` +\n          icon: \"get_app\",\n          label: this.t(\"buttons.downloadToDefault\"), //\"一键下载\",\n          /**\n           * 单击事件\n           * @param success 成功回调事件\n           * @param error 失败回调事件\n           *\n           * 两个事件必需执行一个，可以传递一个参数\n           */\n          click: (success, error) => {\n            // getDownloadURL 方法由继承者提供\n            if (!this.getDownloadURL) {\n              // \"getDownloadURL 方法未定义\"\n              error(this.t(\"getDownloadURLisUndefined\"));\n              return;\n            }\n\n            let url = this.getDownloadURL();\n\n            if (!url) {\n              // \"获取下载链接失败\"\n              error(this.t(\"getDownloadURLFailed\"));\n              return;\n            }\n\n            let title = \"\";\n\n            if (this.getTitle) {\n              title = this.getTitle();\n            } else {\n              title = document.title;\n            }\n\n            this.sendTorrentToDefaultClient({\n              url,\n              title,\n              link: this.currentURL,\n              imdbId: this.getIMDbId ? this.getIMDbId() : null\n            })\n              .then(() => {\n                success();\n              })\n              .catch(result => {\n                error(result);\n              });\n          }\n        });\n    }\n\n    /**\n     * 添加复制下载链接按钮\n     */\n    addCopyTextToClipboardButton() {\n      // 复制下载链接\n      PTService.addButton({\n        title: this.t(\"buttons.copyToClipboardTip\"), //\"复制下载链接到剪切板\",\n        icon: \"file_copy\",\n        label: this.t(\"buttons.copyToClipboard\"), //\"复制链接\",\n        click: (success, error) => {\n          // getDownloadURL 方法有继承者提供\n          if (!this.getDownloadURL) {\n            // \"getDownloadURL 方法未定义\"\n            error(this.t(\"getDownloadURLisUndefined\"));\n            return;\n          }\n\n          console.log(PTService.site, this.defaultPath);\n          let url = this.getDownloadURL();\n\n          if (!url) {\n            // \"获取下载链接失败\"\n            error(this.t(\"getDownloadURLFailed\"));\n            return;\n          }\n\n          PTService.call(PTService.action.copyTextToClipboard, url)\n            .then(result => {\n              console.log(\"命令执行完成\", result);\n              success();\n            })\n            .catch(result => {\n              error(result);\n            });\n        }\n      });\n    }\n\n    /**\n     * 初始化收藏按钮\n     */\n    initCollectionButton() {\n      // 获取收藏情况\n      PTService.call(PTService.action.getTorrentCollention, location.href)\n        .then(result => {\n          this.addRemoveCollectionButton(result);\n        })\n        .catch(() => {\n          this.addToCollectionButton();\n        });\n    }\n\n    /**\n     * 添加收藏按钮\n     */\n    addToCollectionButton() {\n      PTService.removeButton(\"removeFromCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.addToCollection\"),\n        icon: \"favorite_border\",\n        label: this.t(\"buttons.addToCollection\"),\n        key: \"addToCollection\",\n        click: (success, error) => {\n          let title = \"\";\n\n          if (this.getTitle) {\n            title = this.getTitle();\n          } else {\n            title = PTService.getFieldValue(\"title\");\n          }\n\n          if (!title) {\n            title = $(\"title:first\").text();\n          }\n\n          let imdbId = PTService.getFieldValue(\"imdbId\");\n\n          if (!imdbId) {\n            const link = $(\"a[href*='www.imdb.com/title/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n              if (match && match.length >= 2) {\n                imdbId = match[1];\n              }\n            }\n          }\n\n          let doubanId = PTService.getFieldValue(\"doubanId\");\n\n          if (!doubanId) {\n            const link = $(\"a[href*='movie.douban.com/subject/']:first\");\n            if (link.length > 0) {\n              let match = link.attr(\"href\").match(/subject\\/(\\d+)/);\n\n              if (match && match.length >= 2) {\n                doubanId = match[1];\n              }\n            }\n          }\n\n          const data = {\n            title: title,\n            url: this.getDownloadURL(),\n            link: location.href,\n            host: location.host,\n            size: PTService.getFieldValue(\"size\"),\n            subTitle: PTService.getFieldValue(\"subTitle\"),\n            movieInfo: {\n              imdbId: imdbId,\n              doubanId: doubanId\n            }\n          };\n\n          PTService.call(PTService.action.addTorrentToCollection, data)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addRemoveCollectionButton(data);\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 添加移除收藏按钮\n     */\n    addRemoveCollectionButton(item) {\n      PTService.removeButton(\"addToCollection\");\n\n      PTService.addButton({\n        title: this.t(\"buttons.removeFromCollection\"),\n        icon: \"favorite\",\n        label: this.t(\"buttons.removeFromCollection\"),\n        key: \"removeFromCollection\",\n        click: (success, error) => {\n          PTService.call(PTService.action.deleteTorrentFromCollention, item)\n            .then(result => {\n              success();\n              setTimeout(() => {\n                this.addToCollectionButton();\n              }, 1000);\n            })\n            .catch(() => {\n              error();\n            });\n        }\n      });\n    }\n\n    /**\n     * 根据指定的URL获取可用的下载目录及客户端信息\n     * @param url\n     */\n    getContentMenusForUrl(url) {\n      let urlParser = PTService.filters.parseURL(url);\n      if (!urlParser.host) {\n        return [];\n      }\n      let results = [];\n      let clients = [];\n      let site = PTService.getSiteFromHost(urlParser.host);\n      if (!site) {\n        return [];\n      }\n      let host = site.host;\n\n      if (this.siteContentMenus[host]) {\n        return this.siteContentMenus[host];\n      }\n\n      /**\n       * 增加下载目录\n       * @param paths\n       * @param client\n       */\n      function pushPath(paths, client) {\n        paths.forEach(path => {\n          results.push({\n            client: client,\n            path: path,\n            host: host\n          });\n        });\n      }\n\n      PTService.options.clients.forEach(client => {\n        clients.push({\n          client: client,\n          path: \"\",\n          host: host\n        });\n\n        if (client.paths) {\n          // 根据已定义的路径创建菜单\n          for (const _host in client.paths) {\n            let paths = client.paths[host];\n\n            if (_host !== host) {\n              continue;\n            }\n\n            pushPath(paths, client);\n          }\n\n          // 最后添加当前客户端适用于所有站点的目录\n          let publicPaths = client.paths[PTService.allSiteKey];\n          if (publicPaths) {\n            if (results.length > 0) {\n              results.push({});\n            }\n\n            pushPath(publicPaths, client);\n          }\n        }\n      });\n\n      if (results.length > 0) {\n        clients.splice(0, 0, {});\n      }\n\n      results = results.concat(clients);\n\n      this.siteContentMenus[host] = results;\n\n      return results;\n    }\n\n    /**\n     * 显示指定链接的下载服务器及目录菜单\n     * @param options\n     * @param event\n     */\n    showContentMenusForUrl(options, event, success, error) {\n      let items = this.getContentMenusForUrl(options.url);\n      let menus = [];\n\n      items.forEach(item => {\n        if (item.client && item.client.name) {\n          menus.push({\n            title:\n              this.t(\"buttons.menuDownloadTo\", {\n                server: `${item.client.name} -> ${item.client.address}`\n              }) + //`下载到：${item.client.name} -> ${item.client.address}` +\n              (item.path\n                ? ` -> ${PTService.pathHandler.replacePathKey(\n                    item.path,\n                    PTService.site\n                  )}`\n                : \"\"),\n            fn: () => {\n              if (options.url) {\n                // console.log(options, item);\n                this.sendTorrentToClient({\n                  clientId: item.client.id,\n                  url: options.url,\n                  title: options.title,\n                  savePath: item.path,\n                  autoStart: item.client.autoStart,\n                  tagIMDb: item.client.tagIMDb,\n                  link: options.link,\n                  imdbId: options.imdbId\n                })\n                  .then(result => {\n                    success();\n                  })\n                  .catch(result => {\n                    error(result);\n                  });\n              }\n            }\n          });\n        } else {\n          menus.push({});\n        }\n      });\n\n      console.log(items, menus);\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 验证指定元素的大小信息\n     * @param {*} doms\n     */\n    checkSize(doms) {\n      if (!PTService.options.needConfirmWhenExceedSize) {\n        return true;\n      }\n      // 获取所有种子的大小信息\n      let size = this.getTotalSize(doms);\n\n      let exceedSize = 0;\n      switch (PTService.options.exceedSizeUnit) {\n        //\n        case PTService.sizeUnit.MiB:\n          exceedSize = PTService.options.exceedSize * 1048576;\n          break;\n\n        case PTService.sizeUnit.GiB:\n          exceedSize = PTService.options.exceedSize * 1073741824;\n          break;\n\n        case \"T\":\n        case PTService.sizeUnit.TiB:\n          exceedSize = PTService.options.exceedSize * 1099511627776;\n          break;\n      }\n\n      return size >= exceedSize ? PTService.filters.formatSize(size) : true;\n    }\n\n    /**\n     *\n     * @param {*} source\n     */\n    getTotalSize(source) {\n      let total = 0;\n\n      $.each(source, (index, item) => {\n        total += this.getSize($(item).text());\n      });\n\n      return total;\n    }\n\n    /**\n     * @return {number}\n     */\n    getSize(size) {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size.match(\n        /^(\\d*\\.?\\d+)(.*[^TGMK])?([TGMK](B|iB){0,1})$/i\n      );\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Ti?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    }\n\n    /**\n     * 种子大小超限时确认\n     */\n    confirmSize(doms) {\n      let size = this.checkSize(doms);\n\n      if (size !== true) {\n        let content = this.t(\"exceedSizeConfirm\", {\n          size,\n          exceedSize: PTService.options.exceedSize,\n          exceedSizeUnit: PTService.options.exceedSizeUnit\n        });\n        if (!confirm(content)) {\n          return false;\n        }\n      }\n      return true;\n    }\n\n    /**\n     * 准备开始批量下载\n     * @param {*} success\n     * @param {*} error\n     * @param {*} downloadOptions\n     */\n    startDownloadURLs(success, error, downloadOptions) {\n      if (this.confirmWhenExceedSize) {\n        if (!this.confirmWhenExceedSize()) {\n          // \"容量超限，已取消\"\n          error(this.t(\"exceedSizeCanceled\"));\n          return;\n        }\n      }\n\n      if (!this.getDownloadURLs) {\n        // \"getDownloadURLs 方法未定义\"\n        error(this.t(\"getDownloadURLsisUndefined\"));\n        return;\n      }\n\n      let urls = this.getDownloadURLs();\n      if (!urls.length || typeof urls == \"string\") {\n        error(urls);\n        return;\n      }\n\n      // 是否启用后台下载任务\n      if (PTService.options.enableBackgroundDownload) {\n        this.downloadURLsInBackground(\n          urls,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      } else {\n        this.downloadURLs(\n          urls,\n          urls.length,\n          msg => {\n            success({\n              msg\n            });\n          },\n          downloadOptions\n        );\n      }\n    }\n\n    downloadURLsInBackground(urls, callback, downloadOptions) {\n      const items = [];\n\n      const savePath = downloadOptions\n        ? PTService.pathHandler.getSavePath(\n            downloadOptions.savePath || downloadOptions.path,\n            PTService.site\n          )\n        : \"\";\n\n      urls.forEach(url => {\n        if (downloadOptions) {\n          items.push({\n            clientId: downloadOptions.client.id,\n            url,\n            savePath,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb\n          });\n        } else {\n          items.push({\n            url\n          });\n        }\n      });\n\n      PTService.call(PTService.action.sendTorrentsInBackground, items)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 批量下载指定的URL\n     * @param {*} urls\n     * @param {*} count\n     * @param {*} callback\n     * @param {*} downloadOptions 下载选项，如不指定，则发送至默认下载服务器\n     */\n    downloadURLs(urls, count, callback, downloadOptions) {\n      let index = count - urls.length;\n      let url = urls.shift();\n      if (!url) {\n        $(this.statusBar).remove();\n        this.statusBar = null;\n        // count + \"条链接已发送完成。\"\n        callback(\n          this.t(\"downloadURLsFinished\", {\n            count\n          })\n        );\n        return;\n      }\n\n      this.showStatusMessage(\n        this.t(\"downloadURLsTip\", {\n          text:\n            url.replace(PTService.site.passkey, \"***\") +\n            \"(\" +\n            (count - index) +\n            \"/\" +\n            count +\n            \")\"\n        }),\n        0\n      );\n\n      if (!downloadOptions) {\n        this.sendTorrentToDefaultClient(url, false)\n          .then(result => {\n            this.downloadURLs(urls, count, callback);\n          })\n          .catch(result => {\n            this.downloadURLs(urls, count, callback);\n          });\n      } else {\n        this.sendTorrentToClient(\n          {\n            clientId: downloadOptions.client.id,\n            url: url,\n            title: \"\",\n            savePath: downloadOptions.path,\n            autoStart: downloadOptions.client.autoStart,\n            tagIMDb: downloadOptions.client.tagIMDb,\n            imdbId: downloadOptions.imdbId\n          },\n          false\n        )\n          .finally(() => {\n            // 是否设置了时间间隔\n            if (PTService.options.batchDownloadInterval > 0) {\n              setTimeout(() => {\n                this.downloadURLs(urls, count, callback, downloadOptions);\n              }, PTService.options.batchDownloadInterval * 1000);\n            } else {\n              this.downloadURLs(urls, count, callback, downloadOptions);\n            }\n          })\n          .catch(error => {\n            console.log(error);\n          });\n      }\n    }\n\n    showStatusMessage(msg) {\n      if (!this.statusBar) {\n        this.statusBar = PTService.showNotice({\n          text: msg,\n          type: \"info\",\n          width: 600,\n          progressBar: false\n        });\n      } else {\n        this.statusBar.find(\".noticejs-content\").html(msg);\n      }\n    }\n\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */\n    clone(source) {\n      return JSON.parse(JSON.stringify(source));\n    }\n\n    /**\n     * 显示批量下载时可用下载服务器菜单\n     * @param event\n     */\n    showAllContentMenus(event, success, error) {\n      let clients = [];\n      let menus = [];\n      let _this = this;\n\n      function addMenu(item) {\n        let title = _this.t(\"buttons.menuDownloadTo\", {\n          server: `${item.client.name} -> ${item.client.address}`\n        }); //`下载到：${item.client.name} -> ${item.client.address}`;\n        if (item.path) {\n          title += ` -> ${PTService.pathHandler.replacePathKey(\n            item.path,\n            PTService.site\n          )}`;\n        }\n        menus.push({\n          title: title,\n          fn: () => {\n            // 克隆是为了多次选择时，不覆盖原来的值\n            let _item = PPF.clone(item);\n            console.log(item);\n            let savePath = PTService.pathHandler.getSavePath(\n              _item.path,\n              PTService.site\n            );\n            if (savePath === false) {\n              // \"用户取消操作\"\n              error(_this.t(\"userCanceled\"));\n              return;\n            }\n            _item.path = savePath;\n            _this.startDownloadURLs(success, error, _item);\n          }\n        });\n      }\n\n      if (this.clientContentMenus.length == 0) {\n        PTService.options.clients.forEach(client => {\n          clients.push({\n            client: client,\n            path: \"\"\n          });\n        });\n        clients.forEach(item => {\n          if (item.client && item.client.name) {\n            addMenu(item);\n\n            if (item.client.paths) {\n              // 添加适用于所有站点的目录\n              let publicPaths = item.client.paths[PTService.allSiteKey];\n              if (publicPaths) {\n                publicPaths.forEach(path => {\n                  let _item = this.clone(item);\n                  _item.path = path;\n                  addMenu(_item);\n                });\n              }\n            }\n          } else {\n            menus.push({});\n          }\n        });\n        this.clientContentMenus = menus;\n      } else {\n        menus = this.clientContentMenus;\n      }\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      if (!url) {\n        return \"\";\n      }\n      if (url.substr(0, 2) === \"//\") {\n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 初始化说谢谢按钮\n     */\n    initSayThanksButton() {\n      let sayThanksButton = PTService.getFieldValue(\"sayThanksButton\");\n      console.log(\"sayThanksButton\");\n      if (sayThanksButton && sayThanksButton.length) {\n        // 说谢谢\n        PTService.addButton({\n          title: this.t(\"buttons.sayThanksTip\"),\n          icon: \"thumb_up\",\n          label: this.t(\"buttons.sayThanks\"),\n          key: \"sayThanks\",\n          click: (success, error) => {\n            sayThanksButton.click();\n            success();\n            setTimeout(() => {\n              PTService.removeButton(\"sayThanks\");\n            }, 1000);\n          }\n        });\n      }\n    }\n  }\n\n  window.TNodeCommon = Common;\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/TNode/config.json",
    "content": "{\n  \"name\": \"TNode\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"\\/torrent\\/info\\/\\\\d+\"],\n    \"scripts\": [\"common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"\\/list\\/\\\\d+\\/\\\\d+\"],\n    \"scripts\": [\"common.js\", \"torrents.js\"]\n  }],\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/api/torrent/advancedSearch\",\n    \"resultType\": \"json\",\n    \"keepOriginKey\": true,\n    \"requestDataType\": \"json\",\n    \"headers\": {\"x-csrf-token\": \"$user.csrfToken$\"},\n    \"requestMethod\": \"POST\",\n    \"requestData\": {\n      \"page\":1,\n      \"size\":20,\n      \"type\":\"title\",\n      \"sorter\":\"id\",\n      \"order\":\"desc\",\n      \"keyword\":\"$key$\",\n      \"tags\":[],\n      \"category\":[],\n      \"medium\":[],\n      \"videoCoding\":[],\n      \"audioCoding\":[],\n      \"resolution\":[],\n      \"group\":[],\n      \"more\":false\n    },\n    \"parseScriptFile\": \"/schemas/TNode/getSearchResult.js\"\n  },\n  \"checker\": {\n    \"isLogin\": {\n      \"page\": \"/user/info\",\n      \"contains\": \"退出登录\"\n    }\n  },\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img.pro_free, .free_bg, font.free\"\n  }, {\n    \"name\": \"2xFree\",\n    \"selector\": \"img.pro_free2up, .twoupfree_bg, font.twoupfree\"\n  }, {\n    \"name\": \"2xUp\",\n    \"selector\": \"img.pro_2up, .twoup_bg, font.twoup\"\n  }, {\n    \"name\": \"2x50%\",\n    \"selector\": \"img.pro_50pctdown2up, .twouphalfdown_bg, font.twouphalfdown\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"img.pro_30pctdown, .thirtypercentdown_bg, font.thirtypercent\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img.pro_50pctdown, .halfdown_bg, font.halfdown\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/api/plugins/ptppUserInfo\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"bonus\": { \"selector\": [\"data.bonus\"]},\n        \"downloaded\": { \"selector\": [\"data.downloaded\"]},\n        \"id\": { \"selector\": [\"data.id\"]},\n        \"invites\": { \"selector\": [\"data.invites\"]},\n        \"isLogged\": { \"selector\": [\"data.isLogged\"]},\n        \"joinTime\": { \"selector\": [\"data.joinTime\"]},\n        \"lastUpdateTime\": { \"selector\": [\"data.lastUpdateTime\"]},\n        \"levelName\": { \"selector\": [\"data.levelName\"]},\n        \"name\": { \"selector\": [\"data.name\"]},\n        \"seedingPoints\": { \"selector\": [\"data.seedingPoints\"]},\n        \"trueDownloaded\": { \"selector\": [\"data.trueDownloaded\"]},\n        \"uploaded\": { \"selector\": [\"data.uploaded\"]},\n        \"leeching\": { \"selector\": [\"data.leeching\"]},\n        \"seeding\": { \"selector\": [\"data.seeding\"]},\n        \"bonusPerHour\": { \"selector\": [\"data.bonusPerHour\"]},\n        \"seedingSize\": { \"selector\": [\"data.seedingSize\"]},\n        \"messageCount\": { \"selector\": [\"data.messageCount\"]},\n        \"csrfToken\": { \"selector\": [\"data.csrfToken\"]}\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/api/torrent/option\",\n      \"dataType\": \"json\",\n      \"headers\": {\n        \"x-csrf-token\": \"$user.csrfToken$\"\n      },\n      \"fields\": {\n        \"tagsAndCategories\": { \"selector\": [\"data.option\"]}\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/schemas/TNode/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.TNodeCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    // 初始化按钮列表\n    initButtons() {\n      this.initDetailButtons();\n    }\n    // 获取下载链接\n    getDownloadURL() {\n      let url = PTService.getFieldValue(\"downloadURL\") || $(\"a[href*='/api/torrent/download/']:contains('复制')\").attr('href') || $(\"a[href*='/api/torrent/download/']\").attr('href'); \n      if (!url) {\n        return \"\";\n      }\n      return this.getFullURL(url);\n    }\n    // 获取当前种子标题\n    getTitle() {\n      let title = $('label[for=\"form_item_subtitle\"]').parent().next().text() || $('label[for=\"form_item_title\"]').parent().next().text() || '';\n      return title;\n    }\n    // 获取当前种子IMDb Id\n    getIMDbId() {\n      // TODO\n      return null;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/TNode/getSearchResult.js",
    "content": "/** \n * @typedef {object} TNodeSearchResult\n * @property {number} status\n * @property {object} data\n * @property {object[]} data.torrents\n * @property {number} data.torrents.id\n * @property {string} data.torrents.title\n * @property {string} data.torrents.subtitle\n * @property {number} data.torrents.size\n * @property {string} data.torrents.uploader_id\n * @property {number} data.torrents.upload_time\n * @property {number} data.torrents.seeding\n * @property {number} data.torrents.leeching\n * @property {number} data.torrents.complete\n * @property {number} data.torrents.anonymous\n * @property {number} data.torrents.pinned\n * @property {number} data.torrents.is_official\n * @property {number} data.torrents.category\n * @property {number[]} data.torrents.tags\n * @property {string} data.torrents.douban\n * @property {null} data.torrents.imdb\n * @property {object} data.torrents.user\n * @property {boolean} data.torrents.user.anonymous\n * @property {number} data.torrents.uploadRate\n * @property {number} data.torrents.downloadRate\n * @property {number} data.total\n */\n\n/**\n * @typedef {object} TagOrCategory\n * @property {number} id\n * @property {string} name\n * @property {number} display_id\n */\n\n/**\n * TNode 默认搜索结果解析类\n */\n(function (options, Searcher) {\n  class Parser {\n    /**\n     * @param {Site} site\n     * @param {TagOrCategory[]} tagsAndCategories\n     */\n    constructor(site, tagsAndCategories = []) {\n      this.site = site\n      /**\n       * @type Record<number, string>\n       */\n      this.tags_map = {}\n      for (const item of tagsAndCategories) {\n        this.tags_map[item.id] = {\n          color: '#22a2c3',\n          ...item,\n        }\n      }\n    }\n    /**\n     * 获取搜索结果\n     * @param {TNodeSearchResult} response\n     */\n    getResult(response) {\n      if (response.status === 401) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return [];\n      }\n      options.isLogged = true;\n      if (response.data.total === 0 || response.data.torrents.length === 0) {\n        options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\n        return [];\n      }\n      /**\n       * @type {SearchResultItem[]}\n       */\n      const results = []\n      const host = site.url.endsWith('/') ? site.url.slice(0, -1) : site.url;\n      \n      try {\n        for (const torrent of response.data.torrents) {\n          const link = host + '/torrent/info/' + torrent.id;\n          let url = host + '/api/torrent/download/'  + torrent.id;\n          if (site.passkey) {\n            url += '/' + site.passkey;\n          }\n          const tags = (torrent.tags || []).map(x => this.tags_map[x]);\n          const category = torrent.category ? this.tags_map[torrent.category] : undefined;\n          results.push({\n            title: torrent.title,\n            subTitle: torrent.subtitle,\n            link,\n            url,\n            site: this.site,\n            size: torrent.size,\n            time: torrent.upload_time,\n            author: torrent.anonymous ? undefined: torrent.user.username,\n            seeders: torrent.seeding,\n            leechers: torrent.leeching,\n            completed: torrent.complete,\n            comments: undefined,\n            tags,\n            category,\n            progress: undefined,\n            status: undefined,\n            imdbId: undefined,\n            entryName: '全部',\n          });\n        }\n      } catch (error) {\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n        //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n  }\n  let site = options.site;\n  let tagsAndCategories = site.user && site.user.tagsAndCategories || [];\n  let parser = new Parser(site, tagsAndCategories);\n  options.results = parser.getResult(options.page);\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/schemas/TNode/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.TNodeCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let urlParser = PTService.filters.parseURL(location.href);\n      let site = PTService.getSiteFromHost(urlParser.host);\n      \n      let urls = PTService.getFieldValue(\"downloadURLs\");\n      if (!urls) {\n        let links = $(\"a[href*='/api/torrent/download/']\").toArray();\n\n        if (links.length === 0) {\n          //  \"获取下载链接失败，未能正确定位到链接\";\n          return this.t(\"getDownloadURLsFailed\");\n        }\n\n        urls = $.map(links, item => {\n          let url = $(item)\n            .attr(\"href\");\n          // if (url) {\n          //   if (url.indexOf(\"passkey=\") === -1 && PTService.site.passkey) {\n          //     url += \"&passkey=\" + PTService.site.passkey;\n          //   }\n          // }\n          return url;\n        });\n      }\n\n      if (urls) {\n        urls = urls.map((x) => {\n          return this.getFullURL(x)\n        })\n      }\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\".torrents\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB'),td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (!url.getQueryString) {\n        PTService.showNotice({\n          msg:\n            \"系统依赖函数（getQueryString）未正确加载，请尝试刷新页面或重新启用插件。\"\n        });\n        return null;\n      }\n\n      if (url.indexOf(\"download.php\") === -1) {\n        let id = url.getQueryString(\"id\");\n        if (id) {\n          // 如果站点没有配置禁用https，则默认添加https链接\n          url =\n            siteURL +\n            \"download.php?id=\" +\n            id +\n            (PTService.site.passkey\n              ? \"&passkey=\" + PTService.site.passkey\n              : \"\") +\n            (PTService.site.disableHttps ? \"\" : \"&https=1\");\n        } else {\n          url = \"\";\n        }\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/UNIT3D/config.json",
    "content": "{\n  \"name\": \"UNIT3D\",\n  \"ver\": \"0.0.1\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/torrents/(.+)$\", \"^/torrent/(.+)$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"^/torrents$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }, {\n    \"name\": \"个人种子列表页面\",\n    \"pages\": [\"^/users/.*?/(uploads|downloads|seeds|active|torrents|unsatisfieds)\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"userTorrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents\",\n    \"queryString\": \"perPage=100&name=$key$\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"perPage=100&imdbId=$key$\",\n      \"replaceKey\": [\n        \"tt\", \"\"\n      ]\n    }],\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"button.btn.btn-success.btn-circle\", \"button.btn.btn-warning.btn-circle, button.btn.btn-info.btn-circle\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"0\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"button.btn.btn-success.btn-circle\", \"button.btn.btn-warning.btn-circle\", \"button.btn.btn-info.btn-circle\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"1\"],\n          [\"3\"]\n        ]\n      }\n    },\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"i.fa-star.text-gold, i.fa-globe.text-blue\"\n  }, {\n    \"name\": \"2xUp\",\n    \"selector\": \"i.fa-gem.text-green\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": [\"a[href*='settings']:first\"],\n          \"attribute\": \"href\",\n          \"switchFilters\": [\n            [\"new URL(query).pathname.split('/')\", \"(query && query.length>2)?(query[2]):''\"],\n            [\"query ? query.getQueryString('id'):''\"]\n          ]\n        },\n        \"uploaded\": {\n          \"selector\": [\"div.ratio-bar i.fa-arrow-up, ul.top-nav__ratio-bar i.fa-arrow-up, span[title='Upload'], span[title='上传']\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\s|\\\\n/g,'').match(/[\\\\d.]+ ?[ZEPTGMK]?i?B/)\", \"(query && query.length>=1)?(query[0]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"div.ratio-bar i.fa-arrow-down, ul.top-nav__ratio-bar i.fa-arrow-down, span[title='Download'], span[title='下载']\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\s|\\\\n/g,'').match(/[\\\\d.]+ ?[ZEPTGMK]?i?B/)\", \"(query && query.length>=1)?(query[0]).sizeToNumber():0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div.ratio-bar i.fa-coins, ul.top-nav__ratio-bar i.fa-coins, a[title='My Bonus Points'], a[title='我的魔力']\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\s|\\\\n/g,'').match(/[\\\\d.]+/)\", \"(query && query.length>=1)?parseFloat(query[0]):0\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"div.ratio-bar i.fa-upload, ul.top-nav__ratio-bar i.fa-upload, span[title='Seeding'], span[title='做种']\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\s|\\\\n/g,'').match(/[\\\\d.]+/)\", \"(query && query.length>=1)?parseFloat(query[0]):0\"]\n        },\n        \"bonusPage\": {\n          \"selector\": [\"a[href$='bonus']:first\",\"a[href$='earnings']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? new URL(query).pathname : null\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/users/$user.name$\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"table.table-condensed.table-striped.table-bordered:first td:contains('Seeding Size') + td\", \"table.table-condensed.table-striped.table-bordered:first td:contains('做种体积') + td\", \"table.table-condensed.table-striped.table-bordered:first td:contains('做種體積') + td\"\n        , \".panelV2 dt:contains('Seeding Size') + dd\", \".panelV2 dt:contains('做种体积') + dd\", \".panelV2 dt:contains('做種體積') + dd\"],\n          \"filters\": [\"query.text().trim().replace(/,|\\\\s|\\\\n/g,'').sizeToNumber()\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"div.content span.badge-user\", \"a.user-tag__link\"],\n          \"switchFilters\": [[\"query.text()\"],[\"query.attr('title')\"]]\n        },\n        \"messageCount\": {\n          \"selector\": [\".point, ul.top-nav__icon-bar circle\"],\n          \"filters\": [\"query.length?'11':'0'\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"div.content h4:contains('Registration date')\", \"div.content h4:contains('注册日期')\", \"div.content h4:contains('註冊日期')\", \"time.profile__registration\"],\n          \"filters\": [\"query.text().replace(/(Registration date|注册日期|註冊日期)/g, '').trim()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"uploads\": {\n          \"selector\": [\".badge-user .fa-upload + span\"],\n          \"filters\": [\"query ? parseFloat(query.text().trim()) : 0\"]\n        },\n        \"unsatisfiedsPage\": {\n          \"selector\": [\"a[href$='unsatisfieds']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? new URL(query).pathname : null\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!(!user.bonusPage)\",\n      \"page\": \"$user.bonusPage$\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\".table-condensed tr:last\"],\n          \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n        }\n      }\n    },\n    \"hnrExtendInfo\": {\n      \"prerequisites\": \"!(!user.unsatisfiedsPage)\",\n      \"page\": \"$user.unsatisfiedsPage$\",\n      \"fields\": {\n        \"unsatisfieds\": {\n          \"selector\": [\"tr[class='userFiltered'][hr='0'][immune='0']\"],\n          \"filters\": [\"query ? query.length : 0\"]\n        }\n      }\n    },\n    \"common\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"td.col-sm-2:contains('Size') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\".button-block button.btn.btn-sm.btn-primary\"],\n          \"filters\": [\"$(query[0])\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/schemas/UNIT3D/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let url = PTService.getFieldValue(\"downloadURL\");\n      if (!url) {\n        let query = $(\"a[href*='/download/']:first\");\n        if (query.length == 0) {\n          query = $(\"a[href*='/download_check/']\");\n          if (query.length > 0) {\n            url = query.attr(\"href\").replace(\"/download_check/\", \"/download/\");\n          }\n        } else {\n          url = query.attr(\"href\");\n        }\n      }\n\n      return this.getFullURL(url);\n    }\n    \n    /**\n     * 获取当前种子IMDb Id\n     */\n    getIMDbId() {\n      try\n      {\n        let imdbId = PTService.getFieldValue(\"imdbId\");\n        console.log(imdbId);\n        if (imdbId)\n          return imdbId;\n        else {\n          const link = $(\"a[href*='www.imdb.com/title/']:first\");\n          if (link.length > 0) {\n            let match = link.attr(\"href\").match(/(tt\\d+)/);\n\n            if (match && match.length >= 2)\n              return imdbId = match[1];\n\n          }\n        }\n      } catch {\n      }\n      return null;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/schemas/UNIT3D/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/\\/login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector =\n        options.resultSelector || \"table.data-table\";\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 分类\n        category: 0\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n        // 发布时间\n        if (cell.html().match(\"created_at\") || cell.attr('class').endsWith(\"age-header\")) {\n          fieldIndex.time = index;\n          continue;\n        }\n\n        // 大小\n        if (cell.attr('class').indexOf(\"torrent-listings-size\") > -1 || cell.attr('class').endsWith(\"size-header\") || cell.find(\"i.fa-database\").length) {\n          fieldIndex.size = index;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"i.fa-arrow-alt-circle-up\").length) {\n          fieldIndex.seeders = index;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"i.fa-arrow-alt-circle-down\").length) {\n          fieldIndex.leechers = index;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"i.fa-check-circle\").length) {\n          fieldIndex.completed = index;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a.view-torrent, a.torrent-search--list__name\");\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = \"\";\n\n          let downloadURL = row.find(\"a[href*='/download/']\");\n          if (downloadURL.length == 0) {\n            downloadURL = row.find(\"a[href*='/download_check/']\");\n            if (downloadURL.length > 0) {\n              url = downloadURL\n                .attr(\"href\")\n                .replace(\"/download_check/\", \"/download/\");\n            }\n          } else {\n            url = downloadURL.attr(\"href\");\n          }\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let imdbId = row.find(\"div#imdb_id\")\n          if (imdbId.length > 0)\n          {\n            imdbId = imdbId.text().replace(/\\D/g,'');\n            if (imdbId.length < 7)\n              imdbId = imdbId.padStart(7, '0');\n      \n            imdbId = \"tt\" + imdbId;\n          }\n          else {\n            imdbId = null;\n          }\n\n          let data = {\n            title: title.text().trim(),\n            subTitle: this.getSubTitle(title, row).trim(),\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time:\n              fieldIndex.time == -1\n                ? \"\"\n                : cells\n                    .eq(fieldIndex.time)\n                    .find(\"span[title]\")\n                    .attr(\"title\") ||\n                  cells.eq(fieldIndex.time).text().replace('秒前', ' seconds ago').replace('秒前', ' seconds ago').replace('分钟前', ' minutes ago').replace('分鐘前', ' minutes ago').replace('天前', ' day ago').replace('小時前', ' hours ago').replace('小时前', ' hours ago').replace('周前', ' weeks ago').replace('个月前', ' months ago').replace('年前', ' years ago')||\n                  \"\",\n            author:  \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text().trim() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text().trim() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text().trim() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text().trim() || 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category)),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\"),\n            imdbId: imdbId\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取副标题\n     * @param {*} title\n     * @param {*} row\n     */\n    getSubTitle(title, row) {\n      let subTitle = Searcher.getFieldValue(this.site, row, \"subTitle\");\n      if (subTitle) {\n        return subTitle;\n      }\n      return \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: cell.find(\"i:first\").attr(\"data-original-title\"),\n        link: cell.find(\"a:first\").attr(\"href\")\n      };\n      if (result.name) {\n        result.name = result.name.replace(\" torrent\", \"\");\n      }\n      return result;\n    }\n\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text().trim();\n      }\n      if(result == \"\")return null;\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/schemas/UNIT3D/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href*='/download/']\").toArray();\n\n      if (links.length == 0) {\n        links = $(\"a[href*='/download_check/']\").toArray();\n      }\n\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link.replace(\"/download_check/\", \"/download/\");\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"div.table-responsive > table:first\").find(\n          \"td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/schemas/UNIT3D/userTorrents.js",
    "content": "(function($) {\n  console.log(\"this is userTorrents.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a.view-torrent[href*='/torrents/']\").toArray();\n\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link.replace(\"/torrents/\", \"/torrents/download/\");\n      });\n\n      return urls;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/1ptba.com/config.json",
    "content": "{\n  \"name\": \"1PTBar\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://1ptba.com/\",\n  \"description\": \"壹PT吧,PT下载,教育视频,课件资源,发布教育类,学习类,纪录片等资源\",\n  \"icon\": \"https://1ptba.com/kuai360/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"host\": \"1ptba.com\",\n  \"collaborator\": [\"zhuweitung\"],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"5\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.3\",\n      \"seedingPoints\": \"40000\",\n      \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.9\",\n      \"seedingPoints\": \"80000\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.3\",\n      \"seedingPoints\": \"150000\",\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"30\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.7\",\n      \"seedingPoints\": \"250000\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"1024GB\",\n      \"ratio\": \"3.2\",\n      \"seedingPoints\": \"400000\",\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"90\",\n      \"downloaded\": \"2048GB\",\n      \"ratio\": \"3.7\",\n      \"seedingPoints\": \"600000\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"120\",\n      \"downloaded\": \"4096GB\",\n      \"ratio\": \"4.2\",\n      \"seedingPoints\": \"800000\",\n      \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"150\",\n      \"downloaded\": \"10240GB\",\n      \"ratio\": \"5.2\",\n      \"seedingPoints\": \"1000000\",\n      \"privilege\": \"得到十个邀请名额。\"\n    }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/52pt.site/config.json",
    "content": "{\n  \"name\": \"52PT\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://52pt.site/\",\n  \"description\": \"52PT - 我爱PT-低调地在这个PT校园快乐成长 快乐分享\",\n  \"tags\": [\"高清\", \"电影\", \"电视剧\"],\n  \"host\": \"52pt.site\",\n  \"collaborator\": [\"StarGazerQQD\", \"zhuweitung\"],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"得到一个邀请名额；可以查看NFO文档；可以请求续种；可以发送邀请；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；允许发布新的趣味盒内容及编辑自己发布的趣味盒内容;可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"1536GB\",\n      \"ratio\": \"2.55\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"downloaded\": \"2560GB\",\n      \"ratio\": \"3.05\",\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"3072GB\",\n      \"ratio\": \"3.55\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"4608GB\",\n      \"ratio\": \"4.05\",\n      \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"downloaded\": \"5632GB\",\n      \"ratio\": \"4.55\",\n      \"privilege\": \"得到十个邀请名额。\"\n    }\n  ]\n}"
  },
  {
    "path": "resource/sites/README.md",
    "content": "# 站点定义\n\n## 目录说明\n\n该目录存放所有支持的网站，目录名为网站域名\n\n```\n--目录名\n----parser\n-------xxxx.js\n----config.json\n----xxxx.js\n```\n\n- parser : （可选）解析器目录，会在打包时自动将该目录下的所有 js 文件内容生成到 config.js 文件中的 `parser` 字段中\n- config.json : 网站的定义\n- xxxx.js : （可选）页面对应的脚本文件\n\n### config.json 文件示例\n\n```json\n{\n  \"name\": \"OpenCD\",\n  \"description\": \"皇后，专一的音乐类PT站，是目前国内最大的无损音乐PT\",\n  \"url\": \"https://open.cd/\",\n  \"icon\": \"https://open.cd/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"open.cd\",\n  \"plugins\": [\n    {\n      \"name\": \"特殊插件\",\n      \"pages\": [\"/torrents.php\"],\n      \"scripts\": [\"/libs/album/album.js\", \"torrents.js\"],\n      \"styles\": [\"/libs/album/style.css\"]\n    }\n  ],\n  \"searchEntry\": [\n    {\n      \"entry\": \"/torrents.php?search=$key$\",\n      \"name\": \"全部\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrent_list:last > tbody > tr\"\n    }\n  ],\n  \"patterns\": {\n    \"torrentLinks\": [\"*://*/*\"]\n  },\n  \"parser\": {\n    \"downloadURL\": \"解析脚本内容\"\n  },\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \"img.pro_free\",\n      \"color\": \"blue\"\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"*\",\n      \"result\": \"cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 20,\n          \"name\": \"原盘(Full BD)\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n### 属性说明：\n\n- `name` : 网站名称；\n- `description` : （可选）网站描述；\n- `url` : 完整的网站地址，如果网站支持 `https` ，请优先考虑填写 `https` 的地址 ；\n- `icon` : 网站图标地址；\n- `tags` : （可选）标签，是一个数组，多个之间以 `,` 分隔；\n- `schema` : 对应的网站架构；\n- `host` : 域名；\n- `plugins` : （可选）支持的插件列表，是一个数组\n  - `name` : 插件名称；\n  - `pages` : 表示该插件在哪些页面加载；\n  - `scripts` : 插件对应的脚本文件，`JavaScript` 文件\n- `searchEntry` : （可选）搜索入口配置，如果指定则必需为数组，如果不指定则以网站架构定义的为准\n  - `entry` : 入口文件\n  - `name` : 自定义入口的名称\n  - `resultType` : 搜索返回的原始结果类型：html, json, xml\n  - `parseScriptFile` : 解析原始结果的脚本文件\n  - `resultSelector` : 定位种子列表的 `jQuery` 查询表达式\n- `patterns` : （可选）页面匹配规则\n  - `torrentLinks` : 用于匹配有效的种子链接，作用于右键菜单，如果不指定，则匹配所有链接；\n- `parser` : （可选）解析器，打包时根据 parser 目录生成\n  - `downloadURL` : 解析下载链接，用于解析和生成点击右键下载时的链接\n- `torrentTagSelectors` : （可选）种子标签选择器，数组\n  - `name` : 标签名称\n  - `selector` : 选择器\n  - `color` : 标签颜色\n- `categories` : 站点对应搜索入口的种子分类信息，数组\n  - `entry` : 需要匹配的入口，`*` 表示适用于所有入口；`torrents.php` 表示只适用于 `torrents.php` 的入口页面\n  - `result` : 分类配置返回信息 `$id$` 会被替换为具体的分类编号，最终会拼接到入口地址后面，如：`&cat10=1&cat11=1`\n  - `category` : 分类信息，数组\n    - `id` : 分类编号\n    - `name` : 分类名称\n\n### 脚本及脚本文件定义\n\n脚本文件及脚本片段，请使用 `闭包` ，以避免 `命名污染` 。\n\n### 关于脚本及其他资源文件路径说明\n\n- 如果在第一个位置指定了 `/` ，则路径会被指向到：\n  - `https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/`\n- 如果第一个位置不是 `/` ，则表示当前路径为该网站所在目录，如 `open.cd` 的指向目录为：\n  - `https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/sites/open.cd/`\n\n## 如何提交一个新的站点？\n\n> 由于本人精力及能力有限，仅能维护部分站点，如果你有更多更好玩的站点需要在助手中直接下拉选择显示，并愿意分享给其他用户使用，那么赶紧通过以下方式提交吧；（怎么听着像广告~\\_~）\n\n1. 如果你有 `github` 账户，并知道如何使用 `git` ，那么可以按以下步骤进行提交\n\n   - `Fork` 本项目；\n   - 将 `Fork` 后的项目 `clone` 到本地；\n   - 在项目的 `resource\\sites` 目录下新建一个站点目录，如：`pt.mysite.com`\n   - 在 `pt.mysite.com` 目录下新建一个 `config.json` 文件，内容参考上面的 `config.json 文件示例`；\n   - 如有需要，再创建特定的脚本；\n   - 以上操作完成后，使用 `git` 将修改内容 `push` 到自己的 `github` 仓库；\n   - 最后在 `github` 仓库中发起一个 `PR(pull request)` 即可；\n\n2. 加入开发交流 QQ 群：773500545，把你的配置文件分享给我们吧；\n3. 通过 [该主题](https://github.com/pt-plugins/PT-Plugin-Plus/issues/30) 留言，按格式提交已测试可用的站点信息；\n\n## PR 参考资料\n\n- https://blog.csdn.net/vim_wj/article/details/78300239\n- http://www.ruanyifeng.com/blog/2017/07/pull_request.html\n- https://gist.github.com/zxhfighter/62847a087a2a8031fbdf\n- https://github.com/geeeeeeeeek/git-recipes/wiki/3.3-%E5%88%9B%E5%BB%BA-Pull-Request\n"
  },
  {
    "path": "resource/sites/aidoru-online.me/config.json",
    "content": "{\n  \"name\": \"Aidoru!Online\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"AO\",\n  \"icon\": \"https://aidoru-online.me/themes/default/images/favicon.ico\",\n  \"url\": \"https://aidoru-online.me/\",\n  \"tags\": [\"偶像\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/torrents-details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents-today.php\", \"/torrents-search.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"host\": \"aidoru-online.me\",\n  \"searchEntryConfig\": {\n\t\"skipIMDbId\": true,\n    \"page\": \"/get_ttable.php?pcat=Show+All&subbed=&fl=&resd=&p=0&searchstr=$key$&deadlive=1&sortcol=id&sortorder=desc&startdt=&enddt=\",\n    \"loggedRegex\": \"class=\\\"ttable_headinner\\\"\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table\",\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 1,\n\t    \"link\": 1,\n\t    \"url\": 2,\n        \"comments\": 5,\n        \"time\": 10,\n        \"size\": 6,\n        \"author\": 4,\n        \"seeders\": 7,\n        \"leechers\": 8,\n        \"completed\": 9\n\t},\n\t\"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"a\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a\"],\n        \"filters\": [\"query.attr('href')\", \"'https://aidoru-online.me/'+query\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.children().attr('href')\", \"'https://aidoru-online.me/'+query\"]\n\t  },\n\t  \"time\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"'20'+query.text()\"]\n\t  },\n\t  \"progress\": {\n        \"selector\": [\"td.ttable_seeding font[color='green'], td.ttable_seeding font[color='black']\", \"td.ttable_seeding font[color='#ff0000']\", \"\"],\n        \"switchFilters\": [\n          [\"query.length > 0 ? 100:null\"],\n          [\"query.length > 0 ? 0:null\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"td.ttable_seeding font[color='green']\", \"td.ttable_seeding font[color='black']\", \"td.ttable_seeding font[color='#ff0000']\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"255\"],\n          [\"1\"]\n        ]\n      }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }, {\n    \"appendQueryString\": \"&scat=1\",\n    \"name\": \"BD/DVDISO\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=2\",\n    \"name\": \"BD/DVD-RIP\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=3\",\n    \"name\": \"TV\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=4\",\n    \"name\": \"Perf\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=5\",\n    \"name\": \"PV\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=6\",\n    \"name\": \"PV\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=7\",\n    \"name\": \"Image\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=8\",\n    \"name\": \"Audio\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=9\",\n    \"name\": \"Album\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=10\",\n    \"name\": \"Single\",\n    \"enabled\": false\n  }, {\n    \"appendQueryString\": \"&scat=11\",\n    \"name\": \"Radio\",\n    \"enabled\": false\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[src='images/freeleech.png']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"#main > table .myBlock-caption:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='account-logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href*='/forum/private.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\".myBlock-content td:contains('Uploaded:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\".myBlock-content td:contains('Downloaded:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": [\".myBlock-content td:contains('Ratio:') + td\"],\n          \"filters\": [\"query.text()\"]\n        }, \n        \"levelName\": {\n          \"selector\": [\".myBlock-content td:contains('Class:') + td\"],\n          \"filters\":  [\"query.text()\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/account.php\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"td.prof-lbl:contains('Joined:') + td\"],\n          \"filters\": [\"dateTime(query.text()).valueOf()\"]\n        },\n        \"seeding\": {\n\t      \"selector\": [\"b:contains('Currently seeding')\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):null\"]\n        },\n        \"seedingSize\": {\n\t        \"selector\": [\"b:contains('Currently seeding') + br + table tr:not(:first-child) > td:nth-child(4)\"],\n\t        \"filters\": [\"jQuery.map(query, (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/torrents-details.php\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td[align='left']:contains('Total Size:') + td\"],\n          \"filters\": [\"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#ty-button\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.ttable_headinner\"],\n          \"filters\": [\"query.find('td.ttable_size')\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/aither.cc/config.json",
    "content": "{\n  \"name\": \"Aither\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"UNIT3D\",\n  \"url\": \"https://aither.cc/\",\n  \"description\": \"Aither\",\n  \"tags\": [\n    \"综合\"\n  ],\n  \"host\": \"aither.cc\",\n  \"collaborator\": \"MewX\",\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Harmonia\",\n      \"interval\": \"1\",\n      \"uploaded\": \"500Gib\",\n      \"privilege\": \"Unlimited DL slots\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"Zeus\",\n      \"interval\": \"2\",\n      \"uploaded\": \"1TiB\"\n    },\n    {\n      \"level\": \"3\",\n      \"name\": \"Helios\",\n      \"interval\": \"3\",\n      \"uploaded\": \"5TiB\"\n    },\n    {\n      \"level\": \"4\",\n      \"name\": \"Prometheus\",\n      \"interval\": \"4\",\n      \"uploaded\": \"10TiB\"\n    },\n    {\n      \"level\": \"5\",\n      \"name\": \"Oceanus\",\n      \"interval\": \"4\",\n      \"uploaded\": \"20TiB\",\n      \"privilege\": \"Mod Q skip, Special FL, Invite Forum, Sparkly name\"\n    },\n    {\n      \"level\": \"6\",\n      \"name\": \"Gigantes\",\n      \"interval\": \"12\",\n      \"uploaded\": \"40TiB\",\n      \"privilege\": \"Mod Q skip, Special FL, Invite Forum, Sparkly name, HnR immunity\"\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/alpharatio.cc/config.json",
    "content": "{\n  \"name\": \"AlphaRatio\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"0day\",\n  \"url\": \"https://alpharatio.cc/\",\n  \"icon\": \"https://alpharatio.cc/favicon.ico\",\n  \"tags\": [\"综合\", \"0day\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"alpharatio.cc\",\n  \"collaborator\": \"enigamz\",\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"filter_cat[1]=1\",\n      \"name\": \"TvSD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[2]=1\",\n      \"name\": \"TvHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[3]=1\",\n      \"name\": \"TvUHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[4]=1\",\n      \"name\": \"TvDVDRip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[5]=1\",\n      \"name\": \"TvPackSD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[6]=1\",\n      \"name\": \"TvPackHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[7]=1\",\n      \"name\": \"TvPackUHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[8]=1\",\n      \"name\": \"MovieSD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[9]=1\",\n      \"name\": \"MovieHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[10]=1\",\n      \"name\": \"MovieUHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[11]=1\",\n      \"name\": \"MoviePackSD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[12]=1\",\n      \"name\": \"MoviePackHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[13]=1\",\n      \"name\": \"MoviePackUHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[14]=1\",\n      \"name\": \"MovieXXX\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[15]=1\",\n      \"name\": \" Bluray\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[16]=1\",\n      \"name\": \"AnimeSD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[17]=1\",\n      \"name\": \"AnimeHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[18]=1\",\n      \"name\": \"GamesPC\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[19]=1\",\n      \"name\": \"GamesxBox\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[20]=1\",\n      \"name\": \"GamesPS\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[21]=1\",\n      \"name\": \"GamesNin\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[22]=1\",\n      \"name\": \"AppsWindows\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[23]=1\",\n      \"name\": \"AppsMAC\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[24]=1\",\n      \"name\": \"AppsLinux\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[25]=1\",\n      \"name\": \"AppsMobile\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[26]=1\",\n      \"name\": \"0dayXXX\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[27]=1\",\n      \"name\": \"eBook\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[28]=1\",\n      \"name\": \"AudioBook\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[29]=1\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[30]=1\",\n      \"name\": \"Misc\",\n      \"enabled\": false\n    }\n  ],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/animebytes.tv/config.json",
    "content": "{\n  \"name\": \"AB\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"动漫\",\n  \"url\": \"https://animebytes.tv/\",\n  \"icon\": \"https://animebytes.tv/favicon.ico\",\n  \"tags\": [\"动漫\"],\n  \"schema\": \"\",\n  \"host\": \"animebytes.tv\",\n  \"collaborator\": [\n    \"MewX\",\n    \"sabersalv\"\n  ],\n  \"supportedFeatures\": {\n    \"userData\": true,\n    \"search\": true,\n    \"imdbSearch\": false,\n    \"sendTorrent\": false\n  },\n  \"plugins\": [\n    {\n      \"name\": \"Custom Torrent List\",\n      \"pages\": [\n        \"/series.php\",\n        \"/torrents.php\",\n        \"/torrents2.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"userTorrents.js\"\n      ]\n    }\n  ],\n  \"securityKeyFields\": [\n    \"authkey\",\n    \"torrent_pass\"\n  ],\n  \"searchEntry\": [\n    {\n      \"entry\": \"/torrents.php?searchstr=$key$&force_default=1\",\n      \"name\": \"default\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"getSearchResult.js\",\n      \"resultSelector\": \"div.group_cont\",\n      \"enabled\": true\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"#stats_menu > a:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('userid'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a.username:first\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='/user/logout']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\".alertbar.message a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploads\":{\n          \"selector\": [\"dt:contains('Torrents Uploaded:') + dd\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/[\\\\d]+/)[0]\", \"parseInt(query)\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"dt:contains('Uploaded:') + dd > span\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'')\", \"parseFloat(query)\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"dt:contains('Downloaded:') + dd > span\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'')\", \"parseFloat(query)\"]\n        },\n        \"unsatisfieds\": {\n          \"selector\": [\"ul.stats li:contains('H&Rs:')\"],\n          \"attribute\": \"title\",\n          \"filters\": [\"query.replace(/,/g,'').match(/[\\\\d]+/g)\", \"query && query.length >= 2 ? parseInt(query[1]) : 0\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"dt:contains('Ratio:') + dd > span\"],\n          \"attribute\": \"title\",\n          \"filters\": [\"query ? query.replace(/,/g,'') : null\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"dt:contains('Seeding:') + dd\"],\n          \"filters\": [\n            \"query.text().trim().replace(/,|\\\\n/g,'').match(/([\\\\d.]+)/)\",\n            \"(query && query.length>=2)?parseFloat(query[1]):null\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"dt:contains('Total seed size:') + dd > span\"],\n          \"filters\": [\"query.text().trim().sizeToNumber()\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"dt:contains('Class:') + dd\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"#yen_count > a\"],\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+|¥/g,'')\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"dt:contains('Yen per day:') + dd\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/[\\\\d.]+/)\", \"query ? parseFloat(query[0]) / 24 : 0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"dt:contains('Joined:') + dd > span\"],\n          \"filters\": [\n            \"query.attr('title')||query.text()\",\n            \"dateTime(query, 'MMM DD YYYY, HH:mm').isValid()?dateTime(query, 'MMM DD YYYY, HH:mm').valueOf():query\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/animebytes.tv/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function (name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n      \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n    ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function (options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/loginform/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n      options.isLogged = true;\n\n      if (/File slips through fingers/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n      this.haveData = true;\n    }\n\n    /**\n     * Get search results.\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n\n      // Get groups. Each group has one title and several torrents.\n      let groups = options.page.find(options.resultSelector);\n      if (groups.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n\n      try {\n        for (let ig = 0; ig < groups.length; ig++) {\n          // Get group info.\n          let group = groups.eq(ig);\n          let groupTitle = group.find(\".group_title\").find(\"strong:first\").text();\n          if (groupTitle.length == 0) {\n            continue;\n          }\n          let category = group.find(\"span.cat:first\").text();\n\n          // Get torrent info.\n          let torrents = group.find(\".torrent_group:first\").find(\"tr.torrent\");\n          for (let i = 0; i < torrents.length; i++) {\n            let t = torrents.eq(i);\n            let subTitle = t.find(\".torrent_properties:first\").find(\"a:last\").text();\n            let dlLink = site.url + t.find(\".download_link:first\").find(\"a:first\").attr(\"href\");\n            let torrentURL = site.url + t.find(\".torrent_properties:first\").find(\"a:last\").attr(\"href\");\n            let size = t.find(\".torrent_size:first\").text();\n            let snatched = t.find(\".torrent_snatched:first\").text();\n            let seeders = t.find(\".torrent_seeders:first\").text();\n            let leechers = t.find(\".torrent_leechers:first\").text();\n\n            // Basic validations.\n            if (dlLink.length == 0) {\n              console.log(\"[%s] Invalid torrent link for \\\"%s\\\": %s\", site.name, groupTitle, dlLink);\n              continue;\n            }\n\n            let data = {\n              title: groupTitle,\n              subTitle: subTitle,\n              link: torrentURL, // Note: link means the torrent page.\n              url: dlLink, // Note: url means the download link.\n              size: size,\n              time: \"\",\n              author: \"\",\n              seeders: seeders,\n              leechers: leechers,\n              completed: snatched,\n              comments: \"\",\n              site: site,\n              entryName: options.entry.name, // TODO: support specifying entry name.\n              category: category,\n            };\n            results.push(data);\n          }\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n      return results;\n    }\n\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/animebytes.tv/userTorrents.js",
    "content": "(function ($) {\n  console.log(\"this is userTorrents.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[title='Download torrent']\").toArray();\n      console.log(links);\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n      console.log(urls);\n\n      return urls;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/anthelion.me/config.json",
    "content": "{\n  \"name\": \"Anthelion\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Movies\",\n  \"url\": \"https://anthelion.me//\",\n  \"icon\": \"https://anthelion.me/favicon.ico\",\n  \"tags\": [\"电影\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"anthelion.me\",\n  \"collaborator\": \"enigamz\",\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"parseScriptFile\": \"getSearchResult.js\"\n  },\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/torrents.php?type=seeding&userid=$user.id$\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"tr.torrent_row > td.nobr\"],\n          \"filters\": [\"jQuery.map(query, (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"a[href*='store.php']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        }\n      }\n    }\n  }  \n}"
  },
  {
    "path": "resource/sites/anthelion.me/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n      this.authkey = \"\";\n      this.passkey = \"\";\n    }\n\n    start() {\n      this.getAuthKey()\n        .then(() => {\n          options.resolve(this.getResult());\n        })\n        .catch(() => {\n          options.reject({\n            success: false,\n            msg: options.searcher.getErrorMessage(\n              options.site,\n              ESearchResultParseStatus.parseError,\n              options.errorMsg\n            ),\n            data: {\n              site: options.site,\n              isLogged: options.isLogged\n            }\n          });\n        });\n    }\n\n    /**\n     * 获取搜索结果\n     */\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let groups = options.page.response.results;\n      if (groups.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return [];\n      }\n      let results = [];\n      let authkey = this.authkey;\n      let passkey = this.passkey;\n      console.log(\"groups.length\", groups.length);\n      try {\n        groups.forEach(group => {\n          if (group.hasOwnProperty(\"torrents\")) {\n            let torrents = group.torrents;\n            torrents.forEach(torrent => {\n              let data = {\n                title:\n                  group.groupName +\n                  \" [\" +\n                  group.groupYear +\n                  \"]\",\n                subTitle:\n                  torrent.codec +\n                  \" / \" +\n                  torrent.container +\n                  \" / \" +\n                  torrent.media +\n                  \" / \" +\n                  torrent.resolution + \n                  \" / \" +\n                  torrent.audio +\n                  (torrent.hasLog ? ` / Log(${torrent.logScore})` : \"\") +\n                  (torrent.hasCue ? \" / Cue\" : \"\") +\n                  (torrent.remastered ? ` / ${torrent.remasterYear}` : \"\") +\n                  (torrent.scene ? \" / Scene\" : \"\") +\n                  (torrent.isFreeleech ||\n                  torrent.isNeutralLeech ||\n                  torrent.isPersonalFreeleech\n                    ? \" / Freeleech\"\n                    : \"\"),\n                link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${torrent.torrentId}`,\n                url: `${site.url}torrents.php?action=download&id=${torrent.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n                size: parseFloat(torrent.size),\n                time: torrent.time,\n                seeders: torrent.seeders,\n                leechers: torrent.leechers,\n                completed: torrent.snatches,\n                site: site,\n                entryName: options.entry.name,\n                category: group.releaseType\n              };\n              results.push(data);\n            });\n          } else {\n            let data = {\n              title: group.groupName,\n              link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${group.torrentId}`,\n              url: `${site.url}torrents.php?action=download&id=${group.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n              size: parseFloat(group.size),\n              time: group.groupTime,\n              author: \"\",\n              seeders: group.seeders,\n              leechers: group.leechers,\n              completed: group.snatches,\n              comments: 0,\n              site: site,\n              tags: group.tags,\n              entryName: options.entry.name,\n              category: group.category\n            };\n            results.push(data);\n          }\n        });\n        console.log(\"results.length\", results.length);\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n\n    /**\n     * 获取 AuthKey ，用于组合完整的下载链接\n     */\n    getAuthKey() {\n      const url = (options.site.activeURL + \"/ajax.php?action=index\")\n        .replace(\"://\", \"****\")\n        .replace(/\\/\\//g, \"/\")\n        .replace(\"****\", \"://\");\n\n      return new Promise((resolve, reject) => {\n        $.get(url)\n          .done(result => {\n            if (result && result.status === \"success\" && result.response) {\n              this.authkey = result.response.authkey;\n              this.passkey = result.response.passkey;\n              resolve();\n            } else {\n              reject();\n            }\n          })\n          .fail(() => {\n            reject();\n          });\n      });\n    }\n  }\n\n  let parser = new Parser(options);\n  parser.start();\n})(options);\n"
  },
  {
    "path": "resource/sites/asiancinema.me/config.json",
    "content": "{\n  \"name\": \"AsianCinema\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"综合\",\n  \"url\": \"https://asiancinema.me/\",\n  \"icon\": \"https://asiancinema.me/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"UNIT3D\",\n  \"host\": \"asiancinema.me\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"1\",\n    \"uploaded\": \"1TB\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Super User\",\n    \"interval\": \"2\",\n    \"uploaded\": \"5TB\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"3\",\n    \"uploaded\": \"20TB\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"6\",\n    \"uploaded\": \"50TB\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran\",\n    \"interval\": \"12\",\n    \"uploaded\": \"100TB\",\n    \"privilege\": \"Special freeleech\"\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents/filter\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/sites/asiancinema.me/getSearchResult.js\",\n    \"resultSelector\": \"div.table-responsive > table:first\",\n    \"queryString\": \"search=$key$&qty=100\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"imdb=$key$&qty=100\",\n      \"replaceKey\": [\n        \"tt\", \"\"\n      ]\n    }]\n  }\n}"
  },
  {
    "path": "resource/sites/asiancinema.me/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/\\/login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      site.searchEntryConfig = options.entry\n      let selector =\n        options.resultSelector || \"div.table-responsive > table:first\";\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: header.length - 1,\n        // 分类\n        category: 1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n\n        // 评论数\n        if (cell.find(\"a[href*='comments']\").length) {\n          fieldIndex.comments = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 发布时间\n        if (\n          cell.find(\"a[href*='created_at']\").length ||\n          cell.find(\"i.fa-clock\").length\n        ) {\n          fieldIndex.time = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 大小\n        if (\n          cell.find(\"a[href*='size']\").length ||\n          cell.find(\"i.fa-file\").length\n        ) {\n          fieldIndex.size = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 种子数\n        if (\n          cell.find(\"a[href*='seed']\").length ||\n          cell.find(\"i.fa-arrow-circle-up\").length\n        ) {\n          fieldIndex.seeders = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 下载数\n        if (\n          cell.find(\"a[href*='leech']\").length ||\n          cell.find(\"i.fa-arrow-circle-down\").length\n        ) {\n          fieldIndex.leechers = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 完成数\n        if (\n          cell.find(\"a[href*='complete']\").length ||\n          cell.find(\"i.fa-check-square\").length\n        ) {\n          fieldIndex.completed = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 分类\n        if (cell.is(\".torrents-icon\")) {\n          fieldIndex.category = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a.view-torrent\");\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = \"\";\n\n          let downloadURL = row.find(\"a[href*='/download/']\");\n          if (downloadURL.length == 0) {\n            downloadURL = row.find(\"a[href*='/download_check/']\");\n            if (downloadURL.length > 0) {\n              url = downloadURL\n                .attr(\"href\")\n                .replace(\"/download_check/\", \"/download/\");\n            }\n          } else {\n            url = downloadURL.attr(\"href\");\n          }\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: this.getSubTitle(title, row),\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time:\n              fieldIndex.time == -1\n                ? \"\"\n                : cells\n                    .eq(fieldIndex.time)\n                    .find(\"span[title]\")\n                    .attr(\"title\") ||\n                  cells.eq(fieldIndex.time).text().replace('秒前', ' seconds ago').replace('秒前', ' seconds ago').replace('分钟前', ' minutes ago').replace('分鐘前', ' minutes ago').replace('天前', ' day ago').replace('小時前', ' hours ago').replace('小时前', ' hours ago').replace('周前', ' weeks ago').replace('个月前', ' months ago').replace('年前', ' years ago').replace('年', ' years ago')||\n                  \"\",\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category)),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        selectors.forEach(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n            }\n          }\n        });\n      }\n      return tags;\n    }\n\n    /**\n     * 获取副标题\n     * @param {*} title\n     * @param {*} row\n     */\n    getSubTitle(title, row) {\n      return \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: cell.find(\"i:first\").attr(\"data-original-title\"),\n        link: cell.find(\"a:first\").attr(\"href\")\n      };\n      if (result.name) {\n        result.name = result.name.replace(\" torrent\", \"\");\n      }\n      return result;\n    }\n\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/audiences.me/config.json",
    "content": "{\n  \"name\": \"Audiences\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"观众\",\n  \"url\": \"https://audiences.me/\",\n  \"icon\": \"https://audiences.me/favicon.ico\",\n  \"tags\": [\n    \"综合\",\n    \"影视\",\n    \"音乐\",\n    \"电子书\",\n    \"有声书\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"audiences.me\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"2.0\",\n    \"seedingPoints\": \"100000\",\n    \"privilege\": \"查看NFO文档；查看用户列表；请求续种；查看其它用户的种子历史；删除自己上传的字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"240GB\",\n    \"ratio\": \"2.5\",\n    \"seedingPoints\": \"180000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"24\",\n    \"downloaded\": \"400GB\",\n    \"ratio\": \"3.0\",\n    \"seedingPoints\": \"320000\",\n    \"privilege\": \"查看排行榜\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"600GB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"480000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"900GB\",\n    \"ratio\": \"4.0\",\n    \"seedingPoints\": \"660000\",\n    \"privilege\": \"查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"2048GB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"880000\",\n    \"privilege\": \"永远保留账号；更新过期的外部信息\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"100\",\n    \"downloaded\": \"4096GB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"1080000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"112\",\n    \"downloaded\": \"8192GB\",\n    \"ratio\": \"5.0\",\n    \"seedingPoints\": \"1280000\",\n    \"privilege\": \"无\"\n  }],\n  \"collaborator\": \"Audiences\",\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"td:not(.rowfollow):not(.colhead):not(.embedded)\"\n        ],\n        \"filters\": [\n          \"query.text()=='-'?null:query.text()\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \".torrents-progress\", \".torrents-progress2\"\n        ],\n        \"switchFilters\": [\n          [\"query.attr('style').indexOf('100%')!=-1?2:3\"],\n          [\"255\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    }\n  ],\n  \"selectors\":{\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('票根') + td\", \"td.rowhead:contains('爆米花') + td\", \"td.rowhead:contains('Karma Points') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/azusa.wiki/config.json",
    "content": "{\n  \"name\": \"Azusa\",\n  \"description\": \"梓喵\",\n  \"schema\": \"NexusPHP\",\n  \"timezoneOffset\": \"+0800\",\n  \"icon\": \"https://azusa.wiki/favicon.ico\",\n  \"tags\": [\"漫画\", \"轻小说\", \"Galgame\", \"画集\"],\n  \"url\": \"https://azusa.wiki\",\n  \"host\": \"azusa.wiki\",\n  \"collaborator\": \"zhuweitung\",\n  \"formerHosts\": [\n    \"www.azusa.wiki\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"uploads\": \"1\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1\",\n      \"seedingPoints\": \"40000\",\n      \"privilege\": \"得到一个邀请名额；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"uploads\": \"10\",\n      \"downloaded\": \"100GB\",\n      \"ratio\": \"2\",\n      \"seedingPoints\": \"100000\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"uploads\": \"20\",\n      \"downloaded\": \"150GB\",\n      \"ratio\": \"3\",\n      \"seedingPoints\": \"300000\",\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"uploads\": \"40\",\n      \"downloaded\": \"200GB\",\n      \"ratio\": \"4\",\n      \"seedingPoints\": \"500000\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"uploads\": \"80\",\n      \"downloaded\": \"250GB\",\n      \"ratio\": \"5\",\n      \"seedingPoints\": \"1000000\",\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"uploads\": \"150\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"6\",\n      \"seedingPoints\": \"1500000\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"uploads\": \"150\",\n      \"downloaded\": \"350GB\",\n      \"ratio\": \"7\",\n      \"seedingPoints\": \"2000000\",\n      \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"uploads\": \"300\",\n      \"downloaded\": \"400GB\",\n      \"ratio\": \"8\",\n      \"seedingPoints\": \"5000000\",\n      \"privilege\": \"得到十个邀请名额。\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails.php'][class*='Name']:first\"],\n          \"filters\": [\"query ? query.find('i.icon-rank:first').length > 0 ? $(query.find('i.icon-rank:first')[0].previousSibling).text() : query.text() : ''\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"merge\": true,\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text().trim()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim().match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==2)?(query[0]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"userUploadedTorrents\": {\n      \"prerequisites\": \"!user.uploads\",\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=uploaded\",\n      \"fields\": {\n        \"uploads\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text().trim()\"]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/baconbits.org/config.json",
    "content": "{\n  \"name\": \"bB\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"\",\n  \"url\": \"https://baconbits.org/\",\n  \"icon\": \"https://baconbits.org/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"\",\n  \"host\": \"baconbits.org\",\n  \"supportedFeatures\": {\n    \"userData\": true,\n    \"search\": false,\n    \"imdbSearch\": false,\n    \"sendTorrent\": false\n  },\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\".username\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\".username\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"li:contains('Uploaded:') > span\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"li:contains('Downloaded:') > span\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"li:contains('Ratio:') > span\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'')\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"li:contains('Seeding:')\"],\n          \"filters\": [\n            \"query.text().trim().replace(/,|\\\\n/g,'').match(/:.+?([\\\\d.]+)/)\",\n            \"(query && query.length>=2)?parseFloat(query[1]):null\"\n          ]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        },\n        \"levelName\": {\n          \"selector\": [\"li:contains('Class:')\"],\n          \"filters\": [\"query.text().match(/Class:(.+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"li:contains('Bonus Points:') > a\"],\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'')\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"li:contains('Joined:') > span\"],\n          \"filters\": [\n            \"query.attr('title')\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/bemaniso.ws/config.json",
    "content": "{\n  \"name\": \"Bemaniso\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Game,music\",\n  \"url\": \"https://bemaniso.ws/\",\n  \"icon\": \"https://bemaniso.ws/favicon.ico\",\n  \"tags\": [\"Game\",\"Music\",\"sims\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"bemaniso.ws\",\n  \"collaborator\": \"ted423\",\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n    }\n  ],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/beyond-hd.me/config.json",
    "content": "{\r\n  \"name\": \"BeyondHD\",\r\n  \"timezoneOffset\": \"+0000\",\r\n  \"description\": \"Beyond Your Imagination,BeyondHD is a community-built Movie/TV database. Every piece of data has been added by our amazing community since 2012. BeyondHD is blessed to have a proactive userbase that focuses on HD content, an awesome/secure codebase and a helpful and friendly volunteer Staff team.\",\r\n  \"url\": \"https://beyond-hd.me/\",\r\n  \"icon\": \"https://beyond-hd.me/favicon.ico\",\r\n  \"tags\": [\"影视\"],\r\n  \"schema\": \"UNIT3D\",\r\n  \"host\": \"beyond-hd.me\",\r\n  \"collaborator\": \"lengmianxia\",\r\n  \"plugins\": [{\r\n    \"name\": \"个人种子列表页面\",\r\n    \"pages\": [\"^/.*?/(uploads|downloads|seeds|active|torrents|unsatisfieds)\"],\r\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/UNIT3D/torrents.js\"]\r\n  }],\r\n  \"searchEntryConfig\": {\r\n    \"page\": \"/torrents\",\r\n    \"resultType\": \"html\",\r\n    \"parseScriptFile\": \"getSearchResult.js\",\r\n    \"resultSelector\": \"div.table-torrents > table:last\",\r\n    \"queryString\": \"search=$key$&qty=100\",\r\n    \"area\": [{\r\n      \"name\": \"IMDB\",\r\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\r\n      \"queryString\": \"imdb=$key$&qty=100\"\r\n    }],\r\n    \"fieldSelector\": {\r\n      \"progress\": {\r\n        \"selector\": [\"i.fal.fa-seedling, i.fal.fa-check[title='Snatched']\"],\r\n        \"filters\": [\"query.attr('title')?100:null\"]\r\n      },\r\n      \"status\": {\r\n        \"selector\": [\"i.fal.fa-seedling\", \"i.fal.fa-check[title='Snatched']\"],\r\n        \"switchFilters\": [\r\n          [\"2\"],\r\n          [\"255\"]\r\n        ]\r\n      }\r\n    }\r\n  },\r\n  \"searchEntry\": [{\r\n    \"name\": \"全部\",\r\n    \"enabled\": true\r\n  }],\r\n  \"torrentTagSelectors\": [{\r\n    \"name\": \"Free\",\r\n    \"selector\": \".fas.fa-star[title*='100%']\"\r\n  }, {\r\n    \"name\": \"25%\",\r\n    \"selector\": \".fas.fa-star[title*='25%']\"\r\n  }, {\r\n    \"name\": \"50%\",\r\n    \"selector\": \".fas.fa-star[title*='50%']\"\r\n  }, {\r\n    \"name\": \"75%\",\r\n    \"selector\": \".fas.fa-star[title*='75%']\"\r\n  }],\r\n  \"selectors\": {\r\n    \"userBaseInfo\": {\r\n      \"page\": \"/\",\r\n      \"fields\": {\r\n        \"id\": {\r\n          \"selector\": [\"div.dropmenu > a[href]:first\"],\r\n          \"attribute\": \"href\",\r\n          \"switchFilters\": [\r\n            [\"query.match(/me\\\\/(.+)\\\\.(.+)/)\", \"(query && query.length>=3)?(query[2]):''\"],\r\n            [\"query ? query.getQueryString('id'):''\"]\r\n          ]\r\n        },\r\n        \"name\": {\r\n          \"selector\": [\"div.dropmenu > a[href]:first\"],\r\n          \"attribute\": \"href\",\r\n          \"switchFilters\": [\r\n            [\"query.match(/me\\\\/(.+)\\\\.(.+)/)\", \"(query && query.length>=3)?(query[1]):''\"],\r\n            [\"query ? query.getQueryString('id'):''\"]\r\n          ]\r\n        },\r\n        \"uploaded\": {\r\n          \"selector\": [\"a[href*='uploads']:first\"],\r\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\n|\\\\s+/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\r\n        },\r\n        \"downloaded\": {\r\n          \"selector\": [\"a[href*='downloads']:first\"],\r\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\n|\\\\s+/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\r\n        },\r\n        \"bonus\": {\r\n          \"selector\": [\"a[href*='bonus']:first\"],\r\n          \"filters\": [\"query.parent().text().trim().replace(/,|\\\\n|\\\\s+/g,'').replace(/BP:/g,'')\"]\r\n        },\r\n        \"seeding\": {\r\n          \"selector\": [\"#beta-stats i.fa-seedling\"],\r\n          \"filters\": [\"query.parent().text().trim()\"]\r\n        },\r\n        \"messageCount\": {\r\n          \"selector\": [\".beta-alert:not([title='Bets']) .notify\"],\r\n          \"filters\": [\"query[0]?11:0\"]\r\n        }\r\n      }\r\n    },\r\n    \"userExtendInfo\": {\r\n      \"page\": \"/$user.name$.$user.id$\",\r\n      \"fields\": {\r\n        \"seedingSize\": {\r\n          \"selector\": [\"td:contains('Active Seed Size') + td\"],\r\n          \"filters\": [\"query.text().trim().replace(/,/g,'').sizeToNumber()\"]\r\n        },\r\n        \"levelName\": {\r\n          \"selector\": \"div.button-holder span.badge-faded\"\r\n        },\r\n        \"joinTime\": {\r\n          \"selector\": \"div.button-holder h5:contains('Since: ')\",\r\n          \"filters\": [\"query.text().trim().replace('Since: ', '').replace('Member ', '')\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\r\n        },\r\n        \"unsatisfiedsPage\": {\r\n          \"selector\": [\"a[href$='/unsatisfieds']:first\"],\r\n          \"attribute\": \"href\",\r\n          \"filters\": [\"query ? new URL(query).pathname : null\"]\r\n        }\r\n      }\r\n    },\r\n    \"bonusExtendInfo\": {\r\n      \"prerequisites\": \"!user.bonusPerHour\",\r\n      \"page\": \"/$user.name$.$user.id$/bonus\",\r\n      \"fields\": {\r\n        \"bonusPerHour\": {\r\n          \"selector\": [\"div.panel-body > div.hd-table > div:first > div:first\"],\r\n          \"filters\": [\"parseFloat(query.text())\"]\r\n        }\r\n      }\r\n    },\r\n    \"hnrExtendInfo\": {\r\n      \"prerequisites\": \"!(!user.unsatisfiedsPage)\",\r\n      \"page\": \"$user.unsatisfiedsPage$\",\r\n      \"fields\": {\r\n        \"unsatisfieds\": {\r\n          \"selector\": [\"ul.pagination\",\"tr[class='userFiltered'][hr='0'][immune='0']\"],\r\n          \"filters\": [\"query.find('li > a:not([rel])').length > 0 ? query.find('li > a:not([rel])').last().text() * 50 + '+' : query.has('a[href*=\\\"download\\\"]').length\"]\r\n        }\r\n      }\r\n    },\r\n    \"common\": {\r\n\t  \"page\": \"/torrent/\",\r\n      \"merge\": true,\r\n      \"fields\": {\r\n        \"downloadURL\": {\r\n          \"selector\": [\"a.bhd-toolx-button[href*='/download/']\"],\r\n          \"filters\": [\"query.attr('href')\"]\r\n        },\r\n        \"sayThanksButton\": {\r\n          \"selector\": [\"div.torrentthankbuttons[title*='Thank']\"],\r\n          \"filters\": [\"query\"]\r\n        }\r\n      }\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "resource/sites/beyond-hd.me/getSearchResult.js",
    "content": "(function(options, Searcher) {\r\n  class Parser {\r\n    constructor() {\r\n      this.haveData = false;\r\n      if (/\\/login/.test(options.responseText)) {\r\n        options.status = ESearchResultParseStatus.needLogin;\r\n        return;\r\n      }\r\n\r\n      options.isLogged = true;\r\n\r\n      this.haveData = true;\r\n    }\r\n\r\n    /**\r\n     * 获取搜索结果\r\n     */\r\n    getResult() {\r\n      if (!this.haveData) {\r\n        return [];\r\n      }\r\n      let site = options.site;\r\n      let selector =\r\n        options.resultSelector || \"div.table-torrents > table:first\";\r\n      let table = options.page.find(selector);\r\n      // 获取种子列表行\r\n      let rows = table.find(\"> tbody > tr\");\r\n      if (rows.length == 0) {\r\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\r\n        return [];\r\n      }\r\n      let results = [];\r\n      // 获取表头\r\n      let header = table.find(\"> thead > tr > th\");\r\n      let beginRowIndex = 0;\r\n      if (header.length == 0) {\r\n        beginRowIndex = 1;\r\n        header = rows.eq(0).find(\"th,td\");\r\n      }\r\n\r\n      // 用于定位每个字段所列的位置\r\n      let fieldIndex = {\r\n        // 发布时间\r\n        time: -1,\r\n        // 大小\r\n        size: -1,\r\n        // 上传数量\r\n        seeders: -1,\r\n        // 下载数量\r\n        leechers: -1,\r\n        // 完成数量\r\n        completed: -1,\r\n        // 评论数量\r\n        comments: -1,\r\n        // 发布人\r\n        author: header.length - 1,\r\n        // 分类\r\n        category: 1,\r\n        progress: 11,\r\n        status: 11\r\n      };\r\n\r\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\r\n        site.url += \"/\";\r\n      }\r\n\r\n      // 获取字段所在的列\r\n      for (let index = 0; index < header.length; index++) {\r\n        let cell = header.eq(index);\r\n        let text = cell.text();\r\n\r\n        // 评论数\r\n        if (cell.find(\"a[href*='comments']\").length) {\r\n          fieldIndex.comments = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 发布时间\r\n        if (\r\n          cell.find(\"a[href*='created_at']\").length ||\r\n          cell.find(\"i.fa-clock\").length\r\n        ) {\r\n          fieldIndex.time = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 大小\r\n        if (\r\n          cell.find(\"a[href*='size']\").length ||\r\n          cell.find(\"i.fa-file\").length\r\n        ) {\r\n          fieldIndex.size = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 种子数\r\n        if (\r\n          cell.find(\"a[href*='seed']\").length ||\r\n          cell.find(\"i.fa-arrow-circle-up\").length\r\n        ) {\r\n          fieldIndex.seeders = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 下载数\r\n        if (\r\n          cell.find(\"a[href*='leech']\").length ||\r\n          cell.find(\"i.fa-arrow-circle-down\").length\r\n        ) {\r\n          fieldIndex.leechers = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 完成数\r\n        if (\r\n          cell.find(\"a[href*='complete']\").length ||\r\n          cell.find(\"i.fa-check-square\").length\r\n        ) {\r\n          fieldIndex.completed = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n\r\n        // 分类\r\n        if (cell.is(\".torrents-icon\")) {\r\n          fieldIndex.category = index;\r\n          fieldIndex.author =\r\n            index == fieldIndex.author ? -1 : fieldIndex.author;\r\n          continue;\r\n        }\r\n      }\r\n\r\n      try {\r\n        // 遍历数据行\r\n        for (let index = beginRowIndex; index < rows.length; index++) {\r\n          const row = rows.eq(index);\r\n          let cells = row.find(\">td\");\r\n\r\n          let title = row.find(\"a.torrent-name\");\r\n          if (title.length == 0) {\r\n            continue;\r\n          }\r\n          let link = title.attr(\"href\");\r\n          if (link && link.substr(0, 4) !== \"http\") {\r\n            link = `${site.url}${link}`;\r\n          }\r\n\r\n          // 获取下载链接\r\n          let url = row.find(\"a[href*='/download/']\").attr(\"href\");\r\n          if (url.length == 0) {\r\n            continue;\r\n          }\r\n\r\n          if (url && url.substr(0, 4) !== \"http\") {\r\n            url = `${site.url}${url}`;\r\n          }\r\n\r\n          let data = {\r\n            title: title.text().trim(),\r\n            subTitle: this.getSubTitle(title, row).trim(),\r\n            link,\r\n            url: url,\r\n            size:\r\n              cells\r\n                .eq(fieldIndex.size)\r\n                .text()\r\n                .trim() || 0,\r\n            time:\r\n              fieldIndex.time == -1\r\n                ? \"\"\r\n                : cells\r\n                    .eq(fieldIndex.time)\r\n                    .find(\"span[title]\")\r\n                    .attr(\"title\") ||\r\n                  cells.eq(fieldIndex.time).text().trim() ||\r\n                  \"\",\r\n            author:\r\n              fieldIndex.author == -1\r\n                ? \"\"\r\n                : cells.eq(fieldIndex.author).text().trim() || \"\",\r\n            seeders:\r\n              fieldIndex.seeders == -1\r\n                ? \"\"\r\n                : cells.eq(fieldIndex.seeders).text().trim() || 0,\r\n            leechers:\r\n              fieldIndex.leechers == -1\r\n                ? \"\"\r\n                : cells.eq(fieldIndex.leechers).text().trim() || 0,\r\n            completed:\r\n              fieldIndex.completed == -1\r\n                ? \"\"\r\n                : cells.eq(fieldIndex.completed).text().trim() || 0,\r\n            comments:\r\n              fieldIndex.comments == -1\r\n                ? \"\"\r\n                : cells.eq(fieldIndex.comments).text().trim() || 0,\r\n            site: site,\r\n            tags: Searcher.getRowTags(site, row),\r\n            entryName: options.entry.name,\r\n            category:\r\n              fieldIndex.category == -1\r\n                ? null\r\n                : this.getCategory(cells.eq(fieldIndex.category)),\r\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\r\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\r\n          };\r\n          results.push(data);\r\n        }\r\n        if (results.length == 0) {\r\n          options.status = ESearchResultParseStatus.noTorrents;\r\n        }\r\n      } catch (error) {\r\n        console.log(error);\r\n        options.status = ESearchResultParseStatus.parseError;\r\n        options.errorMsg = error.stack;\r\n      }\r\n\r\n      return results;\r\n    }\r\n\r\n    /**\r\n     * 获取副标题\r\n     * @param {*} title\r\n     * @param {*} row\r\n     */\r\n    getSubTitle(title, row) {\r\n      return \"\";\r\n    }\r\n    \r\n    /**\r\n     * 获取分类\r\n     * @param {*} cell 当前列\r\n     */\r\n    getCategory(cell) {\r\n      let result = {\r\n        name: cell.find(\"i:first\").attr(\"data-original-title\"),\r\n        link: cell.find(\"a:first\").attr(\"href\")\r\n      };\r\n      if (result.name) {\r\n        result.name = result.name.replace(\" torrent\", \"\");\r\n      }\r\n      return result;\r\n    }\r\n    \r\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\r\n      let parent = row;\r\n      let cell = null;\r\n      if (\r\n        cells &&\r\n        fieldIndex &&\r\n        fieldIndex[fieldName] !== undefined &&\r\n        fieldIndex[fieldName] !== -1\r\n      ) {\r\n        cell = cells.eq(fieldIndex[fieldName]);\r\n        parent = cell || row;\r\n      }\r\n\r\n      let result = Searcher.getFieldValue(site, parent, fieldName);\r\n\r\n      if (!result && cell) {\r\n        if (returnCell) {\r\n          return cell;\r\n        }\r\n        result = cell.text().trim();\r\n      }\r\n      if(result == \"\")return null;\r\n      return result;\r\n    }\r\n  }\r\n\r\n  let parser = new Parser(options);\r\n  options.results = parser.getResult();\r\n  console.log(options.results);\r\n})(options, options.searcher);\r\n"
  },
  {
    "path": "resource/sites/bibliotik.me/config.json",
    "content": "{\n  \"name\": \"Bibliotik\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Bibliotik\",\n  \"url\": \"https://bibliotik.me/\",\n  \"icon\": \"https://bibliotik.me/favicon.ico\",\n  \"tags\": [\"电子书\"],\n  \"schema\": \"Common\",\n  \"host\": \"bibliotik.me\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/torrents/(\\\\d+)$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"^/torrents/$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents/\",\n    \"resultType\": \"html\",\n    \"queryString\": \"search=$key$&y1=&y2=&p1=&p2=&d1=&d2=&size1=&size2=&retail=any&freeleech=any&img=&my=&bm=&sd=&lch=&sn=&ad1=&ad2=&orderby=added&order=desc\",\n    \"parseScriptFile\": \"/schemas/Common/getSearchResult.js\",\n    \"resultSelector\": \"table#torrents_table:first\",\n    \"skipIMDbId\": true,\n    \"firstDataRowIndex\": 1,\n    \"fieldIndex\": {\n      \"title\": 1,\n      \"time\": 4,\n      \"size\": 4,\n      \"seeders\": 7,\n      \"leechers\": 8,\n      \"completed\": 6,\n      \"comments\": 5,\n      \"author\": 9,\n      \"category\": 1,\n      \"url\": 2,\n      \"link\": 1\n    },\n    \"loggedRegex\": \"logout\\\\?authkey=\",\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"span.title a:first\"]\n      },\n      \"url\": {\n        \"selector\": [\"a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"link\": {\n        \"selector\": [\"span.title a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"time\": {\n        \"selector\": [\"time\"],\n        \"filters\": [\"query.attr('datetime')\"]\n      },\n      \"category\": {\n        \"selector\": [\"span.torFormat\"],\n        \"filters\": [\"query.text()\"]\n      },\n      \"size\": {\n        \"selector\": [\"\"],\n        \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)[1]\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"td:contains('[100% free!]')\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"#pre_header_status a[href*='/users/']\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.match(/(\\\\d+)/)[1]: ''\"]\n        },\n        \"name\": {\n          \"selector\": [\"#pre_header_status a[href*='/users/']\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout?authkey']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/users/$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"#pre_header_status li:contains('Up: ')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"#pre_header_status li:contains('Down: ')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"#detailsbox p:contains('Class: ')\"],\n          \"filters\": [\"query.text().replace(/Class: /g,'')\"]\n        },\n        \"joinTime\": {\n          \"selector\": \"#detailsbox p:contains('Joined ') time\",\n          \"filters\": [\"query.attr('datetime')\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"common\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"p#details_file_info\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='/download']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='/download']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table#torrents_table:first\"],\n          \"filters\": [\"query.find('td.t_files_size_added')\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/users/$user.id$/seeding\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"table#torrents_table:first tbody > tr\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"table#torrents_table:first tbody > tr\"],\n          \"filters\": [\"jQuery.map(query.find('td.t_files_size_added'), (item)=>{return $(item).text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)[1];})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/bibliotik.me/getUserSeedingTorrents.js",
    "content": "(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 0\n      };\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seeding += results.seeding;\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\".pagination a[href*='?page']:contains('Last >>'):first\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 1;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"?page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/bitbr/config.json",
    "content": "{\n  \"name\": \"bitbr\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://bitbr.cc/\",\n  \"icon\": \"https://bitbr.cc/favicon.ico\",\n  \"tags\": [\n      \"综合\",\n      \"成人\"\n  ],\n  \"host\": \"bitbr.cc\",\n  \"collaborator\": \"枕头啊枕头\",\n  \"selectors\": {\n      \"userExtendInfo\": {\n          \"page\": \"/userdetails.php?id=$user.id$\",\n          \"merge\": true,\n          \"fields\": {\n              \"uploaded\": {\n                  \"selector\": \"span.medium\",\n                  \"filters\": [\n                      \"query.text().match(/Uploaded: (.*?)  Downloaded/g)[0].replace('Uploaded:', '').replace('Downloaded', '').trim()\",\n                      \"(query && query.length>=2) ? query.sizeToNumber() : 0\"\n                  ]\n              },\n              \"downloaded\": {\n                  \"selector\": \"span.medium\",\n                  \"filters\": [\n                      \"query.text().match(/Downloaded: (.*?)  Torrents/g)[0].replace('Downloaded:', '').replace('Torrents', '').trim()\",\n                      \"(query && query.length>=2) ? query.sizeToNumber() : 0\"\n                  ]\n              },\n              \"ratio\": {\n                  \"selector\": \"span.medium\",\n                  \"filters\": [\n                      \"query.text().match(/Ratio:(.*?) Uploaded:/g)[0].replace('Ratio:','').replace('Uploaded:','').replace(/,/gi,'').trim()\",\n                      \"(query && query.length>=2) ? query : 0\"\n                  ]\n              },\n              \"levelName\": {\n                  \"selector\": \"table.main tbody tbody td:contains(Classe)\",\n                  \"filters\": [\n                      \"query.parent().children('td').eq(1).find('img').attr('title')\"\n                  ]\n              },\n              \"bonus\": {\n                  \"selector\": [\n                      \"table.main tbody tbody td:contains(Pontos Karma)\"\n                  ],\n                  \"filters\": [\n                      \"query.parent().children('td').eq(1).text()\",\n                      \"parseFloat(query)\"\n                  ]\n              },\n              \"joinTime\": {\n                  \"selector\": \"table.main tbody tbody tr:eq(2) td:eq(1)\",\n                  \"filters\": [\n                      \"query.text().match(/\\\\d{4}-\\\\d{1,2}-\\\\d{1,2} \\\\d{2}:\\\\d{1,2}:\\\\d{1,2}/g)[0]\",\n                      \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n                  ]\n              }\n          }\n      },\n      \"userSeedingTorrents\": {\n        \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n        \"fields\": {\n            \"seedingSize\": {\n                \"selector\": \"\",\n                \"filters\": [\n                    \"query.text().match(/registro(.*?)Tipo/g)\",\n                    \"(query && query.length>0 ) ? query[0].replace('registro', '').replace('Tipo', '').replace('s', '').trim() : 0\",\n                    \"(query != 0) ? query.sizeToNumber() : 0\"\n                ]\n            },\n            \"seeding\": {\n                \"selector\": \"\",\n                \"filters\": [\n                    \"query.text().match(/(.*?)registro/g)\",\n                    \"(query && query.length>0 ) ? query[0].replace('registro', '').trim() : 0\",\n                    \"(query != 0) ? query : 0\"\n                ]\n            }\n        }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/bitpt.cn/config.json",
    "content": "{\n    \"name\": \"极速之星PT\",\n    \"description\": \"极速之星IPV6资源交流平台\",\n    \"url\": \"https://bitpt.cn/\",\n    \"icon\": \"https://bitpt.cn/favicon.ico\",\n    \"tags\": [\n        \"教育网\",\n        \"综合\",\n        \"影视\"\n    ],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"bitpt.cn\",\n    \"collaborator\": \"wanicca\",\n    \"plugins\": [{\n        \"name\": \"种子详情页面\",\n        \"pages\": [\"/bbs\"],\n        \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n      }, {\n        \"name\": \"种子列表\",\n        \"pages\": [\"/browse.php\"],\n        \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n      }],\n    \"searchEntryConfig\": {\n        \"page\": \"/browse.php\",\n        \"queryString\": \"s=$key$\",\n        \"resultType\": \"html\",\n        \"parseScriptFile\": \"getSearchResult.js\",\n        \"resultSelector\": \"table.torrenttable:last\",\n        \"fieldIndex\": {\n            \"title\": 1, \n            \"subTitle\": 1,\n            \"url\": 1, \n            \"link\":1, \n            \"size\":3, \n            \"seeders\": 4,\n            \"leechers\": 5,\n            \"completed\": 6,\n            \"author\": 7,\n            \"category\": 0,\n            \"time\":7\n        }\n    },\n    \"searchEntry\": [\n        {\n            \"name\": \"全部\",\n            \"enabled\": true\n        },\n        {\n            \"queryString\": \"c=1000\",\n            \"name\": \"Movie\",\n            \"enabled\": false\n        }\n    ],\n    \"categories\": [\n        {\n            \"entry\": \"browse.php?\",\n            \"result\": \"c=$id$\",\n            \"category\": [\n                {\n                    \"id\": 1000,\n                    \"name\": \"Movie\"\n                }\n            ]\n        }\n    ],\n    \"torrentTagSelectors\": [\n        {\n            \"name\": \"Free\",\n            \"selector\": \"a[title^='该资源不计下载流量']\"\n        },\n        {\n            \"name\": \"30%\",\n            \"selector\": \"a[title^='该资源计50%流量']\"\n        },\n        {\n            \"name\": \"50%\",\n            \"selector\": \"a[title^='该资源计30%流量']\"\n        }\n    ],\n    \"selectors\": {\n        \"userBaseInfo\": {\n            \"page\": \"/index.php\",\n            \"fields\": {\n                \"id\": {\n                    \"selector\": \"a[href*='userdetails.php']:first\",\n                    \"attribute\": \"href\",\n                    \"filters\": [\n                        \"query ? query.getQueryString('uid'):''\"\n                    ]\n                },\n                \"name\": {\n                    \"selector\": \"a[href*='userdetails.php']:first\"\n                },\n                \"isLogged\": {\n                    \"selector\": [\n                        \"a[href*='logout.php']\"\n                    ],\n                    \"filters\": [\n                        \"query.length>0\"\n                    ]\n                }\n            }\n        },\n        \"userExtendInfo\": {\n            \"page\": \"/userdetails.php?uid=$user.id$\",\n            \"fields\": {\n                \"uploaded\": {\n                    \"selector\": [\n                        \"td:contains('上传流量') + td\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                },\n                \"downloaded\": {\n                    \"selector\": [\n                        \"td:contains('下载流量') + td\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                },\n                \"ratio\": {\n                    \"selector\": \"td:contains('共享率') + td\",\n                    \"filters\": [\n                        \"parseFloat(query.text())\"\n                    ]\n                },\n                \"levelName\": {\n                    \"selector\": [\n                        \"td:contains('用户级别') + td\"\n                    ]\n                },\n                \"bonus\": {\n                    \"selector\": [\n                        \"td:contains('星辰') + td\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\",\n                        \"(query && query.length>=2)?query[1]:''\"\n                    ]\n                },\n                \"joinTime\": {\n                    \"selector\": [\n                        \"td:contains('注册时间') + td\"\n                    ],\n                    \"filters\": [\n                        \"query.text().split(' (')[0]\",\n                        \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n                    ]\n                },\n                \"seeding\": {\n                    \"selector\": [\n                        \"a:contains('当前上传的种子')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().match(/([\\\\d.]+)个/)\",\n                        \"(query && query.length>=1)?query[1]:''\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": [\n                        \"a:contains('当前上传的种子')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/,/g,'').match(/共([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "resource/sites/bitpt.cn/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      if (this.getDownloadURL()) {\n        this.initDetailButtons();\n      }\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='download.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n      if (!url) {\n        return \"\";\n      }\n\n      if (url.substr(0, 2) === '//') { \n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n\n      if (url.indexOf(\"ssl=yes\") === -1) {\n        url += \"&ssl=yes\"\n      }\n\n      return url;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"span#thread_subject\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/bitpt.cn/getSearchResult.js",
    "content": "/**\n * 通用搜索解析脚本\n */\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      // 判断是否已登录\n      if (\n        options.entry.loggedRegex &&\n        !new RegExp(options.entry.loggedRegex, \"\").test(options.responseText)\n      ) {\n        // 需要登录后再搜索\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let selector = options.resultSelector;\n      let dataRowSelector = options.entry.dataRowSelector || \"> tbody > tr\";\n      selector = selector.replace(dataRowSelector, \"\");\n      // 获取数据表格\n      let table = options.page.find(selector);\n      console.log(table)\n      // 获取种子列表行\n      let rows = table.find(dataRowSelector);\n      if (rows.length == 0) {\n        // 没有定位到种子列表，或没有相关的种子\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\n        return [];\n      }\n      let subcats = options.page.find(\"div#subcat\")\n      let results = [];\n      let beginRowIndex = options.entry.firstDataRowIndex || 0;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = options.entry.fieldIndex || {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: -1,\n        // 分类\n        category: -1\n      };\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          // let title = this.getTitle(row, cells, fieldIndex);\n          let title_entry = cells.eq(fieldIndex['title']).find(\"a[href^='details']\")\n          let title = title_entry.text()\n          // 没有获取标题时，继续下一个\n          if (!title) {\n            continue;\n          }\n          // let link = this.getFieldValue(row, cells, fieldIndex, \"link\");\n          let link = title_entry.attr('href')\n\n          // 获取下载链接\n          // let url = this.getFieldValue(row, cells, fieldIndex, \"url\");\n          let url = cells.eq(fieldIndex['url']).find(\"a[title^='下载种子']\").attr('href')\n\n          if (!url || !link) {\n            continue;\n          }\n\n          let data = {\n            title: title,\n            // subTitle: this.getFieldValue(row, cells, fieldIndex, \"subTitle\"),\n            subTitle: cells.eq(fieldIndex['subTitle']).find(\">div:last>table>tbody>tr>td>span\").text(),\n            link: this.getFullURL(link),\n            url: this.getFullURL(url),\n            size: this.getFieldValue(row, cells, fieldIndex, \"size\")+\"B\" || 0,\n            // time: this.getFieldValue(row, cells, fieldIndex, \"time\"),\n            time: cells.eq(fieldIndex['time']).find(\"p.add_t\").text(),\n            author: this.getFieldValue(row, cells, fieldIndex, \"author\") || \"\", //尚未解决\n            seeders: this.getFieldValue(row, cells, fieldIndex, \"seeders\") || 0,\n            leechers:\n              this.getFieldValue(row, cells, fieldIndex, \"leechers\") || 0,\n            completed:\n              this.getFieldValue(row, cells, fieldIndex, \"completed\") || 0,\n            comments:\n              this.getFieldValue(row, cells, fieldIndex, \"comments\") || 0,\n            site: this.site,\n            tags: Searcher.getRowTags(this.site, row),\n            entryName: options.entry.name,\n            // category: this.getFieldValue(row, cells, fieldIndex, \"category\"),\n            category:subcats.find(\"a[href='\"+cells.eq(fieldIndex['category']).find(\"a[href^='browse']\").attr('href').match(/(\\?c=\\d+)/)[1]+\"']\").text(),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        // 获取种子信息出错\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n\n      // 没有搜索到相关的种子\n      if (results.length == 0 && !options.errorMsg) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取指定字段内容\n     * @param {*} row\n     * @param {*} cells\n     * @param {*} fieldIndex\n     * @param {*} fieldName\n     */\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(this.site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      let URL = PTServiceFilters.parseURL(this.site.url);\n      if (url.substr(0, 2) === \"//\") {\n        url = `${URL.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${URL.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${URL.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 获取标题\n     */\n    getTitle(row, cells, fieldIndex) {\n      let title = this.getFieldValue(row, cells, fieldIndex, \"title\", true);\n\n      if (!title) {\n        return \"\";\n      }\n\n      if (typeof title === \"string\") {\n        return title;\n      }\n\n      // 对title进行处理，防止出现cf的email protect\n      let cfemail = title.find(\"span.__cf_email__\");\n      if (cfemail.length > 0) {\n        cfemail.each((index, el) => {\n          $(el).replaceWith(Searcher.cfDecodeEmail($(el).data(\"cfemail\")));\n        });\n      }\n\n      return title.text();\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/bitpt.cn/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href*='download.php'][href*='ssl=yes']\").toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let url =\n          $(item).attr(\"href\") ;\n        if (url) {\n          if (url.substr(0, 1) === \"/\") {\n            url = url.substr(1);\n          }\n          url = siteURL + url;\n\n        }\n        return url;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\".torrents\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/blutopia.cc/config.json",
    "content": "{\n  \"name\": \"Blutopia\",\n  \"timezoneOffset\": \"+0000\",\n  \"schema\": \"UNIT3D\",\n  \"url\": \"https://blutopia.cc/\",\n  \"icon\": \"https://blutopia.cc/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"host\": \"blutopia.cc\",\n  \"formerHosts\": [\n    \"blutopia.xyz\"\n  ],\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"BluUser\",\n    \"interval\": \"1\",\n    \"uploaded\": \"1TiB\",\n    \"privilege\": \"5 download slots\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"BluMaster\",\n    \"interval\": \"2\",\n    \"uploaded\": \"5TiB\",\n    \"privilege\": \"10 download slots\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"BluExtremist\",\n    \"interval\": \"3\",\n    \"uploaded\": \"20TiB\",\n    \"privilege\": \"Automatic torrent approvals; Invite forums; 15 download slots\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"BluLegend\",\n    \"interval\": \"6\",\n    \"uploaded\": \"50TiB\",\n    \"privilege\": \"20 download slots\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Blutopian\",\n    \"interval\": \"12\",\n    \"uploaded\": \"100TiB\",\n    \"privilege\": \"Immunity to automated HnR warnings; Global Freeleech; 25 download slots\"\n  }],\n  \"collaborator\": [\"bimzcy\", \"lengmianxia\", \"bright\"],\n  \"selectors\": {\n    \"hnrExtendInfo\": {\n      \"page\": \"/users/$user.name$/torrents?unsatisfied=include&perPage=100&hitrun=exclude&immune=exclude\",\n      \"fields\": {\n        \"unsatisfieds\": {\n          \"selector\": [\".table-responsive tbody tr\"],\n          \"filters\": [\"query ? query.length : 0\"]\n        }\n      }\n    }\n  },\n  \"cdn\": [\"https://blutopia.cc/\",\"https://blutopia.xyz/\"]\n}\n"
  },
  {
    "path": "resource/sites/broadcasthe.net/config.json",
    "content": "{\n  \"name\": \"BTN\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"著名剧集站点，又被戏称为鼻涕妞\",\n  \"url\": \"https://broadcasthe.net/\",\n  \"icon\": \"https://broadcasthe.net/favicon.ico\",\n  \"tags\": [\"剧集\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"broadcasthe.net\",\n  \"collaborator\": [\"ylxb2016\", \"enigmaz\"],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"searchstr=$key$\"\n  },\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"filter_cat[1]=1\",\n      \"name\": \"Episode\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[2]=1\",\n      \"name\": \"Season\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&filter_cat[$id$]=1\",\n    \"category\": [{\n        \"id\": 1,\n        \"name\": \"Episode\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Season\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(3) > ul > li:nth-child(1)\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/Upload.+?([\\\\d.]+ ?[TGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(3) > ul > li:nth-child(7)\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/Downloaded.+?([\\\\d.]+ ?[TGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"ratio\": {\n          \"value\": null\n        },\n        \"levelName\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(1) > ul > li:nth-child(2)\",\n          \"filters\": [\n            \"query.text().match(/Class:.+?(.+)/)\",\n            \"(query && query.length>=2)?query[1]:''\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(1) > ul > li:nth-child(5) > a\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'')\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(1) > ul > li:nth-child(1) > span\",\n          \"filters\": [\n            \"query.attr('title')||query.text()\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        },\n        \"seeding\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(3) > ul > li:nth-child(4)\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/Seeding:.+?(\\\\d+).+?/)\",\n            \"(query && query.length>=2)?(query[1]):null\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": \"#section2 > div > div.statistics > div:nth-child(3) > ul > li:nth-child(5)\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/Seeding Size:.+?([\\\\d.]+ ?[TGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n          ]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/bonus.php?action=rate\",\n      \"fields\": {\n        \"seedingList\": {\n          \"selector\": [\"a[href*='torrentid=']\"],\n          \"filters\": [\"jQuery.map(query, item=>$(item).attr('href').match(/torrentid=(\\\\d+)/)[1])\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/broadcasthe.net/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table.torrent_table:last > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: -1,\n        size: -1,\n        seeders: -1,\n        leechers: -1,\n        completed: -1,\n        comments: -1,\n        author: -1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        const cell = header.eq(index);\n\n        // 大小\n        if (cell.find(\"a[href*='order_by=size']\").length) {\n          fieldIndex.size = index;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[href*='order_by=seeders']\").length) {\n          fieldIndex.seeders = index;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[href*='order_by=leechers']\").length) {\n          fieldIndex.leechers = index;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[href*='order_by=snatched']\").length) {\n          fieldIndex.completed = index;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          // id\n          let id = row.find(\"a[href*='torrentid=']\").first().attr(\"href\")\n          id = id.match(/torrentid=(\\d+)/)[1]\n\n          // 标题\n          let title = row.find(\"[style='float:none;']\").first().attr(\"title\");\n          \n          // 链接\n          let link = row.find(\"[title='View Torrent']\").first().attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"[title='Download']\").first().attr(\"href\");\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          // 分类\n          let category = row.find(\"a[href*='filter_cat']\").children().first().attr(\"title\");\n\n          // 时间\n          let timeStrMatch = row.find(\"div:contains('Added:')\").text().match(/Added:(.+)ago/);\n          let timeStr = (timeStrMatch && timeStrMatch.length >=2) ? timeStrMatch[1].trim() : \"\";\n\n          let data = {\n            id,\n            title,\n            link,\n            url,\n            size:\n              fieldIndex.size == -1\n                ? \"\"\n                : cells.eq(fieldIndex.size).text() || 0,\n            time: this.getTime(timeStr),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site,\n            entryName: options.entry.name,\n            category\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    getTime(timeStr) {\n      let timeRegex = timeStr.match(\n        /((\\d+).+?(minute|hour|day|week|month|year)s?.*?(\\,|and))?.*?(\\d+).+?(minute|hour|day|week|month|year)s?/\n      );\n      let milliseconds = 0;\n      if (timeRegex) {\n        if (timeRegex[1] == undefined) {\n          milliseconds = this.getMilliseconds(timeRegex[5], timeRegex[6]);\n        } else {\n          milliseconds = this.getMilliseconds(timeRegex[2], timeRegex[3]) + this.getMilliseconds(timeRegex[5], timeRegex[6]);\n        }\n      }\n      console.log(timeRegex);\n      let timeStamp = Date.now() - milliseconds;\n      let date = new Date(timeStamp);\n      return date.toISOString();\n    }\n\n    getMilliseconds(num, unit) {\n      let milliseconds = 0;\n      milliseconds = num*60*1000;\n      if(unit == \"minute\") {return milliseconds;}\n      milliseconds = milliseconds*60;\n      if(unit == \"hour\") {return milliseconds;}\n      milliseconds = milliseconds*24;\n      if(unit == \"day\") {return milliseconds;}\n      milliseconds = milliseconds*7;\n      if(unit == \"week\") {return milliseconds;}\n      milliseconds = milliseconds*30/7;\n      if(unit == \"month\") {return milliseconds;}\n      milliseconds = milliseconds*12;\n      return milliseconds;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/brokenstones.is/config.json",
    "content": "{\n  \"name\": \"BRKS\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Mac Apps\",\n  \"url\": \"https://brokenstones.is\",\n  \"tags\": [\"软件\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"brokenstones.is\",\n  \"formerHosts\": [\n    \"brokenstones.club\"\n  ],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  },\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  }\n}\n"
  },
  {
    "path": "resource/sites/bt.neu6.edu.cn/config.json",
    "content": "{\n  \"name\": \"六维空间\",\n  \"description\": \"东北大学ipv6资源分享平台\",\n  \"url\": \"http://bt.neu6.edu.cn/\",\n  \"icon\": \"http://bt.neu6.edu.cn/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"综合\"\n  ],\n  \"schema\": \"Discuz\",\n  \"supportedFeatures\": {\n    \"search\": false,\n    \"imdbSearch\": false,\n    \"userData\": \"◐\",\n    \"sendTorrent\": false\n  },\n  \"host\": \"bt.neu6.edu.cn\",\n  \"collaborator\": \"xfl03\",\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/forum.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \".vwmy a[href*='home.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\n            \"query ? query.getQueryString('uid'):''\"\n          ]\n        },\n        \"name\": {\n          \"selector\": \".vwmy a[href*='home.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\n            \"a[href*='action=logout']\"\n          ],\n          \"filters\": [\n            \"query.length>0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/home.php?mod=space&uid=$user.id$&do=profile\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\n            \"li:contains('上传')\"\n          ],\n          \"filters\": [\n            \"query.text().match(/上传.*?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\n            \"li:contains('下载')\"\n          ],\n          \"filters\": [\n            \"query.text().match(/下载.*?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": \"li:contains('用户组')\",\n          \"filters\": [\n            \"query.text().replace('用户组','')\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": [\n            \"li:contains('积分')\"\n          ],\n          \"filters\": [\n            \"query.text().match(/积分.*?([\\\\d.]+)/)[1]\",\n            \"parseFloat(query)\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": \"li:contains('注册时间')\",\n          \"filters\": [\n            \"query.text().match(/注册时间.*?([\\\\d.]+-[\\\\d.]+-[\\\\d.]+)/)[1]\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/bwtorrents.tv/config.json",
    "content": "{\n  \"name\": \"BWT\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"bwtorrents\",\n  \"url\": \"https://bwtorrents.tv/\",\n  \"icon\": \"https://bwtorrents.tv/favicon.ico\",\n  \"tags\": [\"综合\", \"印度\"],\n  \"schema\": \"Common\",\n  \"host\": \"bwtorrents.tv\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/index.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/index.php\",\n    \"resultType\": \"html\",\n    \"queryString\": \"search=$key$&blah=0&cat=0&incldead=1\",\n    \"parseScriptFile\": \"/schemas/Common/getSearchResult.js\",\n    \"resultSelector\": \"table.pager:first + table\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"parseScript\": \"(payload && payload.en)?payload.en:key\"\n    }],\n    \"firstDataRowIndex\": 1,\n    \"fieldIndex\": {\n      \"title\": 1,\n      \"time\": 4,\n      \"size\": 5,\n      \"seeders\": 7,\n      \"leechers\": 8,\n      \"completed\": 9,\n      \"comments\": 3,\n      \"author\": 10,\n      \"category\": 0,\n      \"url\": 6,\n      \"link\": 1\n    },\n    \"loggedRegex\": \"logout\\\\.php\",\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"a:first\"]\n      },\n      \"url\": {\n        \"selector\": [\"a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"link\": {\n        \"selector\": [\"a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"time\": {\n        \"selector\": [\"nobr\"],\n        \"filters\": [\"query.text().replace(/(\\\\d{2})-(\\\\d{2})-(\\\\d{4})\\\\n?(\\\\d{2}:\\\\d{2}:\\\\d{2})/,'$3-$2-$1 $4')\"]\n      },\n      \"category\": {\n        \"selector\": [\"img:first\"],\n        \"filters\": [\"query.attr('alt')\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"font[color='red']:contains('[FreeLeech]')\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='userdetails.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails.php']:first\"],\n          \"filters\": [\"query && query.attr('href').getQueryString('id') > 0 ? query.text(): ''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetailsmore.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"td.rowhead:contains('Class') + td\"\n        },\n        \"joinTime\": {\n          \"selector\": \"td.rowhead:contains('Join') + td\",\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"#ka1 table.main > tbody > tr\"],\n          \"filters\": [\"query.length-1\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"#ka1 table.main > tbody > tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(6)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"span[title='File Size']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php/']:first\"],\n          \"filters\": [\"query.attr('href')\"]\n        }\n      }\n    },\n    \"/index.php\": {\n      \"fields\": {\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='download.php']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.pager:first + table\"],\n          \"filters\": [\"query.find(\\\"td:contains('MB'),td:contains('GB'),td:contains('TB')\\\")\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/byr.pt/config.json",
    "content": "{\n  \"name\": \"BYRBT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"著名教育网PT站点（仅支持ipv6访问与下载），有10大类资源，资源更新快，保种好。\",\n  \"url\": \"https://byr.pt/\",\n  \"icon\": \"https://byr.pt/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"byr.pt\",\n  \"collaborator\": [\"Rhilip\", \"yuanyiwei\"],\n  \"formerHosts\": [\n    \"bt.byr.cn\"\n  ],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"2\",\n    \"uploaded\": \"32GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看排行榜；可以查看其它用户的种子历史（如果用户隐私等级未设置为“强”）；可以删除自己上传的字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"uploaded\": \"512GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除；可以发送邀请；可以直接发布种子\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"12\",\n    \"uploaded\": \"1024GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"24\",\n    \"uploaded\": \"2048GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"24\",\n    \"uploaded\": \"4096GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上的用户会永远保留账号\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"24\",\n    \"uploaded\": \"8192GB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以更新过期的外部信息\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"48\",\n    \"uploaded\": \"32768GB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"更加高级\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"48\",\n    \"uploaded\": \"131072GB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"更加高级\"\n  }],\n  \"searchEntry\": [{\n      \"entry\": \"/torrents.php?search=$key$&notnewword=1\",\n      \"name\": \"全站\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": true\n    },\n    {\n      \"entry\": \"/torrents.php?cat408=1&search=$key$&notnewword=1\",\n      \"name\": \"电影\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat401=1&search=$key$&notnewword=1\",\n      \"name\": \"剧集\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat404=1&search=$key$&notnewword=1\",\n      \"name\": \"动漫\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat402=1&search=$key$&notnewword=1\",\n      \"name\": \"音乐\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat405=1&search=$key$&notnewword=1\",\n      \"name\": \"综艺\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat403=1&search=$key$&notnewword=1\",\n      \"name\": \"游戏\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat406=1&search=$key$&notnewword=1\",\n      \"name\": \"软件\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat407=1&search=$key$&notnewword=1\",\n      \"name\": \"资料\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat409=1&search=$key$&notnewword=1\",\n      \"name\": \"体育\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat410=1&search=$key$&notnewword=1\",\n      \"name\": \"纪录\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat=$id$&cat$id$=1\",\n    \"category\": [{\n        \"id\": 408,\n        \"name\": \"电影\"\n      },\n      {\n        \"id\": 401,\n        \"name\": \"剧集\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"音乐\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"综艺\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"资料\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"纪录\"\n      }\n    ]\n  }]\n}\n"
  },
  {
    "path": "resource/sites/carpt.net/config.json",
    "content": "{\n    \"name\": \"CarPT\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"CarPT\",\n    \"url\": \"https://carpt.net/\",\n    \"icon\": \"https://carpt.net/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"carpt.net\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"200GB\",\n            \"ratio\": \"2\",\n            \"seedingPoints\": \"40000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"3\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"4\",\n            \"seedingPoints\": \"150000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"2TB\",\n            \"ratio\": \"5\",\n            \"seedingPoints\": \"250000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"4TB\",\n            \"ratio\": \"6\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"6TB\",\n            \"ratio\": \"7\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛；Extreme User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"8TB\",\n            \"ratio\": \"8\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"10TB\",\n            \"ratio\": \"9\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到十个邀请名额；可以发送邀请。\"\n        }\n    ],\n    \"collaborator\": [\"koal\", \"zhuweitung\", \"tedzhu\"],\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    },\n    \"searchEntryConfig\": {\n        \"fieldSelector\": {\n            \"progress\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n            },\n            \"status\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\n                    \"query ? query.attr('title') : ''\",\n                    \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n                ]\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/ccfbits.org/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons(true);\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a.bookmark\").toArray();\n      let urls = $.map(links, item => {\n        let id = $(item).attr(\"tid\");\n        return this.getDownloadURL(id);\n      });\n\n      if (links.length == 0) {\n        return \"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      return urls;\n    }\n\n    getDownloadURL(id) {\n      // 格式：vvvid|||passkeyzz\n      let key = new Base64().encode(\n        \"vvv\" + id + \"|||\" + PTService.site.passkey + \"zz\"\n      );\n      return `https://${PTService.site.host}/rssdd.php?par=${key}&ssl=yes`;\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (!PTService.site.passkey) {\n        PTService.showNotice({\n          msg: \"请先设置站点密钥（Passkey）。\"\n        });\n        callback();\n        return;\n      }\n\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      let result = this.getDroperURL(data.url);\n\n      if (!result) {\n        callback();\n        return;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let values = url.split(\"/\");\n      let id = values[values.length - 2];\n      let result = this.getDownloadURL(id);\n\n      return result;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table\").find(\n          \"td[align='center']:contains('MB'),td[align='center']:contains('GB'),td[align='center']:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/ccfbits.org/config.json",
    "content": "{\n  \"name\": \"CCFBits\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"\",\n  \"url\": \"https://ccfbits.org/\",\n  \"icon\": \"https://ccfbits.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"剧集\",\n    \"综合\"\n  ],\n  \"host\": \"ccfbits.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"初级会员\",\n    \"interval\": \"4\",\n    \"uploaded\": \"25GB\",\n    \"ratio\": \"1.05\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"中级会员\",\n    \"interval\": \"8\",\n    \"downloaded\": \"50GB\",\n    \"uploaded\": \"200GB\",\n    \"ratio\": \"1.1\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"高级会员\",\n    \"interval\": \"12\",\n    \"downloaded\": \"100GB\",\n    \"uploaded\": \"500GB\",\n    \"ratio\": \"1.2\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"超级会员\",\n    \"interval\": \"24\",\n    \"downloaded\": \"200GB\",\n    \"uploaded\": \"1TB\",\n    \"ratio\": \"1.3\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"支柱会员\",\n    \"interval\": \"32\",\n    \"downloaded\": \"300GB\",\n    \"uploaded\": \"5TB\",\n    \"ratio\": \"2\"\n  }],\n  \"plugins\": [{\n      \"name\": \"种子详情页面\",\n      \"pages\": [\n        \"^/t/(\\\\d+)/$\",\n        \"/details.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"details.js\"\n      ]\n    },\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\n        \"/browse.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"browse.js\"\n      ]\n    }\n  ],\n  \"collaborator\": \"Rhilip\",\n  \"searchEntry\": [{\n    \"entry\": \"browse.php?search=$key$&notnewword=1\",\n    \"name\": \"全站\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n      \"name\": \"Free\",\n      \"selector\": \"font[color=\\\"#C20603\\\"]:contains('免费')\",\n      \"color\": \"blue\"\n    },\n    {\n      \"name\": \"30%\",\n      \"selector\": \"font[color=\\\"#C20603\\\"]:contains('0.3x')\",\n      \"color\": \"indigo\"\n    },\n    {\n      \"name\": \"50%\",\n      \"selector\": \"font[color=\\\"#C20603\\\"]:contains('0.5x')\",\n      \"color\": \"orange\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\n            \"query ? query.getQueryString('id'):''\"\n          ]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\n            \"a[href*='logout.php']\"\n          ],\n          \"filters\": [\n            \"query.length>0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\n            \"td.rowhead:contains('上传量') + td\",\n            \"td.rowhead:contains('上傳量') + td\"\n          ],\n          \"filters\": [\n            \"query.eq(0)\",\n            \"query.text().replace(/,/g,'').sizeToNumber()\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\n            \"td.rowhead:contains('下载量') + td\",\n            \"td.rowhead:contains('下載量') + td\"\n          ],\n          \"filters\": [\n            \"query.eq(0)\",\n            \"query.text().replace(/,/g,'').sizeToNumber()\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": [\n            \"td.rowhead:contains('等级') + td\",\n            \"td.rowhead:contains('等級') + td\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": [\n            \"td.rowhead:contains('积分') + td\",\n            \"td.rowhead:contains('積分') + td\"\n          ],\n          \"filters\": [\n            \"parseFloat(query.text())\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": [\n            \"td.rowhead:contains('注册日期') + td\",\n            \"td.rowhead:contains('註冊日期') + td\"\n          ],\n          \"filters\": [\n            \"dateTime(query.text()).isValid()?dateTime(query.text()).valueOf():query.text()\"\n          ]\n        },\n        \"seeding\": {\n          \"selector\": [\n            \"div#ka1 tr:not(:eq(0))\"\n          ],\n          \"filters\": [\n            \"query.length\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": [\n            \"div#ka1 tr:not(:eq(0))\"\n          ],\n          \"filters\": [\n            \"jQuery.map(query.find('td:eq(3)'), (item)=>{return $(item).text();})\",\n            \"_self.getTotalSize(query)\"\n          ]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}\n"
  },
  {
    "path": "resource/sites/ccfbits.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      // 有一些扩展会为链接添加class，导致选择器失效，因此使用正则来获取链接\n      // let query = $(\"a[href*='/dl/']:not([class])\");\n      let query = $(\"a[href*='download.php']\")\n\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        // 直接获取的链接下载成功率很低\n        // 如果设置了 passkey 则使用 rss 订阅的方式下载\n        if (PTService.site.passkey) {\n          let values = url.split(\"/\");\n          let id = values[values.length - 2];\n\n          // 格式：vvvid|||passkeyzz\n          let key = (new Base64).encode(\"vvv\" + id + \"|||\" + PTService.site.passkey + \"zz\");\n          url = `https://${PTService.site.host}/rssdd.php?par=${key}&ssl=yes`;\n        }\n      }\n\n      return url;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td[valign='top'][align='left']:contains('字节')\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().split(\" (\")[0];\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    getTitle() {\n      return /\"(.*?)\"/.exec($(\"title\").text())[1];\n    }\n  }\n  (new App()).init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/ccfbits.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/loginform/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (/没有找到匹配种子/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        \"table.mainouter > tbody > tr:eq(1) > td > table:last > tbody > tr\"\n      );\n      const time_regex = /(\\d{4}-\\d{2}-\\d{2}[^\\d]+?\\d{2}:\\d{2}:\\d{2})/;\n      const time_regen_replace = /-(\\d{2})[^\\d]+?(\\d{2}):/;\n      const size_regex = /[\\d.]+ [MGT]B/;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: 4, // 时间\n        seeders: 7, // 上传人数\n        leechers: 8, // 下载人数\n        author: 9, // 发布人\n        category: 0\n      };\n\n      if (site.url.substr(-1) === \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n\n      // 遍历数据行\n      for (let index = 1; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n\n        // 主副标题从cells.eq(1)中获取\n        let _title_cell = cells.eq(1);\n        let _title = _title_cell.find(\"> table > tbody > tr\");\n        if (_title.length != 2) {\n          continue;\n        }\n        let main_title_cell = _title.eq(0).find('a[href^=\"details.php\"]');\n        let title = main_title_cell.attr(\"title\").trim();\n\n        let sub_title_cell = _title.eq(1).find(\"td:eq(0)\");\n        let sub_title = sub_title_cell.text().trim();\n\n        let link = main_title_cell.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          if (link.substr(0, 1) == \"/\") {\n            link = `${site.url}${link}`;\n          } else {\n            link = `${site.url}/${link}`;\n          }\n        }\n\n        let url_cell = cells.eq(2).find('a[href^=\"download.php\"]');\n        let url = url_cell.attr(\"href\");\n\n        if (url && url.substr(0, 4) !== \"http\") {\n          if (url.substr(0, 1) == \"/\") {\n            url = `${site.url}${url}`;\n          } else {\n            url = `${site.url}/${url}`;\n          }\n        }\n\n        if (!url) {\n          continue;\n        }\n\n        let size_completed_cell = cells.eq(6);\n        let _size = (size_completed_cell.text().match(size_regex) || [0])[0];\n        let _completed = (size_completed_cell.text().match(/(\\d+) 次/) || [\n          0,\n          0\n        ])[1];\n\n        let comments_cell = cells.eq(3);\n        let _comments = (comments_cell.text().match(/(\\d+) 评论/) || [0, 0])[1];\n\n        let data = {\n          title: title,\n          subTitle: sub_title,\n          link,\n          url: url,\n          size: _size,\n          time:\n            cells\n              .eq(fieldIndex.time)\n              .html()\n              .match(time_regex)[1]\n              .replace(time_regen_replace, \"-$1 $2:\") ||\n            cells.eq(fieldIndex.time).text(),\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders: cells.eq(fieldIndex.seeders).text() || 0,\n          leechers: cells.eq(fieldIndex.leechers).text() || 0,\n          completed: _completed,\n          comments: _comments,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: Searcher.getRowTags(site, row)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/chdbits.co/config.json",
    "content": "{\n  \"name\": \"CHDBits\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"CHDBits\",\n  \"url\": \"https://chdbits.co/\",\n  \"icon\": \"https://chdbits.co/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"chdbits.co\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"2.0\",\n    \"bonus\": \"80000\",\n    \"privilege\": \"查看NFO文档；请求续种；查看排行榜可以查看其它用户的种子历史；删除自己上传的字幕\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"3\",\n    \"bonus\": \"150000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"800GB\",\n    \"ratio\": \"4.0\",\n    \"bonus\": \"300000\",\n    \"privilege\": \"在做种/下载的时候选择匿名模式\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"999GB\",\n    \"ratio\": \"5.0\",\n    \"bonus\": \"650000\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"1500GB\",\n    \"ratio\": \"6.0\",\n    \"bonus\": \"1000000\",\n    \"privilege\": \"查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"7.0\",\n    \"bonus\": \"2200000\",\n    \"privilege\": \"用户封存账号（在控制面板）后不会被删除帐号；首次升级赠送邀请1枚，更新过期的外部信息；查看Extreme User论坛\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"8.0\",\n    \"bonus\": \"3500000\",\n    \"privilege\": \"用户会永远保留；首次升级赠送邀请2枚，保留帐号，在官方活动期间可发放邀请\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"52\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"10.0\",\n    \"bonus\": \"5000000\",\n    \"privilege\": \"首次升级赠送邀请3枚，保留帐号，在官方活动期间可发放邀请\"\n  }],\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"Demo\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"Game\",\n      \"resultType\": \"html\",\n      \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\n      \"resultSelector\": \"table.torrents:last > tbody > tr\",\n      \"enabled\": false\n    }\n  ],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img.pro_free\"\n  }, {\n    \"name\": \"2xFree\",\n    \"selector\": \"img.pro_free2up\"\n  }, {\n    \"name\": \"2xUp\",\n    \"selector\": \"img.pro_2up\"\n  }, {\n    \"name\": \"2x50%\",\n    \"selector\": \"img.pro_50pctdown2up\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"img.pro_30pctdown\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img.pro_50pctdown\"\n  }, {\n    \"name\": \"⛔️\",\n    \"selector\": \".tag.tag-dz\"\n  }],\n  \"searchEntryConfig\": {\n\t\"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td.rowfollow:last-child\"],\n        \"filters\": [\"query.text()=='--'?null:query\"]\n      },\n      \"status\": {\n        \"selector\": [\"td.rowfollow:last-child\"],\n        \"filters\": [\"query.text()=='100%'?255:3\"]\n      }\n    }\n  },\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat=$id$&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"Documentaries\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"Animations\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"TV Series\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"TV Shows\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"Music\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"Sports\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"Demo\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"HQ Audio\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"Game\"\n      }\n    ]\n  }]\n}"
  },
  {
    "path": "resource/sites/cinemageddon.net/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"table.torrenttable:last a[href*='download.php?id=']\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table.torrenttable:last\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      if (url.indexOf(\"download.php\") === -1) {\n        return \"\";\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/cinemageddon.net/config.json",
    "content": "{\n  \"name\": \"CinemaGeddon\",\n  \"timezoneOffset\": \"+0000\",\n  \"schema\": \"CinemaGeddon\",\n  \"url\": \"https://cinemageddon.net/\",\n  \"icon\": \"https://cinemageddon.net/favicon.ico\",\n  \"tags\": [\"影视\"],\n  \"host\": \"cinemageddon.net\",\n  \"collaborator\": \"DXV5\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Power User\",\n      \"uploaded\": \"25GB\",\n      \"downloaded\": \"20GB\",\n      \"ratio\": \"1.2\",\n      \"privilege\": \"Maximum of 8 concurrent downloads\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"CG Superfan\",\n      \"uploaded\": \"200GB\",\n      \"downloaded\": \"20GB\",\n      \"ratio\": \"1.5\",\n      \"privilege\": \"Maximum of 12 concurrent downloads\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"search=$key$&proj=0\",\n    \"resultSelector\": \"table.torrenttable:last > tbody > tr\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \".statusbar a[href*='/userdetails.php?id=']\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \".statusbar a[href*='/userdetails.php?id=']\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.clx > .frames td.rowhead:contains('Uploaded') + td:first\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.clx > .frames td.rowhead:contains('Downloaded') + td:first\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"td.clx > .frames td.rowhead:contains('Class') + td:first\"\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.alert a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"a[href='/credits.php']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": \"td.clx > .frames td.rowhead:contains('Join') + td:first\",\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"div#ka2 table:first tr:not(:eq(0))\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"div#ka2 table:first tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(2)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/cinemageddon.net/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a.index[href*='download.php?id=']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return `${location.origin}/${url}`;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td.rowhead:contains('Size') + td\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().match(/^[^\\(]+/);\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\".frames tr:first td.colhead:first\")\n        .text()        \n        .trim();\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/cinemageddon.net/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (/Nothing found!/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(options.resultSelector);\n      const browsecheck = options.page\n        .find(\"a[href*='browse.php?page']:contains('-'):last\")\n        .attr(\"href\");\n      if (rows.length == 0 || browsecheck) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: 3,\n        size: 4,\n        seeders: 6,\n        leechers: 7,\n        completed: 5,\n        comments: 2,\n        author: 8,\n        category: 0,\n        title: 1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 0; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a[href*='details.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"a[href*='download.php?id=']:first\").attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          cells\n            .eq(fieldIndex.size)\n            .find(\"span\")\n            .remove();\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url,\n            size: cells.eq(fieldIndex.size).text() || 0,\n            time: this.getTime(cells.eq(fieldIndex.time)),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name,\n            category: this.getCategory(cells.eq(fieldIndex.category)),\n            tags: this.getTags(row, options.torrentTagSelectors)\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取时间\n     * @param {*} cell\n     */\n    getTime(cell) {\n      let time = $(\"<span>\")\n        .html(cell.html().replace(\"<br>\", \" \"))\n        .text();\n      return time || \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        // 使用 some 避免错误的背景类名返回多个标签\n        selectors.some(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n              return true;\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/club.hares.top/config.json",
    "content": "{\n  \"name\": \"HaresClub\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"2160p/4k 及以上的高清资源站点\",\n  \"url\": \"https://club.hares.top/\",\n  \"icon\": \"https://club.hares.top/favicon.ico\",\n  \"tags\": [\"影视\", \"纪录片\", \"综合\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"club.hares.top\",\n  \"collaborator\": [\"kevgao\", \"枕头啊枕头\", \"bright\", \"yuanyiwei\"],\n  \"formerHosts\": [],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"2\",\n    \"seedingPoints\": \"20000\",\n    \"privilege\": \"可以在邀请区回复；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"350GB\",\n    \"ratio\": \"2.5\",\n    \"seedingPoints\": \"50000\",\n    \"privilege\": \"可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"16\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"3\",\n    \"seedingPoints\": \"200000\",\n    \"privilege\": \"可以直接发布种子；可以发送邀请；可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"得到两个邀请名额；可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"得到四个邀请名额；可以查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"得到六个邀请名额；可以更新过期的外部信息；可以查看Extreme User论坛。Extreme User用户封存后将永远保留账号\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"6TB\",\n    \"ratio\": \"5\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"得到八个邀请名额\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"52\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"5.5\",\n    \"seedingPoints\": \"1200000\",\n    \"privilege\": \"得到十个邀请名额。Nexus Master用户会永远保留账号\"\n  }],\n  \"searchEntryConfig\": {\n    \"fieldIndex\": {\n      \"title\": 1,\n      \"subTitle\": 1,\n      \"link\": 1,\n      \"url\": 1,\n      \"time\": 3,\n      \"size\": 4,\n      \"seeders\": 5,\n      \"leechers\": 6,\n      \"completed\": 7,\n      \"comments\": 2\n    },\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"a[href*='details.php?id='][title]:first\"],\n        \"filters\": [\"query\"]\n      },\n      \"subTitle\": {\n        \"selector\": [\"p.layui-elip.layui-torrents-descr-width:first\"],\n        \"filters\": [\"query.text()\"]\n      },\n      \"progress\": {\n        \"selector\": [\n          \"div[title^='leeching'], div[title^='seeding'], div[title^='inactivity']\"\n        ],\n        \"filters\": [\n          \"query[0] ? query.attr('title').replace('leeching','').replace('seeding','').replace('inactivity','').replace('%','').trim() : null\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"div[title^='leeching']\",\n          \"div[title^='seeding']\",\n          \"div[title^='inactivity']\"\n        ],\n        \"switchFilters\": [\n          [\"1\"],\n          [\"2\"],\n          [\"query.attr('title').indexOf('100%')!=-1 ? 255:3\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat401=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat402=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat403=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat406=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"M V\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat409=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"演唱会\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat404=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat405=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/torrents.php?search_area=0&search=$key$&search_mode=0&cat407=1&incldead=1&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/official.php?search_area=0&search=$key$&search_mode=0&team1=1&team2=1&team3=1&incldead=0&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"官组(全部)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/official.php?search_area=0&search=$key$&search_mode=0&medium1=1&medium2=1&team1=1&team2=1&team3=1&incldead=0&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"官组(原盘|DIY)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/official.php?search_area=0&search=$key$&search_mode=0&medium3=1&team1=1&team2=1&team3=1&incldead=0&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"官组(Remux)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/official.php?search_area=0&search=$key$&search_mode=0&medium3=1&team1=1&team2=1&team3=1&incldead=0&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"官组(Remux)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"/official.php?search_area=0&search=$key$&search_mode=0&medium5=1&team1=1&team2=1&team3=1&incldead=0&spstate=0&check_state=0&can_claim=0&inclbookmarked=0\",\n      \"name\": \"官组(WEB-DL)\",\n      \"enabled\": false\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [ \".unread\" ],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/)\",\n            \"(query && query.length>=2)?parseInt(query[1]):0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"merge\": true,\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\n            \".layui-row.layui-userdetails.layui-poll-con.layui-margin-bottom\"\n          ],\n          \"filters\": [\n            \"query.html().match(/加入日期(.*?)<span/g)[0].replace('加入日期</td><td colspan=\\\"2\\\">','').replace('(<span','').trim()\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        },\n        \"uploaded\": {\n          \"selector\": [\n            \".layui-row.layui-userdetails.layui-poll-con.layui-margin-bottom\"\n          ],\n          \"filters\": [\n            \"query.text().match(/上传量(.*?)下载量/g)[0].replace('上传量','').replace('下载量','').trim()\",\n            \"(query && query.length >=2)?query.sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\n            \".layui-row.layui-userdetails.layui-poll-con.layui-margin-bottom table:eq(1) tbody td:eq(6)\"\n          ],\n          \"filters\": [\n            \"query.html()\",\n            \"(query && query.length >=2)?query.sizeToNumber():null\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": [\n            \".layui-row.layui-userdetails.layui-poll-con.layui-margin-bottom table tbody td:eq(10)\"\n          ],\n          \"filters\": [ \"query.text()\" ]\n        },\n        \"bonus\": {\n          \"selector\": [\n            \".layui-row.layui-userdetails.layui-poll-con.layui-margin-bottom table tbody td:eq(8)\"\n          ],\n          \"filters\": [ \"query.html()\" ]\n        },\n        \"seeding\": {\n          \"selector\": [ \"i.fas.fa-upload.text-success.fa-fw + span.list-info\" ],\n          \"filters\": [ \"query.text().trim()\" ]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?page=1&limit=50&uid=$user.id$&type=seeding\",\n      \"dataType\": \"json\",\n      \"headers\": {\n        \"Accept\": \"application/json, text/javascript, */*; q=0.01\"\n      },\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [ \"size\" ],\n          \"filters\": [ \"query.sizeToNumber()\" ]\n        }\n      }\n    }\n  },\n    \"plugins\": [\n      {\n        \"name\": \"官方列表\",\n        \"pages\": [ \"/official.php\" ],\n        \"scripts\": [\n          \"/schemas/NexusPHP/common.js\",\n          \"/schemas/NexusPHP/torrents.js\"\n        ]\n      }\n    ],\n    \"mergeSchemaTagSelectors\": true\n  }\n"
  },
  {
    "path": "resource/sites/cnlang.org/config.json",
    "content": "{\n    \"name\": \"国语视界\",\n    \"timezoneOffset\": \"+0800\",\n    \"schema\": \"Discuz\",\n    \"supportedFeatures\": {\n        \"search\": false,\n        \"imdbSearch\": false,\n        \"sendTorrent\": false\n    },\n    \"url\": \"https://cnlang.org/\",\n    \"description\": \"国语视界音轨组，特效字幕组官方首发论坛，国语音轨和特效字幕的分享基地，蓝光DIY和4K电影爱好者的乐园。\",\n    \"icon\": \"https://cnlang.org/favicon.ico\",\n    \"tags\": [\"特效字幕\", \"国语音轨\"],\n    \"collaborator\": [\n      \"fzlins\"\n    ],\n    \"host\": \"cnlang.org\",\n    \"selectors\": {\n      \"userBaseInfo\": {\n        \"page\": \"/home.php?mod=spacecp&ac=credit\",\n        \"fields\": {\n          \"id\": {\n            \"selector\": [\".vwmy a\"],\n            \"attribute\": \"href\",\n            \"filters\": [\"query ? query.replace(/\\\\D+/g, '') : '' \"]\n          },\n          \"name\": {\n            \"selector\": [\".vwmy a\"]\n          },\n          \"messageCount\": {\n            \"selector\": [\"a.a.showmenu.new\"],\n            \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n          },\n          \"isLogged\": {\n            \"selector\": [\n              \"a[href*='action=logout']\"\n            ],\n            \"filters\": [\n              \"query.length>0\"\n            ]\n          },\n          \"bonus\": {\n            \"selector\": \"li:contains('大洋')\",\n            \"filters\": [\n              \"query.text().match(/大洋.*?([\\\\d.]+)/)[1]\",\n              \"parseFloat(query)\"\n            ]\n          }\n        }\n      },\n      \"userExtendInfo\": {\n        \"page\": \"/home.php?mod=spacecp&ac=plugin&id=bt_magnet:action\",\n        \"fields\": {\n          \"uploaded\": {\n            \"selector\": \".attach_magnet_div p:contains('上传量')\",\n            \"filters\": [\n                \"query.text().replace('上传量：', '').trim().sizeToNumber()\"\n            ]\n          },\n          \"downloaded\": {\n            \"selector\": \".attach_magnet_div p:contains('下载量')\",\n            \"filters\": [\n                \"query.text().replace('下载量：', '').trim().sizeToNumber()\"\n            ]\n          },\n          \"ratio\": {\n            \"selector\": \".attach_magnet_div p:contains('分享率：')\",\n            \"filters\": [\n              \"query.text().replace('分享率：', '').replace(/,/g,'').trim()\",\n              \"parseFloat(query)\"\n            ]\n          },\n          \"levelName\": {\n            \"selector\": [\"a[href='home.php?mod=spacecp&ac=usergroup']:first\"],\n            \"filters\": [\"query.text().replace('用户组: ', '').trim()\"]\n          },\n          \"joinTime\": {\n            \"selector\": \".attach_magnet_div p:contains('加入时间：')\",\n            \"filters\": [\n              \"query.text().replace('加入时间：', '').trim()\",\n              \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n            ]\n          }\n        }\n      },\n      \"userSeedingTorrents\": {\n        \"prerequisites\": \"!user.seeding\",\n        \"page\": \"/home.php?mod=spacecp&ac=plugin&id=bt_magnet:action&subop=seeding\",\n        \"parser\": \"getUserSeedingTorrents.js\",\n        \"fields\": {\n            \"seeding\": {\n                \"selector\": [\".mn tr:not(:eq(0))\"],\n                \"filters\": [\"query.length\"]\n            },\n            \"seedingSize\": {\n              \"selector\": [\".mn tr:not(:eq(0))\"],\n              \"filters\": [\"jQuery.map(query.find('td:eq(1)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n            }\n        }\n      } \n    }\n  }"
  },
  {
    "path": "resource/sites/cnlang.org/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 1\n      };\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seeding += results.seeding;\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"input[name='custompage']\")\n        .attr(\"size\");\n      if (infos) {\n        this.pageInfo.count = parseInt(infos);\n      } else {\n        this.pageInfo.count = 1;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */"
  },
  {
    "path": "resource/sites/concertos.live/config.json",
    "content": "{\n  \"name\": \"Concertos\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Concertos\",\n  \"url\": \"https://concertos.live/\",\n  \"tags\": [\"MV\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/torrent/\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"host\": \"concertos.live\",\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents?title=$key$&order_by=created_at&direction=desc\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table.table.table--bordered-big.torrents\",\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 1,\n\t    \"link\": 1,\n\t    \"url\": 1,\n        \"time\": 2,\n        \"size\": 3,\n        \"author\": 1,\n        \"seeders\": 4,\n        \"leechers\": 5,\n        \"completed\": 6\n\t},\n\t\"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"a.torrents__title\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a.torrents__title\"],\n        \"filters\": [\"query.attr('href')\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"a.torrents__title\"],\n        \"filters\": [\"query.attr('href')\", \"query + '/download'\"]\n\t  },\n\t  \"time\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"\"]\n\t  }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[src='images/freeleech.png']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": [\"div.user-info\"],\n          \"filters\": [\"$(query[0].firstChild).text().trim()\"]\n        },\n        \"id\": {\n\t      \"selector\": [\".nav > a.nav__link[href*='/user']:first\"],\n          \"filters\": [\"query.attr('href').replace('https://concertos.live/user/', '')\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"div.user-info\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.info-bar\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\".user-info__item > .fa-upload\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\".user-info__item > .fa-download\"],\n          \"filters\": [\"query.parent().text().trim().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": [\".user-info__item > .fa-percent\"],\n          \"filters\": [\"query.parent().text().trim().replace('Ratio: ', '')\"]\n        }, \n        \"levelName\": {\n          \"selector\": [\".user-info__item > .fa-bolt\"],\n          \"filters\":  [\"query.parent().text().trim().replace(/(?<=.*)\\\\s[^\\\\s]*$/,'')\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user/$user.id$\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"div.profile-block__age\"],\n          \"filters\": [\"dateTime(query.text().replace('Member since ', '')).valueOf()\"]\n        },\n        \"bonus\": {\n\t      \"selector\": [\"td:contains('BONs') + td\"],\n          \"filters\": [\"query.text().trim().replace(' ', '')\"]\n          \n        },\n        \"seeding\": {\n\t      \"selector\": [\"td:contains('Total Seeding') + td\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):null\"]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/torrent/\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='/download']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td.torrent__meta-title:contains('Size') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"a[href*='/thank']\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURLs\": {\n\t\t\"selector\": [\"a.torrents__title\"],\n        \"filters\": [\"query.toArray()\", \"query.map((item)=>{return item.href+'/download'})\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.table.table--bordered-big.torrents\"],\n          \"filters\": [\"query.find('td.torrents__size')\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/cyanbug.net/config.json",
    "content": "{\r\n  \"name\": \"CyanBug\",\r\n  \"description\": \"大青虫们在此聚集\",\r\n  \"timezoneOffset\": \"+0800\",\r\n  \"schema\": \"NexusPHP\",\r\n  \"host\": \"cyanbug.net\",\r\n  \"url\": \"https://cyanbug.net\",\r\n  \"icon\": \"https://cyanbug.net/favicon.ico\",\r\n  \"ver\": \"0.0.1\",\r\n  \"tags\": [\r\n    \"综合\",\r\n    \"影视\"\r\n  ],\r\n  \"collaborator\": [\r\n    \"jinglekang\"\r\n  ],\r\n  \"levelRequirements\": [\r\n    {\r\n      \"level\": \"1\",\r\n      \"name\": \"Power User\",\r\n      \"interval\": \"4\",\r\n      \"downloaded\": \"50GB\",\r\n      \"ratio\": \"1.05\",\r\n      \"seedingPoints\": \"40000\",\r\n      \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以请求续种；可以发送邀请；可以查看排行榜；可以删除自己上传的字幕。\"\r\n    },\r\n    {\r\n      \"level\": \"2\",\r\n      \"name\": \"Elite User\",\r\n      \"interval\": \"8\",\r\n      \"downloaded\": \"120GB\",\r\n      \"ratio\": \"1.55\",\r\n      \"seedingPoints\": \"80000\",\r\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\r\n    },\r\n    {\r\n      \"level\": \"3\",\r\n      \"name\": \"Crazy User\",\r\n      \"interval\": \"15\",\r\n      \"downloaded\": \"300GB\",\r\n      \"ratio\": \"2.05\",\r\n      \"seedingPoints\": \"150000\",\r\n      \"privilege\": \"得到两个邀请名额; 可以在做种/下载/发布的时候选择匿名模式。\"\r\n    },\r\n    {\r\n      \"level\": \"4\",\r\n      \"name\": \"Insane User\",\r\n      \"interval\": \"25\",\r\n      \"downloaded\": \"500GB\",\r\n      \"ratio\": \"2.55\",\r\n      \"seedingPoints\": \"250000\",\r\n      \"privilege\": \"可以查看普通日志。\"\r\n    },\r\n    {\r\n      \"level\": \"5\",\r\n      \"name\": \"Veteran User\",\r\n      \"interval\": \"40\",\r\n      \"downloaded\": \"750GB\",\r\n      \"ratio\": \"3.05\",\r\n      \"seedingPoints\": \"400000\",\r\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史；Veteran User及以上用户会永远保留账号。\"\r\n    },\r\n    {\r\n      \"level\": \"6\",\r\n      \"name\": \"Extreme User\",\r\n      \"interval\": \"60\",\r\n      \"downloaded\": \"1TB\",\r\n      \"ratio\": \"3.55\",\r\n      \"seedingPoints\": \"600000\",\r\n      \"privilege\": \"可可以更新过期的外部信息；可以查看Extreme User论坛。\"\r\n    },\r\n    {\r\n      \"level\": \"7\",\r\n      \"name\": \"Ultimate User\",\r\n      \"interval\": \"80\",\r\n      \"downloaded\": \"1.5TB\",\r\n      \"ratio\": \"4.05\",\r\n      \"seedingPoints\": \"800000\",\r\n      \"privilege\": \"得到五个邀请名额。\"\r\n    },\r\n    {\r\n      \"level\": \"8\",\r\n      \"name\": \"Nexus Master\",\r\n      \"interval\": \"100\",\r\n      \"downloaded\": \"3TB\",\r\n      \"ratio\": \"4.55\",\r\n      \"seedingPoints\": \"1000000\",\r\n      \"privilege\": \"得到十个邀请名额。\"\r\n    }\r\n  ],\r\n  \"securityKeyFields\": [\r\n    \"passkey\"\r\n  ],\r\n  \"searchEntryConfig\": {\r\n    \"page\": \"/torrents.php\",\r\n    \"queryString\": \"search=$key$&notnewword=1\",\r\n    \"area\": [\r\n      {\r\n        \"name\": \"标题\",\r\n        \"appendQueryString\": \"&search_area=0\"\r\n      },\r\n      {\r\n        \"name\": \"简介\",\r\n        \"appendQueryString\": \"&search_area=1\"\r\n      },\r\n      {\r\n        \"name\": \"IMDB\",\r\n        \"keyAutoMatch\": \"^(tt\\\\d+)$\",\r\n        \"appendQueryString\": \"&search_area=4\"\r\n      }\r\n    ],\r\n    \"resultType\": \"html\",\r\n    \"parseScriptFile\": \"/schemas/NexusPHP/getSearchResult.js\",\r\n    \"resultSelector\": \"table.torrents:last\"\r\n  },\r\n  \"searchEntry\": [\r\n    {\r\n      \"name\": \"全部\",\r\n      \"enabled\": true\r\n    }\r\n  ],\r\n  \"checker\": {\r\n    \"isLogin\": {\r\n      \"page\": \"/usercp.php\",\r\n      \"contains\": \"logout.php\"\r\n    }\r\n  },\r\n  \"selectors\": {\r\n    \"userBaseInfo\": {\r\n      \"page\": \"/index.php\",\r\n      \"fields\": {\r\n        \"id\": {\r\n          \"selector\": [\r\n            \"a[href*='userdetails.php'][class*='Name']:first\",\r\n            \"a[href*='userdetails.php']:first\"\r\n          ],\r\n          \"attribute\": \"href\",\r\n          \"filters\": [\r\n            \"query ? query.getQueryString('id'):''\"\r\n          ]\r\n        },\r\n        \"name\": {\r\n          \"selector\": [\r\n            \"a[href*='userdetails.php'][class*='Name']:first\",\r\n            \"a[href*='userdetails.php']:first\"\r\n          ],\r\n          \"filters\": [\r\n            \"query && query.attr('href').getQueryString('id') > 0 ? query.text(): ''\"\r\n          ]\r\n        },\r\n        \"isLogged\": {\r\n          \"selector\": [\r\n            \"a[href*='usercp.php']\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.length>0\"\r\n          ]\r\n        },\r\n        \"messageCount\": {\r\n          \"selector\": [\r\n            \"td[style*='background: red'] a[href*='messages.php']\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.text().match(/(\\\\d+)/)\",\r\n            \"(query && query.length>=2)?parseInt(query[1]):0\"\r\n          ]\r\n        }\r\n      }\r\n    },\r\n    \"userExtendInfo\": {\r\n      \"page\": \"/userdetails.php?id=$user.id$\",\r\n      \"fields\": {\r\n        \"uploaded\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('传输') + td\",\r\n            \"td.rowhead:contains('傳送') + td\",\r\n            \"td.rowhead:contains('Transfers') + td\",\r\n            \"td.rowfollow:contains('分享率')\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.text().replace(/,/g,'').match(/(上[传傳]量|Uploaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\r\n            \"(query && query.length==3)?(query[2]).sizeToNumber():0\"\r\n          ]\r\n        },\r\n        \"downloaded\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('传输') + td\",\r\n            \"td.rowhead:contains('傳送') + td\",\r\n            \"td.rowhead:contains('Transfers') + td\",\r\n            \"td.rowfollow:contains('分享率')\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.text().replace(/,/g,'').match(/(下[载載]量|Downloaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\r\n            \"(query && query.length==3)?(query[2]).sizeToNumber():0\"\r\n          ]\r\n        },\r\n        \"levelName\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('等级')\",\r\n            \"td.rowhead:contains('等級')\",\r\n            \"td.rowhead:contains('Class')\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.next().find('img').attr('title')\"\r\n          ]\r\n        },\r\n        \"bonus\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('魔力') + td\",\r\n            \"td.rowhead:contains('Karma'):contains('Points') + td\",\r\n            \"td.rowhead:contains('麦粒') + td\",\r\n            \"td.rowfollow:contains('魔力值')\",\r\n            \"td.rowhead:contains('bonus') + td\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.is(\\\":contains('魔力值:')\\\")||query.is(\\\":contains('Bonus Points:')\\\")?query.text().replace(/,/g,'').match(/(?:魔力值|Bonus Points).+?([\\\\d.]+)/)[1]:query.text().replace(/,/g,'')\",\r\n            \"parseFloat(query)\"\r\n          ]\r\n        },\r\n        \"seedingPoints\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('做种积分') + td\",\r\n            \"td.rowhead:contains('Seeding Points') + td\",\r\n            \"td.rowhead:contains('做種積分') + td\",\r\n            \"td.rowhead:contains('保种积分') + td\",\r\n            \"td.rowfollow:contains('做种积分')\",\r\n            \"td.rowfollow:contains('Seeding Points')\",\r\n            \"td.rowfollow:contains('做種積分')\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.text().replace(/,/g,'')\",\r\n            \"query.includes('做种积分') || query.includes('做種積分') || query.includes('Seeding Points') ? query.match(/(做种积分|做種積分|Seeding Points).+?[\\\\d.]+/g)[0] : query\",\r\n            \"query ? parseFloat(query.match(/[\\\\d.]+/)[0]) : null\"\r\n          ]\r\n        },\r\n        \"joinTime\": {\r\n          \"selector\": [\r\n            \"td.rowhead:contains('加入日期')\",\r\n            \"td.rowhead:contains('Join'):contains('date')\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.next().text().split(' (')[0].trim()\",\r\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\r\n          ]\r\n        }\r\n      }\r\n    },\r\n    \"bonusExtendInfo\": {\r\n      \"prerequisites\": \"!user.bonusPerHour\",\r\n      \"page\": \"/mybonus.php\",\r\n      \"fields\": {\r\n        \"bonusPerHour\": {\r\n          \"selector\": [\r\n            \"div:contains('你当前每小时能获取'):last\",\r\n            \"div:contains('You are currently getting'):last\",\r\n            \"div:contains('你當前每小時能獲取'):last\"\r\n          ],\r\n          \"filters\": [\r\n            \"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"\r\n          ]\r\n        }\r\n      }\r\n    },\r\n    \"userSeedingTorrents\": {\r\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\r\n      \"fields\": {\r\n        \"seeding\": {\r\n          \"selector\": [\r\n            \"b:first\"\r\n          ],\r\n          \"filters\": [\r\n            \"query.text()\"\r\n          ]\r\n        },\r\n        \"seedingSize\": {\r\n          \"selector\": \"\",\r\n          \"filters\": [\r\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\r\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\r\n            \"(query != 0) ? query.sizeToNumber() : 0\"\r\n          ]\r\n        }\r\n      }\r\n    }\r\n  }\r\n}"
  },
  {
    "path": "resource/sites/dajiao.cyou/config.json",
    "content": "{\n    \"name\": \"打胶 \",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"打胶\",\n    \"url\": \"https://dajiao.cyou/\",\n    \"icon\": \"https://dajiao.cyou/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"dajiao.cyou\",\n    \"collaborator\": [\n        \"IITII\"\n    ],\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"100000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"200000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"350000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"1500000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"2000000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"2500000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/dicmusic.com/config.json",
    "content": "{\n  \"name\": \"DIC\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"music\",\n  \"url\": \"https://dicmusic.com/\",\n  \"icon\": \"https://dicmusic.com/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"dicmusic.com\",\n  \"cdn\": [\"https://dicmusic.club/\", \"https://dicmusic.com/\"],\n  \"formerHosts\": [\n    \"dicmusic.club\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Member\",\n      \"uploaded\": \"10GB\",\n      \"ratio\": \"0.7\",\n      \"interval\": \"1\",\n      \"privilege\": \"发起求种；查看部分排行榜；完全访问「茶话会」版块\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Power User\",\n      \"uploaded\": \"25GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"2\",\n      \"uploads\": \"5\",\n      \"privilege\": \"免疫账号不活跃；发送邀请，赠送1枚永久邀请；佩戴1枚印记；创建1个私人合集；访问「求邀区」「发邀区」「Power User」版块；完全访问排行榜\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Elite\",\n      \"uploaded\": \"75GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"4\",\n      \"uploads\": \"50\",\n      \"privilege\": \"赠送1枚永久邀请；佩戴2枚印记；创建2个私人合集；访问「Elite」版块；检查自己的种子；编辑所有种子；购买「自定义头衔（不允许 BBCode）」\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Torrent Master\",\n      \"uploaded\": \"200GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"8\",\n      \"uploads\": \"150\",\n      \"privilege\": \"赠送2枚永久邀请；每月1枚临时邀请；佩戴3枚印记；创建3个私人合集；访问「Torrent Master」版块\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Power Torrent Master\",\n      \"uploaded\": \"375GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"8\",\n      \"uniqueGroups\": \"300\",\n      \"privilege\": \"赠送2枚永久邀请；每月2枚临时邀请；佩戴4枚印记；创建4个私人合集；检查所有种子\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Elite Torrent Master\",\n      \"uploaded\": \"600GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"12\",\n      \"perfectFLAC\": \"500\",\n      \"privilege\": \"赠送3枚永久邀请；每月3枚临时邀请；无发邀间隔；佩戴5枚印记；创建5个私人合集；访问「Elite Torrent Master」版块\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Elite Torrent Master Plus\",\n      \"uploaded\": \"600GB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"12\",\n      \"perfectFLAC\": \"500\",\n      \"privilege\": \"赠送3枚永久邀请；每月3枚临时邀请；购买「自定义头衔（允许 BBCode）」\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Guru\",\n      \"uploaded\": \"1.2TB\",\n      \"ratio\": \"1.05\",\n      \"interval\": \"16\",\n      \"perfectFLAC\": \"1000\",\n      \"privilege\": \"拥有无限邀请；佩戴6枚印记；创建6个私人合集；访问「Guru」版块；查看种子检查日志\"\n    }\n  ],\n  \"collaborator\": [\n    \"ylxb2016\",\n    \"enigmaz\",\n    \"amorphobia\"\n  ],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"page\": \"/ajax.php?action=user&id=$user.id$\",\n      \"fields\": {\n        \"perfectFLAC\": {\n          \"selector\": [\"response.community.perfectFlacs\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/bonus.php?action=bprates\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"table#bprates_overview > tbody > tr > td:eq(1)\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div#content > div.header > h3\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"table#bprates_overview > tbody > tr > td:eq(2)\"],\n          \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/discfan.net/config.json",
    "content": "{\n  \"name\": \"DiscFan\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"DiscFan\",\n  \"url\": \"https://discfan.net/\",\n  \"icon\": \"https://discfan.net/favicon.ico\",\n  \"tags\": [\"影视\", \"港片\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"discfan.net\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以检视NFO文件；可以检视用户清单；可以要求续种；可以传送邀请；可以检视排行榜；可以检视其他用户的种子历史(如果用户隐私等级未设定为\\\"强\\\")；可以移除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被移除。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选取匿名型态。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"可以检视普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"700GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"得到三个邀请名额；可以检视其他用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以更新过期的外部资讯；可以检视Extreme User论坛。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"得到五个邀请名额。\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"得到十个邀请名额。\"\n  }],\n  \"formerHosts\": [\n    \"pt.gztown.net\"\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n      \"id\": 401,\n      \"name\": \"中国大陆\"\n    }, {\n      \"id\": 404,\n      \"name\": \"中国香港特别行政区\"\n    }, {\n      \"id\": 405,\n      \"name\": \"中国台湾省\"\n    }, {\n      \"id\": 402,\n      \"name\": \"泰国\"\n    }, {\n      \"id\": 403,\n      \"name\": \"日本\"\n    }, {\n      \"id\": 406,\n      \"name\": \"韩国\"\n    }, {\n      \"id\": 410,\n      \"name\": \"世界\"\n    }, {\n      \"id\": 411,\n      \"name\": \"剧集\"\n    }, {\n      \"id\": 414,\n      \"name\": \"音乐\"\n    }, {\n      \"id\": 413,\n      \"name\": \"记录\"\n    }, {\n      \"id\": 416,\n      \"name\": \"综艺\"\n    }, {\n      \"id\": 417,\n      \"name\": \"体育\"\n    }]\n  }],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}"
  },
  {
    "path": "resource/sites/et8.org/config.json",
    "content": "{\n  \"name\": \"TorrentCCF\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"兼有学习资源和软件资源的影视PT站点\",\n  \"url\": \"https://et8.org/\",\n  \"icon\": \"https://et8.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"学习\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"et8.org\",\n  \"collaborator\": [\"Rhilip\", \"cnsunyour\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"士官(Power User)\",\n    \"interval\": \"2\",\n    \"downloaded\": \"64GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"可以上传种子; 可以删除自己上传的字幕; 可以在做种/下载/上传的时候选择匿名模式.\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"尉官(Elite User)\",\n    \"interval\": \"6\",\n    \"downloaded\": \"128GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"购买邀请; 可以查看邀请论坛; 可以查看NFO文档; 可以更新外部信息; 可以请求续种; 可以使用个性条.\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"少校(Crazy User)\",\n    \"interval\": \"14\",\n    \"downloaded\": \"256GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"可以查看排行榜;可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\").\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"中校(Insane User)\",\n    \"interval\": \"26\",\n    \"downloaded\": \"512GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"中校及以上用户Park后不会被删除帐号.\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"上校(Veteran User)\",\n    \"interval\": \"38\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"可以发送邀请; 上校及以上用户会永远保留账号.\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"少将(Extreme User)\",\n    \"interval\": \"54\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以查看种子文件结构.\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"中将(Ultimate User)\",\n    \"interval\": \"70\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史;得到五个邀请名额.\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"上将(Nexus Master)\",\n    \"interval\": \"88\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"得到十个邀请名额。\"\n  }],\n  \"plugins\": [{\n    \"name\": \"小组专区\",\n    \"pages\": [\"/trls.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:parseFloat(query.text().split('%')[0])\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:(query.is(\\\"[bgcolor='#44cef6']\\\")?1:(parseFloat(query.text().split('%')[0])==100?(query.is(\\\"[bgcolor='#d0d0d0']\\\")?255:2):3))\"\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat622=1\",\n      \"name\": \"Movies.电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat623=1\",\n      \"name\": \"TV.电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat624=1\",\n      \"name\": \"Documentaries.纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat625=1\",\n      \"name\": \"Appz.软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat626=1\",\n      \"name\": \"Music&MusicVideos.音乐及MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat627=1\",\n      \"name\": \"Others.其他(非学习类)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat628=1\",\n      \"name\": \"Elearning - 杂项学习\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat629=1\",\n      \"name\": \"Elearning - 电子书/小说\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat630=1\",\n      \"name\": \"Elearning - 电子书/非小说\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat631=1\",\n      \"name\": \"Elearning - 杂志\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat632=1\",\n      \"name\": \"Elearning - 漫画\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat633=1\",\n      \"name\": \"Elearning - 有声书\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat634=1\",\n      \"name\": \"Elearning - 公开课\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat635=1\",\n      \"name\": \"Elearning - 视频教程\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 622,\n        \"name\": \"Movies.电影\"\n      },\n      {\n        \"id\": 623,\n        \"name\": \"TV.电视剧\"\n      },\n      {\n        \"id\": 624,\n        \"name\": \"Documentaries.纪录片\"\n      },\n      {\n        \"id\": 625,\n        \"name\": \"Appz.软件\"\n      },\n      {\n        \"id\": 626,\n        \"name\": \"Music&MusicVideos.音乐及MV\"\n      },\n      {\n        \"id\": 627,\n        \"name\": \"Others.其他(非学习类)\"\n      },\n      {\n        \"id\": 628,\n        \"name\": \"Elearning - 杂项学习\"\n      },\n      {\n        \"id\": 629,\n        \"name\": \"Elearning - 电子书/小说\"\n      },\n      {\n        \"id\": 630,\n        \"name\": \"Elearning - 电子书/非小说\"\n      },\n      {\n        \"id\": 631,\n        \"name\": \"Elearning - 杂志\"\n      },\n      {\n        \"id\": 632,\n        \"name\": \"Elearning - 漫画\"\n      },\n      {\n        \"id\": 633,\n        \"name\": \"Elearning - 有声书\"\n      },\n      {\n        \"id\": 634,\n        \"name\": \"Elearning - 公开课\"\n      },\n      {\n        \"id\": 635,\n        \"name\": \"Elearning - 视频教程\"\n      }\n    ]\n  }]\n}\n"
  },
  {
    "path": "resource/sites/extremlymtorrents.ws/config.json",
    "content": "{\n  \"name\": \"XTR\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"extremlymtorrents\",\n  \"url\": \"https://extremlymtorrents.ws/\",\n  \"icon\": \"https://extremlymtorrents.ws/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"Common\",\n  \"host\": \"extremlymtorrents.ws\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/file.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\", \"/torrents-search.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents-search.php\",\n    \"resultType\": \"html\",\n    \"queryString\": \"search=$key$&cat=0&lang=0&sort=id&order=desc\",\n    \"parseScriptFile\": \"/schemas/Common/getSearchResult.js\",\n    \"resultSelector\": \"table.xtrz\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"parseScript\": \"(payload && payload.en)?payload.en:key\"\n    }],\n    \"firstDataRowIndex\": 1,\n    \"fieldIndex\": {\n      \"title\": 1,\n      \"time\": 8,\n      \"size\": 5,\n      \"seeders\": 6,\n      \"leechers\": 7,\n      \"completed\": -1,\n      \"comments\": -1,\n      \"author\": 3,\n      \"category\": 0,\n      \"url\": 2,\n      \"link\": 1\n    },\n    \"loggedRegex\": \"account-logout\\\\.php\",\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"a:first > b\"]\n      },\n      \"url\": {\n        \"selector\": [\"a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"link\": {\n        \"selector\": [\"a:first\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"time\": {\n        \"selector\": [\"\"],\n        \"filters\": [\"query.text().replace(/(\\\\d{2}).(\\\\d{2}).(\\\\d{4})\\\\n?(\\\\d{2}:\\\\d{2}:\\\\d{2})/,'$3-$2-$1 $4')\"]\n      },\n      \"category\": {\n        \"selector\": [\"img:first\"],\n        \"filters\": [\"query.attr('alt')\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[title='Free Torrents']\"\n  }, {\n    \"name\": \"VIP\",\n    \"selector\": \"img[alt='Only VIP']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='account-details.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='account-details.php']:first\"],\n          \"filters\": [\"query && query.attr('href').getQueryString('id') > 0 ? query.text(): ''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='account-logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/account-details.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.ttable_col2:contains('Uploaded:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.ttable_col2:contains('Downloaded:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"td.ttable_col2:contains('User Class:') + td\"\n        },\n        \"joinTime\": {\n          \"selector\": \"td.ttable_col2:contains('Joined:') + td\",\n          \"filters\": [\"query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"value\": \"N/A\"\n        },\n        \"seedingSize\": {\n          \"value\": \"N/A\"\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"/file.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"extremlymsmall3\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php']:first\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"input[type=submit][value='Thanks!']\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    },\n    \"/torrents.php\": {\n      \"fields\": {\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='download.php']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.xtrz\"],\n          \"filters\": [\"query.find(\\\"td:contains('MB'),td:contains('GB'),td:contains('TB')\\\")\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/femdomcult.org/config.json",
    "content": "{\n  \"name\": \"Femdomcult\",\n  \"timezoneOffset\": \"-1100\",\n  \"description\": \"成人\",\n  \"url\": \"https://femdomcult.org/\",\n  \"icon\": \"https://femdomcult.org/favicon.ico\",\n  \"tags\": [\n    \"成人\"\n  ],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"femdomcult.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"\",\n    \"interval\": \"1\",\n    \"trueDownloaded\": \"5GB\",\n    \"ratio\": \"0\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"\",\n    \"interval\": \"2\",\n    \"trueDownloaded\": \"10GB\",\n    \"ratio\": \"0.1\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"\",\n    \"interval\": \"3\",\n    \"trueDownloaded\": \"20GB\",\n    \"ratio\": \"0.15\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"\",\n    \"interval\": \"4\",\n    \"trueDownloaded\": \"30GB\",\n    \"ratio\": \"0.2\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"\",\n    \"interval\": \"5\",\n    \"trueDownloaded\": \"40GB\",\n    \"ratio\": \"0.3\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"\",\n    \"interval\": \"6\",\n    \"trueDownloaded\": \"50GB\",\n    \"ratio\": \"0.4\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Guru\",\n    \"interval\": \"7\",\n    \"trueDownloaded\": \"60GB\",\n    \"ratio\": \"0.5\",\n    \"privilege\": \"\"\n    }\n  ],\n  \"collaborator\": [\n    \"RichardHu\"\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/ajax.php\",\n    \"resultType\": \"json\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"asyncParse\": true,\n    \"queryString\": \"action=browse&searchstr=$key$\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/ajax.php?action=index\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"response.id\"]\n        },\n        \"name\": {\n          \"selector\": [\"response.username\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"response.userstats.uploaded\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"response.userstats.downloaded\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"response.userstats.ratio\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"response.userstats.class\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/femdomcult.org/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n      this.authkey = \"\";\n      this.passkey = \"\";\n    }\n\n    start() {\n      this.getAuthKey()\n        .then(() => {\n          options.resolve(this.getResult());\n        })\n        .catch(() => {\n          options.reject({\n            success: false,\n            msg: options.searcher.getErrorMessage(\n              options.site,\n              ESearchResultParseStatus.parseError,\n              options.errorMsg\n            ),\n            data: {\n              site: options.site,\n              isLogged: options.isLogged\n            }\n          });\n        });\n    }\n\n    /**\n     * 获取搜索结果\n     */\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let groups = options.page.response.results;\n      if (groups.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return [];\n      }\n      let results = [];\n      let authkey = this.authkey;\n      let passkey = this.passkey;\n      console.log(\"groups.length\", groups.length);\n      try {\n        groups.forEach(group => {\n          if (group.hasOwnProperty(\"torrents\")) {\n            let torrents = group.torrents;\n            torrents.forEach(torrent => {\n              let data = {\n                id: torrent.torrentId,\n                title:\n                  group.groupName +\n                  \" - \" +\n                  group.groupSubName +\n                  \" [\" +\n                  group.groupYear +\n                  \"] [\" +\n                  group.releaseType +\n                  \"]\",\n                subTitle:\n                  torrent.codec +\n                  \" / \" +\n                  torrent.source +\n                  \" / \" +\n                  torrent.resolution +\n                  \" / \" +\n                  torrent.container +\n                  \" / \" +\n                  torrent.processing +\n                  (torrent.remasterTitle ? ` / ${torrent.remasterTitle}` : \"\") +\n                  (torrent.scene ? \" / Scene\" : \"\") +\n                  (torrent.isFreeleech ||\n                  torrent.isNeutralLeech ||\n                  torrent.isPersonalFreeleech\n                    ? \" / Freeleech\"\n                    : \"\") +\n                  (torrent.releaseGroup ? ` / ${torrent.releaseGroup}` : \"\"),\n                link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${torrent.torrentId}`,\n                url: `${site.url}torrents.php?action=download&id=${torrent.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n                size: parseFloat(torrent.size),\n                time: torrent.time,\n                seeders: torrent.seeders,\n                leechers: torrent.leechers,\n                completed: torrent.snatches,\n                site: site,\n                entryName: options.entry.name,\n                category: group.releaseType,\n                imdbId: group.imdbId,\n              };\n              results.push(data);\n            });\n          } else {\n            let data = {\n              title: group.groupName,\n              link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${group.torrentId}`,\n              url: `${site.url}torrents.php?action=download&id=${group.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n              size: parseFloat(group.size),\n              time: group.groupTime,\n              author: \"\",\n              seeders: group.seeders,\n              leechers: group.leechers,\n              completed: group.snatches,\n              comments: 0,\n              site: site,\n              tags: group.tags,\n              entryName: options.entry.name,\n              category: group.category,\n              imdbId: group.imdbId,\n            };\n            results.push(data);\n          }\n        });\n        console.log(\"results.length\", results.length);\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n\n    /**\n     * 获取 AuthKey ，用于组合完整的下载链接\n     */\n    getAuthKey() {\n      const url = (options.site.activeURL + \"/ajax.php?action=index\")\n        .replace(\"://\", \"****\")\n        .replace(/\\/\\//g, \"/\")\n        .replace(\"****\", \"://\");\n\n      return new Promise((resolve, reject) => {\n        $.get(url)\n          .done(result => {\n            if (result && result.status === \"success\" && result.response) {\n              this.authkey = result.response.authkey;\n              this.passkey = result.response.passkey;\n              resolve();\n            } else {\n              reject();\n            }\n          })\n          .fail(() => {\n            reject();\n          });\n      });\n    }\n  }\n\n  let parser = new Parser(options);\n  parser.start();\n})(options);\n"
  },
  {
    "path": "resource/sites/filelist.io/browse.js",
    "content": "(function ($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n      init() {\n        this.initButtons();\n        this.initFreeSpaceButton();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.initListButtons(false);\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURLs() {\n        let siteURL = PTService.site.url;\n        if (siteURL.substr(-1) != \"/\") {\n          siteURL += \"/\";\n        }\n\n        let links = $(\"a[href*='download'][onclick!='return show_confirm();']\").toArray();\n        let urls = $.map(links, (item) => {\n          let link = $(item).attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${siteURL}${link}`+(PTService.site.passkey ? \"&passkey=\" + PTService.site.passkey : \"\");\n          }\n          return link;\n        });\n\n        if (links.length == 0) {\n          return \"获取下载链接失败，未能正确定位到链接\";\n        }\n\n        return urls;\n      }\n\n      getDownloadURL(id) {\n        return `https://${PTService.site.host}/download.php?id=${id}`+(PTService.site.passkey ? \"&passkey=\" + PTService.site.passkey : \"\");\n      }\n\n      /**\n       * 执行指定的操作\n       * @param {*} action 需要执行的执令\n       * @param {*} data 附加数据\n       * @return Promise\n       */\n      call(action, data) {\n        return new Promise((resolve, reject) => {\n          switch (action) {\n            // 从当前的DOM中获取下载链接地址\n            case PTService.action.downloadFromDroper:\n              this.downloadFromDroper(data, () => {\n                resolve()\n              });\n              break;\n          }\n        });\n      }\n\n      /**\n       * 下载拖放的种子\n       * @param {*} data\n       * @param {*} callback\n       */\n      downloadFromDroper(data, callback) {\n        if (typeof data === \"string\") {\n          data = {\n            url: data,\n            title: \"\"\n          };\n        }\n\n        let id = data.url.getQueryString(\"id\");\n        let result = this.getDownloadURL(id);\n\n        if (!result) {\n          callback();\n          return;\n        }\n\n        this.sendTorrentToDefaultClient(result).then((result) => {\n          callback(result);\n        }).catch((result) => {\n          callback(result);\n        });\n      }\n\n      /**\n       * 确认大小是否超限\n       */\n      confirmWhenExceedSize() {\n        return this.confirmSize($(\"div.visitedlinks\").find(\"div[class='torrenttable']:contains('MB'),div[class='torrenttable']:contains('GB'),div[class='torrenttable']:contains('TB')\"));\n      }\n    }\n    (new App()).init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/filelist.io/config.json",
    "content": "{\n  \"name\": \"FileList\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"FileList\",\n  \"url\": \"https://filelist.io/\",\n  \"icon\": \"https://filelist.io/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"FileList\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/t/(\\\\d+)/$\", \"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"host\": \"filelist.io\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"uploaded\": \"25GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"It can download DOX files larger than 1MB. This class has the right to apply to uploader status.\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Addict\",\n      \"interval\": \"26\",\n      \"uploaded\": \"500GB\",\n      \"ratio\": \"4.00\",\n      \"privilege\": \"This class has the right to request a Custom Title. This class is entitled to requests.\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Elite\",\n      \"interval\": \"209\",\n      \"uploaded\": \"4TB\",\n      \"ratio\": \"5.00\",\n      \"privilege\": \"This class gives you the right to give reputation to other users.\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search=$key$&cat=0&searchin=1&sort=2\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"div.visitedlinks:last > div[class=torrentrow]\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"All\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[alt='FreeLeech']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:eq(1)\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\".alert a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"a[href='/shop.php']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.colhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.colhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.colhead:contains('Share ratio') + td\",\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.colhead:contains('Class') + td\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.colhead:contains('Join'):contains('date') + td\"],\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"td.colhead:contains('Seed'):contains('bonus') + td > div > b\"],\n          \"filters\": [\"query.first().text().match(/([\\\\d.]+)/)\", \"(query && query.length>=1)?query[0]:''\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td.colhead:contains('Seed'):contains('bonus') + td > div > b:nth-child(2)\"],\n          \"filters\": [\"query.last().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"td.colhead:contains('Seed'):contains('bonus') + td > div:eq(2) > b\"],\n          \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/filelist.io/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      let query = $(\"a[href*='download.php'][onclick!='return show_confirm();']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        // 直接获取的链接下载成功率很低\n        // 如果设置了 passkey 则使用 rss 订阅的方式下载\n        if (PTService.site.passkey) {\n          let id = url.getQueryString(\"id\");\n          url = `https://${PTService.site.host}/download.php?id=${id}`+\"&passkey=\" + PTService.site.passkey;\n        }\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${siteURL}${url}`;\n        }\n      }\n\n      return url;\n    }\n\n    showTorrentSize() {\n      let query = $(\"div[style='width:25%;float:left;']:contains('Last activity')\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().replace(\"Size\",\"\").replace(/[\\r\\n]/g,\"\").replace(/Files(.*)/,\"\");\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    getTitle() {\n      let query = $(\"div.cblock-header\");\n      return query ? query.text(): \"\";\n    }\n  };\n  (new App()).init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/filelist.io/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector ||\n          \"div.visitedlinks:last > div[class=torrentrow]\"\n      );\n      let time_regex = /(\\d{2}:\\d{2}:\\d{2}[^\\d]+?\\d{2}\\/\\d{2}\\/\\d{4})/;\n      let time_regen_replace1 = /(\\d{2}:\\d{2}:\\d{2})[^\\d]+?(\\d{2}\\/\\d{2}\\/\\d{4})/;\n      let time_regen_replace2 = /(\\d{2})\\/(\\d{2})\\/(\\d{4})/;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        //title\n        title: 1,\n        //downloadlink\n        downloadlink: 3,\n        // 时间\n        time: 5,\n        // 大小\n        size: 6,\n        // 上传人数\n        seeders: 8,\n        // 下载人数\n        leechers: 9,\n        // 完成人数\n        completed: 7,\n        // 评论人数\n        comments: 4,\n        // 发布人\n        author: 10,\n        category: 0\n      };\n\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n\n      // 遍历数据行\n      for (let index = 0; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">div\");\n        let titleStrings = cells\n          .eq(fieldIndex.title)\n          .find(\"a\")\n          .attr(\"title\");\n        let title = cells\n          .eq(fieldIndex.title)\n          .find(\"a\")\n          .first();\n        if (title.length == 0) {\n          continue;\n        }\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}/${link}`;\n        }\n        let id = link.getQueryString(\"id\");\n        let url = \"\";\n        if (site.passkey && id) {\n          url = `https://${site.host}/download.php?id=${id}&passkey=${site.passkey}`;\n        } else {\n          url = cells\n            .eq(fieldIndex.downloadlink)\n            .find(\"a\")\n            .first()\n            .attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}/${url}`;\n          }\n        }\n\n        if (!url) {\n          continue;\n        }\n        let subTitle = \"\";\n        let data = {\n          title: $(\"<span>\")\n            .html(titleStrings)\n            .text(),\n          subTitle: subTitle || \"\",\n          link,\n          url: url,\n          size: cells.eq(fieldIndex.size).text() || 0,\n          time: cells\n            .eq(fieldIndex.time)\n            .html()\n            .replace(\"<br>\", \" \")\n            .match(time_regex)[1]\n            .replace(time_regen_replace1, \"$2 $1\")\n            .replace(time_regen_replace2, \"$3-$2-$1\"),\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text()\n              .split(\"/\")[0] || 0,\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text()\n              .split(\"/\")[1] || 0,\n          completed: cells.eq(fieldIndex.completed).text() || 0,\n          comments: cells.eq(fieldIndex.comments).text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: Searcher.getRowTags(site, row)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/fsm.name/config.json",
    "content": "{\n  \"name\": \"FSM\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"飞天拉面神教 - FSM\",\n  \"url\": \"https://fsm.name/\",\n  \"tags\": [ \"成人\" ],\n  \"schema\": \"Common\",\n  \"host\": \"fsm.name\",\n  \"formerHosts\": [\n    \"nextpt.net\"\n  ],\n  \"collaborator\": [\n    \"Ted423\",\n    \"IITII\"\n  ],\n  \"plugins\": [\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [ \"/Torrents/details\" ],\n      \"scripts\": [ \"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\" ]\n    },\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [ \"/Torrents$\" ],\n      \"scripts\": [ \"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\" ]\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"page\": \"/Torrents?type=0&systematics=0&keyword=$key$\",\n    \"loggedRegex\": \"container-fluid\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table\",\n    \"fieldIndex\": {\n      \"category\": 0,\n      \"title\": 2,\n      \"link\": 2,\n      \"url\": 3,\n      \"comments\": 4,\n      \"time\": 5,\n      \"size\": 6,\n      \"seeders\": 7,\n      \"leechers\": 8,\n      \"completed\": 9\n    },\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [ \"a[href*='/Torrents/details']\" ],\n        \"filters\": [ \"query\" ]\n      },\n      \"url\": {\n        \"selector\": [ \"a[href*='/Torrents/download?passkey=']\" ],\n        \"filters\": [ \"query.attr('href')\", \"'https://fsm.name'+query\" ]\n      },\n      \"progress\": {\n        \"selector\": [ \".progress-bar.progress-bar-success\", \".progress-bar.progress-bar-info,.progress-bar.progress-bar-danger\", \"\" ],\n        \"switchFilters\": [\n          [ \"100\" ],\n          [ \"query.attr('style').replace('width: ','').replace('%;','')\" ],\n          [ \"null\" ]\n        ]\n      },\n      \"status\": {\n        \"selector\": [ \".progress-bar.progress-bar-success\", \".progress-bar.progress-bar-info\", \".progress-bar.progress-bar-danger\" ],\n        \"switchFilters\": [\n          [ \"2\" ],\n          [ \"1\" ],\n          [ \"3\" ]\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/Rules\",\n      \"fields\": {\n        \"isLogged\": {\n          \"selector\": [ \"a.adminUser\" ],\n          \"filters\": [ \"query.length>0\" ]\n        },\n        \"name\": {\n          \"selector\": [ \"#header-navbar .dropdown-toggle\" ],\n          \"filters\": [ \"query.text().trim().replace(/工具\\\\s?/,'')\" ]\n        },\n        \"id\": {\n          \"selector\": [ \"a[href*='/Users/profile']\" ],\n          \"attribute\": \"href\",\n          \"filters\": [ \"query ? query.getQueryString('uid'):''\" ]\n        },\n        \"messageCount\": {\n          \"selector\": [ \"a[href='/Mail']\" ],\n          \"filters\": [ \"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\" ]\n        },\n        \"uploaded\": {\n          \"selector\": [ \"#data-upload\" ],\n          \"filters\": [ \"query.text().replace(/,/g,'').replace('上传量：','').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\" ]\n        },\n        \"downloaded\": {\n          \"selector\": [ \"#data-download\" ],\n          \"filters\": [ \"query.text().replace(/,/g,'').replace('下载量：','').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\" ]\n        },\n        \"seeding\": {\n          \"selector\": [ \"#data-now-seed\" ],\n          \"filters\": [ \"query.text().replace(/当前上传[：:]/,'')\" ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/Users/profile?uid=$user.id$\",\n      \"fields\": {\n        \"comment\": \"暂不获取的数据置 0\",\n        \"bonusPerHour\": {\"value\":\"0\"},\n        \"joinTime\": {\n          \"selector\": [ \"th:contains('加入时间') + td\" ],\n          \"filters\": [ \"dateTime(query.text().trim()).isValid()?dateTime(query.text().trim()).valueOf():query.text().trim()\"]\n        },\n        \"levelName\": {\n          \"selector\": [ \"a[href*='/Users/profile'][class*='User']\" ],\n          \"filters\": [ \"query.attr('class').replace(/[^ ]*\\\\s/,'').replace(/User.*/,'').toUpperCase()\"]\n        },\n        \"bonus\": {\n          \"selector\": [ \"#data-seedGH\" ],\n          \"filters\": [ \"query.text()\" ]\n        }\n      }\n    },\n      \"userSeedingTorrents\": {\n        \"page\": \"/Torrents/mySeed\",\n        \"fields\": {\n          \"seedingSize\": {\n            \"selector\": \".panel-primary .panel-body td(6)\",\n            \"filters\": [\n              \"(query != 0) ? query.sizeToNumber() : 0\",\n              \"query.text()\"\n            ]\n          }\n        }\n      },\n    \"common\": {\n      \"page\": \"/Torrents\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [ \"a[href*='/Torrents/download']\" ],\n          \"filters\": [ \"query.attr('href')\" ]\n        },\n        \"size\": {\n          \"selector\": [ \"div.visible-xs:contains('种子大小') + div\" ],\n          \"filters\": [ \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\" ]\n        },\n        \"downloadURLs\": {\n          \"selector\": [ \"a[href*='/Torrents/download']\" ],\n          \"filters\": [ \"query.toArray()\" ]\n        },\n        \"confirmSize\": {\n          \"selector\": [ \"table.table.table-bordered > tbody td.center.tdCenter > div:contains('B')\" ],\n          \"filters\": [ \"query\" ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/gainbound.net/config.json",
    "content": "{\n    \"name\": \"丐帮\",\n    \"description\": \"降龙掌青竹杖 美酒佳肴来作伴\",\n    \"url\": \"https://gainbound.net/\",\n    \"icon\": \"https://gainbound.net/favicon.ico\",\n    \"tags\": [\"高清\",\"电影\",\"纪录片\"],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"gainbound.net\",\n    \"levelRequirements\":[\n      {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"5\",\n        \"downloaded\": \"50GB\",\n        \"ratio\": \"1.05\",\n        \"seedingPoints\": \"40000\",\n        \"privilege\": \"得到3个邀请名额；可以查看邀请区；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n      },\n      {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"10\",\n        \"downloaded\": \"120GB\",\n        \"ratio\": \"1.55\",\n        \"seedingPoints\": \"80000\",\n        \"privilege\": \"得到1个邀请名额，三袋弟子及以上用户封存账号后不会被删除。\"\n      },\n      {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"300GB\",\n        \"ratio\": \"2.05\",\n        \"seedingPoints\": \"150000\",\n        \"privilege\": \"得到2个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n      },\n      {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"500GB\",\n        \"ratio\": \"2.55\",\n        \"seedingPoints\": \"250000\",\n        \"privilege\": \"得到1个邀请名额，可以查看普通日志。\"\n      },\n      {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"750GB\",\n        \"ratio\": \"3.05\",\n        \"seedingPoints\": \"400000\",\n        \"privilege\": \"得到3个邀请名额；可以查看其它用户的评论、帖子历史。六袋长老及以上用户会永远保留账号。\"\n      },\n      {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"1TB\",\n        \"ratio\": \"3.55\",\n        \"seedingPoints\": \"600000\",\n        \"privilege\": \"得到1个邀请名额，可以更新过期的外部信息；可以查看Extreme User论坛。\"\n      },\n      {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"1.5TB\",\n        \"ratio\": \"4.05\",\n        \"seedingPoints\": \"800000\",\n        \"privilege\": \"得到3个邀请名额。\"\n      },\n      {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"4.55\",\n        \"seedingPoints\": \"1000000\",\n        \"privilege\": \"得到5个邀请名额。\"\n      }\n    ],\n    \"selectors\": {\n      \"userSeedingTorrents\": {\n          \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n          \"fields\": {\n              \"seeding\": {\n                  \"selector\": [\n                      \"b:first\"\n                  ],\n                  \"filters\": [\n                      \"query.text()\"\n                  ]\n              },\n              \"seedingSize\": {\n                  \"selector\": \"\",\n                  \"filters\": [\n                    \"query.text().match(/总大小：(.*?)上一页/g)\",\n                    \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                    \"(query != 0) ? query.sizeToNumber() : 0\"\n                ]\n              }\n          }\n      }\n    },\n    \"patterns\": {\n      \"torrentLinks\": [\"*://*/*\"]\n    },\n    \"parser\": {\n      \"downloadURL\": \"解析脚本内容\"\n    },\n    \"torrentTagSelectors\": [\n      {\n        \"name\": \"Free\",\n        \"selector\": \"img.pro_free\",\n        \"color\": \"blue\"\n      }\n    ],\n    \"categories\": [\n      {\n        \"entry\": \"*\",\n        \"result\": \"cat$id$=1\",\n        \"category\": [\n          {\n            \"id\": 401,\n            \"name\": \"电影\"\n          },\n          {\n            \"id\": 404,\n            \"name\": \"记录片\"\n          },\n          {\n            \"id\": 410,\n            \"name\": \"港台剧\"\n          },\n          {\n            \"id\": 406,\n            \"name\": \"演唱会\"\n          }\n        ]\n      }\n    ]\n  }\n"
  },
  {
    "path": "resource/sites/gay-torrents.org/config.json",
    "content": "{\n  \"name\": \"GTorg\",\n  \"url\": \"https://gay-torrents.org/\",\n  \"cdn\": [\"https://gay-torrents.se/\", \"https://gay-area.org/\"],\n  \"icon\": \"https://gay-torrents.org/favicon.ico\",\n  \"tags\": [\"影视\", \"成人\", \"综合\"],\n  \"schema\": \"GTorg\",\n  \"host\": \"gay-torrents.org\",\n  \"collaborator\": \"davidxuang\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"PowerUser\",\n      \"uploaded\": \"50 GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Access to online users and tracker info\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"ExtraUser\",\n      \"uploaded\": \"250 GB\",\n      \"ratio\": \"2.00\",\n      \"privilege\": \"Access to online users, tracker info, requests, top 10 and users\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents_beta.php\",\n    \"queryString\": \"search=$key$&active=0&options=1\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \".torrentsContainer\"\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"all\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"misc\",\n      \"appendQueryString\": \"&category[]=17&category[]=31&category[]=49&category[]=41\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Non-Porn\",\n      \"appendQueryString\": \"&category[]=41\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"Porn\",\n      \"appendQueryString\": \"&category[]=15&category[]=16&category[]=42&category[]=18&category[]=19&category[]=20&category[]=22&category[]=21&category[]=23&category[]=25&category[]=51&category[]=71&category[]=27&category[]=28&category[]=30&category[]=52&category[]=68&category[]=32&category[]=50&category[]=33&category[]=34&category[]=40&category[]=35&category[]=36&category[]=37&category[]=38&category[]=39\",\n      \"enabled\": false\n    }\n  ],\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \".specsIn.Freee[title~='0%']\",\n      \"color\": \"blue\"\n    },\n    {\n      \"name\": \"50%\",\n      \"selector\": \".specsIn.Freee[title~='50%']\",\n      \"color\": \"orange\"\n    },\n    {\n      \"name\": \"75%\",\n      \"selector\": \".specsIn.Freee[title~='75%']\",\n      \"color\": \"lime darken-3\"\n    },\n    {\n      \"name\": \"Bumped\",\n      \"selector\": \".specsIn.Bump\",\n      \"color\": \"grey\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"#my_menu > ul > li:first-child > a\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('uid') : ''\"]\n        },\n        \"name\": {\n          \"selector\": \"#user_info li:has(i[title='Username'])\",\n          \"filters\": [\"query ? query.text().match(/\\\\s*(.+?)\\\\s*\\\\(/)[1] : ''\"]\n        },\n        \"bonus\": {\n          \"selector\": \"#user_info li:has(i[title~='Bonus'])\",\n          \"filters\": [\"query ? query.text().match(/[\\\\d.]+/)[0] : 0\"]\n        },\n        \"seeding\": {\n          \"selector\": \"#user_info span[title='Seed']\"\n        },\n        \"messageCount\": {\n          \"selector\": \"#my_menu > ul > li:contains('Mailbox') > span\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/usercp.php?uid=$user.id$\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \".simple_table.full_widths.left_column td:contains('User') + td\"\n        },\n        \"levelName\": {\n          \"selector\": \".simple_table.full_widths.left_column td:contains('Rank') + td\"\n        },\n        \"uploaded\": {\n          \"selector\": \".simple_table.full_widths.left_column td:contains('Uploaded') + td\",\n          \"filters\": [\"query.text().sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": \".simple_table.full_widths.left_column td:contains('Downloaded') + td\",\n          \"filters\": [\"query.text().sizeToNumber()\"]\n        },\n        \"joinTime\": {\n          \"selector\": \".simple_table.full_widths.left_column td:contains('Joined') + td\",\n          \"filters\": [\"dateTime(query.text().replace(/(\\\\d{2})\\\\/(\\\\d{2})\\\\/(\\\\d{4})/,'$3-$2-$1')).valueOf()\"]\n        }\n      }\n    }\n  },\n  \"categories\": [\n    {\n      \"entry\": \"*\",\n      \"result\": \"&category[]=$id$\",\n      \"category\": [\n        { \"id\": \"15\", \"name\": \"Amateur\" },\n        { \"id\": \"16\", \"name\": \"Anal\" },\n        { \"id\": \"42\", \"name\": \"Animation\" },\n        { \"id\": \"18\", \"name\": \"Asian\" },\n        { \"id\": \"19\", \"name\": \"Bareback\" },\n        { \"id\": \"20\", \"name\": \"Bears\" },\n        { \"id\": \"22\", \"name\": \"Bisexual\" },\n        { \"id\": \"21\", \"name\": \"Black\" },\n        { \"id\": \"23\", \"name\": \"Chubs\" },\n        { \"id\": \"25\", \"name\": \"Cross Generation\" },\n        { \"id\": \"51\", \"name\": \"Doctor/Medical\" },\n        { \"id\": \"71\", \"name\": \"Fan Sites\" },\n        { \"id\": \"27\", \"name\": \"Fetish\" },\n        { \"id\": \"28\", \"name\": \"Group Sex\" },\n        { \"id\": \"30\", \"name\": \"Hunks\" },\n        { \"id\": \"52\", \"name\": \"Interracial\" },\n        { \"id\": \"68\", \"name\": \"Homo Erotic\" },\n        { \"id\": \"32\", \"name\": \"Latino\" },\n        { \"id\": \"50\", \"name\": \"Middle Eastern\" },\n        { \"id\": \"33\", \"name\": \"Military\" },\n        { \"id\": \"34\", \"name\": \"Oral-Sex\" },\n        { \"id\": \"40\", \"name\": \"Other\" },\n        { \"id\": \"35\", \"name\": \"Solo\" },\n        { \"id\": \"36\", \"name\": \"Trans\" },\n        { \"id\": \"37\", \"name\": \"Twinks\" },\n        { \"id\": \"38\", \"name\": \"Vintage\" },\n        { \"id\": \"39\", \"name\": \"Wrestling\" },\n        { \"id\": \"17\", \"name\": \"Applications\" },\n        { \"id\": \"31\", \"name\": \"Images\" },\n        { \"id\": \"49\", \"name\": \"Books\" },\n        { \"id\": \"41\", \"name\": \"Non-Porn\" }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/gay-torrents.org/getSearchResult.js",
    "content": "(function (options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/Login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n    }\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector = options.resultSelector;\n      let table = options.page.find(selector);\n      let rows = table.find('> div.torrent');\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n\n      try {\n        for (let index = 0; index < rows.length; index++) {\n          const row = rows.eq(index);\n\n          let title_elem = row.find('.torrent_link').first();\n          if (title_elem.length == 0) {\n            continue;\n          }\n\n          let comments = row.find('.downloadInfo > a:first-of-type').first().text().split(' ')[0]\n\n          let data = {\n            title: title_elem.text(),\n            link: `${site.url}${title_elem.attr('href')}`,\n            url: `${site.url}${row.find('.downloadLink').first().attr('href')}&secure=1`,\n            size: row.find('.size').first().text(),\n            time: row.find('.date').first().text().replace(/on\\s*(\\d{2}:\\d{2}(?::\\d{2})?)\\s*(\\d{1,2})-(\\w{3,4})-(\\d{4,})/, '$4 $3 $2 $1'),\n            author: row.find('.uploader > span').first().text(),\n            seeders: row.find('.downloadPeers > div:first-child > a').first().text(),\n            leechers: row.find('.downloadPeers > div:last-child > a').first().text(),\n            completed: row.find('.downloadTimes').first().text().split(' ')[0],\n            comments: comments ? comments : 0,\n            site: site,\n            tags: this.getTags(row, options.torrentTagSelectors),\n            entryName: options.entry.name,\n            category: options.searcher.getCategoryById(\n              site,\n              options.url,\n              row.find('.categoryNew > a').first().attr('href').split('=')[1]\n            )\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        selectors.forEach((item) => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color,\n              });\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, Searcher);\n"
  },
  {
    "path": "resource/sites/gazellegames.net/config.json",
    "content": "{\n  \"name\": \"GGN\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Game\",\n  \"url\": \"https://gazellegames.net/\",\n  \"icon\": \"https://gazellegames.net/favicon.ico\",\n  \"tags\": [\"Game\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"gazellegames.net\",\n  \"collaborator\": \"ted423\",\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"page\": \"/torrents.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"searchstr=$key$\",\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"#color_seeding, #color_snatched\",\n          \"#color_leeching, #color_downloaded\",\n          \"\"\n        ],\n        \"switchFilters\": [[\"100\"], [\"0\"], [\"null\"]]\n      },\n      \"status\": {\n        \"selector\": [\n          \"#color_seeding\",\n          \"#color_snatched\",\n          \"#color_leeching\",\n          \"#color_downloaded\"\n        ],\n        \"switchFilters\": [[\"2\"], [\"255\"], [\"1\"], [\"3\"]]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"all\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"artistname=My+Platforms\",\n      \"name\": \"My Platforms\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"artistname=Applications\",\n      \"name\": \"Applications\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"artistname=Games\",\n      \"name\": \"Games\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"artistname=E-Books\",\n      \"name\": \"E-Books\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"artistname=OST\",\n      \"name\": \"OST\",\n      \"enabled\": false\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [\".newnoti\"],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/)\",\n            \"(query && query.length>=2)?parseInt(query[1]):0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"#upload .stat.tooltip\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"#download .stat.tooltip\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"levelName\": {\n          \"selector\": \"div:contains('Personal') + ul.stats > li:contains('Class:')\",\n          \"filters\": [\n            \"query.text().match(/Class:.+?(.+)/)\",\n            \"(query && query.length>=2)?query[1]:''\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": [\"#gold .stat.tooltip\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\n            \".box_personal_history ul.stats li:nth-child(2) span.time\"\n          ],\n          \"filters\": [\n            \"query.attr('title')||query.text()\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        },\n        \"seeding\": {\n          \"selector\": [\"#seeding\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"#seeding_size\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        }\n      }\n    }\n  },\n  \"torrentTagSelectors\": [{\n    \"name\": \"Neutral\",\n    \"selector\": \"strong.neutralleech_label\",\n    \"color\": \"purple\"\n  },{\n    \"name\": \"Free\",\n    \"selector\": \"strong.freeleech_label\",\n    \"color\": \"blue\"\n  },{\n    \"name\": \"50%\",\n    \"selector\": \"strong.partial_freeleech_label\",\n    \"color\": \"orange\"\n  }],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/gazellegames.net/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table.torrent_table:first > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      let moviename = \"\";\n      let movienames = {};\n      let categories = {};\n      let groupid;\n        let torrentinforows = options.page.find(\"tr.torrent, tr.group\");\n        for(let index = 0; index < torrentinforows.length; index++){\n          let torrentinforow = torrentinforows.eq(index);\n          let torrentinfo = torrentinforow.find(\"td.center.cats_col\").first();\n          let torrenttitle = torrentinforow.find(\"a[title='View Torrent'][href ^='torrents.php?id=']\").first();\n\n          groupid = torrenttitle.attr(\"href\").getQueryString(\"id\");\n          movienames[groupid] = torrenttitle.parent().text().replace(/[\\r\\n]/g,\"\").replace(/Bookmark.*/g,\"\").trim();\n          if(!movienames[groupid] || new RegExp(\"\\t[DL\t| RP]\\t\").test(movienames[groupid])){\n            movienames[groupid] = torrenttitle.parent().text().replace(/[\\r\\n]/g,\"\").replace(/\\t+/g,\"\\t\").replace(\"\\t[DL\t| RP]\\t\",\"\").split('\\t')[0];\n          }\n          categories[groupid] = torrentinfo.find(\"div\").first().attr(\"class\").split(\" \")[0].replace(\"cats_\",\"\");\n        }\n      \n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n          time: 1,\n          size: 3,\n          seeders: 5,\n          leechers: 6,\n          completed: 4,\n          comments: -1,\n          author: 2\n        };\n \n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a[href*='torrents.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n          let subTitle = row.find(\"div.torrent_info\").first();\n\n          // 获取下载链接\n          let url = row.find(\"a[href*='torrents.php?action=download']\").first();                        \n          if (url.length == 0) {\n            continue;\n          }\n\n          url = url.attr(\"href\");\n          let torrentid = url.getQueryString(\"id\");\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let time = \"\";\n            groupid = title.attr(\"href\").getQueryString(\"id\");\n            time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells\n                  .eq(fieldIndex.time)\n                  .find(\"span[title],time[title]\")\n                  .attr(\"title\") ||\n                cells.eq(fieldIndex.time).text() ||\n                \"\";\n          \n\n          if (time) {\n            time += \":00\";\n          }\n          let data = {\n            title: title.text(),\n            subTitle: subTitle.text(),\n            link,\n            url: url,\n            size: cells.eq(fieldIndex.size).html() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name,\n            tags: this.getTags(row, options.torrentTagSelectors),\n            category: categories[groupid],\n            status: Searcher.getFieldValue(site, cells, \"status\"),\n            progress: Searcher.getFieldValue(site, cells, \"progress\")\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        selectors.forEach(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n            }\n          }\n        });\n      }\n      return tags;\n    }\n\n\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/gfxpeers.net/config.json",
    "content": "{\n  \"name\": \"GFXPeers\",\n  \"timezoneOffset\": \"+0000\",\n  \"icon\": \"https://gfxpeers.net/favicon.ico\",\n  \"url\": \"https://gfxpeers.net/\",\n  \"schema\": \"Gazelle\",\n  \"tags\": [\"设计\", \"素材\"],\n  \"host\": \"gfxpeers.net\",\n  \"collaborator\": \"bimzcy\",\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/greatposterwall.com/config.json",
    "content": "{\n  \"name\": \"GPW\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"movie\",\n  \"url\": \"https://greatposterwall.com/\",\n  \"icon\": \"https://greatposterwall.com/favicon.ico\",\n  \"tags\": [\n    \"电影\"\n  ],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"greatposterwall.com\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Member\",\n    \"interval\": \"1\",\n    \"trueDownloaded\": \"80GB\",\n    \"ratio\": \"0.8\",\n    \"privilege\": \"发起求种；查看部分排行榜；完全访问「茶话会」版块\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Power User\",\n    \"interval\": \"2\",\n    \"uploads\": \"1\",\n    \"trueDownloaded\": \"200GB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"免疫账号不活跃；发送邀请；赠送1枚邀请；访问论坛的「求邀区」「发邀区」「Power User」\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Elite\",\n    \"interval\": \"4\",\n    \"uploads\": \"25\",\n    \"trueDownloaded\": \"500GB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"赠送1枚邀请；访问论坛的「Elite」；检查自己发布的种子；编辑所有种子\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Torrent Master\",\n    \"interval\": \"8\",\n    \"uploads\": \"100\",\n    \"trueDownloaded\": \"1TB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"赠送2枚邀请；每月获赠1枚临时邀请；访问论坛的「Torrent Master」\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Power Torrent Master\",\n    \"interval\": \"12\",\n    \"uploads\": \"250\",\n    \"trueDownloaded\": \"2TB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"赠送2枚邀请；每月获赠2枚临时邀请；检查所有种子\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Elite Torrent Master\",\n    \"interval\": \"16\",\n    \"uploads\": \"500\",\n    \"trueDownloaded\": \"5TB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"赠送3枚邀请；每月获赠3枚临时邀请；访问论坛的「Elite Torrent Master」\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Guru\",\n    \"interval\": \"20\",\n    \"uploads\": \"1000\",\n    \"trueDownloaded\": \"10TB\",\n    \"ratio\": \"1.2\",\n    \"privilege\": \"无限邀请；访问论坛的「Guru」；查看种子检查日志\"\n  }],\n  \"collaborator\": [\n    \"MewX\"\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/ajax.php\",\n    \"resultType\": \"json\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"asyncParse\": true,\n    \"queryString\": \"action=browse&searchstr=$key$\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/ajax.php?action=index\",\n      \"dataType\": \"json\",\n      \"merge\": true,\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"response.userstats.seedingSize\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"response.userstats.bonusPoints\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"response.userstats.joinedDate\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"response.userstats.seedingCount\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploads\": {\n          \"selector\": [\"#upload-count-value\"],\n          \"filters\" : [\"query.text().replace(/,/g,'').match(/[\\\\d.]+/)\", \"query ? parseFloat(query[0]): 0\"]\n        },\n        \"trueDownloaded\": {\n          \"selector\": [\"#downloaded-value\"],\n          \"filters\" : [\"query.text().replace(/,/g,'').match(/[\\\\d.]+ ?[ZEPTGMK]?i?B/g)\", \"query ? query[1].sizeToNumber(): 0\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/bonus.php?action=bprates\",\n      \"fields\": {\n        \"seedingList\": {\n          \"selector\": [\"a[href*='torrentid=']\"],\n          \"filters\": [\"jQuery.map(query, item=>$(item).attr('href').match(/torrentid=(\\\\d+)/)[1])\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/greatposterwall.com/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n      this.authkey = \"\";\n      this.passkey = \"\";\n    }\n\n    start() {\n      this.getAuthKey()\n        .then(() => {\n          options.resolve(this.getResult());\n        })\n        .catch(() => {\n          options.reject({\n            success: false,\n            msg: options.searcher.getErrorMessage(\n              options.site,\n              ESearchResultParseStatus.parseError,\n              options.errorMsg\n            ),\n            data: {\n              site: options.site,\n              isLogged: options.isLogged\n            }\n          });\n        });\n    }\n\n    /**\n     * 获取搜索结果\n     */\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let groups = options.page.response.results;\n      if (groups.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return [];\n      }\n      let results = [];\n      let authkey = this.authkey;\n      let passkey = this.passkey;\n      console.log(\"groups.length\", groups.length);\n      try {\n        groups.forEach(group => {\n          if (group.hasOwnProperty(\"torrents\")) {\n            let torrents = group.torrents;\n            torrents.forEach(torrent => {\n              let data = {\n                id: torrent.torrentId,\n                title:\n                  group.groupName +\n                  \" - \" +\n                  group.groupSubName +\n                  \" [\" +\n                  group.groupYear +\n                  \"] [\" +\n                  group.releaseType +\n                  \"]\",\n                subTitle:\n                  torrent.codec +\n                  \" / \" +\n                  torrent.source +\n                  \" / \" +\n                  torrent.resolution +\n                  \" / \" +\n                  torrent.container +\n                  \" / \" +\n                  torrent.processing +\n                  (torrent.remasterTitle ? ` / ${torrent.remasterTitle}` : \"\") +\n                  (torrent.scene ? \" / Scene\" : \"\") +\n                  (torrent.isFreeleech ||\n                  torrent.isNeutralLeech ||\n                  torrent.isPersonalFreeleech\n                    ? \" / Freeleech\"\n                    : \"\") +\n                  (torrent.releaseGroup ? ` / ${torrent.releaseGroup}` : \"\"),\n                link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${torrent.torrentId}`,\n                url: `${site.url}torrents.php?action=download&id=${torrent.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n                size: parseFloat(torrent.size),\n                time: torrent.time,\n                seeders: torrent.seeders,\n                leechers: torrent.leechers,\n                completed: torrent.snatches,\n                site: site,\n                entryName: options.entry.name,\n                category: group.releaseType,\n                imdbId: group.imdbId,\n              };\n              results.push(data);\n            });\n          } else {\n            let data = {\n              title: group.groupName,\n              link: `${site.url}torrents.php?id=${group.groupId}&torrentid=${group.torrentId}`,\n              url: `${site.url}torrents.php?action=download&id=${group.torrentId}&authkey=${authkey}&torrent_pass=${passkey}`,\n              size: parseFloat(group.size),\n              time: group.groupTime,\n              author: \"\",\n              seeders: group.seeders,\n              leechers: group.leechers,\n              completed: group.snatches,\n              comments: 0,\n              site: site,\n              tags: group.tags,\n              entryName: options.entry.name,\n              category: group.category,\n              imdbId: group.imdbId,\n            };\n            results.push(data);\n          }\n        });\n        console.log(\"results.length\", results.length);\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n\n    /**\n     * 获取 AuthKey ，用于组合完整的下载链接\n     */\n    getAuthKey() {\n      const url = (options.site.activeURL + \"/ajax.php?action=index\")\n        .replace(\"://\", \"****\")\n        .replace(/\\/\\//g, \"/\")\n        .replace(\"****\", \"://\");\n\n      return new Promise((resolve, reject) => {\n        $.get(url)\n          .done(result => {\n            if (result && result.status === \"success\" && result.response) {\n              this.authkey = result.response.authkey;\n              this.passkey = result.response.passkey;\n              resolve();\n            } else {\n              reject();\n            }\n          })\n          .fail(() => {\n            reject();\n          });\n      });\n    }\n  }\n\n  let parser = new Parser(options);\n  parser.start();\n})(options);\n"
  },
  {
    "path": "resource/sites/hawke.uno/config.json",
    "content": "{\n    \"name\": \"HUNO\",\n    \"timezoneOffset\": \"+0000\",\n    \"schema\": \"UNIT3D\",\n    \"url\": \"https://hawke.uno/\",\n    \"icon\": \"https://hawke.uno/favicon.ico\",\n    \"tags\": [\"影视\"],\n    \"host\": \"hawke.unoz\",\n    \"collaborator\": [\"fzlins\"],\n    \"searchEntryConfig\": {\n        \"merge\": true,\n        \"resultSelector\": \"#torrent-list-table\"\n    },\n    \"torrentTagSelectors\": [{\n      \"name\": \"Free\",\n      \"selector\": \"i.fal.fa-gift.text-orange\"\n    }],\n    \"selectors\": {\n      \"userExtendInfo\": {\n        \"merge\": true,\n        \"page\": \"/users/$user.name$\",\n        \"fields\": {\n          \"levelName\": {\n            \"selector\": \"span[data-original-title='Class'] span\"\n          },\n          \"seeding\": {\n            \"selector\": \".user-torrent-stats > div:nth(2) > div\"\n          },\n          \"uploaded\": {\n            \"selector\": [\".user-info td span[data-original-title='Recorded Upload']\"],\n            \"filters\": [\"query.text().trim().replace(/,|\\\\s|\\\\n/g,'').sizeToNumber()\"]\n          },\n          \"downloaded\": {\n            \"selector\": [\".user-info td span[data-original-title='Recorded Download']\"],\n            \"filters\": [\"query.text().trim().replace(/,|\\\\s|\\\\n/g,'').sizeToNumber()\"]\n          }\n        }\n      }\n    }\n}"
  },
  {
    "path": "resource/sites/hd-space.org/config.json",
    "content": "{\n  \"name\": \"HD-Space\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"HD-Space\",\n  \"url\": \"https://hd-space.org/\",\n  \"icon\": \"https://hd-space.org/favicon.ico\",\n  \"tags\": [\"影视\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/index.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }],\n  \"host\": \"hd-space.org\",\n  \"searchEntryConfig\": {\n    \"page\": \"/index.php?page=torrents\",\n    \"queryString\": \"search=$key$&active=0\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"#bodyarea > table > tbody > tr > td:nth-child(2) > table > tbody > tr:nth-child(3) > td > table > tbody > tr:not(:first-child)\",\n    \"dataRowSelector\": \" > tbody > tr:not(:first-child)\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"replaceKey\": [\n        \"tt\", \"IMDB\"\n      ]\n    }],\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 1,\n\t    \"link\": 1,\n\t    \"url\": 3,\n        \"comments\": 2,\n        \"time\": 4,\n        \"size\": 5,\n        \"author\": 6,\n        \"seeders\": 7,\n        \"leechers\": 8,\n        \"completed”=\": 9\n\t},\n\t\"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.get(0).firstChild\", \"query.nodeValue||query.innerText||0\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.children().attr('href')\", \"'https://hd-space.org/'+query\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.children().attr('href')\", \"'https://hd-space.org/'+query\"]\n\t  }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[src='gold/gold.png'], img[src='images/sf.png']\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img[src='gold/silver.png']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='index.php?page=usercp']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('uid'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"td[align='center'][style='text-align:center;']:contains('Welcome back')>span\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href*='do=pm']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"td.green:contains('UP')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.red:contains('DL')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.yellow:contains('Ratio')\",\n          \"filters\": [\"parseFloat(query.text().replace(' Ratio: ',''))\"]\n        }, \n        \"levelName\": {\n          \"selector\": [\"td[align='center'][style='text-align:center;']:contains('Rank')\"],\n          \"filters\":  [\"query.text().replace('Rank: ','')\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.green:contains('Bonus')\"],\n          \"filters\": [\"query.text().replace('Bonus: ','')\"]\n        },\n        \"seeding\": {\n\t      \"selector\": [\"#menu + table > tbody > tr > td:nth-child(4) b > font:nth-child(2)\"]\n        },\n        \"seedingSize\": {\n\t      \"value\": -1\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/index.php?page=usercp&uid=$user.id$\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"td.header:contains('Joined on') + td\"],\n          \"filters\": [\"query[0].innerHTML.replace('&nbsp;', '').replace('&nbsp;', '')\", \"dateTime(query).valueOf()\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/index.php?page=torrent-details\",\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td.header:contains('Size') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#ty\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/hd-space.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n   if(/\\?page\\=torrent-details/.test(window.location.search)){\n    console.log(\"torrent-details\");\n    class App extends window.NexusPHPCommon {\n      init() {\n        this.initButtons();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.showTorrentSize();\n        this.initDetailButtons();\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURL() {\n        let query = $(\"a[href*='download.php']:first\");\n        let url = \"\";\n        if (query.length > 0) {\n          url = query.attr(\"href\");\n          if (url.substr(0, 4) != \"http\") {\n            url = PTService.site.url + url;\n          }\n        }\n\n        return url;\n      }\n\n      showTorrentSize() {\n        let size = PTService.filters.formatSize(PTService.getFieldValue(\"size\"));\n        PTService.addButton({\n         title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n      /**\n       * 获取当前种子标题\n       */\n      getTitle() {\n        return $(\"a[href*='download.php']:first\").text().trim();\n      }\n    };\n    (new App()).init();\n  }else if(/\\?page\\=torrents|seedwanted/.test(window.location.search)){\n    class App extends window.NexusPHPCommon {\n      init() {\n        // super();\n        this.initButtons();\n        this.initFreeSpaceButton();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.initListButtons();\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURLs() {\n        let links = $(\"#bodyarea > table > tbody > tr > td:nth-child(2) > table > tbody > tr:nth-child(3) > td > table, #mcol > table > tbody > tr:nth-child(2) > td > table\")\n          .find(\"a[href*='download.php']\")\n          .toArray();\n        let siteURL = PTService.site.url;\n        if (siteURL.substr(-1) != \"/\") {\n          siteURL += \"/\";\n        }\n\n        if (links.length == 0) {\n          return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n        }\n\n        let urls = $.map(links, item => {\n          let link = $(item).attr(\"href\");\n          if (link && link.substr(0, 4) != \"http\") {\n            link = siteURL + link;\n          }\n          return link;\n        });\n\n        return urls;\n      }\n\n      /**\n       * 确认大小是否超限\n       */\n      confirmWhenExceedSize() {\n        return this.confirmSize(\n          $(\"table.mainblockcontenttt:first\").find(\n            \"td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n          )\n        );\n      }\n    }\n    new App().init();\n  }\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/hd-torrents.org/config.json",
    "content": "{\n  \"name\": \"HD-Torrents\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"HD-Torrents.org\",\n  \"url\": \"https://hd-torrents.org/\",\n  \"icon\": \"https://hd-torrents.org/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/libs/album/album.js\", \"torrents.js\"],\n    \"styles\": [\"/libs/album/style.css\"]\n  }],\n  \"host\": \"hd-torrents.org\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"HD Maniac\",\n      \"uploaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Gain access to \\\"Top 10\\\"\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"HD Monster\",\n      \"uploaded\": \"250GB\",\n      \"ratio\": \"2.00\",\n      \"privilege\": \"Gain access to \\\"Tracker Info\\\", \\\"Invites\\\" section of the forums\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"HD Daemon\",\n      \"uploaded\": \"1TB\",\n      \"ratio\": \"4.00\",\n      \"privilege\": \"Gain access to \\\"Users\\\"\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"beforeSearch\": {\n      \"page\": \"/torrents.php\",\n      \"fields\": {\n        \"csrfToken\": {\n          \"selector\": [\"input[name='csrfToken']:first\"],\n          \"filters\": [\"query.val()\"]\n        }\n      },\n      \"dataCacheTime\": 60\n    },\n    \"queryString\": \"csrfToken=$beforeSearchData.csrfToken$&search=$key$&active=0\",\n    \"area\": [{\n      \"name\": \"标题\",\n      \"appendQueryString\": \"&options=0\"\n    }, {\n      \"name\": \"标题和简介\",\n      \"appendQueryString\": \"&options=1\"\n    }, {\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"appendQueryString\": \"&options=2\"\n    }],\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table.mainblockcontenttt:last > tbody > tr\",\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td.mainblockcontentpeersall, td.mainblockcontentpeersseed, td.mainblockcontentpeersleech, td.mainblockcontenthistact, td.mainblockcontentpeersuploaded\"],\n        \"filters\": [\"query.is('.mainblockcontentpeersall')? null : query.is('.mainblockcontentpeersseed, .mainblockcontentpeersuploaded, .mainblockcontenthistact')?100: 0\"]\n      },\n      \"status\": {\n        \"selector\": [\"td.mainblockcontentpeersall, td.mainblockcontentpeersseed, td.mainblockcontentpeersleech, td.mainblockcontenthistact, td.mainblockcontentpeersuploaded\"],\n        \"filters\": [\"query.is('.mainblockcontentpeersall')? null : query.is('.mainblockcontentpeersseed')? 2: query.is('.mainblockcontenthistact, .mainblockcontentpeersuploaded')? 255: 1\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[src*='free.png']\"\n  }, {\n    \"name\": \"75%\",\n    \"selector\": \"img[src*='25.png']\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img[src*='50.png']\"\n  }, {\n    \"name\": \"25%\",\n    \"selector\": \"img[src*='75.png']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='usercp.php?uid=']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('uid'):''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php?']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"csrfToken\": {\n          \"selector\": [\"input[name='csrfToken']\"],\n          \"filters\": [\"query.val()\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\".new-pm.warning\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/usercp.php?uid=$user.id$\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": [\"tr#CurrentDetailsHideShowTR td.header:contains('User') + td\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"td.header:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.header:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.header:contains('Ratio') + td\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"td.header:contains('Rank') + td\"\n        },\n        \"bonus\": {\n          \"selector\": [\"td.header:contains('Seed Bonus Points') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        },\n        \"joinTime\": {\n          \"selector\": \"td.header:contains('Joined on') + td\",\n          \"filters\": [\"query.text().split(' ')\", \"query[0].split('/')[2]+'-'+query[0].split('/')[1]+'-'+query[0].split('/')[0]+' '+query[1]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"td.nav[title='Active-Torrents'] > a[href*='#actives']\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/usercp.php?uid=$user.id$\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"tr#SeedingtorrentsHideShowTR table.lista tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(1)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/seedbonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"#BonusPointsHideShowTR .blockcontent center:eq(0) h1 font[color='blue']:eq(2)\"],\n          \"filters\": [\"parseFloat(query.text())\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/details.php\",\n      \"merge\": true,\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"td.detailsleft:contains('Size:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#ty\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hd-torrents.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n\t  this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='download.php']:first\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        if (url.substr(0, 4) != \"http\") {\n          url = PTService.site.url + url;\n        }\n      }\n\n      return url;\n    }\n\n    showTorrentSize() {\n      let size = PTService.filters.formatSize(PTService.getFieldValue(\"size\"));\n      PTService.addButton({\n       title: \"当前种子大小\",\n        icon: \"attachment\",\n        label: size\n      });\n    }\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\"a[href*='download.php']:first\").text().trim();\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/hd-torrents.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/login\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (/No torrents here/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(options.resultSelector);\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        title: 2,\n        // 时间\n        time: 6,\n        // 大小\n        size: 7,\n        // 上传人数\n        seeders: 9,\n        // 下载人数\n        leechers: 10,\n        // 完成人数\n        completed: 11,\n        // 评论人数\n        comments: 3,\n        // 发布人\n        author: 8,\n        category: 0\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 遍历数据行\n      for (let index = 2; index < rows.length; index += 2) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n\n        let title = cells.eq(fieldIndex.title).find(\"a[href*='details.php']\");\n        if (title.length == 0) {\n          continue;\n        }\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}${link}`;\n        }\n\n        let url = row.find(\"a[href*='download.php']\").attr(\"href\");\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${site.url}${url}`;\n        }\n\n        let dateString = cells\n          .eq(fieldIndex.time)\n          .text()\n          .replace(\"  \", \" \");\n        let dayStringArray = dateString.split(\" \")[1].split(\"/\");\n        let time = dateString.split(\" \")[0];\n\n        let data = {\n          title: title.text(),\n          subTitle: \"\",\n          link,\n          url: url,\n          size: cells.eq(fieldIndex.size).html() || 0,\n          time: `${dayStringArray[2]}-${dayStringArray[1]}-${dayStringArray[0]} ${time}`,\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders: cells.eq(fieldIndex.seeders).text() || 0,\n          leechers: cells.eq(fieldIndex.leechers).text() || 0,\n          completed: cells.eq(fieldIndex.completed).text() || 0,\n          comments: cells.eq(fieldIndex.comments).text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: Searcher.getRowTags(site, row),\n          progress: Searcher.getFieldValue(site, row, \"progress\"),\n          status: Searcher.getFieldValue(site, row, \"status\")\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link && result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/hd-torrents.org/getUserSeedingTorrents.js",
    "content": "(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 0\n      };\n      this.result = {\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='activepage']:contains('1'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"activepage\"));\n      } else {\n        this.pageInfo.count = 1;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"&activepage=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/hd-torrents.org/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n\n      // 添加封面模式\n      PTService.addButton({\n        title: PTService.i18n.t(\"buttons.coverTip\"), //\"以封面的方式进行查看\",\n        icon: \"photo\",\n        label: PTService.i18n.t(\"buttons.cover\"), //\"封面模式\",\n        click: (success, error) => {\n          // 获取目标表格\n          let items = $(\n            \"table.mainblockcontenttt a[href*='details.php?id='][onmouseover]\"\n          );\n          let images = [];\n          items.each((index, item) => {\n            let text = $(item).attr(\"onmouseover\");\n            let query = text.match(/(.+)(img src=\\\\\\')([^\\']+)\\\\\\'/);\n            if (query && query.length > 3) {\n              let url = location.origin + \"/\" + query[3];\n              let href = $(item).attr(\"href\");\n              let title = $(item).text();\n              images.push({\n                url: url,\n                key: href,\n                title: title,\n                link: href\n              });\n            }\n          });\n          success();\n          if (images.length > 0) {\n            // 创建预览\n            new album({\n              images: images,\n              onClose: () => {\n                PTService.buttonBar.show();\n              }\n            });\n            PTService.buttonBar.hide();\n          }\n        }\n      });\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"table.mainblockcontenttt:first\")\n        .find(\"a[href*='download.php']\")\n        .toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table.mainblockcontenttt:first\").find(\n          \"td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/hdatmos.club/config.json",
    "content": "{\n  \"name\": \"HDATMOS\",\n  \"timezoneOffset\": \"+0800\",\n  \"icon\": \"https://hdatmos.club/favicon.ico\",\n  \"schema\": \"NexusPHP\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"url\": \"https://hdatmos.club\",\n  \"host\": \"hdatmos.club\",\n  \"collaborator\": [\"luoyefe\", \"zhuweitung\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"一个邀请名额；直接发布种子；查看NFO文档；查看用户列表；请求续种； 发送邀请；查看排行榜；查看其它用户的种子历史；删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"封存账号后不会被删除\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"两个邀请名额；在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.45\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"三个邀请名额；查看其它用户的评论、帖子历史；永远保留账号\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"更新过期的外部信息；查看Extreme User论坛\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"五个邀请名额\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"十个邀请名额\"\n  }],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"prerequisites\": \"!user.seeding\",\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"body\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/[\\\\d]+/)\", \"query ? query[0] : 0\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"body\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"query ? query[0].sizeToNumber() : 0\"]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/hdbits.org/browse.js",
    "content": "(function ($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n      init() {\n        this.initButtons();\n        this.initFreeSpaceButton();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n\n      isNexusPHP() {//want use same code\n        return PTService.site.schema == \"HDB\";\n      }\n\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.initListButtons(false);\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURLs() {\n        let siteURL = PTService.site.url;\n        let links = $(\"a.js-download\").toArray();\n\n        let urls = $.map(links, (item) => {\n          let link = $(item).attr(\"href\");\n          link = link.replace(\"source=browse\", \"source=rss\");\n          link = link.replace(new RegExp(\"/download.php/.*\\.torrent\"),\"download.php\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${siteURL}${link}`;\n          }\n          return link;\n        });\n\n        if (links.length == 0) {\n          return \"获取下载链接失败，未能正确定位到链接\";\n        }\n\n        return urls;\n      }\n\n      /**\n       * 执行指定的操作\n       * @param {*} action 需要执行的执令\n       * @param {*} data 附加数据\n       * @return Promise\n       */\n      call(action, data) {\n        return new Promise((resolve, reject) => {\n          switch (action) {\n            // 从当前的DOM中获取下载链接地址\n            case PTService.action.downloadFromDroper:\n              this.downloadFromDroper(data, () => {\n                resolve()\n              });\n              break;\n          }\n        });\n      }\n\n      getDroperURL(url) {\n        let siteURL = PTService.site.url;\n        if (siteURL.substr(-1) != \"/\") {\n          siteURL += \"/\";\n        }\n        if (!url.getQueryString) {\n          PTService.showNotice({\n            msg:\n              \"系统依赖函数（getQueryString）未正确加载，请尝试刷新页面或重新启用插件。\"\n          });\n          return null;\n        }\n        if (url.indexOf(\"download.php\") == -1) {\n          let id = url.getQueryString(\"id\");\n          let firstlink = $(\"a.js-download:first\");\n          let passkey = firstlink.attr(\"href\").getQueryString(\"passkey\");\n          if (id) {\n            // 如果站点没有配置禁用https，则默认添加https链接\n            url =\n              siteURL +\n              \"download.php?id=\" +\n              id +\n              (PTService.site.passkey\n                ? \"&passkey=\" + PTService.site.passkey\n                : passkey ? \"&passkey=\"+ passkey : \"\") +\n              \"&source=rss\";\n          } else {\n            url = \"\";\n          }\n        }\n        return url;\n      }\n\n\n      /**\n       * 下载拖放的种子\n       * @param {*} data\n       * @param {*} callback\n       */\n      downloadFromDroper(data, callback) {\n        if (typeof data === \"string\") {\n          data = {\n            url: data,\n            title: \"\"\n          };\n        }\n        let result = this.getDroperURL(data.url);\n\n        if (!result) {\n          callback();\n          return;\n        }\n\n        this.sendTorrentToDefaultClient(result).then((result) => {\n          callback(result);\n        }).catch((result) => {\n          callback(result);\n        });\n      }\n\n      /**\n       * 确认大小是否超限\n       */\n      confirmWhenExceedSize() {\n        return this.confirmSize($(\"#torrent-list\").find(\"td.center:contains('MiB'),td.center:contains('GiB'),td.center:contains('TiB')\"));\n      }\n    }\n    (new App()).init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/hdbits.org/config.json",
    "content": "{\n  \"name\": \"HDB\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"HDB\",\n  \"url\": \"https://hdbits.org/\",\n  \"icon\": \"https://hdbits.org/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"HDB\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/t/(\\\\d+)/$\", \"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"host\": \"hdbits.org\",\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"1080i\",\n      \"interval\": \"4\",\n      \"downloaded\": \"30GB\",\n      \"ratio\": \"0.95\",\n      \"privilege\": \"You can view NFOs and request reseeds on poorly seeded torrents.\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"1080p\",\n      \"interval\": \"4\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"1.4\",\n      \"privilege\": \"As 1080i\"\n    },\n    {\n      \"level\": \"3\",\n      \"name\": \"UHD\",\n      \"interval\": \"4\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.5\",\n      \"privilege\": \"As 1080i\"\n    }\n  ],\n\"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search=$key$\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table#torrent-list:last > tbody > tr\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"imdb=$key$\"\n    }],\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"span.tag.tag_seeding, span.tag.tag_completed\", \"span.tag.tag_leeching\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"0\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"span.tag.tag_seeding\", \"span.tag.tag_completed\", \"span.tag.tag_leeching\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"255\"],\n          [\"1\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat=1\",\n      \"name\": \"Movie\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=2\",\n      \"name\": \"TV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=3\",\n      \"name\": \"Documentary\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=4\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=5\",\n      \"name\": \"Sport\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=6\",\n      \"name\": \"Audio Track\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=7\",\n      \"name\": \"XXX\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=8\",\n      \"name\": \"Misc/Demo\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"browse.php?\",\n    \"result\": \"cat=$id$\",\n    \"category\": [{\n        \"id\": 1,\n        \"name\": \"Movie\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"TV\"\n      },\n      {\n        \"id\": 3,\n        \"name\": \"Documentary\"\n      },\n      {\n        \"id\": 4,\n        \"name\": \"Music\"\n      },\n      {\n        \"id\": 5,\n        \"name\": \"Sport\"\n      },\n      {\n        \"id\": 6,\n        \"name\": \"Audio Track\"\n      },\n      {\n        \"id\": 7,\n        \"name\": \"XXX\"\n      },\n      {\n        \"id\": 8,\n        \"name\": \"Misc/Demo\"\n      }\n    ]\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"a[title^='100% FL:']\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"a[title^='50% Free Leech:']\"\n  }, {\n    \"name\": \"25%\",\n    \"selector\": \"a[title^='25% Free Leech:']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a.alert-box--pm, span.js-notifications-count\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.rowhead:contains('Share ratio') + td\",\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('Class') + td\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('Bonus') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('JOIN'):contains('date') + td\"],\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"td.heading:contains('Currently'):contains('seeding') + td\"],\n          \"filters\": [\"query.text().match(/([\\\\d.]+)/)\", \"(query && query.length>=1)?query[0]:''\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td.heading:contains('Seeding size') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/hdbits.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    isNexusPHP() {//want use same code\n      return PTService.site.schema == \"HDB\";\n    }\n    \n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let siteURL = PTService.site.url;\n      let query = $(\"a[href*='download.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        url = url.replace(\"source=browse\", \"source=rss\");\n        url = url.replace(new RegExp(\"/download.php/.*\\.torrent\"),\"download.php\");\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${siteURL}${url}`;\n        }\n      }\n      return url;\n    }\n\n    showTorrentSize() {\n      let query = $(\"th:contains('Size') + td\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text();\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    getTitle() {\n      let query = $(\"a[href*='download.php']\");\n      return query ? query.text().replace(\".torrent\", \"\"): \"\";\n    }\n  };\n  (new App()).init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/hdbits.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/\\/doLogin/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n\n      if (/Nothing found/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table#torrent-list:last > tbody > tr\"\n      );\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        progress: 2,\n        status: 2,\n        // 时间\n        time: 4,\n        // 大小\n        size: 5,\n        // 上传人数\n        seeders: 7,\n        // 下载人数\n        leechers: 8,\n        // 完成人数\n        completed: 6,\n        // 标题\n        name: 2,\n        // 发布人\n        author: 9,\n        //配置\n        category: 0\n      };\n      if(rows.eq(0).find(\"td[id*=codec]\").length == 0) {\n        fieldIndex = {progress: 1,status: 1,time: 3,size: 4,seeders: 6,leechers: 7,completed: 5,name: 1,author: 8,category: 0};\n      }\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n      // 遍历数据行\n      for (let index = 0; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n\n        let title = cells.eq(fieldIndex.name).find(\"b > a\");\n        if (title.length == 0) {\n          continue;\n        }\n\n        let titleStrings = title.html().split(\"::\");\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}${link}`;\n        }\n        let url = row.find(\"a.js-download\").attr(\"href\");\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${site.url}${url}`;\n        }\n        if (!url) {\n          continue;\n        }\n\n        let subTitle = \"\";\n        if (titleStrings.length > 0) {\n          subTitle = $(\"<span>\")\n            .html(titleStrings[1])\n            .text();\n        }\n\n        let time =\n          cells\n            .eq(fieldIndex.time)\n            .text()\n            .replace(/([a-zA-Z]+)/g, \"$1 \")\n            .replace(/^\\s+|\\s+$/g, \"\") + \".\";\n        let data = {\n          title: $(\"<span>\")\n            .html(titleStrings[0])\n            .text(),\n          subTitle: subTitle || \"\",\n          link,\n          url: url,\n          size: cells.eq(fieldIndex.size).html() || 0,\n          time: time || \"\",\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text()\n              .split(\"/\")[0] || 0,\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text()\n              .split(\"/\")[1] || 0,\n          completed: cells.eq(fieldIndex.completed).text() || 0,\n          comments: cells.eq(fieldIndex.comments).text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: Searcher.getRowTags(site, row),\n          progress: Searcher.getFieldValue(site, cells.eq(fieldIndex.progress), \"progress\"),\n          status: Searcher.getFieldValue(site, cells.eq(fieldIndex.status), \"status\")\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      result.link = link.attr(\"href\");\n      let id = result.link.match(/cat=(\\d+)/)[1];\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n      result.name = this.getCategoryName(id);\n      return result;\n    }\n\n    getCategoryName(id) {\n      if ($.isEmptyObject(this.categories)) {\n        let cells = options.page\n          .find(\"table.bottom > tbody > tr\")\n          .eq(1)\n          .find(\"td\");\n        cells.each((i, dom) => {\n          let id = $(dom)\n            .find(\"input\")\n            .attr(\"id\");\n          id = id.replace(\"c\", \"\");\n          let name = $(dom)\n            .find(\"a\")\n            .text();\n          if (id) {\n            this.categories[id] = name;\n          }\n        });\n      }\n      return this.categories ? this.categories[id] : \"\";\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/hdchina.org/config.json",
    "content": "{\n  \"name\": \"HDChina\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"高清影音人士分享乐园\",\n  \"url\": \"https://hdchina.org/\",\n  \"icon\": \"https://hdchina.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"音乐\",\n    \"纪录片\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdchina.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"1.5\",\n    \"privilege\": \"可以使用道具，可以打开签名和个性化称号\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.0\",\n    \"privilege\": \"可以在候选区投票，可以在论坛建议区发帖，可以上传字幕，可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"可以进入邀请区。\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"3.0\",\n    \"privilege\": \"并可以直接发布种子，无需候选。\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4.0\",\n    \"privilege\": \"可以在个人资料内隐藏个人信息，可以匿名做种。\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"5.0\",\n    \"privilege\": \"发送邀请，可以查看其它会员种子历史，可以更新IMDb信息。\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"6.0\",\n    \"privilege\": \"账号挂起永久保留。取消一个月只能发送一个邀请的限制。\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"50\",\n    \"downloaded\": \"5TB\",\n    \"ratio\": \"8.0\",\n    \"privilege\": \"账号永久保存(无需挂起)\"\n  }],\n  \"searchEntryConfig\": {\n    \"resultSelector\": \"table.torrent_list:last > tbody > tr\",\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\".progress:eq(0) > div\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\".progress:eq(0) > div\"],\n        \"filters\": [\"query.attr('class')\", \"query=='progress_seeding'?2:(query=='progress_completed'?255:(query=='progress_no_downloading'?3:1))\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat20=1\",\n      \"name\": \"原盘(Full BD)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat17=1\",\n      \"name\": \"电影Movie(1080p)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat16=1\",\n      \"name\": \"电影Movie(1080i)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat9=1\",\n      \"name\": \"电影Movie(720p)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat13=1\",\n      \"name\": \"欧美剧(EU/US TV series)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat25=1\",\n      \"name\": \"中港台剧集(Chinese TV series)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat26=1\",\n      \"name\": \"韩剧(Kor Drama)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat24=1\",\n      \"name\": \"日剧(Jpn Drama)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat21=1\",\n      \"name\": \"欧美剧集包(EU/US TV series pack)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat22=1\",\n      \"name\": \"中港台剧集包(Chinese TV series pack)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat23=1\",\n      \"name\": \"日韩剧集包(JPN/KOR drama pack)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat27=1\",\n      \"name\": \"iPad视频(iPad Video)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat5=1\",\n      \"name\": \"纪录片(Documentary)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat15=1\",\n      \"name\": \"体育节目(Sports)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat14=1\",\n      \"name\": \"动画片(Animation)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"综艺(TV Shows)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"演唱会(Vocal Concert)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV(Music Video)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"音乐(Music)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat19=1\",\n      \"name\": \"补充音轨(Audio Track)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"戏剧(Drama)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"书籍(Book)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"其他(Other)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"4K UltraHD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"旅游(Travel)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat412=1\",\n      \"name\": \"饮食(Food)\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 20,\n        \"name\": \"原盘(Full BD)\"\n      },\n      {\n        \"id\": 17,\n        \"name\": \"电影Movie(1080p)\"\n      },\n      {\n        \"id\": 16,\n        \"name\": \"电影Movie(1080i)\"\n      },\n      {\n        \"id\": 9,\n        \"name\": \"电影Movie(720p)\"\n      },\n      {\n        \"id\": 13,\n        \"name\": \"欧美剧(EU/US TV series)\"\n      },\n      {\n        \"id\": 25,\n        \"name\": \"中港台剧集(Chinese TV series)\"\n      },\n      {\n        \"id\": 26,\n        \"name\": \"韩剧(Kor Drama)\"\n      },\n      {\n        \"id\": 24,\n        \"name\": \"日剧(Jpn Drama)\"\n      },\n      {\n        \"id\": 21,\n        \"name\": \"欧美剧集包(EU/US TV series pack)\"\n      },\n      {\n        \"id\": 22,\n        \"name\": \"中港台剧集包(Chinese TV series pack)\"\n      },\n      {\n        \"id\": 23,\n        \"name\": \"日韩剧集包(JPN/KOR drama pack)\"\n      },\n      {\n        \"id\": 27,\n        \"name\": \"iPad视频(iPad Video)\"\n      },\n      {\n        \"id\": 5,\n        \"name\": \"纪录片(Documentary)\"\n      },\n      {\n        \"id\": 15,\n        \"name\": \"体育节目(Sports)\"\n      },\n      {\n        \"id\": 14,\n        \"name\": \"动画片(Animation)\"\n      },\n      {\n        \"id\": 401,\n        \"name\": \"综艺(TV Shows)\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"演唱会(Vocal Concert)\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"MV(Music Video)\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"音乐(Music)\"\n      },\n      {\n        \"id\": 19,\n        \"name\": \"补充音轨(Audio Track)\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"戏剧(Drama)\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"书籍(Book)\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"其他(Other)\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"4K UltraHD\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"旅游(Travel)\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"饮食(Food)\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"topElement\": \"html\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"div#ka1\"],\n          \"filters\": [\"query.parent().text().match(/\\\\(([\\\\d.]+)个种子/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"div#ka1\"],\n          \"filters\": [\"query.parent().text().match(/共计([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.msgalert a[href='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"xCsrf\": {\n          \"selector\": [\"meta[name='x-csrf']\"],\n          \"attribute\": \"content\"\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"prerequisites\": \"!user.seeding\",\n      \"page\": \"/ajax_getusertorrentlist.php\",\n      \"dataType\": \"json\",\n      \"requestMethod\": \"POST\",\n      \"requestData\": {\n        \"userid\": \"$user.id$\",\n        \"type\": \"seeding\",\n        \"csrf\": \"$user.xCsrf$\"\n      },\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"message\"],\n          \"filters\": [\"jQuery('<div/>').html(query).find('tr:not(:eq(0))').length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"message\"],\n          \"filters\": [\"jQuery('<div/>').html(query).find('tr:not(:eq(0))')\", \"jQuery.map(query.find('td.rowfollow:eq(2)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a#clip_target\"],\n          \"filters\": [\"query.attr('href')\"]\n        }\n      }\n    }\n  },\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"img[src*='pic/share_rule_1.gif']\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/hdcity.city/config.json",
    "content": "{\n  \"name\": \"HDCity\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"无\",\n  \"url\": \"https://hdcity.city/\",\n  \"icon\": \"https://hdcity.city/favicon.ico\",\n  \"tags\": [\n    \"综合\",\n    \"影视\"\n  ],\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/pt\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdcity.city\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power Angel\",\n      \"interval\": \"4\",\n      \"uploaded\": \"50GB\",\n      \"ratio\": \"1.0\",\n      \"privilege\": \"可以请求续种；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite Angel\",\n      \"interval\": \"8\",\n      \"uploaded\": \"150GB\",\n      \"ratio\": \"1.1\",\n      \"privilege\": \"权天使及以上等级封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy Angel\",\n      \"interval\": \"12\",\n      \"uploaded\": \"500GB\",\n      \"ratio\": \"1.5\",\n      \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane Angel\",\n      \"interval\": \"16\",\n      \"uploaded\": \"1TB\",\n      \"ratio\": \"2.0\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran Angel\",\n      \"interval\": \"24\",\n      \"uploaded\": \"5TB\",\n      \"ratio\": \"2.5\",\n      \"privilege\": \"主天使及以上市民会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme Angel\",\n      \"interval\": \"36\",\n      \"uploaded\": \"10TB\",\n      \"ratio\": \"2.6\",\n      \"privilege\": \"无\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate Angel\",\n      \"interval\": \"72\",\n      \"uploaded\": \"20TB\",\n      \"ratio\": \"2.8\",\n      \"privilege\": \"比较牛逼的等级。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Master Angel\",\n      \"interval\": \"100\",\n      \"uploaded\": \"40TB\",\n      \"ratio\": \"4.0\",\n      \"privilege\": \"最牛逼的市民，或特殊任务分配。\"\n    }\n  ],\n  \"collaborator\": \"waldens\",\n  \"searchEntryConfig\": {\n    \"page\": \"/pt\",\n    \"queryString\": \"iwannaseethis=$key$&notnewword=1&v=legacyinv\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"appendQueryString\": \"&search_area=1\"\n    }],\n    \"parseScriptFile\": \"/schemas/Common/getSearchResult.js\",\n    \"resultSelector\": \"div.text, div.text_alt, div.tr_review, div.tr_inpro\",\n    \"dataRowSelector\": \"> table\",\n    \"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"span[style='color:#777']\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"subTitle\": {\n\t\t\"selector\": [\"a.torname\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a.torname\"],\n        \"filters\": [\"query.attr('href')\", \"'https://hdcity.city/'+query\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"a[href^=download]\"],\n        \"filters\": [\"query.attr('href')\", \"'https://hdcity.city/'+query\"]\n\t  },\n\t  \"time\": {\n\t\t\"selector\": [\"td:nth-child(8)\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"size\": {\n\t\t\"selector\": [\"nobr:contains('B')\"],\n\t\t\"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)[0]\"]\n\t  },\n\t  \"seeders\": {\n\t\t\"selector\": [\"a[href*='#seeders'] font\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n      \"leechers\": {\n\t\t\"selector\": [\"a[href*='#leechers']\"],\n        \"filters\": [\"$(query[0].childNodes[1]).text()\"]\n\t  },\n      \"completed\": {\n\t\t\"selector\": [\"a[href^='viewsnatches']:first\"],\n        \"filters\": [\"$(query[0].childNodes[1]).text()\"]\n\t  },\n      \"progress\": {\n        \"selector\": [\"div.pbo div.pbc.sd, div.pbo div.pbc.ns\", \".pbo div.pbc.dl\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"query.attr('style').replace('width:','').replace('%;','')\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"div.pbo div.pbc.sd\",\"div.pbo div.pbc.ns\", \".pbo div.pbc.dl\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"255\"],\n          [\"3\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies/电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"Series/剧集\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"Doc/档案记录\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"Anim/动漫\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"Shows/节目\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"MV/音乐视频\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"Sports/体育\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"Audio/音频\"\n      },\n      {\n        \"id\": 727,\n        \"name\": \"XXX/家长指引\"\n      },\n      {\n        \"id\": 728,\n        \"name\": \"Edu/文档/教材\"\n      },\n      {\n        \"id\": 729,\n        \"name\": \"Soft/软件\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"Other/其他\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/userdetails\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"div.text:contains('ID')\"],\n          \"filters\": [\"query.text().match(/\\\\d+/)\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails'] > strong:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout']\"],\n          \"filters\": [\"query.length > 0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"div.text:contains('上传量')\", \"div.text:contains('上傳量')\", \"div.text:contains('Uploaded')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(?:上[传傳]量|Uploaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"div.text:contains('下载量')\", \"div.text:contains('下載量')\", \"div.text:contains('Downloaded')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(?:下[载載]量|Downloaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"img[src*='/pic/class/']\",\n          \"attribute\": \"src\",\n          \"filters\": [\"query.match(/\\\\/pic\\\\/class\\\\/(\\\\d+).gif/)[1]\", \"({0:'堕落者(Peasant)',1: '天使(Angel)',2: '大天使(Power Angel)',3:'权天使(Elite Angel)',4:'能天使(Crazy Angel)',5:'力天使(Insane Angel)',6:'主天使(Veteran Angel)',7:'座天使(Extreme Angel)',8:'智天使(Ultimate Angel)',9:'炽天使(Master Angel)',10:'壕(VIP)',11:'隐天使(Retiree)',12:'射种天使(Uploader)',13:'论坛版主(Forum Moderator)',14:'总版主(Moderator)',15:'管理员(Administrator)',16:'守护天使(Sysop)',17:'市长(Mayor)'})[query]\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div.text:contains('魅力值')\", \"div.text:contains('Karma'):contains('Points')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(?:魅力值|Karma Points).+?([\\\\d.]+)/)\", \"(query && query.length>=2)?parseFloat(query[1]):null\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"li > a[href='messages']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"div.text:contains('加入日期')\", \"div.text:contains('Join'):contains('date')\"],\n          \"filters\": [\"query.text().match(/(?:加入日期|Join date)\\\\s+(.*)\\\\s\\\\(/)\", \"(query && query.length>=2) ? (dateTime(query[1]).isValid()?dateTime(query[1]).valueOf():query[1]) : null\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"query.find('td.rowfollow:eq(2)').length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td.rowfollow:eq(2)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hddolby.com/config.json",
    "content": "{\n  \"name\": \"HD Dolby\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.hddolby.com/\",\n  \"description\": \"高清杜比\",\n  \"icon\": \"https://www.hddolby.com/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"host\": \"hddolby.com\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"2\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"2.0\",\n      \"seedingPoints\": \"47040\",\n      \"privilege\": \"得到0个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"256GB\",\n      \"ratio\": \"2.5\",\n      \"seedingPoints\": \"94080\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"512GB\",\n      \"ratio\": \"3.0\",\n      \"seedingPoints\": \"188160\",\n      \"privilege\": \"得到0个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"12\",\n      \"downloaded\": \"768GB\",\n      \"ratio\": \"3.5\",\n      \"seedingPoints\": \"282240\",\n      \"privilege\": \"无\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"16\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"4.0\",\n      \"seedingPoints\": \"376320\",\n      \"privilege\": \"可以查看其它用户的评论、帖子历史。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"20\",\n      \"downloaded\": \"2TB\",\n      \"ratio\": \"4.5\",\n      \"seedingPoints\": \"470400\",\n      \"privilege\": \"Extreme User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"24\",\n      \"downloaded\": \"4TB\",\n      \"ratio\": \"5.0\",\n      \"seedingPoints\": \"564480\",\n      \"privilege\": \"无\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"48\",\n      \"downloaded\": \"8TB\",\n      \"ratio\": \"5.5\",\n      \"seedingPoints\": \"1128960\",\n      \"privilege\": \"无\"\n    }\n  ],\n  \"collaborator\": [\"iceyuamao0510\", \"tongyifan\"],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:parseFloat(query.text().split('%')[0])\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:(query.is(\\\"[bgcolor='#CC0066']\\\")?1:(parseFloat(query.text().split('%')[0])==100?(query.is(\\\"[bgcolor='#d0d0d0']\\\")?255:2):3))\"\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music Videos\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"Games游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"Study学习\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"Others其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"Movies电影\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"TV Series电视剧\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"Documentaries纪录片\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"Animations动漫\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"TV Shows综艺\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"Music Videos\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"Sports体育\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"HQ Audio音乐\"\n        },\n        {\n          \"id\": 410,\n          \"name\": \"Games游戏\"\n        },\n        {\n          \"id\": 411,\n          \"name\": \"Study学习\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"Others其他\"\n        }\n      ]\n    }\n  ],\n  \"selectors\": {\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\n            \"a[href*='downhash']\"\n          ],\n          \"filters\": [\n            \"query.attr('href')\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hdf.world/config.json",
    "content": "{\n  \"name\": \"HD-Forever\",\n  \"timezoneOffset\": \"+0100\",\n  \"description\": \"HD-F\",\n  \"icon\": \"https://hdf.world/favicon.ico\",\n  \"schema\": \"GazelleJSONAPI\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"url\": \"https://hdf.world/\",\n  \"collaborator\": [\"luckiestone\"],\n  \"host\": \"hdf.world\",\n  \"securityKeyFields\": [\"authkey\", \"torrent_pass\"],\n  \"searchEntryConfig\": {\n\t\"skipIMDbId\": true\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/ajax.php?action=index\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"response.id\"]\n        },\n        \"name\": {\n          \"selector\": [\"response.username\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"response.notifications.messages\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"response.userstats.uploaded\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"response.userstats.downloaded\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"response.userstats.ratio\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"response.userstats.class\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/ajax.php?action=user&id=$user.id$\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"response.stats.joinedDate\"],\n          \"filters\": [\"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"response.community.seeding\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/store.php?action=rate\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"table.torrent_table:first td.nobr\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"bonus\": {\n          \"selector\": \"li#BonusPoints a[href*='store.php']\",\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'')\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}"
  },
  {
    "path": "resource/sites/hdfans.org/config.json",
    "content": "{\n  \"name\": \"HDFans\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"与志同道合之人前行 分享更多值得珍藏的资源\",\n  \"url\": \"https://hdfans.org\",\n  \"icon\": \"https://hdfans.org/favicon.ico\",\n  \"tags\": [\"综合\", \"电影\", \"电视剧\", \"纪录片\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdfans.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.0\",\n    \"seedingPoints\": \"50000\",\n    \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以删除自己上传的字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"100000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"256GB\",\n    \"ratio\": \"2.0\",\n    \"seedingPoints\": \"250000\",\n    \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"512GB\",\n    \"ratio\": \"2.5\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.0\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"50\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛；Extreme User及以上用户会永远保留账号\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"4.0\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"得到五个邀请名额\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"5.0\",\n    \"seedingPoints\": \"1688888\",\n    \"privilege\": \"得到十个邀请名额\"\n  }],\n  \"collaborator\": [\"csi0n\", \"zhuweitung\"],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)</g)\",\n            \"(query && query.length>0 ) ? query[0].replace('总大小：', '').replace('<', '').trim() : 0\",\n            \"(query != 0) ? _self.getTotalSize([query]) : 0\"\n          ]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hdhome.org/config.json",
    "content": "{\n  \"name\": \"HDHome\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HDHome\",\n  \"url\": \"https://hdhome.org/\",\n  \"icon\": \"https://hdhome.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdhome.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"256GB\",\n    \"ratio\": \"2.0\",\n    \"seedingPoints\": \"40000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"386GB\",\n    \"ratio\": \"2.5\",\n    \"seedingPoints\": \"100000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"12\",\n    \"downloaded\": \"512GB\",\n    \"ratio\": \"3.0\",\n    \"seedingPoints\": \"180000\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"16\",\n    \"downloaded\": \"768GB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"280000\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"4.0\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"24\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"540000\",\n    \"privilege\": \"得到1个邀请名额，可以更新过期的外部信息，可以查看Extreme User论坛，账号封存后永久保留\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"5.0\",\n    \"seedingPoints\": \"700000\",\n    \"privilege\": \"得到1个邀请名额，账号永久保留\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"36\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"10\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"得到1个邀请名额\"\n  }],\n  \"collaborator\": [\"tongyifan\", \"yuanyiwei\"],\n  \"plugins\": [\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\n        \"/live.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"/schemas/NexusPHP/torrents.js\"\n      ]\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:parseFloat(query.text().split('%')[0])\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:(query.is(\\\"[bgcolor='#CC0066']\\\")?1:(parseFloat(query.text().split('%')[0])==100?(query.is(\\\"[bgcolor='#d0d0d0']\\\")?255:2):3))\"\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"种子\",\n      \"enabled\": true\n    },\n    {\n      \"entry\": \"/live.php?search=$key$&notnewword=1\",\n      \"name\": \"LIVE\",\n      \"enabled\": true\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 411,\n          \"name\": \"Movies SD\"\n        },\n        {\n          \"id\": 412,\n          \"name\": \"Movies IPad\"\n        },\n        {\n          \"id\": 413,\n          \"name\": \"Movies 720p\"\n        },\n        {\n          \"id\": 414,\n          \"name\": \"Movies 1080p\"\n        },\n        {\n          \"id\": 415,\n          \"name\": \"Movies REMUX\"\n        },\n        {\n          \"id\": 450,\n          \"name\": \"Movies Bluray\"\n        },\n        {\n          \"id\": 499,\n          \"name\": \"Movies UHD Blu-ray\"\n        },\n        {\n          \"id\": 416,\n          \"name\": \"Movies 2160p\"\n        },\n        {\n          \"id\": 417,\n          \"name\": \"Doc SD\"\n        },\n        {\n          \"id\": 418,\n          \"name\": \"Doc IPad\"\n        },\n        {\n          \"id\": 419,\n          \"name\": \"Doc 720p\"\n        },\n        {\n          \"id\": 420,\n          \"name\": \"Doc 1080p\"\n        },\n        {\n          \"id\": 421,\n          \"name\": \"Doc REMUX\"\n        },\n        {\n          \"id\": 451,\n          \"name\": \"Doc Bluray\"\n        },\n        {\n          \"id\": 500,\n          \"name\": \"Doc UHD Blu-ray\"\n        },\n        {\n          \"id\": 422,\n          \"name\": \"Doc 2160p\"\n        },\n        {\n          \"id\": 423,\n          \"name\": \"TVMusic 720p\"\n        },\n        {\n          \"id\": 424,\n          \"name\": \"TVMusic 1080i\"\n        },\n        {\n          \"id\": 425,\n          \"name\": \"TVShow SD\"\n        },\n        {\n          \"id\": 426,\n          \"name\": \"TVShow IPad\"\n        },\n        {\n          \"id\": 471,\n          \"name\": \"TVShow IPad\"\n        },\n        {\n          \"id\": 427,\n          \"name\": \"TVShow 720p\"\n        },\n        {\n          \"id\": 428,\n          \"name\": \"TVShow 1080i\"\n        },\n        {\n          \"id\": 429,\n          \"name\": \"TVShow 1080p\"\n        },\n        {\n          \"id\": 430,\n          \"name\": \"TVShow REMUX\"\n        },\n        {\n          \"id\": 452,\n          \"name\": \"TVShows Bluray\"\n        },\n        {\n          \"id\": 431,\n          \"name\": \"TVShow 2160p\"\n        },\n        {\n          \"id\": 432,\n          \"name\": \"TVSeries SD\"\n        },\n        {\n          \"id\": 433,\n          \"name\": \"TVSeries IPad\"\n        },\n        {\n          \"id\": 434,\n          \"name\": \"TVSeries 720p\"\n        },\n        {\n          \"id\": 435,\n          \"name\": \"TVSeries 1080i\"\n        },\n        {\n          \"id\": 436,\n          \"name\": \"TVSeries 1080p\"\n        },\n        {\n          \"id\": 437,\n          \"name\": \"TVSeries REMUX\"\n        },\n        {\n          \"id\": 453,\n          \"name\": \"TVSereis Bluray\"\n        },\n        {\n          \"id\": 438,\n          \"name\": \"TVSeries 2160p\"\n        },\n        {\n          \"id\": 502,\n          \"name\": \"TVSeries 4K Bluray\"\n        },\n        {\n          \"id\": 439,\n          \"name\": \"Musics APE\"\n        },\n        {\n          \"id\": 440,\n          \"name\": \"Musics FLAC\"\n        },\n        {\n          \"id\": 441,\n          \"name\": \"Musics MV\"\n        },\n        {\n          \"id\": 442,\n          \"name\": \"Sports 720p\"\n        },\n        {\n          \"id\": 443,\n          \"name\": \"Sports 1080i\"\n        },\n        {\n          \"id\": 444,\n          \"name\": \"Anime SD\"\n        },\n        {\n          \"id\": 445,\n          \"name\": \"Anime IPad\"\n        },\n        {\n          \"id\": 446,\n          \"name\": \"Anime 720p\"\n        },\n        {\n          \"id\": 447,\n          \"name\": \"Anime 1080p\"\n        },\n        {\n          \"id\": 448,\n          \"name\": \"Anime REMUX\"\n        },\n        {\n          \"id\": 454,\n          \"name\": \"Anime Bluray\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"Misc\"\n        },\n        {\n          \"id\": 449,\n          \"name\": \"Anime 2160p\"\n        },\n        {\n          \"id\": 501,\n          \"name\": \"Anime UHD Blu-ray\"\n        }\n      ]\n    },\n    {\n      \"entry\": \"live.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 494,\n          \"name\": \"Movies Bluray\"\n        },\n        {\n          \"id\": 495,\n          \"name\": \"Doc Bluray\"\n        },\n        {\n          \"id\": 469,\n          \"name\": \"TVMusic 1080i\"\n        },\n        {\n          \"id\": 472,\n          \"name\": \"TVShow 720p\"\n        },\n        {\n          \"id\": 473,\n          \"name\": \"TVShow 1080i\"\n        },\n        {\n          \"id\": 474,\n          \"name\": \"TVShow 1080p\"\n        },\n        {\n          \"id\": 475,\n          \"name\": \"TVShow REMUX\"\n        },\n        {\n          \"id\": 496,\n          \"name\": \"TVShows Bluray\"\n        },\n        {\n          \"id\": 476,\n          \"name\": \"TVShow 2160p\"\n        },\n        {\n          \"id\": 477,\n          \"name\": \"TVSeries SD\"\n        },\n        {\n          \"id\": 478,\n          \"name\": \"TVSeries IPad\"\n        },\n        {\n          \"id\": 479,\n          \"name\": \"TVSeries 720p\"\n        },\n        {\n          \"id\": 480,\n          \"name\": \"TVSeries 1080p\"\n        },\n        {\n          \"id\": 481,\n          \"name\": \"TVSeries REMUX\"\n        },\n        {\n          \"id\": 497,\n          \"name\": \"TVSereis Bluray\"\n        },\n        {\n          \"id\": 482,\n          \"name\": \"TVSeries 2160p\"\n        },\n        {\n          \"id\": 483,\n          \"name\": \"Musics APE\"\n        },\n        {\n          \"id\": 484,\n          \"name\": \"Musics FLAC\"\n        },\n        {\n          \"id\": 485,\n          \"name\": \"Musics MV\"\n        },\n        {\n          \"id\": 486,\n          \"name\": \"Sports 720p\"\n        },\n        {\n          \"id\": 487,\n          \"name\": \"Sports 1080i\"\n        },\n        {\n          \"id\": 488,\n          \"name\": \"Anime SD\"\n        },\n        {\n          \"id\": 489,\n          \"name\": \"Anime IPad\"\n        },\n        {\n          \"id\": 490,\n          \"name\": \"Anime 720p\"\n        },\n        {\n          \"id\": 491,\n          \"name\": \"Anime 1080p\"\n        },\n        {\n          \"id\": 492,\n          \"name\": \"Anime REMUX\"\n        },\n        {\n          \"id\": 498,\n          \"name\": \"Anime Bluray\"\n        },\n        {\n          \"id\": 493,\n          \"name\": \"Anime 2160p\"\n        }\n      ]\n    }\n  ],\n  \"selectors\": {\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='downhash']\"],\n          \"filters\": [\"query.attr('href')\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hdmayi.com/config.json",
    "content": "{\n    \"name\": \"HDmayi\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"HDmayi\",\n    \"url\": \"http://hdmayi.com/\",\n    \"icon\": \"http://hdmayi.com/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"hdmayi.com\",\n    \"levelRequirements\":\n    [\n      {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"4\",\n        \"downloaded\": \"50GB\",\n        \"ratio\": \"1.05\",\n        \"seedingPoints\": \"20000\",\n        \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n      },\n      {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"8\",\n        \"downloaded\": \"120GB\",\n        \"ratio\": \"1.55\",\n        \"seedingPoints\": \"40000\",\n        \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n      },\n      {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"300GB\",\n        \"ratio\": \"2.05\",\n        \"seedingPoints\": \"80000\",\n        \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n      },\n      {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"500GB\",\n        \"ratio\": \"2.55\",\n        \"seedingPoints\": \"120000\",\n        \"privilege\": \"可以查看普通日志。\"\n      },\n      {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"750GB\",\n        \"ratio\": \"3.05\",\n        \"seedingPoints\": \"200000\",\n        \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n      },\n      {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"1TB\",\n        \"ratio\": \"3.55\",\n        \"seedingPoints\": \"300000\",\n        \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n      },\n      {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"1.5TB\",\n        \"ratio\": \"4.05\",\n        \"seedingPoints\": \"400000\",\n        \"privilege\": \"得到五个邀请名额。\"\n      },\n      {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"4.55\",\n        \"seedingPoints\": \"1000000\",\n        \"privilege\": \"得到十个邀请名额。\"\n      }\n    ],\n    \"collaborator\": [\"koal\", \"zhuweitung\"],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hdpt.xyz/config.json",
    "content": "{\n    \"name\": \"明教\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"综合性的PT论坛    欢迎您的加入！\",\n    \"url\": \"https://hdpt.xyz/\",\n    \"icon\": \"https://hdpt.xyz/favicon.ico\",\n    \"tags\": [\"影视\", \"综合\"],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"hdpt.xyz\",\n    \"levelRequirements\": [\n      {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"4\",\n        \"downloaded\": \"200GB\",\n        \"ratio\": \"2\",\n        \"seedingPoints\": \"50000\",\n        \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以请求续种； 可以发送邀请。\"\n      },\n      {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"8\",\n        \"downloaded\": \"400GB\",\n        \"ratio\": \"3\",\n        \"seedingPoints\": \"110000\",\n        \"privilege\": \"得到一个邀请名额。\"\n      },\n      {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"700GB\",\n        \"ratio\": \"4\",\n        \"seedingPoints\": \"200000\",\n        \"privilege\": \"得到二个邀请名额。\"\n      },\n      {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"1TB\",\n        \"ratio\": \"5\",\n        \"seedingPoints\": \"300000\",\n        \"privilege\": \"得到二个邀请名额；\"\n      },\n      {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"2TB\",\n        \"ratio\": \"6\",\n        \"seedingPoints\": \"500000\",\n        \"privilege\": \"得到二个邀请名额。神蛇 (Veteran User)及以上等级的账号如果在封存后将保留，封存的账号如果连续400天不登录，将被封禁；未封存的账号如果连续90天不登录，将被封禁；没有流量的用户（即上传/下载数据都为0）如果连续90天不登录，将被封禁账号。\"\n      },\n      {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"7\",\n        \"seedingPoints\": \"700000\",\n        \"privilege\": \"得到二个邀请名额；可以更新过期的外部信息；可以查看Extreme User论坛。紫微 (Extreme User)及以上用户会永远保留账号。\"\n      },\n      {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"4TB\",\n        \"ratio\": \"8\",\n        \"seedingPoints\": \"1000000\",\n        \"privilege\": \"得到五个邀请名额。\"\n      },\n      {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"8TB\",\n        \"ratio\": \"9\",\n        \"seedingPoints\": \"1500000\",\n        \"privilege\": \"得到七个邀请名额。\"\n      }\n    ],\n    \"collaborator\": [\"koal\", \"zhuweitung\"],\n    \"selectors\": {\n      \"userSeedingTorrents\": {\n          \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n          \"fields\": {\n              \"seeding\": {\n                  \"selector\": [\n                      \"b:first\"\n                  ],\n                  \"filters\": [\n                      \"query.text()\"\n                  ]\n              },\n              \"seedingSize\": {\n                  \"selector\": \"\",\n                  \"filters\": [\n                      \"query.text().match(/总大小：(.*?)上一页/g)\",\n                      \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                      \"(query != 0) ? query.sizeToNumber() : 0\"\n                  ]\n              }\n          }\n      }\n    },\n    \"searchEntry\": [{\n        \"name\": \"全站\",\n        \"enabled\": true\n      },\n      {\n        \"queryString\": \"cat401=1\",\n        \"name\": \"电影\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat404=1\",\n        \"name\": \"纪录片\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat405=1\",\n        \"name\": \"动漫\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat402=1\",\n        \"name\": \"电视剧\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat403=1\",\n        \"name\": \"综艺\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat406=1\",\n        \"name\": \"MV\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat407=1\",\n        \"name\": \"体育\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat409=1\",\n        \"name\": \"其他\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat408=1\",\n        \"name\": \"音乐\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat410=1\",\n        \"name\": \"软件\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat411=1\",\n        \"name\": \"电子书\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat412=1\",\n        \"name\": \"卡通\",\n        \"enabled\": false\n      },\n      {\n        \"queryString\": \"cat413=1\",\n        \"name\": \"学习资料\",\n        \"enabled\": false\n      }\n    ],\n    \"categories\": [{\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [{\n          \"id\": 401,\n          \"name\": \"电影\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"纪录片\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"动漫\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"电视剧\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"综艺\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"MV\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"体育\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"其他\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"音乐\"\n        },\n        {\n          \"id\": 410,\n          \"name\": \"软件\"\n        },\n        {\n          \"id\": 411,\n          \"name\": \"电子书\"\n        },\n        {\n          \"id\": 412,\n          \"name\": \"卡通\"\n        },\n        {\n          \"id\": 413,\n          \"name\": \"学习资料\"\n        }\n      ]\n    }],\n    \"searchEntryConfig\": {\n      \"fieldSelector\": {\n        \"progress\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n        },\n        \"status\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\n            \"query ? query.attr('title') : ''\",\n            \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n          ]\n        }\n      }\n    }\n}\n"
  },
  {
    "path": "resource/sites/hdroute.org/config.json",
    "content": "{\n  \"name\": \"HDRoute\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HDRoute\",\n  \"url\": \"http://hdroute.org/\",\n  \"icon\": \"http://hdroute.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"host\": \"hdroute.org\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"s=$key$&dp=0&add=0&action=s&or=1\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"s=&dp=0&add=0&action=s&or=1&imdb=$key$\",\n      \"replaceKey\": [\n        \"tt\", \"\"\n      ]\n    }],\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"#unsticky-torrent-table dl\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"figure.sprite_dlp000\"\n  }, {\n    \"name\": \"~Free\",\n    \"selector\": \"figure.sprite_tempo_free\",\n    \"title\": \"title\",\n    \"color\": \"teal\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"figure.sprite_dlp030\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"figure.sprite_dlp050\"\n  }, {\n    \"name\": \"70%\",\n    \"selector\": \"figure.sprite_dlp070\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\".headerRightInfo a[href*='userdetail.php']\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\".headerRightInfo a[href*='userdetail.php']\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetail.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\".headerRightInfo span:contains('上传量: ')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/上传量:.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\".headerRightInfo span:contains('下载量: ')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/下载量:.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"levelName\": {\n          \"selector\": [\".userdetail-list-title:contains('用户等级') + div\"],\n          \"filters\": [\"query.text().replace(' 级别', '')\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\".userdetail-list-title:contains('注册日期') + div\"],\n          \"filters\": [\"query.text().trim()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\".header-user-data a[href*='list_seeding.php']\"],\n          \"filters\": [\"query.text().trim()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\".header-user-data a[href*='list_seeding.php']\"],\n          \"filters\": [\"query.next().text().match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/api.php?action=getAllPeeringInfo\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"seedingList\": {\n          \"selector\": [\"seeding\"],\n          \"filters\": [\"let r=[];query.forEach(q=>{r.push(q.torrentid)});r\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/hdroute.org/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"button.buttonDownload\");\n      let url = \"\";\n      if (query.length > 0) {\n        let id = location.href.getQueryString(\"id\");\n        if (id) {\n          url = PTService.site.url + \"download.php?id=\" + id;\n        }\n      }\n\n      return url;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\".details-title-section > p:first\")\n        .text()\n        .trim();\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/hdroute.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/loginSection/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (/对不起，没有您搜索的相关结果/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector = options.resultSelector;\n      let rows = options.page.find(selector);\n      let results = [];\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 0; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let id = row.attr(\"id\").replace(\"dl_torrent_\", \"\");\n          let url = `${site.url}download.php?id=${id}`;\n          let link = `${site.url}details.php?id=${id}`;\n\n          let data = {\n            id,\n            title: row.find(\".title_chs\").text(),\n            subTitle: row.find(\".title_eng\").text(),\n            link,\n            url,\n            size: row.find(\".torrent_size\").text(),\n            time: this.getTime(row.find(\".torrent_added\")),\n            author: \"\",\n            seeders: this.getTorrentCount(\n              row\n                .find(\".torrent_count.strong\")\n                .eq(0)\n                .text()\n            ),\n            leechers: this.getTorrentCount(\n              row\n                .find(\".torrent_count.strong\")\n                .eq(1)\n                .text()\n            ),\n            completed: -1,\n            comments: 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category: null\n          };\n          results.push(data);\n        }\n\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    getTorrentCount(text) {\n      return text == \"---\" ? 0 : text;\n    }\n\n    /**\n     * 获取时间\n     * @param {*} el\n     */\n    getTime(el) {\n      let time = $(\"<span>\")\n        .html(el.html().replace(\"<br>\", \" \"))\n        .text();\n      return time || \"\";\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/hdroute.org/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"#unsticky-torrent-table dl:has(.buttonDownload)\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let id = $(item)\n          .attr(\"id\")\n          .replace(\"dl_torrent_\", \"\");\n        let link = `${siteURL}download.php?id=${id}`;\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#unsticky-torrent-table dl:has(.buttonDownload) .torrent_size\")\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/hdsky.me/config.json",
    "content": "{\n  \"name\": \"HDSky\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"高清发烧友后花园PT\",\n  \"url\": \"https://hdsky.me/\",\n  \"icon\": \"https://hdsky.me/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"纪录片\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdsky.me\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"2.0\",\n    \"privilege\": \"NFO文档；请求续种；查看其它用户的种子历史；删除自己上传的字幕\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"查看邀请区\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.0\",\n    \"privilege\": \"在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"3.5\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"4.0\",\n    \"privilege\": \"封存账号后不会被删除；查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"6TB\",\n    \"ratio\": \"4.5\",\n    \"privilege\": \"更新过期的外部信息；查看Extreme User论坛\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"45\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"5.0\",\n    \"privilege\": \"永远保留账号\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"65\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"5.5\",\n    \"privilege\": \"直接发布种子；可以查看排行榜；在网站开放邀请期间发送邀请\"\n  }],\n  \"searchEntry\": [{\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies/电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries/纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"iPad/iPad影视\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations/动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series/剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows/综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music Videos/音乐MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports/体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio/无损音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"Misc/其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies/电影\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"Documentaries/纪录片\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"iPad/iPad影视\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"Animations/动漫\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"TV Series/剧集\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"TV Shows/综艺\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"Music Videos/音乐MV\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"Sports/体育\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"HQ Audio/无损音乐\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"Misc/其他\"\n      }\n    ]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div.progressseeding, div.progressfinished, div.progressdownloading, div.progressdownloaded, div.progressuploaded\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.progressseeding\", \"div.progressfinished\", \"div.progressdownloading\", \"div.progressdownloaded\", \"div.progressuploaded[title='由我上传 ']\", \"div.progressuploaded[title='由我上传 , 正在做种 ']\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"255\"],\n          [\"1\"],\n          [\"255\"],\n          [\"255\"],\n          [\"2\"]\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"/torrents.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURLs\": {\n          \"selector\": \"input.download\",\n          \"filters\": [\"query.map(function() {return jQuery(this).parent().attr('action')}).toArray()\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": \"td.rowfollow a:contains('passkey')\",\n          \"filters\": [\"query.attr('href')\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/hdtime.org/config.json",
    "content": "{\n  \"name\": \"HDTime\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HDTime, time to forever!\",\n  \"url\": \"https://hdtime.org/\",\n  \"icon\": \"https://hdtime.org/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hdtime.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"seedingPoints\": \"40000\",\n    \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种；可以发送邀请；可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"150GB\",\n    \"ratio\": \"1.55\",\n    \"seedingPoints\": \"80000\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.05\",\n    \"seedingPoints\": \"150000\",\n    \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"2.55\",\n    \"seedingPoints\": \"250000\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"3.05\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"免除增量考核；得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"3.55\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"5TB\",\n    \"ratio\": \"4.05\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"得到五个邀请名额\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"5.05\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"得到十个邀请名额\"\n  }],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div[title*='seeding']\", \"div[title*='inactivity']\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\n            \"query.attr('title').split(' ')\",\n            \"query[1]?parseInt(query[1].substr(0,query[1].length-1)):undefined\"\n          ],\n          [\"undefined\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"div[title*='seeding']\", \"div[title*='inactivity']\", \"\"],\n        \"switchFilters\": [[\"2\"], [\"3\"], [\"undefined\"]]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat424=1\",\n      \"name\": \"Blu-Ray原盘\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat414=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"文档\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"电影\"\n        },\n        {\n          \"id\": 424,\n          \"name\": \"Blu-Ray原盘\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"剧集\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"综艺\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"动漫\"\n        },\n        {\n          \"id\": 414,\n          \"name\": \"软件\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"体育\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"纪录片\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"MV\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"音乐\"\n        },\n        {\n          \"id\": 410,\n          \"name\": \"游戏\"\n        },\n        {\n          \"id\": 411,\n          \"name\": \"文档\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"其他\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/hdvideo.one/config.json",
    "content": "{\n    \"name\": \"HDVideo\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"HDVideo\",\n    \"url\": \"https://hdvideo.one/\",\n    \"icon\": \"https://hdvideo.one/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"hdvideo.one\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"128GB\",\n            \"ratio\": \"2.0\",\n            \"seedingPoints\": \"60480\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"256GB\",\n            \"ratio\": \"2.5\",\n            \"seedingPoints\": \"137088\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"12\",\n            \"downloaded\": \"512GB\",\n            \"ratio\": \"3.0\",\n            \"seedingPoints\": \"262080\",\n            \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"18\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.5\",\n            \"seedingPoints\": \"453600\",\n            \"privilege\": \"得到两个邀请名额；可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"24\",\n            \"downloaded\": \"2TB\",\n            \"ratio\": \"4.0\",\n            \"seedingPoints\": \"604800\",\n            \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"32\",\n            \"downloaded\": \"4TB\",\n            \"ratio\": \"4.5\",\n            \"seedingPoints\": \"806400\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"6TB\",\n            \"ratio\": \"5.0\",\n            \"seedingPoints\": \"1008000\",\n            \"privilege\": \"得到三个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"52\",\n            \"downloaded\": \"8TB\",\n            \"ratio\": \"5.5\",\n            \"seedingPoints\": \"1310400\",\n            \"privilege\": \"得到五个邀请名额。\"\n        }\n    ],\n    \"collaborator\": \"koal\",\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/hdzone.me/config.json",
    "content": "{\n    \"name\": \"HDZone\",\n    \"description\": \"\",\n    \"url\": \"https://hdzone.me/\",\n    \"icon\": \"https://hdzone.me/favicon.ico\",\n    \"tags\": [\n        \"电影\"\n    ],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"hdzone.me\",\n    \"levelRequirements\": [{\n      \"level\": \"1\",\n      \"name\": \"Power User\",\n      \"interval\": \"5\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"2.0\",\n      \"privilege\": \"新晋等级用户，只能在每周六中午12点至每周日晚上11点59分发布种子。\"\n    },{\n      \"level\": \"2\",\n      \"name\": \"Elite User\",\n      \"interval\": \"5\",\n      \"downloaded\": \"220GB\",\n      \"ratio\": \"2.5\",\n      \"privilege\": \"Elite User权限同上。\"\n    },{\n      \"level\": \"3\",\n      \"name\": \"Crazy User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"400GB\",\n      \"ratio\": \"3.0\",\n      \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n    },{\n      \"level\": \"4\",\n      \"name\": \"Insane User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"600GB\",\n      \"ratio\": \"3.5\",\n      \"privilege\": \"可以查看普通日志。\"\n    },{\n      \"level\": \"5\",\n      \"name\": \"Veteran User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"900GB\",\n      \"ratio\": \"4.0\",\n      \"privilege\": \"可以查看其它用户的评论、帖子历史。\"\n    },{\n      \"level\": \"6\",\n      \"name\": \"Extreme User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"2TB\",\n      \"ratio\": \"4.5\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },{\n      \"level\": \"7\",\n      \"name\": \"Ultimate User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"4TB\",\n      \"ratio\": \"5.0\",\n      \"privilege\": \"得到2个邀请名额。\"\n    },{\n      \"level\": \"8\",\n      \"name\": \"Nexus Master\",\n      \"interval\": \"10\",\n      \"downloaded\": \"8TB\",\n      \"ratio\": \"5.5\",\n      \"privilege\": \"得到3个邀请名额。账号永久保留。\"\n    }],\n    \"collaborator\": \"ian\",\n    \"searchEntryConfig\": {\n        \"fieldSelector\": {\n            \"progress\": {\n                \"selector\": [\n                    \"> td:eq(8)\"\n                ],\n                \"filters\": [\n                    \"query.text()==='-'?null:parseFloat(query.text())\"\n                ]\n            },\n            \"status\": {\n                \"selector\": [\n                    \"\"\n                ],\n                \"filters\": [\n                    \"3\"\n                ]\n            }\n        }\n    }\n}"
  },
  {
    "path": "resource/sites/hhanclub.top/config.json",
    "content": "{\n    \"name\": \"憨憨\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"憨憨\",\n    \"url\": \"https://hhanclub.top/\",\n    \"icon\": \"https://hhanclub.top/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"hhanclub.top\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"150000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"500000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"900000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"1100000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"1300000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"1500000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n    \"collaborator\": [\"koal\", \"zhuweitung\"],\n    \"selectors\": {\n        \"userExtendInfo\": {\n            \"merge\": true,\n            \"fields\": {\n                \"bonus\": {\n                    \"selector\": [\"td.rowhead:contains('憨豆') + td\", \"td.rowhead:contains('Seed points') + td\"],\n                    \"filters\": [\"parseFloat(query.text().replace(/,/g,''))\"]\n                }\n            }\n        },\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        },\n        \"bonusExtendInfo\": {\n            \"prerequisites\": \"!user.bonusPerHour\",\n            \"page\": \"/mybonus.php\",\n            \"fields\": {\n                \"bonusPerHour\": {\n                    \"selector\": [\"h1:contains('每小时获得的合计憨豆') + div table tr:eq(1) td:last\", \"h1:contains('Total bonus gained per hour') + div table tr:eq(1) td:last\"],\n                    \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n                }\n            }\n        }\n    },\n    \"searchEntryConfig\": {\n        \"fieldSelector\": {\n            \"progress\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n            },\n            \"status\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\n                    \"query ? query.attr('title') : ''\",\n                    \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n                ]\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/htpt.cc/config.json",
    "content": "{\n  \"name\": \"海棠PT\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.htpt.cc\",\n  \"description\": \"主打曲艺、戏曲、相声、评书、小品、广播剧、有声小说等中国传统有声资源\",\n  \"icon\": \"https://www.htpt.cc/favicon.ico\",\n  \"tags\": [\n    \"曲艺\",\n    \"小品\",\n    \"有声小说\"\n  ],\n  \"host\": \"htpt.cc\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"得到一个邀请名额。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"privilege\": \"得到一个邀请名额，且Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"privilege\": \"得到一个邀请名额。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\",\n      \"privilege\": \"得到一个邀请名额。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\",\n      \"privilege\": \"得到一个邀请名额，且Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\",\n      \"privilege\": \"得到一个邀请名额，可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\",\n      \"privilege\": \"得到一个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"downloaded\": \"3TB\",\n      \"ratio\": \"4.55\",\n      \"privilege\": \"得到一个邀请名额。\"\n    }\n  ],\n  \"collaborator\": [\n    \"sabersalv\",\n    \"amorphobia\"\n  ],\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  },\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"selectors\": {\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/mybonus_new.php\",\n      \"merge\": true\n    }\n  },\n  \"plugins\": [\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\n        \"/torrents.php\",\n        \"/live.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"/schemas/NexusPHP/torrents.js\"\n      ]\n    },\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [\n        \"/details.php\"\n      ],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"/schemas/NexusPHP/details.js\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/hudbt.hust.edu.cn/config.json",
    "content": "{\n  \"name\": \"蝴蝶-HUDBT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HUDBT,教育网高速IPv6BT下载站。\",\n  \"url\": \"https://hudbt.hust.edu.cn/\",\n  \"icon\": \"https://hudbt.hust.edu.cn/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"hudbt.hust.edu.cn\",\n  \"collaborator\": [\"Rhilip\", \"枕头啊枕头\",\"Yincircle\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"易形(Power User)\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"化蛹(Elite User)\",\n    \"interval\": \"9\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"化蛹(Elite User)及以上用户封存账号后不会被删除。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"破茧(Crazy User)\",\n    \"interval\": \"16\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"得到一个邀请名额； 可以发送邀请（注意：网站会视情况提高或者降低允许发送邀请的最低等级，此处不一定会及时修改）；可以在做种/下载/发布的时候选择匿名模式。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"恋风(Insane User)\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"可以查看普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"翩跹(Veteran User)\",\n    \"interval\": \"36\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"得到一个邀请名额；可以查看其它用户的评论、帖子历史。翩跹(Veteran User)及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"归尘(Extreme User)\",\n    \"interval\": \"49\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。（未启用）\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"幻梦(Ultimate User)\",\n    \"interval\": \"64\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"逍遥(Nexus Master)\",\n    \"interval\": \"81\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"\"\n  }],\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat1=1&cat401=1&cat413=1&cat414=1&cat415=1&cat430=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat2=1&cat402=1&cat417=1&cat416=1&cat418=1\",\n      \"name\": \"剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat3=1&cat405=1&cat427=1&cat428=1&cat429=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat4=1&cat410=1&cat431=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat5=1&cat403=1&cat419=1&cat420=1&cat421=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat6=1&cat409=1&cat412=1\",\n      \"name\": \"资料\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat7=1&cat407=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat8=1&cat408=1&cat422=1&cat423=1&cat424=1&cat425=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat9=1&cat404=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat10=1&cat411=1&cat426=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat11=1&cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat12=1&cat432=1\",\n      \"name\": \"电子书\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat13=1&cat1037=1\",\n      \"name\": \"华中科技大学\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"大陆电影\"\n      },\n      {\n        \"id\": 413,\n        \"name\": \"港台电影\"\n      },\n      {\n        \"id\": 414,\n        \"name\": \"亚洲电影\"\n      },\n      {\n        \"id\": 415,\n        \"name\": \"欧美电影\"\n      },\n      {\n        \"id\": 430,\n        \"name\": \"iPad\"\n      },\n      {\n        \"id\": 433,\n        \"name\": \"抢先视频\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"大陆剧集\"\n      },\n      {\n        \"id\": 417,\n        \"name\": \"港台剧集\"\n      },\n      {\n        \"id\": 416,\n        \"name\": \"亚洲剧集\"\n      },\n      {\n        \"id\": 418,\n        \"name\": \"欧美剧集\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"纪录片\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"大陆综艺\"\n      },\n      {\n        \"id\": 419,\n        \"name\": \"港台综艺\"\n      },\n      {\n        \"id\": 420,\n        \"name\": \"亚洲综艺\"\n      },\n      {\n        \"id\": 421,\n        \"name\": \"欧美综艺\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"华语音乐\"\n      },\n      {\n        \"id\": 422,\n        \"name\": \"日韩音乐\"\n      },\n      {\n        \"id\": 423,\n        \"name\": \"欧美音乐\"\n      },\n      {\n        \"id\": 424,\n        \"name\": \"古典音乐\"\n      },\n      {\n        \"id\": 425,\n        \"name\": \"原声音乐\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"音乐MV\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"其他\"\n      },\n      {\n        \"id\": 432,\n        \"name\": \"电子书\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"完结动漫\"\n      },\n      {\n        \"id\": 427,\n        \"name\": \"连载动漫\"\n      },\n      {\n        \"id\": 428,\n        \"name\": \"剧场OVA\"\n      },\n      {\n        \"id\": 429,\n        \"name\": \"动漫周边\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 431,\n        \"name\": \"游戏视频\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"学习\"\n      },\n      {\n        \"id\": 426,\n        \"name\": \"MAC\"\n      },\n      {\n        \"id\": 1037,\n        \"name\": \"HUST\"\n      }\n    ]\n  }]\n}"
  },
  {
    "path": "resource/sites/ihdbits.me/config.json",
    "content": "{\n  \"name\": \"ihdbits\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"http://ihdbits.me/\",\n  \"description\": \"The Ultimate File Sharing Experience\",\n  \"icon\": \"http://ihdbits.me/favicon.ico\",\n  \"tags\": [\n    \"影视\"\n  ],\n  \"host\": \"ihdbits.me\",\n  \"collaborator\": \"koal\",\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/iptorrents.com/config.json",
    "content": "{\n  \"name\": \"IPTorrents\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"IPTorrents - #1 Private Tracker\",\n  \"url\": \"https://iptorrents.com/\",\n  \"icon\": \"https://iptorrents.com/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"IPTorrents\",\n  \"host\": \"iptorrents.com\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"uploaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Are able to make requests for torrents, view the Top 10, and apply for Uploader status\"\n    }\n  ],\n  \"supportedFeatures\": {\n    \"userData\": \"◐\"\n  },\n    \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/torrent.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"^/t$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntry\": [{\n    \"entry\": \"/t?q=$key$\",\n    \"name\": \"全部\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table#torrents:first\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"span.t_tag_free_leech\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='/u/']:first\", \"a[href*='userdetails.php']:first\"],\n          \"attribute\": \"href\",\n          \"switchFilters\": [\n            [\"query.match(/u\\\\/(.+)/)\", \"(query && query.length>=2)?(query[1]):''\"],\n            [\"query ? query.getQueryString('id'):''\"]\n          ]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\", \"form[action*='lout']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background: red'] a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": [\"h1.c0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"th:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"th:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"ratio\": {\n          \"selector\": \"th:contains('Share ratio') + td\",\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        },\n        \"levelName\": {\n          \"selector\": \"th:contains('Class') + td\"\n        },\n        \"bonus\": {\n          \"selector\": [\"a[href='/mybonus.php']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": \"th:contains('Join date') + td\",\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"th:contains('Seeding') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"th.ar:contains('Size') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/iptorrents.com/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='download.php/']:first\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        if (url.substr(0, 4) != \"http\") {\n          url = PTService.site.url + url;\n        }\n      }\n\n      return url;\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/iptorrents.com/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/\\/login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector =\n        options.resultSelector || \"div.table-responsive > table:first\";\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: header.length - 1,\n        // 分类\n        category: 0\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n\n        // 评论数\n        if (cell.find(\"a[href*='comments']\").length) {\n          fieldIndex.comments = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"a[href*='size']\").length) {\n          fieldIndex.size = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[href*='seeders']\").length) {\n          fieldIndex.seeders = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[href*='leechers']\").length) {\n          fieldIndex.leechers = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[href*='complete']\").length) {\n          fieldIndex.completed = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 分类\n        if (text == \"Type\") {\n          fieldIndex.category = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\n            \"a[href*='details.php']:not([href*='startcomments']):first\"\n          );\n          if (title.length == 0) {\n            title = row.find(\"a[href*='/t/']:first\");\n          }\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"a[href*='/download.php']\").attr(\"href\");\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time: this.getTime(row),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text().replace(\"Go to comments\",\"\") || 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category))\n          };\n          results.push(data);\n        }\n\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    getTime(row) {\n      let text = row.find(\"td.al div.sub\").text();\n      if (text) {\n        if (text.indexOf(\"|\") > 0) {\n\t        text=text.split(\"|\")[1];\n\t        if (text.indexOf(\"by\") > 0) {\n              return text.split(\"|\")[0].trim();\n          }\n        }\n      }\n      return text;\n    }\n\n    /**\n     * 获取副标题\n     * @param {*} title\n     * @param {*} row\n     */\n    getSubTitle(title, row) {\n      return \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: cell.find(\"img:first\").attr(\"alt\"),\n        link: \"\"\n      };\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/iptorrents.com/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"table#torrents:first, table#torrentTable:first\")\n        .find(\"a[href*='/download.php/']\")\n        .toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table#torrents:first, table#torrentTable:first\").find(\n          \"td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/joyhd.net/config.json",
    "content": "{\n  \"name\": \"JoyHD\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"JoyHD成立於2013年，發佈藍光原碟，藍光DIY和原抓音樂。\",\n  \"url\": \"https://www.joyhd.net\",\n  \"icon\": \"https://www.joyhd.net/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"joyhd.net\",\n  \"collaborator\": \"ylxb2016\",\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('银元') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/jpopsuki.eu/config.json",
    "content": "{\n  \"name\": \"JPopsuki\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"日韩音乐\",\n  \"url\": \"https://jpopsuki.eu/\",\n  \"icon\": \"https://jpopsuki.eu/favicon.ico\",\n  \"tags\": [\"音乐\", \"日韩\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"jpopsuki.eu\",\n  \"collaborator\": [\n    \"ronggang\",\n    \"ted423\",\n    \"luckiestone\",\n    \"amorphobia\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Member\",\n      \"interval\": \"1\",\n      \"uploaded\": \"10GB\",\n      \"ratio\": \"0.7\",\n      \"downloaded\": \"1KB\",\n      \"privilege\": \"Can use invites, notifications, set a forum signature, access the Top 10 and edit the Knowledge base.\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Power User\",\n      \"interval\": \"2\",\n      \"uploaded\": \"25GB\",\n      \"ratio\": \"1.05\",\n      \"downloaded\": \"1KB\",\n      \"uploads\": \"5\",\n      \"privilege\": \"advanced Top 10, can view torrent snatched list, edit torrent's description, original title and release date and access the advanced user search. Receives a new invite once per month (up to a maximum of 10 available invites).\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/artist.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntry\": [{\n    \"entry\": \"/torrents.php?searchstr=$key$&searchsubmit=1\",\n    \"name\": \"all\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table.torrent_table:last > tbody > tr\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"strong:contains('Freeleech!')\"\n  }],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&filter_cat[$id$]=1\",\n    \"category\": [{\n      \"id\": 1,\n      \"name\": \"Album\"\n    }, {\n      \"id\": 2,\n      \"name\": \"Single\"\n    }, {\n      \"id\": 3,\n      \"name\": \"PV\"\n    }, {\n      \"id\": 4,\n      \"name\": \"DVD\"\n    }, {\n      \"id\": 5,\n      \"name\": \"TV-Music\"\n    }, {\n      \"id\": 6,\n      \"name\": \"TV-Variety\"\n    }, {\n      \"id\": 7,\n      \"name\": \"TV-Drama\"\n    }, {\n      \"id\": 8,\n      \"name\": \"Fansubs\"\n    }, {\n      \"id\": 9,\n      \"name\": \"Pictures\"\n    }, {\n      \"id\": 10,\n      \"name\": \"Misc\"\n    }]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n\t    \"uploaded\": {\n          \"selector\": \"div:contains('Stats') + ul.stats > li:contains('Uploaded'), div:contains('統計情報') + ul.stats > li:contains('アップロード数')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/[\\\\d.]+ ?[ZEPTGMK]?i?B/)\", \" query ?(query[0]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": \"div:contains('Stats') + ul.stats > li:contains('Downloaded'), div:contains('統計情報') + ul.stats > li:contains('Downloaded')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Downloaded.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"seeding\": {\n          \"selector\": \"div:contains('Community') + ul.stats > li:contains('Seeding:'), div:contains('コミュニティ') + ul.stats > li:contains('シード中')\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ?query[0]:null\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div:contains('Stats') + ul.stats > li:contains('Bonus Points:')\", \"div:contains('統計情報') + ul.stats > li:contains('ボーナスポイント')\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"query.match(/Bonus Points.+?([\\\\d.]+)/)||query.match(/ボーナスポイント.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"div:contains('Personal') + ul.stats > li:contains('Class:'), div:contains('個人情報') + ul.stats > li:contains('階級:')\",\n          \"filters\": [\"query.text().match(/(Class:|階級:).+?(.+)/)\", \"(query && query.length>=2)?query[2]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"div:contains('Stats') + ul.stats > li:contains('Joined:') > span, div:contains('統計情報') + ul.stats > li:contains('Joined:') > span\"],\n          \"filters\": [\"query.attr('title')||query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"#alerts > .alertbar > a[href='notice.php']\", \"div.alertbar > a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"uploads\": {\n          \"selector\": \"div.box:eq(5) > div.head + ul.stats > li:eq(3)\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ? query[0] : null\"]\n        },\n        \"downloads\": {\n          \"selector\": \"div.box:eq(5) > div.head + ul.stats > li:eq(7)\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ? query[0] : null\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/torrents.php?type=seeding&userid=$user.id$\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"tr.torrent\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(5)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"page\": \"/bonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"h3:contains('Bonus Points') + div.box > strong:contains('/')\",\"h3:contains('ボーナスポイント') + div.box > strong:contains('/')\"],\n          \"filters\": [\"parseFloat(query.text().split('/')[0])\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/jpopsuki.eu/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/You will be banned for 6 hours after your login attempts run out/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(options.resultSelector);\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: -1,\n        size: -1,\n        seeders: -1,\n        leechers: -1,\n        completed: -1,\n        comments: -1,\n        author: -1,\n        category: 1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        const cell = header.eq(index);\n\n        // 发布时间\n        if (cell.find(\"a[href*='order_by=s3']\").length) {\n          fieldIndex.time = index;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"a[href*='order_by=s4']\").length) {\n          fieldIndex.size = index;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[href*='order_by=s6']\").length) {\n          fieldIndex.seeders = index;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[href*='order_by=s7']\").length) {\n          fieldIndex.leechers = index;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[href*='order_by=s5']\").length) {\n          fieldIndex.completed = index;\n          continue;\n        }\n      }\n\n      try {\n        let albumRow = null;\n        let albumTitle = null;\n        // 遍历数据行\n        for (let index = 0; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n          let subTitle = \"\";\n\n          let title = row.find(\"a[href*='torrents.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          // 判断行类型\n          switch (true) {\n            // 专辑行\n            // 仅获取标题即可\n            case row.is(\".group_redline\"):\n              albumRow = row;\n              albumTitle = title;\n              continue;\n\n            // 专辑对应的不同格式行\n            case row.is(\".group_torrent_redline\"):\n              let tmpRow = row.clone().get(0);\n              // 补全前面的单元格，使后续的 fieldIndex 索引位置生效\n              tmpRow.insertCell(0);\n              tmpRow.insertCell(0);\n              tmpRow.insertCell(0);\n              cells = $(tmpRow).find(\">td\");\n              subTitle = title.text();\n              break;\n\n            // 单种行\n            case row.is(\".torrent_redline\"):\n              albumRow = row;\n              albumTitle = title;\n              break;\n\n            default:\n              continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row\n            .find(\"a[href*='torrents.php?action=download'][title='Download']\")\n            .first();\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          url = url.attr(\"href\");\n\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          title = albumTitle.parent();\n          title.find(\">span, div.tags, a[title='View Comments']\").remove();\n          let time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells.eq(fieldIndex.time).attr(\"title\") ||\n                cells.eq(fieldIndex.time).text() ||\n                \"\";\n          if (time) {\n            time += \":00\";\n          }\n\n          let data = {\n            title: title\n              .text()\n              .trim()\n              .replace(\"()\", \"\"),\n            link,\n            subTitle: subTitle,\n            url: url,\n            size: cells.eq(fieldIndex.size).html() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            tags: Searcher.getRowTags(site, row),\n            site: site,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(albumRow.find(\">td\").eq(fieldIndex.category))\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.message}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      if (!cell) {\n        return result;\n      }\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      if (img.length) {\n        result.name = img.attr(\"title\");\n      } else {\n        result.name = link.text();\n      }\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/jpopsuki.eu/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 1\n      };\n      this.result = {\n        seedingSize: 0,\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='torrents.php?page=']:contains('Last'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 2;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 1) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/jpopsuki.eu/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"tr:not(.filter_hide) > td > span > a[title='Download']\").toArray();\n\n      if (links.length == 0) {\n        // 排除使用免费令牌的链接\n        links = $(\n          \"a[href*='torrents.php?action=download']:not([href*='usetoken'])\"\n        ).toArray();\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table, .torrent_table tr.basic-movie-list__torrent-row\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB'),td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n        )\n      );\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      let authkey = data.url.getQueryString(\"authkey\");\n      let torrent_pass = data.url.getQueryString(\"torrent_pass\");\n      // authkey=&torrent_pass\n      if (!authkey && !torrent_pass) {\n        PTService.showNotice({\n          msg: this.t(\"dropInvalidURL\") //\"无效的链接，请拖放下载链接\"\n        });\n        callback();\n        return;\n      }\n\n      data.url = this.getFullURL(data.url);\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/jptv.club/config.json",
    "content": "{\n    \"name\": \"JPTV\",\n    \"timezoneOffset\": \"+0800\",\n    \"schema\": \"UNIT3D\",\n    \"url\": \"https://jptv.club/\",\n    \"description\": \"JPTV\",\n    \"tags\": [\n        \"影视\",\n        \"剧集\",\n        \"动漫\"\n    ],\n    \"host\": \"jptv.club\",\n    \"levelRequirements\": [{\n      \"level\": \"1\", \n      \"name\": \"PowerUser\",\n      \"interval\": \"1\",\n      \"uploaded\": \"1TB\",\n      \"privilege\": \"Invite forums\"\n    },{\n      \"level\": \"2\", \n      \"name\": \"SuperUser\",\n      \"interval\": \"2\",\n      \"uploaded\": \"5TB\",\n      \"privilege\": \"\"\n    },{\n      \"level\": \"3\", \n      \"name\": \"ExtremeUser\",\n      \"interval\": \"3\",\n      \"uploaded\": \"20TB\",\n      \"privilege\": \"Prune Immunity\"\n    },{\n      \"level\": \"4\", \n      \"name\": \"InsaneUser\",\n      \"interval\": \"6\",\n      \"uploaded\": \"50TB\",\n      \"privilege\": \"\"\n    },{\n      \"level\": \"5\", \n      \"name\": \"Veteran\",\n      \"interval\": \"12\",\n      \"uploaded\": \"15TB\",\n      \"privilege\": \"Special FL\"\n    }],\n    \"collaborator\": \"MewX\",\n    \"searchEntryConfig\": {\n    \"page\": \"/torrents/filter\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/sites/asiancinema.me/getSearchResult.js\",\n    \"resultSelector\": \"div.table-responsive > table:first\",\n    \"queryString\": \"search=$key$\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"imdb=$key$\",\n      \"replaceKey\": [\n        \"tt\", \"\"\n      ]\n    }]\n  }\n}\n"
  },
  {
    "path": "resource/sites/jptv.club/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/\\/login/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      site.searchEntryConfig = options.entry\n      let selector =\n        options.resultSelector || \"div.table-responsive > table:first\";\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: header.length - 1,\n        // 分类\n        category: 1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n\n        // 评论数\n        if (cell.find(\"a[href*='comments']\").length) {\n          fieldIndex.comments = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 发布时间\n        if (\n          cell.find(\"a[href*='created_at']\").length ||\n          cell.find(\"i.fa-clock\").length\n        ) {\n          fieldIndex.time = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 大小\n        if (\n          cell.find(\"a[href*='size']\").length ||\n          cell.find(\"i.fa-file\").length\n        ) {\n          fieldIndex.size = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 种子数\n        if (\n          cell.find(\"a[href*='seed']\").length ||\n          cell.find(\"i.fa-arrow-circle-up\").length\n        ) {\n          fieldIndex.seeders = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 下载数\n        if (\n          cell.find(\"a[href*='leech']\").length ||\n          cell.find(\"i.fa-arrow-circle-down\").length\n        ) {\n          fieldIndex.leechers = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 完成数\n        if (\n          cell.find(\"a[href*='complete']\").length ||\n          cell.find(\"i.fa-check-square\").length\n        ) {\n          fieldIndex.completed = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 分类\n        if (cell.is(\".torrents-icon\")) {\n          fieldIndex.category = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a.view-torrent\");\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = \"\";\n\n          let downloadURL = row.find(\"a[href*='/download/']\");\n          if (downloadURL.length == 0) {\n            downloadURL = row.find(\"a[href*='/download_check/']\");\n            if (downloadURL.length > 0) {\n              url = downloadURL\n                .attr(\"href\")\n                .replace(\"/download_check/\", \"/download/\");\n            }\n          } else {\n            url = downloadURL.attr(\"href\");\n          }\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: this.getSubTitle(title, row),\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time:\n              fieldIndex.time == -1\n                ? \"\"\n                : cells\n                    .eq(fieldIndex.time)\n                    .find(\"span[title]\")\n                    .attr(\"title\") ||\n                  cells.eq(fieldIndex.time).text().replace('秒前', ' seconds ago').replace('秒前', ' seconds ago').replace('分钟前', ' minutes ago').replace('分鐘前', ' minutes ago').replace('天前', ' day ago').replace('小時前', ' hours ago').replace('小时前', ' hours ago').replace('周前', ' weeks ago').replace('个月前', ' months ago').replace('年前', ' years ago').replace('年', ' years ago')||\n                  \"\",\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            tags: Searcher.getRowTags(site, row),\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category)),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        selectors.forEach(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n            }\n          }\n        });\n      }\n      return tags;\n    }\n\n    /**\n     * 获取副标题\n     * @param {*} title\n     * @param {*} row\n     */\n    getSubTitle(title, row) {\n      return \"\";\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: cell.find(\"i:first\").attr(\"data-original-title\"),\n        link: cell.find(\"a:first\").attr(\"href\")\n      };\n      if (result.name) {\n        result.name = result.name.replace(\" torrent\", \"\");\n      }\n      return result;\n    }\n\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/jptvts.us/config.json",
    "content": "{\n  \"name\": \"jptvts.us\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"JPTVTS\",\n  \"icon\": \"https://jptvts.us/themes/default/images/favicon.ico\",\n  \"url\": \"https://jptvts.us/\",\n  \"tags\": [\"日剧\", \"综艺\"],\n  \"schema\": \"Common\",\n  \"plugins\": [\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [\"/torrents-details.php\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n    },\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\"/torrents-today.php\", \"/torrents-search.php\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n    }\n  ],\n  \"host\": \"jptvts.us\",\n  \"searchEntryConfig\": {\n    \"page\": \"/get_ttable.php?pcat=Show+All&subbed=&fl=&resd=&p=0&searchstr=$key$&deadlive=1&sortcol=id&sortorder=desc&startdt=&enddt=\",\n    \"loggedRegex\": \"class=\\\"ttable_headinner\\\"\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table\",\n    \"fieldIndex\": {\n      \"category\": 0,\n      \"title\": 1,\n      \"link\": 1,\n      \"url\": 2,\n      \"comments\": 5,\n      \"time\": 10,\n      \"size\": 6,\n      \"author\": 4,\n      \"seeders\": 7,\n      \"leechers\": 8,\n      \"completed\": 9\n    },\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"a\"],\n        \"filters\": [\"query.text()\"]\n      },\n      \"link\": {\n        \"selector\": [\"a\"],\n        \"filters\": [\"query.attr('href')\", \"'https://jptvts.us/'+query\"]\n      },\n      \"url\": {\n        \"selector\": [\"\"],\n        \"filters\": [\n          \"query.children().attr('href')\",\n          \"'https://jptvts.us/'+query\"\n        ]\n      },\n      \"time\": {\n        \"selector\": [\"\"],\n        \"filters\": [\"'20'+query.text()\"]\n      },\n      \"progress\": {\n        \"selector\": [\n          \"td.ttable_seeding font[color='green'], td.ttable_seeding font[color='black']\",\n          \"td.ttable_seeding font[color='#ff0000']\",\n          \"\"\n        ],\n        \"switchFilters\": [\n          [\"query.length > 0 ? 100:null\"],\n          [\"query.length > 0 ? 0:null\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"td.ttable_seeding font[color='green']\",\n          \"td.ttable_seeding font[color='black']\",\n          \"td.ttable_seeding font[color='#ff0000']\"\n        ],\n        \"switchFilters\": [[\"2\"], [\"255\"], [\"1\"]]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    }\n  ],\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \"img[src='images/freeleech.png']\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"#main > table .myBlock-caption:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='account-logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href*='/forum/private.php']\"],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/)\",\n            \"(query && query.length>=2)?parseInt(query[1]):0\"\n          ]\n        },\n        \"uploaded\": {\n          \"selector\": [\".myBlock-content td:contains('Uploaded:') + td\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\".myBlock-content td:contains('Downloaded:') + td\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"ratio\": {\n          \"selector\": [\".myBlock-content td:contains('Ratio:') + td\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"levelName\": {\n          \"selector\": [\".myBlock-content td:contains('Class:') + td\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/account.php\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"td.prof-lbl:contains('Joined:') + td\"],\n          \"filters\": [\"dateTime(query.text()).valueOf()\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"b:contains('Currently seeding')\"],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/)\",\n            \"(query && query.length>=2)?parseInt(query[1]):null\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": [\n            \"b:contains('Currently seeding') + br + table tr:not(:first-child) > td:nth-child(4)\"\n          ],\n          \"filters\": [\n            \"jQuery.map(query, (item)=>{return $(item).text();})\",\n            \"_self.getTotalSize(query)\"\n          ]\n        }\n      }\n    },\n    \"common\": {\n      \"page\": \"/torrents-details.php\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td[align='left']:contains('Total Size:') + td\"],\n          \"filters\": [\n            \"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>1)?(query[1]).sizeToNumber():0\"\n          ]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#ty-button\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.ttable_headinner\"],\n          \"filters\": [\"query.find('td.ttable_size')\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/kamept.com/config.json",
    "content": "{\n    \"name\": \"kamept \",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"kamept\",\n    \"url\": \"https://kamept.com/\",\n    \"icon\": \"https://kamept.com/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"kamept.com\",\n    \"collaborator\": [\n        \"koal\",\n        \"amorphobia\",\n        \"IITII\"\n    ],\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"40000\",\n            \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以进入论坛的“PT交流区”板块；可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"150000\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"250000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到两个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"得到两个邀请名额；可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/karagarga.in/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"table#browse:last a[href*='down.php']\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table#browse:last\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      if (url.indexOf(\"down.php\") === -1) {\n        return \"\";\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/karagarga.in/config.json",
    "content": "{\n  \"name\": \"KaraGarga\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"KG\",\n  \"url\": \"https://karagarga.in/\",\n  \"icon\": \"https://karagarga.in/favicon.ico\",\n  \"tags\": [\"影视\", \"音乐\", \"文学\"],\n  \"schema\": \"karagarga\",\n  \"host\": \"karagarga.in\",\n  \"collaborator\": \"luckiestone\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Power User\",\n      \"interval\": \"13\",\n      \"uploaded\": \"50GB\",\n      \"ratio\": \"1.05\"\n    }\n  ],\n\"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search=$key$&search_type=torrent\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table[id='browse']:last > tbody > tr\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"replaceKey\": [\"tt\", \"\"],\n      \"queryString\": \"search_type=imdb&search=$key$\"\n    }]\n  },\n  \"searchEntry\": [{\n    \"name\": \"All\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[title='click to see your details page']:last\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[title='click to see your details page']:last\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background: #DF0101'] a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,|\\\\r|\\\\n|\\\\s/g,'').match(/.*?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,|\\\\r|\\\\n|\\\\s/g,'').match(/.*?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.rowhead:contains('Share ratio') + td > table > tbody > tr > td:nth-child(1) > font\",\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('Class') + td\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('Join'):contains('date') + td\"],\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/current.php?id=$user.id$\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"table[id='browse'] > tbody > tr[style*='padding-top:0px']\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"table[id='browse'] > tbody > tr[style*='padding-top:0px']\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(9)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n\n  }\n}"
  },
  {
    "path": "resource/sites/karagarga.in/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a.index[href*='down.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return `${location.origin}${url}`;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td.heading:contains('Size') + td\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().match(/^[^\\(]+/);\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\"table.main h1:first\")\n        .text()\n        .trim();\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/karagarga.in/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table[id='browse'] > tbody > tr[style*='padding-top:0px']\"\n      );\n      let time_regex = /([A-Za-z]{3})\\s(\\d+)\\s'(\\d{2})/;\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        //title\n        title: 1,\n        //downloadlink\n        downloadlink: 1,\n        // 时间\n        time: 8,\n        // 大小\n        size: 10,\n        // 上传人数\n        seeders: 12,\n        // 下载人数\n        leechers: 13,\n        // 完成人数\n        completed: 11,\n        // 评论人数\n        comments: 6,\n        // 发布人\n        author: 7,\n        category: 0\n      };\n\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n\n      // 遍历数据行\n      for (let index = 1; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n        let title = cells.eq(fieldIndex.title).find(\"a[href*='details.php?id=']\").first();\n        if (title.length == 0) {\n          continue;\n        }\n        let titleStrings = title.text();\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}/${link}`;\n        }\n        let url = \"\";\n        url = cells.eq(fieldIndex.downloadlink).find(\"a[href*='/down.php/']\").first().attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}/${url}`;\n          }\n\n        if (!url) {\n          continue;\n        }\n        let time = cells.eq(fieldIndex.time).text().match(time_regex)[1];\n        if(RegExp.$1 == \"Jan\") {\n          time = \"20\"+RegExp.$3+\"-01-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Feb\") {\n          time = \"20\"+RegExp.$3+\"-02-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Mar\") {\n          time = \"20\"+RegExp.$3+\"-03-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Apr\") {\n          time = \"20\"+RegExp.$3+\"-04-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"May\") {\n          time = \"20\"+RegExp.$3+\"-05-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Jun\") {\n          time = \"20\"+RegExp.$3+\"-06-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Jul\") {\n          time = \"20\"+RegExp.$3+\"-07-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Aug\") {\n          time = \"20\"+RegExp.$3+\"-08-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Sep\") {\n          time = \"20\"+RegExp.$3+\"-09-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Oct\") {\n          time = \"20\"+RegExp.$3+\"-10-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Nov\") {\n          time = \"20\"+RegExp.$3+\"-11-\"+RegExp.$2+\" 00:00\";\n        }\n        if(RegExp.$1 == \"Dec\") {\n          time = \"20\"+RegExp.$3+\"-12-\"+RegExp.$2+\" 00:00\";\n        }\n        let data = {\n          title: titleStrings,\n          link: link,\n          url: url,\n          size: cells.eq(fieldIndex.size).text() || 0,\n          time: time,\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text(),\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text(),\n          completed: cells.eq(fieldIndex.completed).text().match(/(\\d+)/)[0] || 0,\n          comments: cells.eq(fieldIndex.comments).find(\"a[href*='#startcomments']\").text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: this.getTags(row, options.torrentTagSelectors)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"title\").match(/[^::]+/)[0];\n      return result;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        // 使用 some 避免错误的背景类名返回多个标签\n        selectors.some(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n              return true;\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/kp.m-team.cc/config.json",
    "content": "{\n  \"name\": \"M-Team\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"M-Team\",\n  \"url\": \"https://kp.m-team.cc/\",\n  \"icon\": \"https://kp.m-team.cc/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\",\"Adult\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"kp.m-team.cc\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"2\",\n    \"privilege\": \"魔力值加成：+1%；可以使用匿名發表候選種子；可以上傳字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"400GB\",\n    \"ratio\": \"3\",\n    \"privilege\": \"魔力值加成：+2%；可以發送邀請；可以管理自己上傳的字幕；可以檢視別人的下載紀錄（當對方的隱私權設定不為強才會生效）；可以使用個性條\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"12\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"4\",\n    \"privilege\": \"魔力值加成：+3%\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"16\",\n    \"downloaded\": \"800GB\",\n    \"ratio\": \"5\",\n    \"privilege\": \"魔力值加成：+4%；可以檢視排行榜\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1000GB\",\n    \"ratio\": \"6\",\n    \"privilege\": \"魔力值加成：+5%\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"24\",\n    \"downloaded\": \"2000GB\",\n    \"ratio\": \"7\",\n    \"privilege\": \"魔力值加成：+6%\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"28\",\n    \"downloaded\": \"2500GB\",\n    \"ratio\": \"8\",\n    \"privilege\": \"魔力值加成：+7%\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"32\",\n    \"downloaded\": \"3000GB\",\n    \"ratio\": \"9\",\n    \"privilege\": \"魔力值加成：+8%\"\n  }],\n  \"formerHosts\": [\n    \"pt.m-team.cc\",\n    \"tp.m-team.cc\"\n  ],\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/adult.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }, {\n    \"name\": \"种子列表封面模式\",\n    \"pages\": [\"/torrents.php\", \"/movie.php\", \"/music.php\", \"/adult.php\"],\n    \"scripts\": [\"/libs/album/album.js\", \"torrents.js\"],\n    \"styles\": [\"/libs/album/style.css\"]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td:eq(8)\"],\n        \"filters\": [\"query.text()==='--'?null:parseFloat(query.text())\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td:eq(8)\"],\n        \"filters\": [\"query.text()==='--'?null:query.is('.peer-active')?(parseFloat(query.text())==100?2:1):(parseFloat(query.text())==100?255:3)\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"综合\",\n    \"enabled\": true\n  }, {\n    \"entry\": \"/movie.php?search=$key$&notnewword=1\",\n    \"name\": \"电影\",\n    \"enabled\": false\n  }, {\n    \"entry\": \"/music.php?search=$key$&notnewword=1\",\n    \"name\": \"音乐\",\n    \"enabled\": true\n  }, {\n    \"entry\": \"/adult.php?search=$key$&notnewword=1\",\n    \"name\": \"adult\",\n    \"enabled\": true\n  }],\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n      \"id\": 401,\n      \"name\": \"Movie(電影)/SD\"\n    }, {\n      \"id\": 419,\n      \"name\": \"Movie(電影)/HD\"\n    }, {\n      \"id\": 420,\n      \"name\": \"Movie(電影)/DVDiSo\"\n    }, {\n      \"id\": 421,\n      \"name\": \"Movie(電影)/Blu-Ray\"\n    }, {\n      \"id\": 439,\n      \"name\": \"Movie(電影)/Remux\"\n    }, {\n      \"id\": 403,\n      \"name\": \"TV Series(影劇/綜藝)/SD\"\n    }, {\n      \"id\": 402,\n      \"name\": \"TV Series(影劇/綜藝)/HD\"\n    }, {\n      \"id\": 435,\n      \"name\": \"TV Series(影劇/綜藝)/DVDiSo\"\n    }, {\n      \"id\": 438,\n      \"name\": \"TV Series(影劇/綜藝)/BD\"\n    }, {\n      \"id\": 404,\n      \"name\": \"紀錄教育\"\n    }, {\n      \"id\": 405,\n      \"name\": \"Anime(動畫)\"\n    }, {\n      \"id\": 407,\n      \"name\": \"Sports(運動)\"\n    }, {\n      \"id\": 422,\n      \"name\": \"Software(軟體)\"\n    }, {\n      \"id\": 423,\n      \"name\": \"PCGame(PC遊戲)\"\n    }, {\n      \"id\": 427,\n      \"name\": \"eBook(電子書)\"\n    }, {\n      \"id\": 409,\n      \"name\": \"Misc(其他)\"\n    }]\n  }, {\n    \"entry\": \"movie.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n      \"id\": 401,\n      \"name\": \"Movie(電影)/SD\"\n    }, {\n      \"id\": 419,\n      \"name\": \"Movie(電影)/HD\"\n    }, {\n      \"id\": 420,\n      \"name\": \"Movie(電影)/DVDiSo\"\n    }, {\n      \"id\": 421,\n      \"name\": \"Movie(電影)/Blu-Ray\"\n    }, {\n      \"id\": 439,\n      \"name\": \"Movie(電影)/Remux\"\n    }, {\n      \"id\": 404,\n      \"name\": \"紀錄教育\"\n    }]\n  }, {\n    \"entry\": \"music.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n      \"id\": 406,\n      \"name\": \"MV(演唱)\"\n    }, {\n      \"id\": 408,\n      \"name\": \"Music(AAC/ALAC)\"\n    }, {\n      \"id\": 434,\n      \"name\": \"Music(無損)\"\n    }]\n  }, {\n    \"entry\": \"adult.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n      \"id\": 410,\n      \"name\": \"AV(有碼)/HD Censored\"\n    }, {\n      \"id\": 429,\n      \"name\": \"AV(無碼)/HD Uncensored\"\n    }, {\n      \"id\": 424,\n      \"name\": \"AV(有碼)/SD Censored\"\n    }, {\n      \"id\": 430,\n      \"name\": \"AV(無碼)/SD Uncensored\"\n    }, {\n      \"id\": 426,\n      \"name\": \"AV(無碼)/DVDiSo Uncensored\"\n    }, {\n      \"id\": 437,\n      \"name\": \"AV(有碼)/DVDiSo Censored\"\n    }, {\n      \"id\": 431,\n      \"name\": \"AV(有碼)/Blu-Ray Censored\"\n    }, {\n      \"id\": 432,\n      \"name\": \"AV(無碼)/Blu-Ray Uncensored\"\n    }, {\n      \"id\": 436,\n      \"name\": \"AV(網站)/0Day\"\n    }, {\n      \"id\": 425,\n      \"name\": \"IV(寫真影集)/Video Collection\"\n    }, {\n      \"id\": 433,\n      \"name\": \"IV(寫真圖集)/Picture Collection\"\n    }, {\n      \"id\": 411,\n      \"name\": \"H-Game(遊戲)\"\n    }, {\n      \"id\": 412,\n      \"name\": \"H-Anime(動畫)\"\n    }, {\n      \"id\": 413,\n      \"name\": \"H-Comic(漫畫)\"\n    }]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowfollow:contains('分享率')\", \"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(?:上[传傳]量|Uploaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowfollow:contains('分享率')\", \"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(?:下[载載]量|Downloaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlist.php?userid=$user.id$&type=seeding\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"query.find('td.rowfollow:eq(2)').length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td.rowfollow:eq(2)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/mybonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"td:contains('您目前每小時合計可獲得'):last\",\n                       \"td:contains('您目前每小时合计可获得'):last\",\n                       \"td:contains('目前將會獲取'):last\",\n                       \"td:contains('目前将会获取'):last\"],\n          \"filters\": [\"parseFloat(query.text().match(/[獲获][得取](\\\\d+(?:\\\\.\\\\d+)?)/)[1])\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/kp.m-team.cc/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 0\n      };\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seeding += results.seeding;\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='type=seeding']:contains('1'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 1;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/kp.m-team.cc/torrents.js",
    "content": "(function($, window) {\n  // 添加封面模式\n  PTService.addButton({\n    title: PTService.i18n.t(\"buttons.coverTip\"), //\"以封面的方式进行查看\",\n    icon: \"photo\",\n    label: PTService.i18n.t(\"buttons.cover\"), //\"封面模式\",\n    click: (success, error) => {\n      // 获取目标表格\n      let tables = $(\"table.torrentname\");\n      let images = [];\n      tables.each((index, item) => {\n        let img = $(\"img[onmouseover]\", item);\n        let url = img.attr(\"src\");\n        let href = img.parent().attr(\"href\");\n        let title = $(\"td.embedded\", item).text();\n        images.push({\n          url: url,\n          key: href,\n          title: title, //img.parent().attr(\"title\"),\n          link: img.parent().attr(\"href\")\n        });\n      });\n\n      // 创建预览\n      new album({\n        images: images,\n        onClose: () => {\n          PTService.buttonBar.show();\n        }\n      });\n      success();\n      PTService.buttonBar.hide();\n    }\n  });\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/learnflakes.net/config.json",
    "content": "{\n  \"name\": \"Learn Flakes\",\n  \"timezoneOffset\": \"+0000\",\n  \"url\": \"https://learnflakes.net/\",\n  \"description\": \"Learnflakes is a private torrent tracker, opened in 2013, which specializes in educational materials on computer and Internet topics\",\n  \"icon\": \"https://learnflakes.net/favicon.ico\",\n  \"tags\": [\"学习\"],\n  \"schema\": \"Common\",\n  \"collaborator\": [\n    \"fzlins\"\n  ],\n  \"host\": \"learnflakes.net\",\n  \"searchEntryConfig\": {\n    \"page\": \"/\",\n    \"queryString\": \"p=torrents&pid=10&keywords=$key$&search_type=name\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"div#plugins > div#content\",\n    \"dataRowSelector\": \" > div.torrent-box[id^='torrent_']\",\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [ \"strong.newIndicator > a\" ]\n      },\n      \"link\": {\n        \"selector\": [ \"strong.newIndicator > a\" ],\n        \"attribute\": \"href\"\n      },\n      \"url\": {\n        \"selector\": [ \"div.torrentImages a\" ],\n        \"attribute\": \"href\"\n      },\n      \"time\": {\n        \"selector\": [ \".torrentOwner\" ],\n        \"filters\": [\"query.text().substring(9,25)\", \"dateTime(query, 'DD-MM-YYYY HH:mm').isValid() ? dateTime(query, 'DD-MM-YYYY HH:mm').format('YYYY-MM-DD HH:mm') : query\"]\n      },\n      \"size\": {\n        \"selector\": [ \"a[rel='torrent_size']\" ],\n        \"filters\": [\"query ? query.text().trim().sizeToNumber() : 0\"]\n      },\n      \"seeders\": {\n        \"selector\": [ \"a[rel='torrent_seeders']\" ]\n      },\n      \"leechers\": {\n        \"selector\": [ \"a[rel='torrent_leechers']\" ]\n      },\n      \"completed\": {\n        \"selector\": [ \"a[rel='times_completed']\" ]\n      }\n    }\n  },\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"div.torrent-box\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"isLogged\": {\n          \"selector\": [ \"a#logout\" ],\n          \"filters\": [ \"query.length>0\" ]\n        },\n        \"id\": {\n          \"selector\": [\"#sidebar a[href*='profile']\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('memberid'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"#sidebar a[href*='profile']\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a.a.showmenu.new\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\".showStats a[href*='p=market']\"]\n        },\n        \"bonusPerHour\": {\n          \"value\": \"N/A\"\n        },\n        \"seeding\": {\n          \"value\": \"N/A\"\n        },\n        \"seedingSize\": {\n          \"value\": \"N/A\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/?p=profile&pid=18&memberid=$user.id$\",\n      \"fields\": {\n        \"levelName\": {\n          \"selector\": \".memberCardDetails > span\"\n        },\n        \"uploaded\": {\n          \"selector\": [\"#memberinfoUpDownStats\"],\n          \"filters\": [\"query.text().trim().split('\\\\n\\\\t\\\\t\\\\t')\", \"(query && query.length > 1) ? (query[0].trim()).sizeToNumber() : 0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"#memberinfoUpDownStats\"],\n          \"filters\": [\"query.text().trim().split('\\\\n\\\\t\\\\t\\\\t')\", \"(query && query.length > 2) ? (query[1].trim()).sizeToNumber() : 0\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"#memberinfoUpDownStats\"],\n          \"filters\": [\"query.text().trim().split('\\\\n\\\\t\\\\t\\\\t')\", \"(query && query.length > 3) ? parseInt(query[2].trim()) : 0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"div.memberCardDetails\"],\n          \"filters\": [\"query.clone().children().remove().end().text().trim().split('\\\\n\\\\t\\\\t')\", \"(query && query.length>=2) ? dateTime(query[2], 'DD-MM-YYYY HH:mm').valueOf() : ''\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/leaves.red/config.json",
    "content": "{\n  \"name\": \"红叶\",\n  \"description\": \"红叶成立于2022年10月，主打有声小说，有综合区。目前站内设立有声官组,资源产出稳定。喜欢有声内容的朋友，欢迎你的加入\",\n  \"url\": \"https://leaves.red/\",\n  \"icon\": \"https://leaves.red/favicon.ico\",\n  \"tags\": [\"有声书\", \"综合\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"leaves.red\",\n  \"collaborator\": \"CosmoGao\",\n  \"plugins\": [\n    {\n        \"isCustom\": true,\n        \"name\": \"有声区\",\n        \"pages\": [\n            \"/special.php\"\n        ],\n        \"readonly\": false,\n        \"script\": \"\",\n        \"scripts\": [\n            \"/schemas/nexusPHP/common.js\",\n            \"/schemas/nexusPHP/torrents.js\"\n        ],\n        \"style\": \"\",\n        \"styles\": []\n    }\n],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\", \n    \"downloaded\": \"100GB\",\n    \"ratio\": \"1.05\",\n    \"seedingPoints\": \"60000\",\n    \"privilege\": \"首次升级PU将获得1个邀请 \"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\", \n    \"downloaded\": \"200GB\",\n    \"ratio\": \"1.55\",\n    \"seedingPoints\": \"120000\",\n    \"privilege\": \"Elite User及以上等级用户封存账号（在控制面板）后不会被禁用账号\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\", \n    \"downloaded\": \"400GB\",\n    \"ratio\": \"2.05\",\n    \"seedingPoints\": \"200000\",\n    \"privilege\": \"首次升级CU将分别2个邀请\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\", \n    \"downloaded\": \"800GB\",\n    \"ratio\": \"2.55\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \" \"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\", \n    \"downloaded\": \"1600GB\",\n    \"ratio\": \"3.05\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"Veteran User及以上等级用户会永远保留；首次升级VU将获得3个邀请\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\", \n    \"downloaded\": \"2400GB\",\n    \"ratio\": \"3.55\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \" \"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\", \n    \"downloaded\": \"3200GB\",\n    \"ratio\": \"4.05\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"首次升级UU将获得5邀请\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\", \n    \"downloaded\": \"4000GB\",\n    \"ratio\": \"4.55\",\n    \"seedingPoints\": \"2000000\",\n    \"privilege\": \"首次升级NM将获得10个邀请\"\n  }],\n  \"searchEntry\": [\n    {\n        \"entry\": \"/torrents.php?search=$key$&search_mode=2\",\n        \"name\": \"综合\",\n        \"enabled\": true\n    },\n    {\n        \"entry\": \"/special.php?search=$key$&search_mode=2\",\n        \"name\": \"有声\",\n        \"enabled\": true\n    }\n  ],\n  \"selectors\": {\n    \"merge\": true,\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)</g)\",\n            \"(query && query.length>0 ) ? query[0].replace('总大小：', '').replace('<', '').trim() : 0\",\n            \"(query != 0) ? _self.getTotalSize([query]) : 0\"\n          ]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/mybonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"#outer td[rowspan='3']\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/lztr.me/config.json",
    "content": "{\n  \"name\": \"LzTr\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"music\",\n  \"url\": \"https://lztr.me/\",\n  \"icon\": \"https://lztr.me/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"lztr.me\",\n  \"collaborator\": [\n    \"ylxb2016\",\n    \"amorphobia\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Member\",\n      \"interval\": \"1\",\n      \"uploaded\": \"10GB\",\n      \"ratio\": \"0.7\",\n      \"downloads\": \"1\",\n      \"privilege\": \"Can make requests, bookmarks, edit Collages, and can access the Top 10\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Power User\",\n      \"interval\": \"2\",\n      \"uploads\": \"5\",\n      \"uploaded\": \"25GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Receives invites, can access notifications, create new collages, access power user & invites forums.\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Elite\",\n      \"interval\": \"4\",\n      \"uploads\": \"50\",\n      \"uploaded\": \"100GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Top 10 filters\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Torrent Master\",\n      \"interval\": \"8\",\n      \"uploads\": \"200\",\n      \"uploaded\": \"200GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"Can invite users even when invites are closed, Can send unlimited invites\"\n    }\n  ],\n  \"selectors\": {\n    \"levelExtendInfo\": {\n      \"page\": \"/user.php?action=user_ajax&type=community&id=$user.id$\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": \"li:contains('Seeding:')\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ? query[0] : null\"]\n        },\n        \"uploads\": {\n          \"selector\": \"li:contains('Uploaded:')\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ? query[0] : null\"]\n        },\n        \"downloads\": {\n          \"selector\": \"li:contains('Snatched:')\",\n          \"filters\": [\"query.text().match(/[\\\\d.]+/)\", \" query ? query[0] : null\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/monikadesign.uk/config.json",
    "content": "{\n  \"name\": \"MDU\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"UNIT3D\",\n  \"url\": \"https://monikadesign.uk/\",\n  \"icon\": \"https://monikadesign.uk/favicon.ico\",\n  \"description\": \"一个以动画为主体，涵盖日语影视音乐的资源站，诣在为广大动画、日剧日影、ACG Live爱好者提供一个资源信息交流平台。\",\n  \"tags\": [\n      \"影视\",\n      \"剧集\",\n      \"动漫\"\n  ],\n  \"host\": \"monikadesign.uk\",\n  \"levelRequirements\": [{\n      \"level\": \"1\", \n      \"name\": \"PowerUser\",\n      \"interval\": \"4\",\n      \"uploaded\": \"1TB\",\n      \"privilege\": \"访问邀请区\"\n    },{\n      \"level\": \"2\", \n      \"name\": \"SuperUser\",\n      \"interval\": \"8\",\n      \"uploaded\": \"2TB\",\n      \"privilege\": \"无\"\n    },{\n      \"level\": \"3\", \n      \"name\": \"ExtremeUser\",\n      \"interval\": \"12\",\n      \"uploaded\": \"5TB\",\n      \"privilege\": \"无\"\n    },{\n      \"level\": \"4\", \n      \"name\": \"InsaneUser\",\n      \"interval\": \"18\",\n      \"uploaded\": \"10TB\",\n      \"privilege\": \"自动通过候选\"\n    },{\n      \"level\": \"5\", \n      \"name\": \"Veteran\",\n      \"interval\": \"36\",\n      \"uploaded\": \"15TB\",\n      \"privilege\": \"个人全局双倍上传\"\n    }],\n  \"collaborator\": \"fzlins\",\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"resultSelector\": \"#torrent-list-table\",\n    \"fieldSelector\": {\n      \"subTitle\": {\n        \"selector\": [\"td.torrent-listings-overview span:first\"]\n      }\n    }\n  },\n  \"selectors\": {\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!(!user.bonusPage)\",\n      \"page\": \"$user.bonusPage$\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\".panelV2 div.panel__body:first\"],\n          \"filters\": [\"parseFloat(query.text().split('：')[1].replace(',',''))\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/nanyangpt.com/config.json",
    "content": "{\n  \"name\": \"南洋PT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"网站由西安交通大学学生自主创建与管理，汇集学习资料、纪录片、电影、剧集等各类优质资源\",\n  \"url\": \"https://nanyangpt.com/\",\n  \"icon\": \"https://nanyangpt.com/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"nanyangpt.com\",\n  \"collaborator\": [\"Rhilip\",\"Yincircle\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"小小学士\",\n    \"interval\": \"2\",\n    \"downloaded\": \"30GB\",\n    \"ratio\": \"1.5\",\n    \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"优秀硕士\",\n    \"interval\": \"5\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"优秀硕士及以上用户封存账号后不会被删除。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"初为博士\",\n    \"interval\": \"10\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"3.5\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式，可以在邀请传送门版块发帖。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"海归博后\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"4.5\",\n    \"privilege\": \"可以查看普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"大学讲师\",\n    \"interval\": \"20\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"5.5\",\n    \"privilege\": \"可以查看排行榜；可以查看其它用户的评论、帖子历史。大学讲师及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"晋升副教\",\n    \"interval\": \"30\",\n    \"downloaded\": \"700GB\",\n    \"ratio\": \"6.5\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看晋升副教论坛。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"终身教授\",\n    \"interval\": \"80\",\n    \"downloaded\": \"900GB\",\n    \"ratio\": \"7.5\",\n    \"privilege\": \"更加高级。\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"荣誉院士\",\n    \"interval\": \"100\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"8.5\",\n    \"privilege\": \"更加高级。\"\n  }],\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"纪录\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"学习\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\".rowfollow[title='Downloading'], .rowfollow[title='Seeding'], .rowfollow[title='Stopped'], .rowfollow[title='Completed']\"],\n        \"filters\": [\"query.text()?query.text():null\"]\n      },\n      \"status\": {\n        \"selector\": [\".rowfollow[title='Downloading']\", \".rowfollow[title='Seeding']\", \".rowfollow[title='Stopped']\", \".rowfollow[title='Completed']\"],\n        \"switchFilters\": [\n          [\"1\"],\n          [\"2\"],\n          [\"3\"],\n          [\"255\"]\n        ]\n      }\n    }\n  },\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"剧集\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"综艺\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"纪录\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"音乐\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"学习\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"其他\"\n      }\n    ]\n  }],\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"td.embedded > a[title] > b > font[color='red']\"\n  }]\n}"
  },
  {
    "path": "resource/sites/nebulance.io/config.json",
    "content": "{\n  \"name\": \"Nebulance\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"NBL\",\n  \"url\": \"https://nebulance.io/\",\n  \"icon\": \"https://nebulance.io/favicon.ico\",\n  \"tags\": [\"剧集\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"nebulance.io\",\n  \"collaborator\": \"luckiestone\",\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"searchtext=$key$\"\n  },\n  \"searchEntry\": [{\n      \"name\": \"All\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Episodes\",\n      \"queryString\": \"filter_cat[1]=1\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"Season\",\n      \"queryString\": \"filter_cat[3]=1\",\n      \"enabled\": false\n    }\n  ],\n\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/ajax.php?action=index\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"response.id\"]\n        },\n        \"name\": {\n          \"selector\": [\"response.username\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"response.userstats.uploaded\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"response.userstats.downloaded\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"response.userstats.ratio\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"response.userstats.class\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [\"div.alertbar a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().replace(/\\\\s+/g,'').match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"seeding\": {\n          \"selector\": \"ul.stats.nobullet > li:contains('Seeding:')\",\n          \"filters\": [\"query.text().trim().replace(/,|\\\\n/g,'').match(/:.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?parseFloat(query[1]):0\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"ul.stats.nobullet > li:contains('Seeding Size:')\",\n          \"filters\": [\"query.text().trim().replace(/,/g,'').match(/Seeding Size:.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"bonus\": {\n          \"selector\": \"ul#userinfo_major > li > a:contains('Cubits:')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Cubits:.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"ul.stats.nobullet > li:contains('Joined:') > span\"],\n          \"filters\": [\"query.attr('title')||query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}"
  },
  {
    "path": "resource/sites/nebulance.io/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table.torrent_table:first > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: -1,\n        size: -1,\n        seeders: -1,\n        leechers: -1,\n        completed: -1,\n        comments: -1,\n        author: -1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        const cell = header.eq(index);\n\n        // 发布时间\n        if (cell.find(\"a[href*='order_by=time']\").length) {\n          fieldIndex.time = index;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"a[href*='order_by=size']\").length) {\n          fieldIndex.size = index;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[href*='order_by=seeders']\").length) {\n          fieldIndex.seeders = index;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[href*='order_by=leechers']\").length) {\n          fieldIndex.leechers = index;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[href*='order_by=snatched']\").length) {\n          fieldIndex.completed = index;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a[href*='torrents.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row\n            .find(\"a[href*='torrents.php?action=download']\")\n            .first();\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          url = url.attr(\"href\");\n\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells.eq(fieldIndex.time).attr(\"title\") ||\n                cells.eq(fieldIndex.time).text() ||\n                \"\";\n          if (time) {\n            time += \":00\";\n          }\n\n          let data = {\n            title: title.attr(\"data-src\"),\n            link,\n            url: url,\n            size: cells.eq(fieldIndex.size).find(\"div\").first().text().replace(/,/g,'').trim() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name,\n            category: \n              fieldIndex.category == -1\n                ? null\n                : this.getCategory(cells.eq(fieldIndex.category))\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} link 当前列\n     */\n    getCategory(link) {\n      if (link.length == 0) {\n        return null;\n      }\n\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n\n      result.link = link.attr(\"href\");\n      let id = result.link.match(/filter_cat\\[(\\d+)\\]/)[1];\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = link.text().trim();\n\n      if (!result.name) {\n        result.name = this.getCategoryName(id);\n      }\n      return result;\n    }\n\n    getCategoryName(id) {\n      if ($.isEmptyObject(this.categories)) {\n        let cells = options.page.find(\".cat_list:first\").find(\"td\");\n        cells.each((i, dom) => {\n          let id = $(dom)\n            .find(\"input\")\n            .attr(\"id\")\n            .replace(\"cat_\", \"\");\n          let name = $(dom)\n            .find(\"label\")\n            .text();\n          if (id) {\n            this.categories[id] = name;\n          }\n        });\n      }\n\n      return this.categories ? this.categories[id] : \"\";\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/nicept.net/config.json",
    "content": "{\n  \"name\": \"NicePT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"老师站，又称小馒头\",\n  \"url\": \"https://www.nicept.net/\",\n  \"icon\": \"https://www.nicept.net/favicon.ico\",\n  \"tags\": [\n    \"Adult\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"www.nicept.net\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"seedingPoints\": \"40000\",\n      \"privilege\": \"得到一個邀請名額；可以直接發布種子；可以檢視NFO文件；可以檢視用戶清單；可以要求續種； 可以傳送邀請； 可以檢視排行榜；可以檢視其他用戶的種子曆史(如果用戶隱私等級未設定為\\\"強\\\")； 可以移除自己上傳的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"seedingPoints\": \"80000\",\n      \"privilege\": \"Elite User及以上用戶封存賬號后不會被移除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"seedingPoints\": \"150000\",\n      \"privilege\": \"得到兩個邀請名額；可以在做種/下載/發布的時候選取匿名型態。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\",\n      \"seedingPoints\": \"250000\",\n      \"privilege\": \"可以檢視普通日誌。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\",\n      \"seedingPoints\": \"400000\",\n      \"privilege\": \"得到三個邀請名額；可以檢視其他用戶的評論、帖子曆史。Veteran User及以上用戶會永遠保留賬號。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\",\n      \"seedingPoints\": \"600000\",\n      \"privilege\": \"可以更新過期的外部資訊；可以檢視Extreme User論壇。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\",\n      \"seedingPoints\": \"800000\",\n      \"privilege\": \"得到五個邀請名額。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"downloaded\": \"3TB\",\n      \"ratio\": \"4.55\",\n      \"seedingPoints\": \"1000000\",\n      \"privilege\": \"得到十個邀請名額。\"\n    }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n        \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n        \"fields\": {\n            \"seeding\": {\n                \"selector\": [\n                    \"b:first\"\n                ],\n                \"filters\": [\n                    \"query.text()\"\n                ]\n            },\n            \"seedingSize\": {\n                \"selector\": \"\",\n                \"filters\": [\n                    \"query.text().match(/總大小：(.*?)上一頁/g)\",\n                    \"(query && query.length>0) ? query[0].replace('總大小：', '').replace('<< 上一頁', '').trim() : 0\",\n                    \"(query != 0) ? query.sizeToNumber() : 0\"\n                ]\n            }\n        }\n    }\n  },\n  \"collaborator\": \"DXV5\",\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"无码（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat500=1\",\n      \"name\": \"有码（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"三级情色（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat501=1\",\n      \"name\": \"其他（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"动漫（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat503=1\",\n      \"name\": \"真人秀，自拍（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"套图（限制级）\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat504=1\",\n      \"name\": \"SM调教（限制级）\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"torrents.php\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"无码（限制级）\"\n      },\n      {\n        \"id\": 500,\n        \"name\": \"有码（限制级）\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"三级情色（限制级）\"\n      },\n      {\n        \"id\": 501,\n        \"name\": \"其他（限制级）\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"动漫（限制级）\"\n      },\n      {\n        \"id\": 503,\n        \"name\": \"真人秀，自拍（限制级）\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"套图（限制级）\"\n      },\n      {\n        \"id\": 504,\n        \"name\": \"SM调教（限制级）\"\n      }\n    ]\n  }]\n}\n"
  },
  {
    "path": "resource/sites/npupt.com/config.json",
    "content": "{\n  \"name\": \"NPUBits\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"界面独具一格的教育网站点\",\n  \"url\": \"https://npupt.com/\",\n  \"icon\": \"https://npupt.com/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"npupt.com\",\n  \"collaborator\": [\"Rhilip\", \"xfl03\"],\n  \"searchEntryConfig\": {\n    \"parseScriptFile\": \"getSearchResult.js\"\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"资料\",\n      \"queryString\": \"cat=411\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"电影\",\n      \"queryString\": \"cat=401\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"动漫\",\n      \"queryString\": \"cat=405\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"剧集\",\n      \"queryString\": \"cat=402\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"综艺\",\n      \"queryString\": \"cat=403\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"体育\",\n      \"queryString\": \"cat=407\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"纪录\",\n      \"queryString\": \"cat=404\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"音乐\",\n      \"queryString\": \"cat=414\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"MV\",\n      \"queryString\": \"cat=406\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"软件\",\n      \"queryString\": \"cat=408\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"游戏\",\n      \"queryString\": \"cat=410\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"校园\",\n      \"queryString\": \"cat=412\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"其他\",\n      \"queryString\": \"cat=409\",\n      \"enabled\": false\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"span#curuser a[href*='userdetails.php'][class*='Name']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\n            \"query ? query.getQueryString('id'):''\"\n          ]\n        },\n        \"name\": {\n          \"selector\": \"span#curuser a[href*='userdetails.php'][class*='Name']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\n            \"a[href*='logout.php']\"\n          ],\n          \"filters\": [\n            \"query.length>0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\n            \"td.rowfollow:contains('分享率')\",\n            \"td.rowhead:contains('传输') + td\"\n          ],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/上[传傳]量.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\n            \"td.rowfollow:contains('分享率')\",\n            \"td.rowhead:contains('传输') + td\"\n          ],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/下[载載]量.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n          ]\n        },\n        \"ratio\": {\n          \"selector\": \"td.rowfollow:contains('分享率')\",\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/分享率.+?([\\\\d.]+|无限)/)\",\n            \"(query && query.length>=2)?query[1]:0\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": \"td.rowhead:contains('等級'), td.rowhead:contains('等级')\",\n          \"filters\": [\n            \"query.next().find('img').attr('title')\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": [\n            \"td.rowhead:contains('沙粒') + td\"\n          ],\n          \"filters\": [\n            \"query.is(\\\":contains('沙粒:')\\\")?query.text().replace(/,/g,'').match(/沙粒.+?([\\\\d.]+)/)[1]:query.text().replace(/,/g,'')\",\n            \"parseFloat(query)\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": \"td.rowhead:contains('加入日期')\",\n          \"filters\": [\n            \"query.next().text().split(' (')[0]\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\n            \"tr:not(:eq(0))\"\n          ],\n          \"filters\": [\n            \"query.find('td.rowfollow:eq(2)').length\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": [\n            \"tr:not(:eq(0))\"\n          ],\n          \"filters\": [\n            \"jQuery.map(query.find('td.rowfollow:eq(2)'), (item)=>{return $(item).text();})\",\n            \"_self.getTotalSize(query)\"\n          ]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\n            \"a[href*='passkey'][href*='https']\"\n          ],\n          \"filters\": [\n            \"query.attr('href').match(/https?:\\\\/\\\\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g)\",\n            \"(query && query.length > 0) ? query[0] : ''\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/npupt.com/getSearchResult.js",
    "content": "/**\n * @see https://github.com/Rhilip/PT-help/blob/master/docs/js/ptsearch.user.js\n */\n(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/login|未登录/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (/找到0条结果/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let site = options.site;\n      let results = [];\n\n      const time_regex = /(\\d{4}-\\d{2}-\\d{2}[^\\d]+?\\d{2}:\\d{2}:\\d{2})/;\n\n      // 以下解析方法修改自 ： https://github.com/Rhilip/PT-help/blob/master/docs/js/ptsearch.user.js#L216_L241\n      let tr_list = options.page.find(\"#torrents_table > tbody > tr:gt(0)\");\n      for (let i = 0; i < tr_list.length; i++) {\n        let torrent_data_raw = tr_list.eq(i);\n\n        // 标题信息\n        let _title_tag = torrent_data_raw.find(\"a[href*='hit']:eq(0)\");\n        let _title = _title_tag.attr(\"title\") || _title_tag.text();\n\n        let _sub_title_tag = _title_tag.siblings(\"span\");\n        let _sub_title = _sub_title_tag\n          ? _sub_title_tag.attr(\"title\") || _sub_title_tag.text()\n          : \"\";\n\n        // 下载链接 （该站搜索页点击下载按钮是一个JavaScript事件）\n        let details_tag = torrent_data_raw.find('a[href^=\"details\"]');\n        let details_link = details_tag.attr(\"href\");\n        let _download_url =\n          site.url +\n          details_link.replace(\"details\", \"download\") +\n          \"&trackerssl=1\";\n\n        // 定位种子大小，做种和优惠tag\n        let _size_peer_block = torrent_data_raw.find(\n          \".rowfollow.vcenter.nowrap\"\n        );\n        let _size_tag = _size_peer_block.find(\"center\");\n        let _seeders_tag = _size_peer_block.find(\"span.badge\").eq(0);\n        let _leechers_tag = _size_peer_block.find(\"span.badge\").eq(1);\n        let _completed_tag = torrent_data_raw.find(\n          \"a[href^='viewsnatches.php?id=']\"\n        );\n        let _buff_tag = _title_tag.parent(\"td.embedded\"); // 转交给 this.getTags() 处理\n\n        // 发布时间\n        let _date_tag = torrent_data_raw.find(\"div.small\").filter(function() {\n          return time_regex.test(\n            $(this).html()\n          );\n        });\n        let _date = ((_date_tag.html().match(time_regex) || [\"\", \"0000-00-00 00:00:00\"] )[1]).trim();\n\n        // 做种，评论信息\n        let _tag_comments = torrent_data_raw.find(\"a[href$='#startcomments']\");\n\n        let _comments = 0;\n        if (_tag_comments) {\n          _comments =\n            _tag_comments\n              .text()\n              .trim()\n              .replace(\",\", \"\") || 0;\n        }\n\n        let _category = torrent_data_raw\n          .find(\"div.category_text\")\n          .text()\n          .trim();\n\n        let data = {\n          title: _title,\n          subTitle: _sub_title,\n          link: site.url + _title_tag.attr(\"href\"),\n          url: _download_url,\n          size: _size_tag.text() || 0,\n          time: _date,\n          // author,  // 该站种子列表无author信息\n          seeders: _seeders_tag.text().replace(\",\", \"\") || 0,\n          leechers: _leechers_tag.text().replace(\",\", \"\") || 0,\n          completed: _completed_tag\n            ? _completed_tag.text().replace(\",\", \"\")\n            : 0,\n          comments: _comments,\n          site: site,\n          tags: this.getTags(_buff_tag, options.torrentTagSelectors),\n          entryName: options.entry.name,\n          category: _category\n        };\n        results.push(data);\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        // 使用 some 避免错误的背景类名返回多个标签\n        selectors.some(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n              return true;\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/oldtoons.world/config.json",
    "content": "{\n  \"name\": \"ihdbits\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://oldtoons.world/\",\n  \"description\": \"\",\n  \"icon\": \"https://oldtoons.world/favicon.ico\",\n  \"tags\": [\n    \"影视\"\n  ],\n  \"host\": \"oldtoons.world\",\n  \"collaborator\": \"koal\",\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/open.cd/config.json",
    "content": "{\n  \"name\": \"OpenCD\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"皇后，专一的音乐类PT站，是目前国内最大的无损音乐PT\",\n  \"url\": \"https://open.cd/\",\n  \"icon\": \"https://open.cd/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"open.cd\",\n  \"collaborator\": [\"*\", \"cnsunyour\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"常在-正七品(Power User)\",\n    \"interval\": \"5\",\n    \"ratio\": \"1.5\",\n    \"alternative\": {\n      \"downloaded\": \"20GB\",\n      \"uploads\": \"5\"\n    },\n    \"privilege\": \"得到一个邀请名额；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"贵人-正六品(Elite User)\",\n    \"interval\": \"10\",\n    \"ratio\": \"2.0\",\n    \"alternative\": {\n      \"downloaded\": \"60GB\",\n      \"uploads\": \"20\"\n    },\n    \"privilege\": \"得到两个邀请名额；贵人-正六品(Elite User)及以上用户封存账号后规定时间内不会被删除；发布三个种子后无需经过候选 可直接发布种子。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"良媛-正五品(Crazy User)\",\n    \"interval\": \"15\",\n    \"ratio\": \"2.5\",\n    \"alternative\": {\n      \"downloaded\": \"200GB\",\n      \"uploads\": \"50\"\n    },\n    \"privilege\": \"得到三个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"容华-正四品(Insane User)\",\n    \"interval\": \"20\",\n    \"ratio\": \"3.0\",\n    \"alternative\": {\n      \"downloaded\": \"400GB\",\n      \"uploads\": \"100\"\n    },\n    \"privilege\": \"得到四个邀请名额；可以查看普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"贵嫔-正三品(Veteran User)\",\n    \"interval\": \"25\",\n    \"ratio\": \"3.5\",\n    \"alternative\": {\n      \"downloaded\": \"600GB\",\n      \"uploads\": \"200\"\n    },\n    \"privilege\": \"得到五个邀请名额；可以查看用户列表，可以查看其它用户的评论、帖子历史。贵嫔-正三品(Veteran User)及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"淑仪-正二品(Extreme User)\",\n    \"interval\": \"25\",\n    \"ratio\": \"4.0\",\n    \"alternative\": {\n      \"downloaded\": \"1TB\",\n      \"uploads\": \"300\"\n    },\n    \"privilege\": \"得到六个邀请名额；可以更新过期的外部信息。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"贵妃-正一品(Ultimate User)\",\n    \"interval\": \"30\",\n    \"ratio\": \"4.5\",\n    \"alternative\": {\n      \"downloaded\": \"2TB\",\n      \"uploads\": \"450\"\n    },\n    \"privilege\": \"得到七个邀请名额；查看种子文件的结构。\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"皇后(Nexus Master)\",\n    \"interval\": \"30\",\n    \"ratio\": \"5.0\",\n    \"alternative\": {\n      \"downloaded\": \"3TB\",\n      \"uploads\": \"600\"\n    },\n    \"privilege\": \"得到十个邀请名额。\"\n  }],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\".progress:eq(0) > div\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.progress_seeding\", \"div.progress_completed, div.progress_completed_hr\", \"div.progress_no_downloading\", \"div.progress_downloading\"],\n        \"switchFilters\": [\n          [2],\n          [255],\n          [3],\n          [1]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"appendQueryString\": \"&boardid=2\",\n    \"name\": \"原抓区\",\n    \"enabled\": true\n  }, {\n    \"appendQueryString\": \"&boardid=1\",\n    \"name\": \"普通区\",\n    \"enabled\": true\n  }],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&source$id$=1\",\n    \"category\": [{\n      \"id\": 2,\n      \"name\": \"流行(Pop)\"\n    }, {\n      \"id\": 3,\n      \"name\": \"古典(Classical)\"\n    }, {\n      \"id\": 11,\n      \"name\": \"器乐(Instrumental)\"\n    }, {\n      \"id\": 4,\n      \"name\": \"原声(OST)\"\n    }, {\n      \"id\": 5,\n      \"name\": \"摇滚(Rock)\"\n    }, {\n      \"id\": 8,\n      \"name\": \"爵士(Jazz)\"\n    }, {\n      \"id\": 12,\n      \"name\": \"新世纪(NewAge)\"\n    }, {\n      \"id\": 13,\n      \"name\": \"舞曲(Dance)\"\n    }, {\n      \"id\": 14,\n      \"name\": \"电子(Electronic)\"\n    }, {\n      \"id\": 15,\n      \"name\": \"民谣(Folk)\"\n    }, {\n      \"id\": 16,\n      \"name\": \"独立(Indie)\"\n    }, {\n      \"id\": 17,\n      \"name\": \"嘻哈(Hip Hop)\"\n    }, {\n      \"id\": 9,\n      \"name\": \"其他(Others)\"\n    }]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"div#ka1\"],\n          \"filters\": [\"query.parent().text().match(/\\\\(([\\\\d.]+)个种子/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"div#ka1\"],\n          \"filters\": [\"query.parent().text().match(/共计([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    }\n  },\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"img[src*='pic/share_rule_1.gif']\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/orpheus.network/config.json",
    "content": "{\n  \"name\": \"OPS\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"music\",\n  \"url\": \"https://orpheus.network/\",\n  \"icon\": \"https://orpheus.network/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"orpheus.network\",\n  \"collaborator\": [\"ylxb2016\", \"enigmaz\"],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/bonus.php?action=bprates\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"div#content > table > tbody > tr > td:eq(1)\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"div#content > table > tbody > tr > td:eq(2)\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}\n"
  },
  {
    "path": "resource/sites/ourbits.club/config.json",
    "content": "{\n  \"name\": \"OurBits\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"综合性网站，有分享率要求\",\n  \"url\": \"https://ourbits.club/\",\n  \"icon\": \"https://ourbits.club/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"动漫\",\n    \"纪录片\",\n    \"综艺\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"ourbits.club\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"2.0\",\n    \"privilege\": \"可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕。最多可以同时下载20个种子\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"350GB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除。此等级及以上没有下载数限制。可以查看论坛Elite User(邀请交流版)\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"3.0\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.5\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4.0\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"4.5\",\n    \"privilege\": \"得到一个永久邀请；可以更新过期的外部信息\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"6TB\",\n    \"ratio\": \"5.0\",\n    \"privilege\": \"得到两个永久邀请\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"52\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"5.5\",\n    \"privilege\": \"得到三个永久邀请\"\n  }],\n  \"collaborator\": \"Rhilip\",\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat=401\",\n      \"name\": \"Movies\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=402\",\n      \"name\": \"Movies-3D\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=419\",\n      \"name\": \"Concert\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=412\",\n      \"name\": \"TV-Episode\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=405\",\n      \"name\": \"TV-Pack\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=413\",\n      \"name\": \"TV-Show\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=410\",\n      \"name\": \"Documentary\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=411\",\n      \"name\": \"Animation\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=415\",\n      \"name\": \"Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=414\",\n      \"name\": \"Music-Video\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=416\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat[]=$id$\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"Movies-3D\"\n      },\n      {\n        \"id\": 419,\n        \"name\": \"Concert\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"TV-Episode\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"TV-Pack\"\n      },\n      {\n        \"id\": 413,\n        \"name\": \"TV-Show\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"Documentary\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"Animation\"\n      },\n      {\n        \"id\": 415,\n        \"name\": \"Sports\"\n      },\n      {\n        \"id\": 414,\n        \"name\": \"Music-Video\"\n      },\n      {\n        \"id\": 416,\n        \"name\": \"Music\"\n      }\n    ]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div.progressBar > div\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:.?(\\\\d.+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.progressBar.doing > div\", \"div.progressBar > div\"],\n        \"switchFilters\": [\n          [\"query.attr('style')||''\", \"query.match(/width:.?(\\\\d.+)%/)\", \"(query && query.length>=2)?query[1]:0\", \"parseInt(query)==100?2:1\"],\n          [\"255\"]\n        ]\n      }\n    }\n  },\n  \"plugins\": [{\n    \"name\": \"保种列表\",\n    \"pages\": [\"/rescue.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }],\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"div.tag-jz\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/passthepopcorn.me/config.json",
    "content": "{\n  \"name\": \"PTP\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"电影\",\n  \"url\": \"https://passthepopcorn.me/\",\n  \"icon\": \"https://passthepopcorn.me/favicon.ico\",\n  \"tags\": [\"电影\"],\n  \"schema\": \"Gazelle\",\n  \"host\": \"passthepopcorn.me\",\n  \"collaborator\": \"lengmianxia\",\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"resultType\": \"json\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"searchstr=$key$&grouping=0&inallakas=1&json=noredirect\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"Normal\",\n    \"enabled\": true\n  },\n  {\n    \"name\": \"filelist\",\n    \"skipIMDbId\": true,\n    \"queryString\":\"filelist=$key$&grouping=0&json=noredirect\",\n    \"enabled\": false\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a.user-info-bar__link[href*='user.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a.user-info-bar__link[href*='user.php']:first\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.alert-bar a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().replace(/\\\\s+/g,'').match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"a.user-info-bar__link[href*='type=seeding']:first\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"a.user-info-bar__link[href*='type=leeching']:first\"],\n          \"filters\": [\"query.attr('title').replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"ratio\": {\n          \"selector\": \"ul.list > li:contains('Ratio:')\",\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'').match(/Ratio.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"seeding\": {\n          \"selector\": \"ul.list > li:contains('Seeding:')\",\n          \"filters\": [\"query.text().trim().replace(/,|\\\\n/g,'').match(/:.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?parseFloat(query[1]):null\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"ul.list > li:contains('Seeding size:')\",\n          \"filters\": [\"query.text().trim().replace(/,/g,'').match(/:.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"levelName\": {\n          \"selector\": \"ul.list > li:contains('Class:')\",\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'').match(/Class:(.+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"ul.list > li:contains('Points:')\", \"div:contains('Stats') + ul.stats > li:contains('SeedBonus:')\"],\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'')\", \"query.match(/Points.+?([\\\\d.]+)/)||query.match(/SeedBonus.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"ul.list > li:contains('Joined:') > span\"],\n          \"filters\": [\"query.attr('title')||query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/torrents.php?type=seeding&userid=$user.id$\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seedingList\": {\n          \"selector\": [\"script:last\"],\n          \"filters\": [\"query.text()\", \"[...query.matchAll(/\\\"TorrentId\\\":(\\\\d+)/g)]\", \"jQuery.map(query, i=>i[1])\"]\n        }\n      }\n    },\n    \"common\": {\n\t    \"page\": \"/torrents.php\",\n      \"fields\": {\n        \"confirmSize\": {\n          \"selector\": [\"tr.basic-movie-list__torrent-row > td:contains('iB')\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/passthepopcorn.me/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\r\n  String.prototype.getQueryString = function(name, split) {\r\n    if (split == undefined) split = \"&\";\r\n    var reg = new RegExp(\r\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\r\n      ),\r\n      r;\r\n    if ((r = this.match(reg))) return decodeURI(r[2]);\r\n    return null;\r\n  };\r\n}\r\n\r\n(function(options) {\r\n  class Parser {\r\n    constructor() {\r\n      this.haveData = false;\r\n      this.categories = {};\r\n      if (/auth_form/.test(options.responseText)) {\r\n        options.status = ESearchResultParseStatus.needLogin;\r\n        return;\r\n      }\r\n\r\n      options.isLogged = true;\r\n      this.haveData = true;\r\n    }\r\n\r\n    /**\r\n     * 获取搜索结果\r\n     */\r\n\r\n    getResult() {\r\n      if (!this.haveData) {\r\n        return [];\r\n      }\r\n      let site = options.site;\r\n      // 获取种子列表行\r\n      let Movies = options.page.Movies;\r\n      if (Movies.length == 0) {\r\n        options.status = ESearchResultParseStatus.noTorrents;\r\n        return [];\r\n      }\r\n      let results = [];\r\n      let authkey = options.page.AuthKey;\r\n      let passkey = options.page.PassKey;\r\n      console.log(\"Movies.length\", Movies.length);\r\n      //console.log(\"Movies\", Movies.text());\r\n      try {\r\n        // 遍历数据行\r\n        for (let index = 0; index < Movies.length; index++) {\r\n          let row = Movies[index];\r\n          let torrent = row.Torrents[0];\r\n          let data = {\r\n            id: `${torrent.Id}`,\r\n            title:\r\n              row.Title +\r\n              \"[\" +\r\n              row.Year +\r\n              \"]\" +\r\n              \"-\" +\r\n              torrent.Codec +\r\n              \"/\" +\r\n              torrent.Container +\r\n              \"/\" +\r\n              torrent.Source +\r\n              \"/\" +\r\n              torrent.Resolution,\r\n            subTitle: torrent.ReleaseName,\r\n            link: `${site.url}torrents.php?id=${row.GroupId}&torrentid=${\r\n              torrent.Id\r\n            }`,\r\n            url: `${site.url}torrents.php?action=download&id=${\r\n              torrent.Id\r\n            }&authkey=${authkey}&torrent_pass=${passkey}`,\r\n            size: parseFloat(torrent.Size),\r\n            time: torrent.UploadTime,\r\n            author: \"\",\r\n            seeders: torrent.Seeders,\r\n            leechers: torrent.Leechers,\r\n            completed: torrent.Snatched,\r\n            comments: 0,\r\n            site: site,\r\n            tags: null,\r\n            entryName: options.entry.name,\r\n            category: \"Movie\"\r\n          };\r\n          results.push(data);\r\n        }\r\n        console.log(\"results.length\", results.length);\r\n        if (results.length == 0) {\r\n          options.status = ESearchResultParseStatus.noTorrents;\r\n        }\r\n      } catch (error) {\r\n        console.log(error);\r\n        options.status = ESearchResultParseStatus.parseError;\r\n        options.errorMsg = error.stack;\r\n      }\r\n\r\n      return results;\r\n    }\r\n  }\r\n\r\n  let parser = new Parser(options);\r\n  options.results = parser.getResult();\r\n  console.log(options.results);\r\n})(options);\r\n"
  },
  {
    "path": "resource/sites/passthepopcorn.me/getUserSeedingTorrents.js",
    "content": "(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 1\n      };\n      this.result = {\n        seedingList: []\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seedingList = this.result.seedingList.concat(results.seedingList)\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='torrents.php?page=']:contains('Last'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 2;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 1) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/passthepopcorn.me/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      // super();\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[title='Download']\").toArray();\n\n      if (links.length == 0) {\n        // 排除使用免费令牌的链接\n        links = $(\n          \"a[href*='torrents.php?action=download']:not([href*='usetoken'])\"\n        ).toArray();\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"tr.basic-movie-list__torrent-row > td:contains('iB')\")\n      );\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} url\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      let authkey = data.url.getQueryString(\"authkey\");\n      let torrent_pass = data.url.getQueryString(\"torrent_pass\");\n      // authkey=&torrent_pass\n      if (!authkey && !torrent_pass) {\n        PTService.showNotice({\n          msg: this.t(\"dropInvalidURL\") //\"无效的链接，请拖放下载链接\"\n        });\n        callback();\n        return;\n      }\n\n      data.url = this.getFullURL(data.url);\n\n      this.sendTorrentToDefaultClient(data)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/piggo.me/config.json",
    "content": "{\n    \"name\": \"Pig\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"Pig\",\n    \"url\": \"https://piggo.me/\",\n    \"icon\": \"https://piggo.me/favicon.ico\",\n    \"tags\": [\n        \"综合\",\n        \"3D原盘\",\n        \"儿童区\"\n      ],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"piggo.me\",\n    \"plugins\": [\n        {\n            \"isCustom\": true,\n            \"name\": \"儿童专区\",\n            \"pages\": [\n                \"/special.php\"\n            ],\n            \"readonly\": false,\n            \"script\": \"\",\n            \"scripts\": [\n                \"/schemas/nexusPHP/common.js\",\n                \"/schemas/nexusPHP/torrents.js\"\n            ],\n            \"style\": \"\",\n            \"styles\": []\n        }\n    ],\n    \"priority\": 100,\n    \"searchEntry\": [\n        {\n            \"enabled\": true,\n            \"entry\": \"/search.php\",\n            \"isCustom\": true,\n            \"name\": \"全站搜索\",\n            \"resultType\": \"html\",\n            \"valid\": true\n        }\n    ],\n    \"levelRequirements\": [{\n        \"level\": \"1\",\n        \"name\": \"Power User\",\n        \"interval\": \"4\",\n        \"downloaded\": \"100GB\",\n        \"ratio\": \"2.0\",\n        \"seedingPoints\": \"40000\",\n        \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕\"\n    },{\n        \"level\": \"2\",\n        \"name\": \"Elite User\",\n        \"interval\": \"8\",\n        \"downloaded\": \"300GB\",\n        \"ratio\": \"2.6\",\n        \"seedingPoints\": \"80000\",\n        \"privilege\": \"Elite User及以上用户封存账号后不会被删除\"\n    },{\n        \"level\": \"3\",\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"500GB\",\n        \"ratio\": \"3.0\",\n        \"seedingPoints\": \"150000\",\n        \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式\"\n    },{\n        \"level\": \"4\",\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"1TB\",\n        \"ratio\": \"3.6\",\n        \"seedingPoints\": \"250000\",\n        \"privilege\": \"可以查看普通日志\"\n    },{\n        \"level\": \"5\",\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"4\",\n        \"seedingPoints\": \"400000\",\n        \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号\"\n    },{\n        \"level\": \"6\",\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"4.6\",\n        \"seedingPoints\": \"600000\",\n        \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛\"\n    },{\n        \"level\": \"7\",\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"4TB\",\n        \"ratio\": \"5.0\",\n        \"seedingPoints\": \"800000\",\n        \"privilege\": \"得到五个邀请名额\"\n    },{\n        \"level\": \"8\",\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"6TB\",\n        \"ratio\": \"6.0\",\n        \"seedingPoints\": \"1000000\",\n        \"privilege\": \"得到十个邀请名额\"\n    }],\n    \"collaborator\": \"koal\",\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/pt.0ff.cc/config.json",
    "content": "{\n    \"name\": \"Farm\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"Farm\",\n    \"url\": \"https://pt.0ff.cc/\",\n    \"icon\": \"https://pt.0ff.cc/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"pt.0ff.cc\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"40000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"150000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"250000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n    \"collaborator\": \"koal\",\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "resource/sites/pt.2xfree.org/config.json",
    "content": "{\n  \"name\": \"2xFree\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"2xFree\",\n  \"url\": \"https://pt.2xfree.org/\",\n  \"icon\": \"https://pt.2xfree.org/favicon.ico\",\n  \"tags\": [\n    \"综合\",\n    \"VR\",\n    \"成人\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.2xfree.org\",\n  \"collaborator\": [\n    \"ysmox\",\n    \"IITII\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"100GB\",\n      \"ratio\": \"1.05\",\n      \"seedingPoints\": \"3000\",\n      \"privilege\": \"得到一个邀请名额；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"1T\",\n      \"ratio\": \"1.55\",\n      \"seedingPoints\": \"80000\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"2T\",\n      \"ratio\": \"2.05\",\n      \"seedingPoints\": \"150000\",\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"4T\",\n      \"ratio\": \"2.55\",\n      \"seedingPoints\": \"250000\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"52\",\n      \"downloaded\": \"8T\",\n      \"ratio\": \"3.05\",\n      \"seedingPoints\": \"400000\",\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"16T\",\n      \"ratio\": \"3.55\",\n      \"seedingPoints\": \"600000\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"104\",\n      \"downloaded\": \"35000GB\",\n      \"ratio\": \"4.05\",\n      \"seedingPoints\": \"800000\",\n      \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"130\",\n      \"downloaded\": \"70000GB\",\n      \"ratio\": \"4.55\",\n      \"seedingPoints\": \"1000000\",\n      \"privilege\": \"得到十个邀请名额。\"\n    }\n  ],\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"电子书\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"*\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"电影\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"剧集\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"综艺\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"纪录片\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"动漫\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"MV\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"体育\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"音乐\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"其他\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"电子书\"\n        },\n        {\n          \"id\": 411,\n          \"name\": \"游戏\"\n        }\n      ]\n    }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\n            \"b:first\"\n          ],\n          \"filters\": [\n            \"query.text()\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/pt.btschool.club/config.json",
    "content": "{\n  \"name\": \"BTSCHOOL\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"汇聚每一个人的影响力\",\n  \"url\": \"https://pt.btschool.club/\",\n  \"icon\": \"https://pt.btschool.club/favicon.ico\",\n  \"tags\": [ \"影视\", \"综合\" ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.btschool.club\",\n  \"formerHosts\": [\n    \"pt.btschool.net\"\n  ],\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"2.0\",\n    \"seedingPoints\": \"40000\",\n    \"privilege\": \"一个邀请名额；查看NFO文档；查看用户列表；请求续种；查看其它用户的种子历史； 删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"2.5\",\n    \"seedingPoints\": \"80000\",\n    \"privilege\": \"直接发布种子； 查看排行榜\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"3.0\",\n    \"seedingPoints\": \"150000\",\n    \"privilege\": \"两个邀请名额；在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"250000\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"4.0\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"三个邀请名额；查看其它用户的评论、帖子历史；封存账号后不会被删除\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"更新过期的外部信息；查看Extreme User论坛；永远保留账号\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"5TB\",\n    \"ratio\": \"5.0\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"五个邀请名额\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"5.5\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"十个邀请名额；发送邀请\"\n  }],\n  \"searchEntryConfig\": {\n    \"area\": [\n      {\n        \"name\": \"IMDB\",\n        \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n        \"appendQueryString\": \"&search_area=1\"\n      }\n    ],\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [ \".progress:eq(0) > div\" ],\n        \"filters\": [ \"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\" ]\n      },\n      \"status\": {\n        \"selector\": [ \".progress:eq(0) > div\" ],\n        \"filters\": [ \"query.attr('class')\", \"query=='progress_seeding'?2:(query=='progress_completed'?255:(query=='progress_no_downloading'?3:1))\" ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [ \"span.medium img.arrowup\" ],\n          \"filters\": [ \"$(query[0].nextSibling).text().trim().replace(/,/g,'')\" ]\n        },\n        \"seedingSize\": {\n          \"selector\": [ \"td.rowhead:contains('当前做种') + td\", \"td.rowhead:contains('目前做種') + td\", \"td.rowhead:contains('Current Seeding') + td\" ],\n          \"filters\": [ \"query.text().replace(/.*共计/g,'').replace(')','')\", \"query.sizeToNumber()\" ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pt.eastgame.org/config.json",
    "content": "{\n  \"name\": \"TLFBits\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://pt.eastgame.org/\",\n  \"description\": \"水管虽小，坚持则大！宛如TLF，虽弱却奢华\",\n  \"icon\": \"https://pt.eastgame.org/favicon.ico\",\n  \"tags\": [\"影视\"],\n  \"host\": \"pt.eastgame.org\",\n  \"collaborator\": \"waldens\",\n  \"levelRequirements\": [\n    {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"4\",\n        \"downloaded\": \"50GB\",\n        \"ratio\": \"1.5\",\n        \"privilege\": \"可以查看NFO文档；可以请求续种； 查看种子结构; 可以删除自己上传的字幕。\"\n    },\n    {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"8\",\n        \"downloaded\": \"120GB\",\n        \"ratio\": \"2.55\",\n        \"privilege\": \"可以查看用户的种子历史记录，如下载种子的历史记录（只有用户的隐私等级没有设为’强‘时才生效）; 可以查看高级会员区。\"\n    },\n    {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"300GB\",\n        \"ratio\": \"3.05\",\n        \"privilege\": \"可以查看排行榜；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"500GB\",\n        \"ratio\": \"4.55\",\n        \"privilege\": \"可以发送邀请；查看一般日志，不能查看机密日志; Insane User及以上等级的账号如果在封存后将永远保留。\"\n    },\n    {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"750GB\",\n        \"ratio\": \"5.05\",\n        \"privilege\": \"得到一个邀请名额；可以查看其它用户的评论、帖子历史(如果用户隐私等级未设置为'强'); Veteran User及以上等级的账号将永远保留。\"\n    },\n    {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"1TB\",\n        \"ratio\": \"6.55\",\n        \"privilege\": \"得到三个邀请名额；可以更新过期的外部信息。\"\n    },\n    {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"1.5TB\",\n        \"ratio\": \"7.05\",\n        \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"3TB\",\n        \"ratio\": \"8.55\",\n        \"privilege\": \"得到十个邀请名额。\"\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/pt.hd4fans.org/config.json",
    "content": "{\n  \"name\": \"HD4FANS\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://pt.hd4fans.org\",\n  \"icon\": \"https://pt.hd4fans.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"兽组\"\n  ],\n  \"host\": \"pt.hd4fans.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"可以查看普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"得到五个邀请名额。\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"得到十个邀请名额。\"\n  }],\n  \"collaborator\": [\"lilungpo\", \"tongyifan\"],\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"其它\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"音轨\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"*\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"电影\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"纪录片\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"动漫\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"电视剧\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"综艺\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"MV\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"体育\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"其它\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"音轨\"\n        }\n      ]\n    }\n  ],\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \"img.pro_free, .free_bg, font.free\"\n    },\n    {\n      \"name\": \"2xFree\",\n      \"selector\": \"img.pro_free2up, font.twoupfree\"\n    },\n    {\n      \"name\": \"2xUp\",\n      \"selector\": \"img.pro_2up, .twoup_bg, font.twoup\"\n    },\n    {\n      \"name\": \"2x50%\",\n      \"selector\": \"img.pro_50pctdown2up, .twouphalfdown_bg, font.twouphalfdown\"\n    },\n    {\n      \"name\": \"30%\",\n      \"selector\": \"img.pro_30pctdown, .thirtypercentdown_bg, font.thirtypercent\"\n    },\n    {\n      \"name\": \"50%\",\n      \"selector\": \"img.pro_50pctdown, .halfdown_bg, font.halfdown\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"div[class='progressarea'] > div\"\n        ],\n        \"filters\": [\n          \"query.attr('style').match(/(\\\\d+(?:\\\\.\\\\d+)?)%/)[1]\"\n        ]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/pt.hdbd.us/config.json",
    "content": "{\n  \"name\": \"伊甸园\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"这里是伊甸园 让我们赤裸坦诚相见\",\n  \"url\": \"https://pt.hdbd.us\",\n  \"icon\": \"https://pt.hdbd.us/favicon.ico\",\n  \"tags\": [\"综合\", \"XXX\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.hdbd.us\",\n   \"searchEntryConfig\": {\n\t\"merge\":true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div[class*='probar_a']\"],\n        \"filters\": [\"query.attr('title')?(query.attr('title')=='下载过，已完成'?100:query.children().attr('style').match(/width:([ \\\\d.]+)%/)[1]):null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div[class*='probar_a']\"],\n        \"filters\": [\"query.attr('title')||''\", \"query=='下载过，已完成'?255:(query.indexOf('下载过，未完成')!=-1?3:(query.indexOf('正在做种')!=-1?2:1))\"]\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pt.hdpost.top/config.json",
    "content": "{\n  \"name\": \"HDPOST\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HDPOST\",\n  \"url\": \"https://pt.hdpost.top/\",\n  \"icon\": \"https://pt.hdpost.top/favicon.ico\",\n  \"tags\": [\n    \"电影\",\n    \"电视剧\"\n  ],\n  \"schema\": \"UNIT3D\",\n  \"host\": \"pt.hdpost.top\"\n}"
  },
  {
    "path": "resource/sites/pt.hdupt.com/config.json",
    "content": "{\n  \"name\": \"HDU\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"HDU\",\n  \"url\": \"https://pt.hdupt.com/\",\n  \"icon\": \"https://pt.hdupt.com/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.hdupt.com\",\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td[class='embedded'][style*='color: blue;font-weight: bold'],td[class='embedded'] img[src*='zuozhong.gif']\"],\n        \"filters\": [\"query.attr('src')?100:(query.text()?query.text():null)\"]\n      },\n      \"status\": {\n        \"selector\": [\"td[class='embedded'][style*='color: blue;font-weight: bold'],td[class='embedded'] img[src*='zuozhong.gif']\"],\n        \"filters\": [\"query.attr('src')?2:(query.text()=='100%'?255:3)\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies/电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series/电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows/综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries/纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations/动画\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music Videos/音乐 MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports/体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio/无损音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"Misc/其他\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"Games/游戏\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies/电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"TV Series/电视剧\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"TV Shows/综艺\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"Documentaries/纪录片\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"Animations/动画\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"Music Videos/音乐 MV\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"Sports/体育\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"HQ Audio/无损音乐\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"Misc/其他\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"Games/游戏\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('魔力值') + td\"],\n          \"filters\": [\"query.is(\\\":contains('魔力值:')\\\")?query.text().replace(/,/g,'').match(/魔力值.+?([\\\\d.]+)/)[1]:query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pt.keepfrds.com/config.json",
    "content": "{\n  \"name\": \"PT@KEEPFRDS\",\n  \"timezoneOffset\": \"+0000\",\n  \"url\": \"https://pt.keepfrds.com/\",\n  \"icon\": \"https://pt.keepfrds.com/static/favicon-64x64.png\",\n  \"tags\": [\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.keepfrds.com\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.0\",\n    \"bonus\": \"3200\",\n    \"privilege\": \"请求续种；查看排行榜；查看其它用户的种子历史；查看IMDB/Douban信息；使用魔力值\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"150GB\",\n    \"ratio\": \"1.5\",\n    \"bonus\": \"19200\",\n    \"privilege\": \"封存账号后不会被删除；查看排行榜，IMDB/Douban Top榜单和论坛的邀请区\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.0\",\n    \"bonus\": \"76800\",\n    \"privilege\": \"在做种/下载的时候选择匿名模式；使用自动合集功能\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.5\",\n    \"bonus\": \"256000\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.5\",\n    \"bonus\": \"640000\",\n    \"privilege\": \"查看其它用户的评论、帖子历史；永远保留账号\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"90\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"4.0\",\n    \"bonus\": \"1280000\",\n    \"privilege\": \"上传量按照等级对应的限速计算\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"120\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.5\",\n    \"bonus\": \"1920000\",\n    \"privilege\": \"上传速度限制提升为普通用户的二倍\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"150\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"5.0\",\n    \"bonus\": \"2560000\",\n    \"privilege\": \"除了数据考核要求，其他权利等同于VIP，包括没有上传速度的限制\"\n  }],\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"img[src='/static/pic/newpic/2s_dl.gif']\", \"img[src='/static/pic/newpic/2s_up.gif'], img[src='/static/pic/newpic/2s_dled.gif']\", \"\"],\n        \"switchFilters\": [\n          [\"query.parent().parent().next().attr('style').split(';')[0].replace('width: ','').replace('px','')/3\"],\n          [\"100\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"img[src='/static/pic/newpic/2s_dl.gif']\", \"img[src='/static/pic/newpic/2s_up.gif']\", \"img[src='/static/pic/newpic/2s_dled.gif']\"],\n        \"switchFilters\": [\n          [\"1\"],\n          [\"2\"],\n          [\"255\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music Videos\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"Misc\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"Movies\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"Documentaries\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"Animations\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"TV Series\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"TV Shows\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"Music Videos\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"Sports\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"Misc\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"HQ Audio\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [\"a[href*='messages.php'] b span[style*='color: red']\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"img[alt='Torrents seeding']\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim()\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('魔力值') + td, td.rowhead:contains('Karma Points') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/魔力值:.+?([\\\\d.]+)/)[1]\", \"parseFloat(query)\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"td.rowhead:contains('魔力值') + td, td.rowhead:contains('Karma Points') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/[\\\\d.]+/g)\",\n          \"query.length == 5 ? parseFloat(query[2]) + parseFloat(query[4]) : parseFloat(query[2])\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td.rowhead:contains('当前做种') + td, td.rowhead:contains('Current Seeding') + td, td.rowhead:contains('目前做種') + td\"],\n          \"filters\": [\"query.text().trim().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"input#download_link\"],\n          \"filters\": [\"query.val()\"]\n        }\n      }\n    }\n  },\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"td.embedded b > font.recommended:contains('禁转')\"\n  },{\n    \"name\": \"Neutral\",\n    \"selector\": \"img.pro_nl\",\n    \"color\": \"purple\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/pt.newworld.plus/config.json",
    "content": "{\n  \"name\": \"ihdbits\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://pt.newworld.plus/\",\n  \"description\": \"The Ultimate File Sharing Experience\",\n  \"icon\": \"https://pt.newworld.plus/favicon.ico\",\n  \"tags\": [\n    \"影视\"\n  ],\n  \"host\": \"pt.newworld.plus\",\n  \"collaborator\": \"koal\",\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/pt.sjtu.edu.cn/config.json",
    "content": "{\n  \"name\": \"葡萄\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"Free Share, Join us\",\n  \"url\": \"https://pt.sjtu.edu.cn/\",\n  \"icon\": \"https://pt.sjtu.edu.cn/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.sjtu.edu.cn\",\n  \"collaborator\": [\"Rhilip\",\"Yincircle\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"可以查看NFO文档; 可以查看用户列表; 可以请求续种; 可以在求种补种区发主题帖;可以查看友站邀请专区;可以查看排行榜; 可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\");  可以在魔力值系统购买更多邀请名额.可以同时下载5个种子.\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"Elite User及以上用户Park后不会被删除帐号;可以直接上传种子.可以同时下载8个种子.\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"得到一个邀请名额; 可以发送邀请; 可以在做种/下载/上传的时候选择匿名模式.可以同时下载10个种子.\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"得到一个邀请名额; 可以查看普通日志.同时下载种子线程无限制.\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史;Veteran User及以上用户会永远保留账号.\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"得到一个邀请名额; 可以更新过期的外部信息.\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"可以查看种子文件结构.\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"得到一个邀请名额.\"\n  }],\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td.snatched_no_yes, td.snatched_yes_yes\", \"td.snatched_no_no, td.snatched_yes_no\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"0\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"td.snatched_no_yes, td.snatched_yes_yes\", \"td.snatched_no_no, td.snatched_yes_no\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"3\"]\n        ]\n      }\n    }\n  },\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"华语电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"欧美电影\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"亚洲电影\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"纪录片\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"港台电视剧\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"亚洲电视剧\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"大陆电视剧\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"欧美电视剧\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"大陆综艺节目\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"港台综艺节目\"\n      },\n      {\n        \"id\": 413,\n        \"name\": \"欧美综艺节目\"\n      },\n      {\n        \"id\": 414,\n        \"name\": \"日韩综艺节目\"\n      },\n      {\n        \"id\": 420,\n        \"name\": \"华语音乐\"\n      },\n      {\n        \"id\": 421,\n        \"name\": \"日韩音乐\"\n      },\n      {\n        \"id\": 422,\n        \"name\": \"欧美音乐\"\n      },\n      {\n        \"id\": 423,\n        \"name\": \"原声音乐\"\n      },\n      {\n        \"id\": 425,\n        \"name\": \"古典音乐\"\n      },\n      {\n        \"id\": 426,\n        \"name\": \"mp3合辑\"\n      },\n      {\n        \"id\": 427,\n        \"name\": \"Music Videos\"\n      },\n      {\n        \"id\": 429,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 431,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 432,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 434,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 435,\n        \"name\": \"学习\"\n      },\n      {\n        \"id\": 440,\n        \"name\": \"mac\"\n      },\n      {\n        \"id\": 451,\n        \"name\": \"校园原创\"\n      },\n      {\n        \"id\": 450,\n        \"name\": \"其他\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"merge\": true,\n      \"page\": \"/viewusertorrents.php?id=$user.id$&show=seeding\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td.rowfollow:eq(1)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pt.soulvoice.club/config.json",
    "content": "{\n  \"name\": \"聆音Club\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://pt.soulvoice.club/\",\n  \"description\": \"致力于建设一个有声资源，电子书为主，学习资料，影视资源为辅的PT分享站。\",\n  \"tags\": [\n      \"综合\",\n      \"电子书\",\n      \"有声书\"\n  ],\n  \"host\": \"pt.soulvoice.club\",\n  \"collaborator\": [\n      \"Gold John King\",\n      \"枕头啊枕头\",\n      \"Yincircle\",\n      \"yum\",\n      \"yiyule\"\n  ],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"可以查看普通日志。\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"\"\n  }],\n  \"plugins\": [{\n    \"name\": \"阅听专区\",\n    \"pages\": [\"/special.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n      \"fieldSelector\": {\n          \"progress\": {\n              \"selector\": [\n                  \"> td:eq(8)\"\n              ],\n              \"filters\": [\n                  \"query.text()=='--'?null:parseFloat(query.text())\"\n              ]\n          },\n          \"status\": {\n              \"selector\": [\n                  \"> td:eq(8)\"\n              ],\n              \"filters\": [\n                  \"query.text()==='--'?null:(parseFloat(query.text())==100?255:3)\"\n              ]\n          }\n      }\n  },\n  \"searchEntry\": [\n      {\n          \"name\": \"种子\",\n          \"enabled\": true\n      },\n      {\n          \"entry\": \"special.php?search=$key$&notnewword=1\",\n          \"name\": \"阅听专区\",\n          \"enabled\": false\n      }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pt.xauat6.edu.cn/config.json",
    "content": "{\n  \"name\": \"溪涧草堂PT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"以热播电影，热播剧集，热播综艺为主，纪录，动漫，GTV，资料等资源为辅，多方面地为用户提供丰富的资源下载\",\n  \"url\": \"http://pt.xauat6.edu.cn/\",\n  \"icon\": \"http://pt.xauat6.edu.cn/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pt.xauat6.edu.cn\",\n  \"collaborator\": \"Rhilip\",\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"剧集\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"纪录\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"资料\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n\n    {\n      \"queryString\": \"cat421=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat424=1\",\n      \"name\": \"游戏视频\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat422=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"剧集\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"纪录\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"资料\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"综艺\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"MV\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"音乐\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 421,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 424,\n        \"name\": \"游戏视频\"\n      },\n      {\n        \"id\": 422,\n        \"name\": \"其他\"\n      }\n    ]\n  }]\n}"
  },
  {
    "path": "resource/sites/pt.zhixing.bjtu.edu.cn/config.json",
    "content": "{\n    \"name\": \"知行PT\",\n    \"description\": \"北京交通大学知行pt\",\n    \"url\": \"http://pt.zhixing.bjtu.edu.cn/\",\n    \"icon\": \"http://pt.zhixing.bjtu.edu.cn/favicon.ico\",\n    \"tags\": [\n        \"教育网\",\n        \"综合\",\n        \"影视\"\n    ],\n    \"plugins\": [{\n        \"name\": \"种子详情页面\",\n        \"pages\": [\"/torrents/\"],\n        \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n      }, {\n        \"name\": \"种子列表\",\n        \"pages\": [\"/search/\"],\n        \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n    }],\n    \"schema\": \"Common\",\n    \"host\": \"pt.zhixing.bjtu.edu.cn\",\n    \"collaborator\": \"wanicca\",\n    \"searchEntryConfig\": {\n        \"page\": \"/search/x$key$\",\n        \"resultType\": \"html\",\n        \"parseScriptFile\": \"getSearchResult.js\",\n        \"resultSelector\": \"table.torrenttable:last\",\n        \"fieldIndex\": {\n            \"title\": 1, \n            \"url\": 1, \n            \"link\":1, \n            \"size\":3, \n            \"seeders\": 7,\n            \"leechers\": 8,\n            \"completed\": 9,\n            \"author\": 10,\n            \"category\": 0,\n            \"time\": 6\n        }\n    },\n    \"searchEntry\": [\n        {\n            \"name\": \"全部\",\n            \"enabled\": true\n        }\n    ],\n    \"torrentTagSelectors\": [\n        {\n            \"name\": \"Free\",\n            \"selector\": \"img[src^='/static/images/btn_free.gif']\"\n        },\n        {\n            \"name\": \"50%\",\n            \"selector\": \"img[src^='/static/images/btn_50p.gif']\"\n        },\n        {\n            \"name\": \"30%\",\n            \"selector\": \"img[src^='/static/images/btn_30p.gif']\"\n        }\n\n    ],\n    \"selectors\": {\n        \"userBaseInfo\": {\n            \"page\": \"/\",\n            \"fields\": {\n                \"id\": {\n                    \"selector\": \"strong.vwmy > a[href*='/user/']:first\",\n                    \"attribute\": \"href\",\n                    \"filters\": [\n                        \"query ? query.split('/')[2]:''\"\n                    ]\n                },\n                \"name\": {\n                    \"selector\": \"strong.vwmy > a[href*='/user/']:first\"\n                },\n                \"isLogged\": {\n                    \"selector\": [\n                        \"a[href*='/user/logout']\"\n                    ],\n                    \"filters\": [\n                        \"query.length>0\"\n                    ]\n                }\n            }\n        },\n        \"userExtendInfo\": {\n            \"page\": \"/user/$user.id$/\",\n            \"fields\": {\n                \"uploaded\": {\n                    \"selector\": [\n                        \"p:contains('上传流量:')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/上传流量: /g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                },\n                \"downloaded\": {\n                    \"selector\": [\n                        \"p:contains('下载流量:')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/下载流量: /g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                },\n                \"ratio\": {\n                    \"selector\": \"p:contains('共享率')\",\n                    \"filters\": [\n                        \"parseFloat(query.text().replace(/共享率: | (下载-虚拟下载小于20G则共享率为0)/,'')\"\n                    ]\n                },\n                \"levelName\": {\n                    \"selector\": [\n                        \"p:contains('用户组：')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().match(/用户组：([^ ]+)/)\",\n                        \"(query && query.length>=2)?(query[1]):''\"\n                    ]\n                },\n                \"bonus\": {\n                    \"selector\": [\n                        \"p:contains('保种积分')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/保种积分: /g,'').match(/([\\\\d.]+)/)\",\n                        \"(query && query.length>=2)?query[1]:''\"\n                    ]\n                },\n                \"joinTime\": {\n                    \"selector\": [\n                        \"p:contains('注册时间：')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().split('：')[1]\",\n                        \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n                    ]\n                },\n                \"seeding\": {\n                    \"selector\": [\n                        \"p:contains('当前保种数量：')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().match(/当前保种数量：([\\\\d.]+)/)\",\n                        \"(query && query.length>=1)?query[1]:''\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": [\n                        \"p:contains('当前保种容量：')\"\n                    ],\n                    \"filters\": [\n                        \"query.text().replace(/当前保种容量：/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n                        \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n                    ]\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "resource/sites/pt.zhixing.bjtu.edu.cn/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      if (this.getDownloadURL()) {\n        this.initDetailButtons();\n      }\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='/download/']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n      if (!url) {\n        return \"\";\n      }\n\n      if (url.substr(0, 2) === '//') { \n        url = `${location.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${location.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${location.origin}/${url}`;\n      }\n\n\n      return url;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"div.torrent-title\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n  };\n  (new App()).init();\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/pt.zhixing.bjtu.edu.cn/getSearchResult.js",
    "content": "/**\n * 通用搜索解析脚本\n */\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      // 判断是否已登录\n      if (\n        options.entry.loggedRegex &&\n        !new RegExp(options.entry.loggedRegex, \"\").test(options.responseText)\n      ) {\n        // 需要登录后再搜索\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let selector = options.resultSelector;\n      let dataRowSelector = options.entry.dataRowSelector || \"> tbody > tr\";\n      selector = selector.replace(dataRowSelector, \"\");\n      // 获取数据表格\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(dataRowSelector);\n      if (rows.length == 0) {\n        // 没有定位到种子列表，或没有相关的种子\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\n        return [];\n      }\n      let cats = options.page.find(\"div#tabContainer\")\n      let results = [];\n      let beginRowIndex = options.entry.firstDataRowIndex || 0;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = options.entry.fieldIndex || {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: -1,\n        // 分类\n        category: -1\n      };\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          // let title = this.getTitle(row, cells, fieldIndex);\n          let title_entry = cells.eq(fieldIndex['title']).find(\"a[href^='/torrents/']\")\n          let title = title_entry.text()\n          // 没有获取标题时，继续下一个\n          if (!title) {\n            continue;\n          }\n          // let link = this.getFieldValue(row, cells, fieldIndex, \"link\");\n          let link = title_entry.attr('href')\n\n          // 获取下载链接\n          // let url = this.getFieldValue(row, cells, fieldIndex, \"url\");\n          let url = link+\"download/\"\n\n          if (!url || !link) {\n            continue;\n          }\n\n          let time = cells.eq(fieldIndex['time']).text()\n          if(time.indexOf('-')==2){\n            var d = new Date()\n            time = d.getFullYear().toString() + '-' + time\n          }\n          \n\n          let data = {\n            title: title,\n            subTitle: this.getFieldValue(row, cells, fieldIndex, \"subTitle\"),\n            link: this.getFullURL(link),\n            url: this.getFullURL(url),\n            size: this.getFieldValue(row, cells, fieldIndex, \"size\") || 0,\n            // time: this.getFieldValue(row, cells, fieldIndex, \"time\"),\n            time: time,\n            author: this.getFieldValue(row, cells, fieldIndex, \"author\") || \"\", //尚未解决\n            seeders: this.getFieldValue(row, cells, fieldIndex, \"seeders\") || 0,\n            leechers:\n              this.getFieldValue(row, cells, fieldIndex, \"leechers\") || 0,\n            completed:\n              this.getFieldValue(row, cells, fieldIndex, \"completed\") || 0,\n            comments:\n              this.getFieldValue(row, cells, fieldIndex, \"comments\") || 0,\n            site: this.site,\n            tags: Searcher.getRowTags(this.site, row),\n            entryName: options.entry.name,\n            // category: this.getFieldValue(row, cells, fieldIndex, \"category\"),\n            category:cats.find(\"a[href='/search/\"+cells.eq(fieldIndex['category']).find(\">img\").attr('src').match(/catpic\\/([^\\.]+).png/)[1]+\"/']\").text(),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        // 获取种子信息出错\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n\n      // 没有搜索到相关的种子\n      if (results.length == 0 && !options.errorMsg) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取指定字段内容\n     * @param {*} row\n     * @param {*} cells\n     * @param {*} fieldIndex\n     * @param {*} fieldName\n     */\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(this.site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      let URL = PTServiceFilters.parseURL(this.site.url);\n      if (url.substr(0, 2) === \"//\") {\n        url = `${URL.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${URL.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${URL.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 获取标题\n     */\n    getTitle(row, cells, fieldIndex) {\n      let title = this.getFieldValue(row, cells, fieldIndex, \"title\", true);\n\n      if (!title) {\n        return \"\";\n      }\n\n      if (typeof title === \"string\") {\n        return title;\n      }\n\n      // 对title进行处理，防止出现cf的email protect\n      let cfemail = title.find(\"span.__cf_email__\");\n      if (cfemail.length > 0) {\n        cfemail.each((index, el) => {\n          $(el).replaceWith(Searcher.cfDecodeEmail($(el).data(\"cfemail\")));\n        });\n      }\n\n      return title.text();\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/pt.zhixing.bjtu.edu.cn/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href^='/torrents/']\").toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let url =\n          $(item).attr(\"href\")+\"download/\" ;\n        if (url) {\n          if (url.substr(0, 1) === \"/\") {\n            url = url.substr(1);\n          }\n          url = siteURL + url;\n\n        }\n        return url;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\".torrents\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/ptchina.org/config.json",
    "content": "{\n    \"name\": \"PTChina\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"PTChina\",\n    \"url\": \"https://ptchina.org/\",\n    \"icon\": \"https://ptchina.org/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"ptchina.org\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"40000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"150000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"250000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n    \"collaborator\": [\"koal\", \"zhuweitung\"],\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    },\n    \"searchEntryConfig\": {\n        \"fieldSelector\": {\n            \"progress\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n            },\n            \"status\": {\n                \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(0) > div:last\"],\n                \"filters\": [\n                    \"query ? query.attr('title') : ''\",\n                    \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n                ]\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/pterclub.com/config.json",
    "content": "{\n  \"name\": \"PTer\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"PT之友俱乐部\",\n  \"url\": \"https://pterclub.com/\",\n  \"icon\": \"https://pterclub.com/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"pterclub.com\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"直接发布种子；查看邀请区；请求续种；上传字幕和删除自己上传的字幕\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"可以查看排行榜\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"用户封存账号后不会被封禁\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"35\",\n    \"downloaded\": \"1024GB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"1536GB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"45\",\n    \"downloaded\": \"3072GB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"永远保留账号\"\n  }],\n  \"formerHosts\": [\n    \"pter.club\"\n  ],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"a[href*='details'][href*='php?id=']:first\"],\n        \"filters\": [\"query\"]\n      },\n      \"subTitle\": {\n        \"selector\": [\"div>span:eq(0)\"],\n        \"filters\": [\"query.text().trim()\"]\n      },\n      \"imdbId\": {\n        \"selector\": [\"a span[data-imdbid]:first\"],\n        \"attribute\": \"data-imdbid\",\n        \"filters\": [\"this.formatIMDbId(query)\"]\n      },\n      \"progress\": {\n        \"selector\": [\".progbargreen\", \".progbarred + .progbarrest\", \".progbarred\", \".progbarrest\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"query.prev().attr('style').replace('width: ','').replace('%;','')\"],\n          [\"100\"],\n          [\"0\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\".progbargreen\", \".progbarred + .progbarrest\", \".progbarred\", \".progbarrest\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"3\"],\n          [\"255\"],\n          [\"3\"]\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"name\": \"综合\",\n    \"enabled\": true\n  }, {\n    \"entry\": \"/music.php?search=$key$&notnewword=1\",\n    \"name\": \"音乐\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('猫粮') + td, td.rowhead:contains('Karma Points') + td, td.rowhead:contains('貓糧') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"a[href*='getusertorrentlist.php'][href*='seeding']\"],\n          \"filters\": [\"query ? parseInt(query.text().replace(/,/g,'')) : 0\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td.rowhead:contains('做种大小') + td, td.rowhead:contains('Seeding Size') + td, td.rowhead:contains('做種大小') + td\"],\n          \"filters\": [\"query.text().trim().sizeToNumber()\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"prerequisites\": \"!user.seeding\",\n      \"page\": \"/getusertorrentlist.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"tr:not(:eq(0))\"],\n          \"filters\": [\"query.find('td.rowfollow:eq(1)').length\"]\n        }\n      }\n    }\n  },\n  \"plugins\": [\n    {\n    \"name\": \"保种和官方列表\",\n    \"pages\": [\"/reseed.php\", \"/officialgroup.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n    },{\n      \"name\": \"游戏种子详情页面\",\n      \"pages\": [\"/detailsgame.php\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n    }\n  ],\n  \"mergeSchemaTagSelectors\": true,\n  \"torrentTagSelectors\": [{\n    \"name\": \"⛔️\",\n    \"selector\": \"a[href*='torrents.php?tag_exclusive=yes']\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/pthome.net/config.json",
    "content": "{\n  \"name\": \"PTHome\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://pthome.net/\",\n  \"description\": \"只为成为您的家，快乐下载，分享至美！\",\n  \"icon\": \"https://pthome.net/favicon.ico\",\n  \"tags\": [\"综合\", \"影视\", \"游戏\"],\n  \"host\": \"pthome.net\",\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td:not(.rowfollow):not(.colhead):not(.embedded)\"],\n        \"filters\": [\"query.text()=='-'?null:query.text()\"]\n      },\n      \"status\": {\n        \"selector\": [\".torrents-progress\"],\n        \"switchFilters\": [\n          [\"query.attr('style').indexOf('100%')!=-1?255:3\"]\n        ]\n      }\n    }\n  },\n  \"collaborator\": [\"waldens\", \"cnsunyour\"],\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"钢铁(Power User)\",\n    \"interval\": \"4\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"1.0\",\n    \"seedingPoints\": \"20000\",\n    \"privilege\": \"可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。可以浏览论坛邀请专版。\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"铝锭(Elite User)\",\n    \"interval\": \"8\",\n    \"downloaded\": \"350GB\",\n    \"ratio\": \"1.1\",\n    \"seedingPoints\": \"50000\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"锌锭(Crazy User)\",\n    \"interval\": \"15\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"1.2\",\n    \"seedingPoints\": \"200000\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"紫铜(Insane User)\",\n    \"interval\": \"25\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"1.3\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"白锡(Veteran User)\",\n    \"interval\": \"40\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"白银(Extreme User)\",\n    \"interval\": \"60\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"黄金(Ultimate User)\",\n    \"interval\": \"80\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"1.7\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"同白银用户等级权限\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"铂金(Nexus Master)\",\n    \"interval\": \"100\",\n    \"downloaded\": \"6TB\",\n    \"ratio\": \"1.8\",\n    \"seedingPoints\": \"1200000\",\n    \"privilege\": \"同白银用户等级权限\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/ptsbao.club/config.json",
    "content": "{\n  \"name\": \"烧包\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://ptsbao.club/\",\n  \"description\": \"烧包 - 扬帆远航 风雨同路\",\n  \"icon\": \"https://ptsbao.club/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"host\": \"ptsbao.club\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"1\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"2\",\n      \"downloaded\": \"150GB\",\n      \"ratio\": \"1.55\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"16\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"24\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"36\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"52\",\n      \"downloaded\": \"3TB\",\n      \"ratio\": \"4.55\"\n    }\n  ],\n  \"collaborator\": [\"laizony\", \"ted423\"],\n  \"searchEntryConfig\": {\n\t\"merge\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"tr.finished,tr.seeders\"],\n        \"filters\": [\"query.length?100:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"tr.finished\", \"tr.seeders\"],\n        \"switchFilters\": [\n        \t[\"255\"],\n        \t[\"2\"]\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background: indigo'] a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('等级') + td b b[class]\"],\n          \"filters\": [\"query.text().trim() + '(' + query.attr('class').replace('User_Name',' User') + ')'\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/pussytorrents.org/config.json",
    "content": "{\n  \"name\": \"pussytorrents\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"pussytorrents\",\n  \"url\": \"https://pussytorrents.org/\",\n  \"icon\": \"https://pussytorrents.org/favicon.ico\",\n  \"tags\": [\"xxx\"],\n  \"schema\": \"common\",\n  \"host\": \"pussytorrents.org\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/torrent/(\\\\d+)$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents/browse\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents/browse\",\n    \"queryString\": \"query=$key$&page=1&inclusions=&exclusions=&inclusionsOp=ALL&order=&orderby=\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"#torrenttable\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"#memberBar .span8 a[href*='/profile/']\"]\n        },\n        \"name\": {\n          \"selector\": [\"#memberBar .span8 a[href*='/profile/']\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"#memberBar .span8 span[title='Uploaded']\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"#memberBar .span8 span[title='Downloaded']\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href='/users/messages'] i.news-notify\"],\n          \"filters\": [\"query.length>0? 255: 0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/profile/$user.name$\",\n      \"fields\": {\n        \"levelName\": {\n          \"selector\": [\"#profileTable td:contains('Class') + td\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"#profileTable td:contains('Join Date') + td\"],\n          \"filters\": [\"dateTime(query.text().split(' ').slice(1).join(' '), 'Do MMMM YYYY').valueOf()\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/user/account/snatchlist\",\n      \"dataType\": \"json\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"requestMethod\": \"POST\",\n      \"requestData\": {\n        \"userID\": \"$user.id$\",\n        \"sEcho\": 0,\n        \"iColumns\": \"9\",\n        \"sColumns\": \",,,,,,,,\",\n        \"iDisplayStart\": \"0\",\n        \"iDisplayLength\": \"50\",\n        \"mDataProp_0\": \"0\",\n        \"sSearch_0\": \"\",\n        \"bRegex_0\": \"false\",\n        \"bSearchable_0\": \"true\",\n        \"bSortable_0\": \"true\",\n        \"mDataProp_1\": \"1\",\n        \"sSearch_1\": \"\",\n        \"bRegex_1\": \"false\",\n        \"bSearchable_1\": \"true\",\n        \"bSortable_1\": \"true\",\n        \"mDataProp_2\": \"2\",\n        \"sSearch_2\": \"\",\n        \"bRegex_2\": \"false\",\n        \"bSearchable_2\": \"true\",\n        \"bSortable_2\": \"true\",\n        \"mDataProp_3\": \"3\",\n        \"sSearch_3\": \"\",\n        \"bRegex_3\": \"false\",\n        \"bSearchable_3\": \"true\",\n        \"bSortable_3\": \"true\",\n        \"mDataProp_4\": \"4\",\n        \"sSearch_4\": \"\",\n        \"bRegex_4\": \"false\",\n        \"bSearchable_4\": \"true\",\n        \"bSortable_4\": \"true\",\n        \"mDataProp_5\": \"5\",\n        \"sSearch_5\": \"\",\n        \"bRegex_5\": \"false\",\n        \"bSearchable_5\": \"true\",\n        \"bSortable_5\": \"true\",\n        \"mDataProp_6\": \"6\",\n        \"sSearch_6\": \"\",\n        \"bRegex_6\": \"false\",\n        \"bSearchable_6\": \"true\",\n        \"bSortable_6\": \"true\",\n        \"mDataProp_7\": \"7\",\n        \"sSearch_7\": \"\",\n        \"bRegex_7\": \"false\",\n        \"bSearchable_7\": \"true\",\n        \"bSortable_7\": \"true\",\n        \"mDataProp_8\": \"8\",\n        \"sSearch_8\": \"\",\n        \"bRegex_8\": \"false\",\n        \"bSearchable_8\": \"true\",\n        \"bSortable_8\": \"true\",\n        \"sSearch\": \"\",\n        \"bRegex\": \"false\",\n        \"iSortCol_0\": \"0\",\n        \"sSortDir_0\": \"desc\",\n        \"iSortingCols\": \"1\"\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/pussytorrents.org/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      if (this.getDownloadURL()) {\n        this.initDetailButtons();\n      }\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"#torrentTools form\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"action\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return this.getFullURL(url);\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"title\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/pussytorrents.org/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/login-form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector =\n        options.resultSelector || \"div.table-responsive > table:first\";\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(\"> tbody > tr\");\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      // 获取表头\n      let header = table.find(\"> thead > tr > th\");\n      let beginRowIndex = 0;\n      if (header.length == 0) {\n        beginRowIndex = 1;\n        header = rows.eq(0).find(\"th,td\");\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 发布时间\n        time: 0,\n        // 大小\n        size: 3,\n        // 上传数量\n        seeders: 5,\n        // 下载数量\n        leechers: 6,\n        // 完成数量\n        completed: 4,\n        // 评论数量\n        comments: 2,\n        // 发布人\n        author: header.length - 1,\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      // 获取字段所在的列\n      for (let index = 0; index < header.length; index++) {\n        let cell = header.eq(index);\n        let text = cell.text();\n\n        // 评论数\n        if (cell.find(\"a[data-orderby*='numComments']\").length) {\n          fieldIndex.comments = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 大小\n        if (cell.find(\"a[data-orderby*='size']\").length) {\n          fieldIndex.size = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 种子数\n        if (cell.find(\"a[data-orderby*='Seeders']\").length) {\n          fieldIndex.seeders = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 下载数\n        if (cell.find(\"a[data-orderby*='Leechers']\").length) {\n          fieldIndex.leechers = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n\n        // 完成数\n        if (cell.find(\"a[data-orderby*='complete']\").length) {\n          fieldIndex.completed = index;\n          fieldIndex.author =\n            index == fieldIndex.author ? -1 : fieldIndex.author;\n          continue;\n        }\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\n            \"span.title a[href*='/torrent/']\"\n          );\n          if (title.length == 0) {\n            title = row.find(\"a[href*='/t/']:first\");\n          }\n          if (title.length == 0) {\n            continue;\n          }\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"a[href*='/download/']\").attr(\"href\");\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url: url,\n            size:\n              cells\n                .eq(fieldIndex.size)\n                .text()\n                .trim() || 0,\n            time: this.getTime(row),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name\n          };\n          results.push(data);\n        }\n\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    getTime(row) {\n      let text = row.find(\".subnote\").text().replace('Added on ','');\n      if (text) {\n        if (text.indexOf(\"|\") > 0) {\n          return text.split(\"|\")[1].trim();\n        }\n      }\n      return text;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/pussytorrents.org/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 1\n      };\n      this.result = {\n        seedingSize: 0,\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n      this.getPageInfo();\n\n      let results = {\n        seedingSize: 0\n      };\n\n      let status = this.body\n      .find(\"table.table > tbody > tr\")\n      .find(\">td[class*='text-']\");\n\n      let torrentsize = this.body.find(\"table.table > tbody > tr span[title='File Size']\");\n      \n      for (let index = 0; index < status.length; index++){\n        let status_i = status.eq(index).text();\n        let torrentsize_i = torrentsize.eq(index).text().replace(/\\\"+|\\n+|\\s+/g,'');\n        if (status_i == \"seed\") {\n          results.seedingSize += torrentsize_i.sizeToNumber();\n        }\n      }\n      this.result.seedingSize += results.seedingSize;\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='/active']:contains('›'):last\")\n        .parent().prev().find(\"a[href*='/active']\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 2;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 1) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/pussytorrents.org/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrents.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href*='/download/']\").toArray();\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n\n      return urls;\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      if (data.url.substr(0, 1) === \"/\") {\n        data.url = `${location.origin}${data.url}`;\n      } else if (data.url.substr(0, 4) !== \"http\") {\n        data.url = `${location.origin}/${data.url}`;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table\").find(\n          \"td.td-size:contains('MB'),td[align='center']:contains('GB'),td[align='center']:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/redacted.ch/config.json",
    "content": "{\n  \"name\": \"RED\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"music\",\n  \"url\": \"https://redacted.ch/\",\n  \"icon\": \"https://redacted.ch/favicon.ico\",\n  \"tags\": [\"音乐\"],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"redacted.ch\",\n  \"collaborator\": [\"ylxb2016\", \"enigamz\"],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"filter_cat[1]=1\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[2]=1\",\n      \"name\": \"Applications\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[3]=1\",\n      \"name\": \"E-Books\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[4]=1\",\n      \"name\": \"Audiobooks\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[5]=1\",\n      \"name\": \"E-Learning Videos\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[6]=1\",\n      \"name\": \"Comedy\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[7]=1\",\n      \"name\": \"Comics\",\n      \"enabled\": false\n    }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/ajax.php?action=community_stats&userid=$user.id$\",\n      \"dataType\": \"json\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"response.seedingsize\"],\n          \"filters\": [\"query.replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"bonus\": {\n\t      \"value\":\"N/A\"\n\t    }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/resource.xidian.edu.cn/config.json",
    "content": "{\n  \"name\": \"睿思\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"西电睿思PT\",\n  \"url\": \"http://resource.xidian.edu.cn/\",\n  \"icon\": \"http://resource.xidian.edu.cn/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"resource.xidian.edu.cn\",\n  \"collaborator\": \"luy\",\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"电视剧\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"学习\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat412=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"电视剧\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"综艺\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"纪录片\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"MV\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"音乐\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"学习\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"其他\"\n      }\n    ]\n  }],\n  \"cdn\": [\"http://resource.xidian.edu.cn/\"]\n}"
  },
  {
    "path": "resource/sites/sdbits.org/browse.js",
    "content": "(function ($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n      init() {\n        this.initButtons();\n        this.initFreeSpaceButton();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n\n      isNexusPHP() {//want use same code\n        return PTService.site.schema == \"SDB\";\n      }\n\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.initListButtons(false);\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURLs() {\n        let siteURL = PTService.site.url;\n        let links = $(\"a[href*='download.php']\").toArray();\n\n        let urls = $.map(links, (item) => {\n          let link = $(item).attr(\"href\");\n          link = link.replace(\"source=browse\", \"source=rss\");\n          link = link.replace(new RegExp(\"/download.php/.*\\.torrent\"),\"download.php\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${siteURL}${link}`;\n          }\n          return link;\n        });\n\n        if (links.length == 0) {\n          return \"获取下载链接失败，未能正确定位到链接\";\n        }\n\n        return urls;\n      }\n\n      /**\n       * 执行指定的操作\n       * @param {*} action 需要执行的执令\n       * @param {*} data 附加数据\n       * @return Promise\n       */\n      call(action, data) {\n        return new Promise((resolve, reject) => {\n          switch (action) {\n            // 从当前的DOM中获取下载链接地址\n            case PTService.action.downloadFromDroper:\n              this.downloadFromDroper(data, () => {\n                resolve()\n              });\n              break;\n          }\n        });\n      }\n\n      getDroperURL(url) {\n        let siteURL = PTService.site.url;\n        if (siteURL.substr(-1) != \"/\") {\n          siteURL += \"/\";\n        }\n        if (!url.getQueryString) {\n          PTService.showNotice({\n            msg:\n              \"系统依赖函数（getQueryString）未正确加载，请尝试刷新页面或重新启用插件。\"\n          });\n          return null;\n        }\n        if (url.indexOf(\"download.php\") == -1) {\n          let id = url.getQueryString(\"id\");\n          let firstlink = $(\"a[href*='download.php']:first\");\n          let passkey = firstlink.attr(\"href\").getQueryString(\"passkey\");\n          if (id) {\n            // 如果站点没有配置禁用https，则默认添加https链接\n            url =\n              siteURL +\n              \"download.php?id=\" +\n              id +\n              (PTService.site.passkey\n                ? \"&passkey=\" + PTService.site.passkey\n                : passkey ? \"&passkey=\"+ passkey : \"\") +\n              \"&source=rss\";\n          } else {\n            url = \"\";\n          }\n        }\n        return url;\n      }\n\n\n      /**\n       * 下载拖放的种子\n       * @param {*} data\n       * @param {*} callback\n       */\n      downloadFromDroper(data, callback) {\n        if (typeof data === \"string\") {\n          data = {\n            url: data,\n            title: \"\"\n          };\n        }\n        let result = this.getDroperURL(data.url);\n\n        if (!result) {\n          callback();\n          return;\n        }\n\n        this.sendTorrentToDefaultClient(result).then((result) => {\n          callback(result);\n        }).catch((result) => {\n          callback(result);\n        });\n      }\n\n      /**\n       * 确认大小是否超限\n       */\n      confirmWhenExceedSize() {\n        return this.confirmSize($(\"#torrent-list\").find(\"td.center:contains('MiB'),td.center:contains('GiB'),td.center:contains('TiB')\"));\n      }\n    }\n    (new App()).init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/sdbits.org/config.json",
    "content": "{\n  \"name\": \"SDBits\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"SDB, HDB姊妹站\",\n  \"url\": \"https://sdbits.org/\",\n  \"icon\": \"https://sdbits.org/favicon.ico\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"SDB\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/t/(\\\\d+)/$\", \"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"host\": \"sdbits.org\",\n  \"collaborator\": \"luckiestone\",\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"DVD\",\n      \"interval\": \"4\",\n      \"downloaded\": \"30GB\",\n      \"ratio\": \"0.95\",\n      \"privilege\": \"Are able to leech 100 torrents at a time. You can view NFOs and request reseeds on poorly seeded torrents.\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"SuperBit\",\n      \"interval\": \"4\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"1.4\",\n      \"privilege\": \"As DVD\"\n    },\n    {\n      \"level\": \"3\",\n      \"name\": \"Criterion\",\n      \"interval\": \"4\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.5\",\n      \"privilege\": \"As DVD\"\n    }\n  ],\n\"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search=$key$\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table#torrent-list:last > tbody > tr:not(:eq(0))\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"imdb=$key$\"\n    }]\n  },\n  \"searchEntry\": [{\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat=1\",\n      \"name\": \"Movie\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=2\",\n      \"name\": \"TV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=3\",\n      \"name\": \"Documentary\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=4\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=5\",\n      \"name\": \"Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=6\",\n      \"name\": \"Audio\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat=7\",\n      \"name\": \"Stand-up Comedy\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"browse.php?\",\n    \"result\": \"cat=$id$\",\n    \"category\": [{\n        \"id\": 1,\n        \"name\": \"Movie\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"TV\"\n      },\n      {\n        \"id\": 3,\n        \"name\": \"Documentary\"\n      },\n      {\n        \"id\": 4,\n        \"name\": \"Music\"\n      },\n      {\n        \"id\": 5,\n        \"name\": \"Sports\"\n      },\n      {\n        \"id\": 6,\n        \"name\": \"Audio\"\n      },\n      {\n        \"id\": 7,\n        \"name\": \"Stand-up Comedy\"\n      }\n    ]\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"a[style^='color:#000099']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"table[bgcolor*='red'] a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"font:contains('Ratio') +\",\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('Class') + td\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.heading:contains('Bonus') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('JOIN'):contains('date') + td\"],\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/userdetails.php?id=$user.id$&seeding=1\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\"td.heading:contains('Currently'):contains('seeding') + td tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(3)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"td.heading:contains('Currently'):contains('seeding') + td tr:not(:eq(0))\"],\n          \"filters\": [\"query.length\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/sdbits.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    isNexusPHP() {//want use same code\n      return PTService.site.schema == \"SDB\";\n    }\n    \n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let siteURL = PTService.site.url;\n      let query = $(\"a[href*='download.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        url = url.replace(\"source=browse\", \"source=rss\");\n        url = url.replace(new RegExp(\"/download.php/.*\\.torrent\"),\"download.php\");\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${siteURL}${url}`;\n        }\n      }\n      return url;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td.heading:contains('Size') + td\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().match(/^[^\\(]+/);\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    getTitle() {\n      let query = $(\"a[href*='download.php']\");\n      return query ? query.text().replace(\".torrent\", \"\"): \"\";\n    }\n  };\n  (new App()).init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/sdbits.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/\\/doLogin/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n\n      if (/Nothing found/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table#torrent-list:last > tbody > tr:not(:eq(0))\"\n      );\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 时间\n        time: 4,\n        // 大小\n        size: 5,\n        // 上传人数\n        seeders: 7,\n        // 下载人数\n        leechers: 8,\n        // 完成人数\n        completed: 6,\n        // 标题\n        name: 2,\n        // 发布人\n        author: 9,\n        //配置\n        category: 0\n      };\n\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n      // 遍历数据行\n      for (let index = 0; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n\n        let title = cells.eq(fieldIndex.name).find(\"b > a\");\n        if (title.length == 0) {\n          continue;\n        }\n\n        let titleStrings = title.html().split(\"::\");\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}/${link}`;\n        }\n        let url = row.find(\"a[href*='download.php']\").attr(\"href\");\n        if (url && url.substr(0, 4) !== \"http\") {\n          url = `${site.url}/${url}`;\n        }\n        if (!url) {\n          continue;\n        }\n\n        let subTitle = \"\";\n        if (titleStrings.length > 0) {\n          subTitle = $(\"<span>\")\n            .html(titleStrings[1])\n            .text();\n        }\n\n        let time =\n          cells\n            .eq(fieldIndex.time)\n            .text()\n            .replace(/([a-zA-Z]+)/g, \"$1 \")\n            .replace(/^\\s+|\\s+$/g, \"\") + \".\";\n        let data = {\n          title: $(\"<span>\")\n            .html(titleStrings[0])\n            .text(),\n          subTitle: subTitle || \"\",\n          link,\n          url: url,\n          size: cells.eq(fieldIndex.size).html() || 0,\n          time: time || \"\",\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text()\n              .split(\"/\")[0] || 0,\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text()\n              .split(\"/\")[1] || 0,\n          completed: cells.eq(fieldIndex.completed).text() || 0,\n          comments: cells.eq(fieldIndex.comments).text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: Searcher.getRowTags(site, row)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      result.link = link.attr(\"href\");\n      let id = result.link.match(/cat=(\\d+)/)[1];\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n      result.name = this.getCategoryName(id);\n      return result;\n    }\n\n    getCategoryName(id) {\n      if ($.isEmptyObject(this.categories)) {\n        let cells = options.page\n          .find(\"table.bottom > tbody > tr\")\n          .eq(1)\n          .find(\"td\");\n        cells.each((i, dom) => {\n          let id = $(dom)\n            .find(\"input\")\n            .attr(\"id\");\n          id = id.replace(\"c\", \"\");\n          let name = $(dom)\n            .find(\"a\")\n            .text();\n          if (id) {\n            this.categories[id] = name;\n          }\n        });\n      }\n      return this.categories ? this.categories[id] : \"\";\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/shadowthein.net/browse.js",
    "content": "(function ($) {\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"table#torrenttable:last a[href*='download.php']\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table#browse:last\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      if (url.indexOf(\"down.php\") === -1) {\n        return \"\";\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/shadowthein.net/config.json",
    "content": "{\n  \"name\": \"inTheShadow\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"iTS\",\n  \"url\": \"https://shadowthein.net/\",\n  \"icon\": \"https://shadowthein.net/favicon.ico\",\n  \"tags\": [\"影视\", \"音乐\", \"文学\"],\n  \"schema\": \"iTS\",\n  \"host\": \"shadowthein.net\",\n  \"collaborator\": [\n    \"luckiestone\",\n    \"MewX\"\n  ],\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search=$key$&incldead=1\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table[id='torrenttable']:last > tbody > tr\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"queryString\": \"incldead=1&search=$key$&search_in=all\"\n    }]\n  },\n  \"searchEntry\": [{\n    \"name\": \"All\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[bgcolor*='red'] a[href*='message.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"div#seeds_div\"],\n          \"filters\": [\"query.text().match(/([\\\\d.]+).*?-/)\", \"(query && query.length>=2)?query[1]:0\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.row2:contains('Tracker'):contains('Class') + td\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.row2:contains('Join'):contains('Date') + td\"],\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"bonus\": {\n          \"selector\": \"td.row2:contains('Karma') + td\",\n          \"filters\": [\"query.text().replace(/,|\\\\n|\\\\s+/g,'')\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"to do\"],\n          \"filters\": [\"to do\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"to do\"],\n          \"filters\": [\"to do\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"to do\"],\n          \"filters\": [\"to do\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"to do\"],\n          \"filters\": [\"to do\"]\n        }\n      }\n    }\n\n  }\n}\n"
  },
  {
    "path": "resource/sites/shadowthein.net/details.js",
    "content": "(function ($, window) {\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a.index[href*='download.php']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return `${location.origin}${url}`;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td.row2:contains('Size') + td\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().match(/^[^\\(]+/);\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\"table.main h1:first\")\n        .text()\n        .trim();\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/shadowthein.net/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table[id='torrenttable']:last > tbody > tr\"\n      );\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        //title\n        title: 1,\n        //downloadlink\n        downloadlink: 3,\n        // 时间\n        time: 6,\n        // 大小\n        size: 7,\n        // 上传人数\n        seeders: 9,\n        // 下载人数\n        leechers: 10,\n        // 完成人数\n        completed: 8,\n        // 评论人数\n        comments: 5,\n        // 发布人\n        author: 11,\n        category: 0\n      };\n\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n\n      // 遍历数据行\n      for (let index = 1; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n        let title = cells.eq(fieldIndex.title).find(\"a[href*='details.php?id=']\").first();\n        if (title.length == 0) {\n          continue;\n        }\n        let titleStrings = title.text();\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}/${link}`;\n        }\n        let url = \"\";\n        url = cells.eq(fieldIndex.downloadlink).find(\"a[href*='/download.php']\").first().attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}/${url}`;\n          }\n\n        if (!url) {\n          continue;\n        }\n        let time = cells.eq(fieldIndex.time).text();\n        let data = {\n          title: titleStrings,\n          link: link,\n          url: url,\n          size: cells.eq(fieldIndex.size).text() || 0,\n          time: time,\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text(),\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text(),\n          completed: cells.eq(fieldIndex.completed).text(),\n          comments: cells.eq(fieldIndex.comments).find(\"a[href*='#startcomments']\").text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: this.getTags(row, options.torrentTagSelectors)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        // 使用 some 避免错误的背景类名返回多个标签\n        selectors.some(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: item.name,\n                color: item.color\n              });\n              return true;\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/speedapp.io/config.json",
    "content": "{\n  \"name\": \"SpeedApp\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Romania site, Half i18n support\",\n  \"url\": \"https://speedapp.io/\",\n  \"tags\": [\"影视\", \"综合\",\"Adult\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/browse/[1-9](.+)$\"],\n    \"scripts\": [\"/schemas/Common/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"^/browse$\",\"^/internal$\",\"^/adult$\"],\n    \"scripts\": [\"/schemas/Common/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"host\": \"speedapp.io\",\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Power User\",\n      \"interval\": \"13\",\n      \"uploaded\": \"200GB\",\n      \"ratio\": \"2\",\n      \"privilege\": \"Can make requests\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"Elite User\",\n      \"interval\": \"26\",\n      \"uploaded\": \"1TB\",\n      \"ratio\": \"3\",\n      \"privilege\": \"Can be chosen as Hyperseeders\"\n    },\n    {\n      \"level\": \"3\",\n      \"name\": \"Xtreme User\",\n      \"interval\": \"52\",\n      \"uploaded\": \"5TB\",\n      \"ratio\": \"4\",\n      \"privilege\": \"None\"\n    },\n    {\n      \"level\": \"4\",\n      \"name\": \"Super User\",\n      \"interval\": \"103\",\n      \"uploaded\": \"20TB\",\n      \"ratio\": \"5\",\n      \"privilege\": \"None\"\n    },\n    {\n      \"level\": \"5\",\n      \"name\": \"Legend User\",\n      \"interval\": \"309\",\n      \"uploaded\": \"100TB\",\n      \"ratio\": \"6\",\n      \"privilege\": \"None\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse?search=$key$\",\n    \"loggedRegex\": \"href=\\\"\\/profile\\\"\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"div.card-body.p-0\",\n    \"dataRowSelector\": \"div.row.mr-0.ml-0.py-3\",\n    \"dataCellSelector\": \">div\",\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 0,\n\t    \"link\": 0,\n\t    \"url\": 5,\n        \"comments\": 4,\n        \"time\": 1,\n        \"size\": 3,\n        \"seeders\": 4,\n        \"leechers\": 4,\n        \"completed\": 2\n\t},\n\t\"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"a[data-poload]\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"category\": {\n\t\t\"selector\": [\"use\"],\n        \"filters\": [\"query.attr('xlink:href').split('#')[1]\"]\n\t  },\n\t  \"time\":  {\n\t\t\"selector\": [\"\"],\n      \"filters\": [ \"query.attr('title')\", \"dateTime(query).isValid() ? dateTime(query).valueOf() : dateTime(query.replace('下午','PM ').replace('上午','AM ').replace('日','').replaceAll(/年|月/g,'-'), 'YYYY-M-D A hh:mm:ss').format('YYYY-M-D HH:mm')\" ]\n\t  },\n\t  \"seeders\":  {\n\t\t\"selector\": [\"span:contains('seeders')\"],\n        \"filters\": [\"parseInt(query.text().replace('seeders','').replace(/,/g,''))\"]\n\t  },\n\t  \"leechers\":  {\n\t\t\"selector\": [\"span:contains('leechers')\"],\n        \"filters\": [\"parseInt(query.text().replace('leechers','').replace(/,/g,''))\"]\n\t  },\n\t  \"comments\":  {\n\t\t\"selector\": [\"a:contains('comments')\"],\n        \"filters\": [\"parseInt(query.text().replace('comments','').replace(/,/g,''))\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a[data-poload]\"],\n        \"filters\": [\"query.attr('href')\", \"'https://speedapp.io/'+query\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"a.btn.btn-success\"],\n        \"filters\": [\"query.attr('href')\", \"'https://speedapp.io/'+query\"]\n\t  }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"Normal\",\n    \"enabled\": true\n  }, {\n    \"entry\": \"/internal?search=$key$\",\n    \"name\": \"internal\",\n    \"enabled\": false\n  }, {\n    \"entry\": \"/adult?search=$key$\",\n    \"name\": \"adult\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"span.label.label-succes:contains('free')\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"span.label.label-dark:contains('half')\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/profile\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"#kt_quick_user_toggle > span.text-dark-50\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"#kt_quick_user_toggle > span.text-dark-50\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"#notifications-oc-toggle > div.btn > .label-danger\"],\n          \"filters\": [\"query.length?parseInt(query.text()):0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"dt:contains('已上传') + dd\",\"dt:contains('Uploaded') + dd\",\"dt:contains('Incarcat') + dd\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"dt:contains('已下载') + dd\",\"dt:contains('Downloaded') + dd\",\"dt:contains('Descarcat') + dd\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"dt:contains('比率') + dd\",\"dt:contains('Ratio') + dd\",\"dt:contains('Ratie') + dd\"],\n          \"filters\": [\"query.text()\"]\n        }, \n        \"levelName\": {\n          \"selector\": [\"div.card-body.pt-4 >div.align-items-center div.text-muted\"],\n          \"filters\":  [\"$(query.contents()[0]).text().trim()\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"dt:contains('注册日期') + dd, dt:contains('Signup date') + dd, dt:contains('Data inregistrarii') + dd\"],\n          \"filters\": [ \"query.text()\", \"dateTime(query).isValid() ? dateTime(query).valueOf() : dateTime(query.replace('下午','PM ').replace('上午','AM ').replace('日','').replaceAll(/年|月/g,'-'), 'YYYY-M-D A hh:mm:ss').valueOf()\" ]\n        },\n        \"seedingSize\": {\n\t        \"selector\": [\"dt:contains('奖励积分') + dd > b:nth-of-type(2)\",\"dt:contains('Bonus points') + dd > b:nth-of-type(2)\",\"dt:contains('Puncte bonus') + dd > b:nth-of-type(2)\"],\n\t        \"filters\": [\"query.text().replace(/,|\\\\s|\\\\n/g,'').sizeToNumber()\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\n            \"dt:contains('奖励积分') + dd > b:eq(0)\",\n            \"dt:contains('Bonus points') + dd > b:eq(0)\",\n            \"dt:contains('Puncte bonus') + dd > b:eq(0)\"\n          ],\n          \"filters\": [\"parseFloat(query.text())\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/profile/menu-stats\",\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"a[href='/shop'][title='奖励积分'], a[href='/shop'][title='Bonus points'], a[href='/shop'][title='Puncte bonus']\"],\n          \"filters\": [\"parseFloat(query.text().replace(/,|\\\\s|\\\\n/g,''))\"]\n        },\n        \"seeding\": {\n\t      \"selector\": [\"a[href='/snatch/seeding'][title='目前正在播种种子'],a[href='/snatch/seeding'][title='Currently seeding torrents'],a[href='/snatch/seeding'][title='Torrente ce se incarca']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):null\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/browse/\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='.torrent']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"div.card-body > div.row > div.justify-content-end > div:contains('B')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"a.btn.btn-hover-success[href*='?thanks']\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='.torrent']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"div.container > div.row >div.col-12.gutter-b div.row.mr-0.ml-0.py-3 > div.col-6.col-sm-4.col-md-1.text-center.text-muted:contains('B')\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/sportscult.org/config.json",
    "content": "{\n  \"name\": \"sportscult\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"sportscult\",\n  \"url\": \"https://sportscult.org/\",\n  \"icon\": \"https://sportscult.org/favicon.ico\",\n  \"tags\": [\"体育\"],\n  \"schema\": \"Common\",\n  \"collaborator\": [\"枕头啊枕头\", \"zhuweitung\"],\n  \"plugins\": [\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [\"/index.php\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n    }\n  ],\n  \"host\": \"sportscult.org\",\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Athlete\",\n      \"uploaded\": \"50GB\",\n      \"ratio\": \"0.90\",\n      \"privilege\": \"Access to: Online Users, Tracker info, Live TV\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"ProAthlete\",\n      \"uploaded\": \"250GB\",\n      \"ratio\": \"1.80\",\n      \"privilege\": \"Access to: Online Users, Tracker info, Live TV, Requests, Top 10, and Users\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/index.php?page=torrents\",\n    \"queryString\": \"search=$key$&active=0\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"#bodyarea > table > tbody > tr > td:nth-child(2) > div > .block-content > div > div > div table:nth-child(4) > tbody > tr:nth-child(2) > td > table > tbody > tr:not(:first-child)\",\n    \"dataRowSelector\": \" > tbody > tr:not(:first-child)\",\n    \"fieldIndex\": {\n      \"title\": 1,\n      \"link\": 1,\n      \"url\": 2,\n      \"time\": 4,\n      \"size\": 3,\n      \"seeders\": 5,\n      \"leechers\": 6,\n      \"completed\": 7\n    },\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\"\"],\n        \"filters\": [\n          \"query.get(0).firstChild\",\n          \"query.nodeValue||query.innerText||0\"\n        ]\n      },\n      \"link\": {\n        \"selector\": [\"\"],\n        \"filters\": [\n          \"query.children().attr('href')\",\n          \"'https://sportscult.org/'+query\"\n        ]\n      },\n      \"url\": {\n        \"selector\": [\"\"],\n        \"filters\": [\n          \"query.children().attr('href')\",\n          \"'https://sportscult.org/'+query\"\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=47&active=1&gold=0\",\n      \"name\": \"EPL\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=41&active=1&gold=0\",\n      \"name\": \"American Football\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=54\",\n      \"name\": \"AutoMotoRacing\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=17\",\n      \"name\": \"Athletics\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=51\",\n      \"name\": \"Baseball\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=34\",\n      \"name\": \"Bodybuilding/Fitness\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=50\",\n      \"name\": \"Golf\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=29\",\n      \"name\": \"Boxing\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=19\",\n      \"name\": \"BrainGames\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=36\",\n      \"name\": \"BreakDance\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=40\",\n      \"name\": \"Golf\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=23\",\n      \"name\": \"Cycling\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=31\",\n      \"name\": \"Documentary\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=1\",\n      \"name\": \"European Basketball\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=6\",\n      \"name\": \"European Soccer\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=37\",\n      \"name\": \"Extreme Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=9\",\n      \"name\": \"Fight Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=32\",\n      \"name\": \"Formula1\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=45\",\n      \"name\": \"GAA (Gaelic)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=8\",\n      \"name\": \"Golf\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=22\",\n      \"name\": \"Gymnastics\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=39\",\n      \"name\": \"Handball\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=2\",\n      \"name\": \"International Basket\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=25\",\n      \"name\": \"IceHockey\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=4\",\n      \"name\": \"International Soccer\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=42\",\n      \"name\": \"KHL\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=35\",\n      \"name\": \"KickBoxing/Muay Thai\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=43\",\n      \"name\": \"La Liga\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=15\",\n      \"name\": \"MotorSport\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=24\",\n      \"name\": \"MLB/Baseball\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=28\",\n      \"name\": \"MMA\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=11\",\n      \"name\": \"NBA/WNBA\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=3\",\n      \"name\": \"NCAA Basket/Football\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=5\",\n      \"name\": \"NFL\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=27\",\n      \"name\": \"NHL\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=26\",\n      \"name\": \"Olympic games\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=7\",\n      \"name\": \"Rugby\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=44\",\n      \"name\": \"Serie A\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=38\",\n      \"name\": \"Snooker/Pool\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=30\",\n      \"name\": \"Streetball\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=18\",\n      \"name\": \"Swimming/Aquatics\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=46\",\n      \"name\": \"AFL(AustralianFB)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=12\",\n      \"name\": \"Tennis\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=20\",\n      \"name\": \"Volleyball/Beach\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=21\",\n      \"name\": \"Weightlifting\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=16\",\n      \"name\": \"WinterSport\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=33\",\n      \"name\": \"Wrestling/Grapling\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=48\",\n      \"name\": \"Uncategorised\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"index.php?page=torrents&search=$key$&category=53\",\n      \"name\": \"Cricket\",\n      \"enabled\": false\n    }\n  ],\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \"img[src='gold/gold.gif']\"\n    },\n    {\n      \"name\": \"50%\",\n      \"selector\": \"img[src='gold/silver.gif']\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"merge\": true,\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='index.php?page=usercp']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('uid'):''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href*='do=pm']\"],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/)\",\n            \"(query && query.length>=2)?parseInt(query[1]):0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/index.php?page=usercp&uid=$user.id$\",\n      \"merge\": true,\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": \"td.header:contains('Uploaded:') + td\",\n          \"filters\": [\n            \"(query && query.length > 0) ? query.text().sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": \"td.header:contains('Downloaded:') + td\",\n          \"filters\": [\n            \"(query && query.length > 0) ? query.text().sizeToNumber():null\"\n          ]\n        },\n        \"ratio\": {\n          \"selector\": \"td.header:contains('Ratio:') + td\",\n          \"filters\": [\"parseFloat(query.text().trim())\"]\n        },\n        \"levelName\": {\n          \"selector\": \"td.header:contains('Rank:') + td\"\n        },\n        \"bonus\": {\n          \"selector\": [\"td.green:contains('Bonus')\"],\n          \"filters\": [\"query.text().replace('(Bonus ','').replace(')','')\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"#mcol div.b-content table.lista:eq(2) tbody tr:gt(1)\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"#mcol div.b-content table.lista:eq(2) tbody tr:gt(1)\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(1)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"name\": {\n          \"selector\": \"td.header:contains('User') + td\"\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.header:contains('Joined on') + td\"],\n          \"filters\": [\n            \"query[0].innerHTML.split('/')\",\n            \"query[1]+'-'+query[0]+'-'+query[2]\",\n            \"dateTime(query).valueOf()\"\n          ]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/index.php?page=modules&module=seedbonus\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"#mcol div.b-content > h3:contains('points per hour')\"],\n          \"filters\": [\"parseFloat(query.text().match(/\\\\d+(?:\\\\.\\\\d+)?/)[0])\"]\n        }\n      }\n    },\n    \"common\": {\n      \"page\": \"/index.php?page=torrent-details\",\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td.header:contains('Size') + td\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"\n          ]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#ty\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/sportscult.org/details.js",
    "content": "(function ($, window) {\n   if(/\\?page\\=torrent-details/.test(window.location.search)){\n    class App extends window.NexusPHPCommon {\n      init() {\n        this.initButtons();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.showTorrentSize();\n        this.initDetailButtons();\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURL() {\n        let query = $(\"a[href*='download.php']:first\");\n        let url = \"\";\n        if (query.length > 0) {\n          url = query.attr(\"href\");\n          if (url.substr(0, 4) != \"http\") {\n            url = PTService.site.url + url;\n          }\n        }\n\n        return url;\n      }\n\n      showTorrentSize() {\n        let size = PTService.filters.formatSize(PTService.getFieldValue(\"size\"));\n        PTService.addButton({\n         title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n      /**\n       * 获取当前种子标题\n       */\n      getTitle() {\n        return $(\"a[href*='download.php']:first\").text().trim();\n      }\n    };\n    (new App()).init();\n  }else if(/\\?page\\=torrents|seedwanted/.test(window.location.search)){\n    console.log(\"this is torrents.js\");\n    class App extends window.NexusPHPCommon {\n      init() {\n        // super();\n        this.initButtons();\n        this.initFreeSpaceButton();\n        // 设置当前页面\n        PTService.pageApp = this;\n      }\n\n      /**\n       * 初始化按钮列表\n       */\n      initButtons() {\n        this.initListButtons();\n      }\n\n      /**\n       * 获取下载链接\n       */\n      getDownloadURLs() {\n        let links = $(\"#bodyarea > table > tbody > tr > td:nth-child(2) > div > .block-content > div > div > div table:nth-child(4) > tbody > tr:nth-child(2) > td > table\")\n          .find(\"a[href*='download.php']\")\n          .toArray();\n        let siteURL = PTService.site.url;\n        if (siteURL.substr(-1) != \"/\") {\n          siteURL += \"/\";\n        }\n\n        if (links.length == 0) {\n          return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n        }\n\n        let urls = $.map(links, item => {\n          let link = $(item).attr(\"href\");\n          if (link && link.substr(0, 4) != \"http\") {\n            link = siteURL + link;\n          }\n          return link;\n        });\n\n        return urls;\n      }\n\n      /**\n       * 确认大小是否超限\n       */\n      confirmWhenExceedSize() {\n        return this.confirmSize(\n          $(\"#bodyarea > table > tbody > tr > td:nth-child(2) > div > .block-content > div > div > div table:nth-child(4) > tbody > tr:nth-child(2) > td > table\").find(\n            \"td:contains('MB'),td:contains('GB'),td:contains('TB'),td:contains('MiB'),td:contains('GiB'),td:contains('TiB')\"\n          )\n        );\n      }\n\n  \n    /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n     downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      if (data.url.substr(0, 1) === \"/\") {\n        data.url = `${location.origin}${data.url}`;\n      } else if (data.url.substr(0, 4) !== \"http\") {\n        data.url = `${location.origin}/${data.url}`;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n    }\n    new App().init();\n  }\n})(jQuery, window);"
  },
  {
    "path": "resource/sites/springsunday.net/config.json",
    "content": "{\n  \"name\": \"SSD\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"Classic Movie Compression Team\",\n  \"url\": \"https://springsunday.net/\",\n  \"icon\": \"https://springsunday.net/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"音乐\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"springsunday.net\",\n  \"formerHosts\": [\n    \"hdcmct.org\"\n  ],\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"1.1\",\n    \"seedingPoints\": \"20000\",\n    \"privilege\": \"查看NFO文档；请求续种；上传字幕或删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"1.2\",\n    \"seedingPoints\": \"50000\",\n    \"privilege\": \"查看用户列表；查看排行榜。\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"1.2\",\n    \"seedingPoints\": \"100000\",\n    \"privilege\": \"在做种/下载/发布的时候选择匿名模式, 浏览论坛邀请区。封存账号后不会被删除\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"1.2\",\n    \"seedingPoints\": \"200000\",\n    \"privilege\": \"查看其它用户的种子历史\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"1.2\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"永远保留账号。免除站点常规考核\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"得到1个邀请名额。\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"查看其它用户的评论、帖子历史。\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"downloaded\": \"5TB\",\n    \"ratio\": \"1.5\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"得到1个邀请名额。\"\n  },{\n    \"level\": \"9\", \n    \"name\": \"Nexus God\",\n    \"downloaded\": \"11.5TB\",\n    \"ratio\": \"2.0\",\n    \"seedingPoints\": \"2300000\",\n    \"privilege\": \"彩色ID特权；查看普通日志；购买及发送邀请。\"\n  }],\n  \"searchEntry\": [{\n      \"name\": \"全部\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat501=1\",\n      \"name\": \"Movies(电影)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat502=1\",\n      \"name\": \"TV Series(剧集)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat503=1\",\n      \"name\": \"Docs(纪录)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat504=1\",\n      \"name\": \"Animations(动画)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat505=1\",\n      \"name\": \"TV Shows(综艺)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat506=1\",\n      \"name\": \"Sports(体育)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat507=1\",\n      \"name\": \"MV(音乐视频)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat508=1\",\n      \"name\": \"Music(音乐)\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat509=1\",\n      \"name\": \"Others(其他)\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 501,\n        \"name\": \"Movies(电影)\"\n      },\n      {\n        \"id\": 502,\n        \"name\": \"TV Series(剧集)\"\n      },\n      {\n        \"id\": 503,\n        \"name\": \"Docs(纪录)\"\n      },\n      {\n        \"id\": 504,\n        \"name\": \"Animations(动画)\"\n      },\n      {\n        \"id\": 505,\n        \"name\": \"TV Shows(综艺)\"\n      },\n      {\n        \"id\": 506,\n        \"name\": \"Sports(体育)\"\n      },\n      {\n        \"id\": 507,\n        \"name\": \"MV(音乐视频)\"\n      },\n      {\n        \"id\": 508,\n        \"name\": \"Music(音乐)\"\n      },\n      {\n        \"id\": 509,\n        \"name\": \"Others(其他)\"\n      }\n    ]\n  }],\n  \"plugins\": [{\n    \"name\": \"保种列表\",\n    \"pages\": [\"/rescue.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"subTitle\": {\n        \"selector\": [\"div.torrent-smalldescr:first\"],\n        \"filters\": [\"query.prop('lastChild').nodeValue.trim()\"]\n      },\n      \"progress\": {\n        \"selector\": [\"a[id*='subscription'] > img\"],\n        \"filters\": [\"query.is('.uploading') ? 100 : query.is('.downloading') ? query.attr('title').match(/(\\\\d.+)%/)[1] : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"a[id*='subscription'] > img\"],\n        \"filters\": [\"query.is('.uploading') ? 2 : query.is('.downloading') ? 1: null\"]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"messageCount\": {\n          \"selector\": [\"a[href*='messages.php'][style*='background: red']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"prerequisites\": \"!user.bonusPerHour\",\n      \"page\": \"/mybonus.php\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"td:contains('我的数据'):last\", \"td:contains('我的數據'):last\", \"td:contains('My Data'):last\"],\n          \"filters\": [\"parseInt(query.parent().children().eq(1).text().replace(/,/g,''))\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td:contains('我的数据'):last\", \"td:contains('我的數據'):last\", \"td:contains('My Data'):last\"],\n          \"filters\": [\"parseFloat(query.parent().children().eq(2).text().replace(/,/g,''))*1024*1024*1024\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"td:contains('我的数据'):last\", \"td:contains('我的數據'):last\", \"td:contains('My Data'):last\"],\n          \"filters\": [\"parseFloat(query.parent().children().last().text().replace(/,/g,''))\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/sugoimusic.me/config.json",
    "content": "{\n  \"name\": \"SugoiMusic\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"music\",\n  \"url\": \"https://sugoimusic.me/\",\n  \"icon\": \"https://sugoimusic.me/favicon.ico\",\n  \"tags\": [\n    \"音乐\"\n  ],\n  \"schema\": \"GazelleJSONAPI\",\n  \"host\": \"sugoimusic.me\",\n  \"collaborator\": [\n    \"MewX\"\n  ],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"merge\": true,\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": [\n            \"li:contains('Total Seeding:') > span\"\n          ],\n          \"filters\": [\n            \"query.text().trim().sizeToNumber()\"\n          ]\n        },\n        \"bonus\": {\n          \"selector\": [\n            \"#bonus_points > span\"\n          ],\n          \"filters\": [\n            \"query.text().trim().replace(',', '')\"\n          ]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  }\n}\n"
  },
  {
    "path": "resource/sites/teamhd.org/config.json",
    "content": "{\n  \"name\": \"TeamHD\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"俄国站\",\n  \"url\": \"https://teamhd.org/\",\n  \"tags\": [\"影视\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details/\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"host\": \"teamhd.org\",\n  \"searchEntryConfig\": {\n    \"page\": \"/browse?search=$key$\",\n    \"loggedRegex\": \"/logout.php\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \".table.table-borderless.table-hover.browse\",\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 1,\n\t    \"link\": 1,\n\t    \"url\": 1,\n        \"comments\": 2,\n        \"time\": 1,\n        \"size\": 4,\n        \"author\": 4,\n        \"seeders\": 3,\n        \"leechers\": 3,\n        \"completed\": 4\n\t},\n\t\"fieldSelector\": {\n\t  \"title\": {\n\t\t\"selector\": [\"a\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a\"],\n        \"filters\": [\"query.attr('href')\", \"'https://teamhd.org/'+query\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"a[href*='download.php?id=']\"],\n        \"filters\": [\"query.attr('href')\", \"'https://teamhd.org/'+query\"]\n\t  },\n\t  \"time\": {\n\t\t\"selector\": [\"small\"]\n\t  },\n\t  \"size\": {\n\t\t\"selector\": [\"br\"],\n        \"filters\": [\"$(query[0].previousSibling).text().replace(/,/g,'').sizeToNumber()\"]\n\t  },\n\t  \"completed\": {\n\t\t\"selector\": [\"strong\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"leechers\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.text().split('|')[1]\"]\n\t  },\n\t  \"progress\": {\n        \"selector\": [\"div.seeder\"],\n        \"filters\": [\"query.length>0?100:undefined\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.seeder\"],\n        \"filters\": [\"query.length>0?2:undefined\"]\n      }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"a[href*='/details'][style='color:#f2b101']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"a[href*='/user/']:first\"\n        },\n        \"id\": {\n          \"selector\": \"a[href*='/user/']\",\n          \"filters\": [\"query[0].href.split('/')[4]\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href='/logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"#message_box > a > font\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"div.col-8.mb-4 > font[color='green']\"],\n          \"filters\": [\"$(query[0].nextSibling).text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"div.col-8.mb-4 > font[color='darkred']\"],\n          \"filters\": [\"$(query[0].nextSibling).text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"div.col-8.mb-4 > font[color='#1900D1']\"],\n          \"filters\": [\"$(query[0].nextSibling).text()\"]\n        }, \n        \"bonus\": {\n\t        \"selector\": [\"a.online[href='/mybonus.php']\"],\n          \"filters\": [\"parseFloat(query.text().replaceAll(' ',''))\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user/$user.id$\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"#profile_right > table.inlay > tbody > tr:nth-child(1) > td:nth-child(2)\"],\n          \"filters\": [\n            \"query.text().split('(')[0].trim()\",\n            \"query.replace('января', 'January')\",\n            \"query.replace('февраля', 'February')\",\n            \"query.replace('марта', 'March')\",\n            \"query.replace('апреля', 'April')\",\n            \"query.replace('мая', 'May')\",\n            \"query.replace('июня', 'June')\",\n            \"query.replace('июля', 'July')\",\n            \"query.replace('августа', 'August')\",\n            \"query.replace('сентября', 'September')\",\n            \"query.replace('октября', 'October')\",\n            \"query.replace('ноября', 'November')\",\n            \"query.replace('декабря', 'December')\",\n            \"dateTime(query).valueOf()\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": [\"#profile_left > table > tbody > tr > td:nth-child(2) > p:nth-child(1) > u > span\"],\n          \"filters\":  [\"query.text()\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/bprate.php\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"table.table:first > tbody > tr > td:nth-child(1)\"],\n          \"filters\": [\"parseInt(query.text())\"]\n        },\n        \"seedingSize\": {\n\t        \"selector\": [\"table.table:first > tbody > tr > td:nth-child(2)\"],\n\t        \"filters\": [\"_self.getTotalSize([query.text()])\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/torrents-details.php\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a[href*='download.php?']\"],\n          \"filters\": [\"query.attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"td[align='left']:contains('Total Size:') + td\"],\n          \"filters\": [\"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='download.php?id=']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.ttable_headinner\"],\n          \"filters\": [\"query.find('td.ttable_size')\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/thegeeks.click/config.json",
    "content": "{\n  \"name\": \"TG\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"TheGeeks\",\n  \"url\": \"https://thegeeks.click\",\n  \"icon\": \"https://thegeeks.click/favicon.ico\",\n  \"tags\": [\"学习\"],\n  \"schema\": \"\",\n  \"host\": \"thegeeks.click\",\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/main.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='userdetails.php?id=']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails.php?id=']:first\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href='message.php?action=viewmailbox'] + b\"],\n          \"filters\": [\"query.text().match(/(\\\\d+ New)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"td.statuslink span:contains('UL:'):first\"],\n          \"filters\": [\"query[0].nextSibling.nodeValue.trim().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.statuslink span:contains('DL:'):first\"],\n          \"filters\": [\"query[0].nextSibling.nodeValue.trim().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.statuslink span:contains('Ratio:') + span\",\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"query === '---' ? 'N/A' : parseFloat(query)\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"img[title='seeders'] + span:first\"]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        },\n        \"levelName\": {\n          \"selector\": [\"a[href*='userdetails.php?id='] + span:first\"],\n          \"filters\": [\"query.text().replace(/\\\\(|\\\\)/g, '')\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"bonus\": {\n          \"value\": \"N/A\"\n        },\n        \"joinTime\": {\n          \"selector\": [\".embedded td:contains('Join date') + td:first\"],\n          \"filters\": [\"query.text().replace(/\\\\(.*\\\\)/g, '').trim()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"userData\": true,\n    \"search\": false,\n    \"imdbSearch\": false,\n    \"sendTorrent\": false\n  }\n}"
  },
  {
    "path": "resource/sites/tjupt.org/config.json",
    "content": "{\n  \"name\": \"北洋园\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"北洋园PT\",\n  \"url\": \"https://tjupt.org/\",\n  \"icon\": \"https://tjupt.org/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"tjupt.org\",\n  \"collaborator\": [\"tongyifan\", \"echo094\"],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"拜师学艺\",\n      \"interval\": \"4\",\n      \"downloads\": \"20\",\n      \"seedingTime\": \"30\",\n      \"uploaded\": \"50GB\",\n      \"bonus\": \"10000\",\n      \"privilege\": \"查看用户列表；请求续种；查看其他用户种子历史（隐私等级不为高时）；删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"持剑下山\",\n      \"interval\": \"8\",\n      \"downloads\": \"60\",\n      \"seedingTime\": \"120\",\n      \"uploaded\": \"200GB\",\n      \"bonus\": \"30000\",\n      \"privilege\": \"封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"初入江湖\",\n      \"interval\": \"16\",\n      \"downloads\": \"150\",\n      \"seedingTime\": \"450\",\n      \"uploaded\": \"800GB\",\n      \"uploads\": \"1\",\n      \"bonus\": \"80000\",\n      \"privilege\": \"首次升级至此等级时将获得1个永久邀请；发送邀请；做种/下载/发布时可以选择匿名。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"小有名气\",\n      \"interval\": \"28\",\n      \"downloads\": \"300\",\n      \"seedingTime\": \"1500\",\n      \"uploaded\": \"2000GB\",\n      \"uploads\": \"5\",\n      \"bonus\": \"150000\",\n      \"privilege\": \"首次升级至此等级时将获得1个永久邀请；查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"威震一方\",\n      \"interval\": \"48\",\n      \"downloads\": \"600\",\n      \"seedingTime\": \"4200\",\n      \"uploaded\": \"5000GB\",\n      \"uploads\": \"10\",\n      \"bonus\": \"300000\",\n      \"privilege\": \"首次升级至此等级时将获得1个永久邀请；查看其它用户的评论、帖子历史；永久保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"横扫群雄\",\n      \"interval\": \"72\",\n      \"downloads\": \"1000\",\n      \"seedingTime\": \"28000\",\n      \"uploaded\": \"10000GB\",\n      \"uploads\": \"15\",\n      \"bonus\": \"400000\",\n      \"privilege\": \"首次升级至此等级时将获得1个永久邀请。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"开宗立派\",\n      \"interval\": \"100\",\n      \"downloads\": \"1800\",\n      \"seedingTime\": \"90000\",\n      \"uploaded\": \"20000GB\",\n      \"uploads\": \"30\",\n      \"bonus\": \"600000\",\n      \"privilege\": \"首次升级至此等级时将获得2个永久邀请。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"天下无敌\",\n      \"interval\": \"132\",\n      \"downloads\": \"3000\",\n      \"seedingTime\": \"300000\",\n      \"uploads\": \"50\",\n      \"uploaded\": \"50000GB\",\n      \"bonus\": \"1000000\",\n      \"privilege\": \"首次升级至此等级时将获得3个永久邀请。\"\n    }\n  ],\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"电影\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"资料\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"软件\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat412=1\",\n      \"name\": \"移动视频\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&cat$id$=1\",\n    \"category\": [{\n        \"id\": 401,\n        \"name\": \"电影\"\n      },\n      {\n        \"id\": 402,\n        \"name\": \"剧集\"\n      },\n      {\n        \"id\": 403,\n        \"name\": \"综艺\"\n      },\n      {\n        \"id\": 404,\n        \"name\": \"资料\"\n      },\n      {\n        \"id\": 405,\n        \"name\": \"动漫\"\n      },\n      {\n        \"id\": 406,\n        \"name\": \"音乐\"\n      },\n      {\n        \"id\": 407,\n        \"name\": \"体育\"\n      },\n      {\n        \"id\": 408,\n        \"name\": \"软件\"\n      },\n      {\n        \"id\": 409,\n        \"name\": \"游戏\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"纪录片\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"移动视频\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"其他\"\n      }\n    ]\n  }],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div.probar_b1, div.probar_b2, div.probar_b3\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.probar_b1\", \"div.probar_b2\", \"div.probar_b3\"],\n        \"switchFilters\": [\n          [\"1\"],\n          [\"2\"],\n          [\"255\"]\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n\t\"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\".color_uploaded\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim().sizeToNumber()\"]\n        }\n      }\n\t},\n\t\"userSeedingTorrents\": {\n      \"merge\": true,\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text().trim()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"$(query[0].nextSibling).text().trim().match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==2)?(query[0]).sizeToNumber():0\"]\n        }\n      }\n    },\n    \"levelExtendInfo\": {\n      \"page\": \"/classes.php\",\n      \"fields\": {\n        \"downloads\": {\n          \"selector\": [\"#9 td:eq(1) li:eq(1)\"],\n          \"filters\": [\"parseInt(query.text().trim().split('：')[1].split('/')[0])\"]\n        },\n        \"seedingTime\": {\n          \"selector\": [\"#9 td:eq(1) li:eq(2)\"],\n          \"filters\": [\"parseFloat(query.text().trim().split('：')[1].split('/')[0])\"]\n        },\n        \"uploads\": {\n          \"selector\": [\"#9 td:eq(1) li:eq(4)\"],\n          \"filters\": [\"parseInt(query.text().trim().split('：')[1].split('/')[0])\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"a#direct_link\"],\n          \"filters\": [\"query.attr('data-clipboard-text')\"]\n        }\n      }\n    }\n  },\n  \"cdn\": [\"https://tjupt.org/\",\"https://www.tjupt.org/\"]\n}"
  },
  {
    "path": "resource/sites/totheglory.im/bookmarks.js",
    "content": "(function($) {\n  console.log(\"this is bookmarks.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons(true);\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a.dl_a\").toArray();\n\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n   /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      if (data.url.substr(0, 1) === \"/\") {\n        data.url = `${location.origin}${data.url}`;\n      } else if (data.url.substr(0, 4) !== \"http\") {\n        data.url = `${location.origin}/${data.url}`;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table\").find(\n          \"td[align='center']:contains('MB'),td[align='center']:contains('GB'),td[align='center']:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/totheglory.im/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons(true);\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a.bookmark\").toArray();\n      let urls = $.map(links, item => {\n        let id = $(item).attr(\"tid\");\n        return this.getDownloadURL(id);\n      });\n\n      if (links.length == 0) {\n        return \"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      return urls;\n    }\n\n    getDownloadURL(id) {\n      // 格式：vvvid|||passkeyzz\n      let key = new Base64().encode(\n        \"vvv\" + id + \"|||\" + PTService.site.passkey + \"zz\"\n      );\n      return `https://${PTService.site.host}/rssdd.php?par=${key}&ssl=yes`;\n    }\n\n    /**\n     * 执行指定的操作\n     * @param {*} action 需要执行的执令\n     * @param {*} data 附加数据\n     * @return Promise\n     */\n    call(action, data) {\n      return new Promise((resolve, reject) => {\n        switch (action) {\n          // 从当前的DOM中获取下载链接地址\n          case PTService.action.downloadFromDroper:\n            this.downloadFromDroper(data, () => {\n              resolve();\n            });\n            break;\n        }\n      });\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (!PTService.site.passkey) {\n        PTService.showNotice({\n          msg: \"请先设置站点密钥（Passkey）。\"\n        });\n        callback();\n        return;\n      }\n\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      let result = this.getDroperURL(data.url);\n\n      if (!result) {\n        callback();\n        return;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let values = url.split(\"/\");\n      let id = values[values.length - 2];\n      let result = this.getDownloadURL(id);\n\n      return result;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table\").find(\n          \"td[align='center']:contains('MB'),td[align='center']:contains('GB'),td[align='center']:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/totheglory.im/config.json",
    "content": "{\n  \"name\": \"TTG\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"ToTheGlory\",\n  \"url\": \"https://totheglory.im/\",\n  \"icon\": \"https://totheglory.im/favicon.ico\",\n  \"tags\": [\"影视\", \"音乐\", \"游戏\", \"综合\"],\n  \"schema\": \"TTG\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/t/(\\\\d+)/$\", \"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }, {\n    \"name\": \"收藏列表\",\n    \"pages\": [\"/bookmarks.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"bookmarks.js\"]\n  }],\n  \"host\": \"totheglory.im\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"KiloByte\",\n    \"interval\": \"5\",\n    \"downloaded\": \"60GB\",\n    \"ratio\": \"1.1\",\n    \"privilege\": \"可申请种子候选\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"MegaByte\",\n    \"interval\": \"8\",\n    \"downloaded\": \"150GB\",\n    \"ratio\": \"2.0\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"GigaByte\",\n    \"interval\": \"8\",\n    \"downloaded\": \"250GB\",\n    \"ratio\": \"2.0\",\n    \"privilege\": \"可挂起，可进入积分商城\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"TeraByte\",\n    \"interval\": \"8\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"可用积分购买邀请，并可浏览全站（新加游戏分类页），可以访问邀请区\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"PetaByte\",\n    \"interval\": \"15\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"2.5\",\n    \"privilege\": \"可直接发布种子\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"ExaByte\",\n    \"interval\": \"24\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.0\",\n    \"privilege\": \"自行挂起账号后不会被清除\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"ZettaByte\",\n    \"interval\": \"24\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"3.5\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"YottaByte\",\n    \"interval\": \"24\",\n    \"downloaded\": \"2.5TB\",\n    \"ratio\": \"4.0\",\n    \"privilege\": \"可查看排行榜\"\n  },{\n    \"level\": \"9\", \n    \"name\": \"BrontoByte\",\n    \"interval\": \"32\",\n    \"downloaded\": \"3.5TB\",\n    \"ratio\": \"5.0\",\n    \"privilege\": \"永远保留账号\"\n  },{\n    \"level\": \"10\", \n    \"name\": \"NonaByte\",\n    \"interval\": \"48\",\n    \"downloaded\": \"5TB\",\n    \"ratio\": \"6.0\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"11\", \n    \"name\": \"DoggaByte\",\n    \"interval\": \"48\",\n    \"downloaded\": \"10TB\",\n    \"ratio\": \"6.0\",\n    \"privilege\": \"无\"\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"queryString\": \"search_field=$key$\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table#torrent_table:last > tbody > tr\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"replaceKey\": [\n        \"tt\", \"IMDB\"\n      ]\n    }],\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div.process > span\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:.?(\\\\d.+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"div.process > span\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:.?(\\\\d.+)%/)\", \"(query && query.length>=2)?query[1]:0\", \"parseInt(query)==100?2:1\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n    \"appendQueryString\": \"&c=M\",\n    \"name\": \"影视&音乐\",\n    \"enabled\": true\n  }, {\n    \"appendQueryString\": \"&c=G\",\n    \"name\": \"游戏&软件\",\n    \"enabled\": true\n  }],\n  \"categories\": [{\n    \"entry\": \"browse.php?c=M\",\n    \"appendToSearchKey\": true,\n    \"result\": \"分类:`$id$` \",\n    \"category\": [{\n      \"id\": \"电影DVDRip\",\n      \"name\": \"电影DVDRip\"\n    }, {\n      \"id\": \"电影720p\",\n      \"name\": \"电影720p\"\n    }, {\n      \"id\": \"电影1080i/p\",\n      \"name\": \"电影1080i/p\"\n    }, {\n      \"id\": \"BluRay原盘\",\n      \"name\": \"BluRay原盘\"\n    }, {\n      \"id\": \"影视2160p\",\n      \"name\": \"影视2160p\"\n    }, {\n      \"id\": \"UHD原盘\",\n      \"name\": \"UHD原盘\"\n    }, {\n      \"id\": \"纪录片720p\",\n      \"name\": \"纪录片720p\"\n    }, {\n      \"id\": \"纪录片1080i/p\",\n      \"name\": \"纪录片1080i/p\"\n    }, {\n      \"id\": \"纪录片BluRay原盘\",\n      \"name\": \"纪录片BluRay原盘\"\n    }, {\n      \"id\": \"欧美剧720p\",\n      \"name\": \"欧美剧720p\"\n    }, {\n      \"id\": \"欧美剧1080i/p\",\n      \"name\": \"欧美剧1080i/p\"\n    }, {\n      \"id\": \"高清日剧\",\n      \"name\": \"高清日剧\"\n    }, {\n      \"id\": \"大陆港台剧1080i/p\",\n      \"name\": \"大陆港台剧1080i/p\"\n    }, {\n      \"id\": \"大陆港台剧720p\",\n      \"name\": \"大陆港台剧720p\"\n    }, {\n      \"id\": \"高清韩剧\",\n      \"name\": \"高清韩剧\"\n    }, {\n      \"id\": \"欧美剧包\",\n      \"name\": \"欧美剧包\"\n    }, {\n      \"id\": \"日剧包\",\n      \"name\": \"日剧包\"\n    }, {\n      \"id\": \"华语剧包\",\n      \"name\": \"华语剧包\"\n    }, {\n      \"id\": \"韩剧包\",\n      \"name\": \"韩剧包\"\n    }, {\n      \"id\": \"(电影原声&Game)OST\",\n      \"name\": \"(电影原声&Game)OST\"\n    }, {\n      \"id\": \"无损音乐FLAC&APE\",\n      \"name\": \"无损音乐FLAC&APE\"\n    }, {\n      \"id\": \"MV&演唱会\",\n      \"name\": \"MV&演唱会\"\n    }, {\n      \"id\": \"高清体育节目\",\n      \"name\": \"高清体育节目\"\n    }, {\n      \"id\": \"高清动漫\",\n      \"name\": \"高清动漫\"\n    }, {\n      \"id\": \"韩国综艺\",\n      \"name\": \"韩国综艺\"\n    }, {\n      \"id\": \"高清综艺\",\n      \"name\": \"高清综艺\"\n    }, {\n      \"id\": \"日本综艺\",\n      \"name\": \"日本综艺\"\n    }, {\n      \"id\": \"MiniVideo\",\n      \"name\": \"MiniVideo\"\n    }, {\n      \"id\": \"补充音轨\",\n      \"name\": \"补充音轨\"\n    }, {\n      \"id\": \"iPhone/iPad视频\",\n      \"name\": \"iPhone/iPad视频\"\n    }]\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[alt='free']\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"img[alt='30%']\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img[alt='50%']\"\n  }, {\n    \"name\": \"⛔️\",\n    \"selector\": \"span.browse.excl\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"a[href*='userdetails.php']:first\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a[href*='userdetails.php']:first\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='logout.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background'] > b > a[href*='messages.php'], a[href='#notice']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('上传量') + td\", \"td.rowhead:contains('上傳量') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('下载量') + td\", \"td.rowhead:contains('下載量') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').sizeToNumber()\"]\n        },\n        \"ratio\": {\n          \"selector\": \"td.rowhead:contains('分享率') + td\",\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('等级') + td\", \"td.rowhead:contains('等級') + td\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('积分') + td\", \"td.rowhead:contains('積分') + td\"],\n          \"filters\": [\"parseFloat(query.text())\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('注册日期') + td\", \"td.rowhead:contains('註冊日期') + td\"],\n          \"filters\": [\"dateTime(query.text()).isValid()?dateTime(query.text()).valueOf():query.text()\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"div#ka2 tr:not(:eq(0))\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"div#ka2 tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(3)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"page\": \"/mybonus.php\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"td.rowhead:contains('总计') + td\"],\n          \"filters\": [\"parseFloat(query.text().match(/[\\\\d.]+/)[0])\"]\n        }\n      }\n    },\n    \"common\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"td[valign='top'][align='left']:contains('字节')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/totheglory.im/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      // 有一些扩展会为链接添加class，导致选择器失效，因此使用正则来获取链接\n      // let query = $(\"a[href*='/dl/']:not([class])\");\n      let query = $(\"a[href*='/dl/']\").filter(function() {\n        return this.href.match(/\\/[0-9a-f]{32}$/)\n      });\n\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n        // 直接获取的链接下载成功率很低\n        // 如果设置了 passkey 则使用 rss 订阅的方式下载\n        if (PTService.site.passkey) {\n          let values = url.split(\"/\");\n          let id = values[values.length - 2];\n\n          // 格式：vvvid|||passkeyzz\n          let key = (new Base64).encode(\"vvv\" + id + \"|||\" + PTService.site.passkey + \"zz\");\n          url = `https://${PTService.site.host}/rssdd.php?par=${key}&ssl=yes`;\n        }\n      }\n\n      return url;\n    }\n\n    showTorrentSize() {\n      let query = $(\"td[valign='top'][align='left']:contains('字节')\");\n      let size = \"\";\n      if (query.length > 0) {\n        size = query.text().split(\" (\")[0];\n        // attachment\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    getTitle() {\n      return /\"(.*?)\"/.exec($(\"title\").text())[1];\n    }\n  }\n  (new App()).init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/totheglory.im/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n      let results = [];\n      let site = options.site;\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table#torrent_table:last > tbody > tr\"\n      );\n      let time_regex = /(\\d{4}-\\d{2}-\\d{2}[^\\d]+?\\d{2}:\\d{2}:\\d{2})/;\n      let time_regen_replace = /-(\\d{2})[^\\d]+?(\\d{2}):/;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        // 时间\n        time: 4,\n        // 大小\n        size: 6,\n        // 上传人数\n        seeders: 8,\n        // 下载人数\n        leechers: 8,\n        // 完成人数\n        completed: 7,\n        // 评论人数\n        comments: 3,\n        // 发布人\n        author: 9,\n        category: 0\n      };\n\n      if (site.url.substr(-1) == \"/\") {\n        site.url = site.url.substr(0, site.url.length - 1);\n      }\n\n      // 遍历数据行\n      for (let index = 1; index < rows.length; index++) {\n        const row = rows.eq(index);\n        let cells = row.find(\">td\");\n\n        let title = row.find(\"div.name_left > a\");\n        if (title.length == 0) {\n          continue;\n        }\n\n        // 对title进行处理，防止出现cf的email protect\n        if (title.find(\"span.__cf_email__\")) {\n          title.find(\"span.__cf_email__\").each(function() {\n            $(this).replaceWith(\n              Searcher.cfDecodeEmail($(this).data(\"cfemail\"))\n            );\n          });\n        }\n\n        let titleStrings = title.html().split(\"<br>\");\n        let link = title.attr(\"href\");\n        if (link && link.substr(0, 4) !== \"http\") {\n          link = `${site.url}${link}`;\n        }\n\n        let values = link.split(\"/\");\n        let id = values[values.length - 2];\n        let url = \"\";\n\n        if (site.passkey && id) {\n          // 格式：vvvid|||passkeyzz\n          let key = new Base64().encode(\n            \"vvv\" + id + \"|||\" + site.passkey + \"zz\"\n          );\n          url = `https://${site.host}/rssdd.php?par=${key}&ssl=yes`;\n        } else {\n          url = row.find(\"a.dl_a\").attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n        }\n\n        if (!url) {\n          continue;\n        }\n\n        let subTitle = \"\";\n        if (titleStrings.length > 0) {\n          subTitle = $(\"<span>\")\n            .html(titleStrings[1])\n            .text();\n        }\n\n        let data = {\n          title: $(\"<span>\")\n            .html(titleStrings[0])\n            .text(),\n          subTitle: subTitle || \"\",\n          link,\n          url: url,\n          size: cells.eq(fieldIndex.size).html() || 0,\n          time:\n            cells\n              .eq(fieldIndex.time)\n              .html()\n              .match(time_regex)[1]\n              .replace(time_regen_replace, \"-$1 $2:\") ||\n            cells.eq(fieldIndex.time).text(),\n          author: cells.eq(fieldIndex.author).text() || \"\",\n          seeders:\n            cells\n              .eq(fieldIndex.seeders)\n              .text()\n              .split(\"/\")[0] || 0,\n          leechers:\n            cells\n              .eq(fieldIndex.leechers)\n              .text()\n              .split(\"/\")[1] || 0,\n          completed: cells.eq(fieldIndex.completed).text() || 0,\n          comments: cells.eq(fieldIndex.comments).text() || 0,\n          site: site,\n          entryName: options.entry.name,\n          category: this.getCategory(cells.eq(fieldIndex.category)),\n          tags: options.searcher.getRowTags(site, row),\n          progress: options.searcher.getFieldValue(site, row, \"progress\"),\n          status: options.searcher.getFieldValue(site, row, \"status\"),\n          imdbId: this.getIMDbId(row)\n        };\n        results.push(data);\n      }\n\n      if (results.length == 0) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取IMDbId\n     * @param {*} row\n     */\n    getIMDbId(row)\n    {\n      try {\n        let link = row.find(\"a[href*='imdb.com/title/tt']\").first().attr(\"href\");\n        if (link)\n        {\n          let imdbId = link.match(/(tt\\d+)/);\n          if (imdbId)\n            return imdbId[0];\n        }\n      } catch (error){\n        console.log(error)\n        return null;\n      }\n      return null;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/totheglory.im/parser/downloadURL.js",
    "content": "(function (options) {\n  if (options.url && options.url.href) {\n    if (!/(\\/t\\/(\\d+)|\\/dl\\/(\\d+)\\/(\\d+))/.test(options.url.href)) {\n      options.error = {\n        success: false,\n        msg: \"无效的下载地址\"\n      }\n      return;\n    }\n    // 匹配直接下载地址\n    if (/\\/dl\\/(\\d+)\\/(\\d+)/.test(options.url.href)) {\n      options.result = options.url.href;\n      return;\n    }\n\n    // 匹配种子详细页面地址\n    let id_match = options.url.href.match(/\\/t\\/(\\d+)/);\n    let passkey = options.site.passkey;\n    if (passkey) {\n      let id = id_match[1];\n      // 格式：vvvid|||passkeyzz\n      let key = (new Base64).encode(\"vvv\" + id + \"|||\" + options.site.passkey + \"zz\");\n      options.result = `https://${options.site.host}/rssdd.php?par=${key}&ssl=yes`;\n    } else {\n      options.error = {\n        success: false,\n        msg: \"请先设置站点的passkey\"\n      }\n    }\n  } else {\n    options.error = {\n      success: false,\n      msg: \"无效的下载地址\"\n    }\n  }\n})(options)\n"
  },
  {
    "path": "resource/sites/u2.dmhy.org/config.json",
    "content": "{\n  \"name\": \"U2\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"动漫花园分享园\",\n  \"url\": \"https://u2.dmhy.org/\",\n  \"icon\": \"https://u2.dmhy.org/favicon.ico\",\n  \"tags\": [\n    \"影视\",\n    \"动漫\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"u2.dmhy.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"御宅族\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"查看会员列表; 请求补种; 查看普通日志; 使用流量信息条\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"宅修士\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"宅教士\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"宅传教士\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"宅护法\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"使用邀请名额; 无可用邀请时，购买邀请\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"宅贤者\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1024GB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"宅圣\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1536GB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"账号封存后永久保留.\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"宅神\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3072GB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"账号永久保留\"\n  }],\n  \"searchEntryConfig\": {\n    \"merge\": true,\n    \"skipIMDbId\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"td[class*='seedhlc_']\", \"td[class*='leechhlc_']\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"query[0].innerHTML.split('<br>')[1]\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"td[class*='seedhlc_ever']\", \".seedhlc_current\", \".leechhlc_inactive\", \".leechhlc_current\"],\n        \"switchFilters\": [\n          [\"255\"],\n          [\"2\"],\n          [\"3\"],\n          [\"1\"]\n        ]\n      },\n      \"leechers\": {\n        \"selector\": [\"\"],\n        \"filters\": [\"query.get(0).firstChild\", \"query.nodeValue||query.innerText||0\"]\n      }\n    }\n  },\n  \"searchEntry\": [{\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat9=1\",\n      \"name\": \"U2-Rip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat411=1\",\n      \"name\": \"U2-RBD\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat10=1\",\n      \"name\": \"R3TRAW\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat11=1\",\n      \"name\": \"R2JRAW\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat12=1\",\n      \"name\": \"BDRip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat13=1\",\n      \"name\": \"DVDRip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat14=1\",\n      \"name\": \"HDTVRip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat15=1\",\n      \"name\": \"DVDISO\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat16=1\",\n      \"name\": \"BDMV\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat17=1\",\n      \"name\": \"LQRip\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat410=1\",\n      \"name\": \"外挂结构\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat412=1\",\n      \"name\": \"加流重灌\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat21=1\",\n      \"name\": \"Raw Books\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat22=1\",\n      \"name\": \"港译漫画\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat23=1\",\n      \"name\": \"台译漫画\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat30=1\",\n      \"name\": \"Lossless Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat40=1\",\n      \"name\": \"Others\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"cat$id$=1\",\n    \"category\": [{\n        \"id\": 9,\n        \"name\": \"U2-Rip\"\n      },\n      {\n        \"id\": 411,\n        \"name\": \"U2-RBD\"\n      },\n      {\n        \"id\": 10,\n        \"name\": \"R3TRAW\"\n      },\n      {\n        \"id\": 11,\n        \"name\": \"R2JRAW\"\n      },\n      {\n        \"id\": 12,\n        \"name\": \"BDRip\"\n      },\n      {\n        \"id\": 13,\n        \"name\": \"DVDRip\"\n      },\n      {\n        \"id\": 14,\n        \"name\": \"HDTVRip\"\n      },\n      {\n        \"id\": 15,\n        \"name\": \"DVDISO\"\n      },\n      {\n        \"id\": 16,\n        \"name\": \"BDMV\"\n      },\n      {\n        \"id\": 17,\n        \"name\": \"LQRip\"\n      },\n      {\n        \"id\": 410,\n        \"name\": \"外挂结构\"\n      },\n      {\n        \"id\": 412,\n        \"name\": \"加流重灌\"\n      },\n      {\n        \"id\": 21,\n        \"name\": \"Raw Books\"\n      },\n      {\n        \"id\": 22,\n        \"name\": \"港译漫画\"\n      },\n      {\n        \"id\": 23,\n        \"name\": \"台译漫画\"\n      },\n      {\n        \"id\": 30,\n        \"name\": \"Lossless Music\"\n      },\n      {\n        \"id\": 40,\n        \"name\": \"Others\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('UCoin') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/\\\\(([\\\\d.]+)/)\", \"(query && query.length>=2)?parseFloat(query[1]):null\"]\n        }\n      }\n    },\n    \"bonusExtendInfo\": {\n      \"page\": \"/mprecent.php?user=$user.id$\",\n      \"fields\": {\n        \"bonusPerHour\": {\n          \"selector\": [\"#outer > table.main td.embedded table td.text\"],\n          \"filters\": [\"query.contents().filter(function() {return this.nodeType == 3;}).text()\",\n                      \"query.match(/\\\\d+[.,]\\\\d+/)\",\n                      \"query ? parseFloat(query[0].replace(/,/g, '.')) / 24 : null\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"merge\": true,\n      \"fields\": {\n        \"sayThanksButton\": {\n          \"selector\": [\"span#thanksbutton input:button:first:not(:disabled)\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/ubits.club/config.json",
    "content": "{\n    \"name\": \"UBits\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"ubits.club\",\n    \"url\": \"https://ubits.club/\",\n    \"icon\": \"https://ubits.club/favicon.ico\",\n    \"tags\": [\"影视\"],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"ubits.club\",\n    \"collaborator\": [\"IITII\"],\n    \"levelRequirements\": [\n      {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"5\",\n        \"downloaded\": \"200GB\",\n        \"seedingPoints\": \"80000\",\n        \"ratio\": \"2\",\n        \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕。\"\n      },\n      {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"10\",\n        \"downloaded\": \"500G\",\n        \"seedingPoints\": \"150000\",\n        \"ratio\": \"3\",\n        \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n      },\n      {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"800G\",\n        \"seedingPoints\": \"300000\",\n        \"ratio\": \"4\",\n        \"privilege\": \"得到1个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n      },\n      {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"seedingPoints\": \"650000\",\n        \"interval\": \"20\",\n        \"downloaded\": \"1T\",\n        \"ratio\": \"5\",\n        \"privilege\": \"得到1个邀请名额；可以查看普通日志。\"\n      },\n      {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"1.5T\",\n        \"seedingPoints\": \"1000000\",\n        \"ratio\": \"6\",\n        \"privilege\": \"得到1个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n      },\n      {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"30\",\n        \"seedingPoints\": \"2000000\",\n        \"downloaded\": \"2T\",\n        \"ratio\": \"7\",\n        \"privilege\": \"得到2个邀请名额；可以更新过期的外部信息；可以查看Extreme User论坛。\"\n      },\n      {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"40\",\n        \"seedingPoints\": \"3000000\",\n        \"downloaded\": \"3T\",\n        \"ratio\": \"8\",\n        \"privilege\": \"得到3个邀请名额。\"\n      },\n      {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"seedingPoints\": \"5000000\",\n        \"interval\": \"60\",\n        \"downloaded\": \"4T\",\n        \"ratio\": \"10\",\n        \"privilege\": \"得到4个邀请名额。\"\n      }\n    ],\n    \"searchEntry\": [\n      {\n        \"name\": \"全站\",\n        \"enabled\": true\n      }\n    ],\n    \"searchEntryConfig\": {\n      \"fieldSelector\": {\n        \"progress\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n        },\n        \"status\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\n            \"query ? query.attr('title') : ''\",\n            \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n          ]\n        }\n      }\n    },\n    \"selectors\": {\n      \"userSeedingTorrents\": {\n        \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n        \"fields\": {\n          \"seeding\": {\n            \"selector\": [\n              \"b:first\"\n            ],\n            \"filters\": [\n              \"query.text()\"\n            ]\n          },\n          \"seedingSize\": {\n            \"selector\": \"\",\n            \"filters\": [\n              \"query.text().match(/总大小：(.*?)上一页/g)\",\n              \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n              \"(query != 0) ? query.sizeToNumber() : 0\"\n            ]\n          }\n        }\n      }\n    }\n  }\n  "
  },
  {
    "path": "resource/sites/uhdbits.org/config.json",
    "content": "{\n  \"name\": \"UHDBits\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"HD\",\n  \"icon\": \"https://uhdbits.org/favicon.ico\",\n  \"schema\": \"GazelleJSONAPI\",\n  \"tags\": [\"影视\"],\n  \"url\": \"https://uhdbits.org/\",\n  \"collaborator\": [\"bimzcy\", \"enigamz\"],\n  \"host\": \"uhdbits.org\",\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"searchstr=$key$&group_results=0&searchsubmit=1\",\n    \"asyncParse\": false,\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"replaceKey\": [\"tt\", \"\"],\n      \"queryString\": \"imdbid=$key$&group_results=0&searchsubmit=1\"\n    }]\n  },\n  \"searchEntry\": [{\n      \"name\": \"all\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"filter_cat[1]=1\",\n      \"name\": \"Movie\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[2]=1\",\n      \"name\": \"Music\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"filter_cat[3]=1\",\n      \"name\": \"TV\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"&filter_cat[$id$]=1\",\n    \"category\": [{\n        \"id\": 1,\n        \"name\": \"Movie\"\n      },\n      {\n        \"id\": 2,\n        \"name\": \"Music\"\n      },\n      {\n        \"id\": 3,\n        \"name\": \"TV\"\n      }\n    ]\n  }],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/torrents.php?type=seeding&userid=$user.id$\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"[href='bonus.php']+span\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"td.number_column.nobr\"],\n          \"filters\": [\"jQuery.map(query, (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        },\n        \"seedingList\": {\n          \"selector\": [\"a[href*='torrentid=']\"],\n          \"filters\": [\"jQuery.map(query, item=>$(item).attr('href').match(/torrentid=(\\\\d+)/)[1])\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/uhdbits.org/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/auth_form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table.torrent_table:last > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: 4,\n        size: 5,\n        seeders: 7,\n        leechers: 8,\n        completed: 6,\n        comments: 3,\n        author: 9\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let id = row.find(\"a[href*='#torrent']\").first()\n          id = id.attr('href').match(/#torrent(\\d+)/)[1]\n\n          let title = row.find(\"a[href*='torrents.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          let subTitle = row.find(\"div.torrent_info\").first();\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row\n            .find(\"a[href*='torrents.php?action=download'][title='Download']\")\n            .first();\n\n          if (url.length == 0) {\n            continue;\n          }\n\n          url = url.attr(\"href\");\n\n          if (url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells\n                  .eq(fieldIndex.time)\n                  .find(\"span[title],time[title]\")\n                  .attr(\"title\") ||\n                cells.eq(fieldIndex.time).text() ||\n                \"\";\n          if (time) {\n            time += \":00\";\n          }\n\n          let data = {\n            id,\n            title: title.text() + ' / ' +subTitle.text(),\n            //subTitle: subTitle.text(),\n            link,\n            url: url,\n            size: cells.eq(fieldIndex.size).html() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            tags: this.getTags(row),\n            site: site,\n            entryName: options.entry.name,\n            category: this.getCategory(cells.find(\"a[href*='filter_cat']\"))\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} link 当前列\n     */\n    getCategory(link) {\n      if (link.length == 0) {\n        return null;\n      }\n\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n\n      result.link = link.attr(\"href\");\n      let id = result.link.match(/filter_cat\\[(\\d+)\\]/)[1];\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = link.text().trim();\n\n      if (!result.name) {\n        result.name = this.getCategoryName(id);\n      }\n      return result;\n    }\n\n\n    getTags(row){\n        var query = row.find(\"strong:contains('Free'), strong:contains('2x'), strong:contains('%')\");\n        var BASE_TAG_COLORS = {\n          // 免费下载\n          Free: \"blue\",\n          // 免费下载 + 2x 上传\n          \"2xFree\": \"green\",\n          // 2x 上传\n          \"2xUp\": \"lime\",\n          // 2x 上传 + 50% 下载\n          \"2x50%\": \"light-green\",\n          // 25% 下载\n          \"25%\": \"purple\",\n          // 30% 下载\n          \"30%\": \"indigo\",\n          // 35% 下载\n          \"35%\": \"indigo darken-3\",\n          // 50% 下载\n          \"50%\": \"orange\",\n          // 70% 下载\n          \"70%\": \"blue-grey\",\n          // 75% 下载\n          \"75%\": \"lime darken-3\",\n          // 仅 VIP 可下载\n          VIP: \"orange darken-2\",\n          // 禁止转载\n          \"⛔️\": \"deep-orange darken-1\"\n        };\n        if(query.length > 0) {\n            query = query.text().replace(' ','').replace('↓','');\n            var result = [{\n\t            name: query,\n\t            color: BASE_TAG_COLORS[query]\n            }]\n            return result;\n        }\n    }\n\n    \n\n    getCategoryName(id) {\n      if ($.isEmptyObject(this.categories)) {\n        let cells = options.page.find(\".cat_list:first\").find(\"td\");\n        cells.each((i, dom) => {\n          let id = $(dom)\n            .find(\"input\")\n            .attr(\"id\")\n            .replace(\"cat_\", \"\");\n          let name = $(dom)\n            .find(\"label\")\n            .text();\n          if (id) {\n            this.categories[id] = name;\n          }\n        });\n      }\n\n      return this.categories ? this.categories[id] : \"\";\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, Searcher);\n"
  },
  {
    "path": "resource/sites/uhdbits.org/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 1\n      };\n      this.result = {\n        seedingSize: 0,\n        bonus: 0,\n        seedingList: []\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seedingSize += results.seedingSize;\n        this.result.seedingList = this.result.seedingList.concat(results.seedingList)\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        if (results) {\n          this.result.bonus = this.body\n          .find(\"[href='bonus.php']+span\")\n          .text();\n        }\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='torrents.php?page=']:contains('Last'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 2;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 1) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/ultrahd.net/config.json",
    "content": "{\n  \"name\": \"UltraHD\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://ultrahd.net\",\n  \"description\": \"韩剧\",\n  \"icon\": \"https://ultrahd.net/favicon.ico\",\n  \"tags\": [\n    \"电影\",\n    \"电视剧\",\n    \"综艺\",\n    \"纪录片\",\n    \"动漫\"\n  ],\n  \"host\": \"ultrahd.net\",\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \".torrentname td:first-child > div[title]:last-child\"\n        ],\n        \"filters\": [\n          \"query ? query.attr('title').match(/([\\\\d\\\\.]+)%/) : null\",\n          \"(query && query.length >= 2) ? parseFloat(query[1]) : null\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \".torrentname td:first-child > div[title]:last-child\"\n        ],\n        \"filters\": [\n          \"query ? query.attr('title').split(' ')[0] : null\",\n          \"query === 'leeching' ? 1 : query === 'seeding' ? 2 : query === 'inactivity' ? 255 : null\"\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n              \"query.text().match(/总大小：(.*?)上一页/g)\",\n              \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n              \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  },\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \" Power User\",\n    \"interval\": \"5\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"3.0\",\n    \"seedingPoints\": \"100000\",\n    \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"10\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"3.5\",\n    \"seedingPoints\": \"200000\",\n    \"privilege\": \"Elite User及以上用户封存账号后不会被删除\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"4.0\",\n    \"seedingPoints\": \"400000\",\n    \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"20\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"4.5\",\n    \"seedingPoints\": \"600000\",\n    \"privilege\": \"可以查看普通日志\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"2TB\",\n    \"ratio\": \"5.0\",\n    \"seedingPoints\": \"800000\",\n    \"privilege\": \"可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"30\",\n    \"downloaded\": \"4TB\",\n    \"ratio\": \"5.5\",\n    \"seedingPoints\": \"1000000\",\n    \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。【可以开启特别区和查看特别区资源】。\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"35\",\n    \"downloaded\": \"6TB\",\n    \"ratio\": \"6.0\",\n    \"seedingPoints\": \"1200000\",\n    \"privilege\": \"得到一个邀请名额\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"40\",\n    \"downloaded\": \"8TB\",\n    \"ratio\": \"6.5\",\n    \"seedingPoints\": \"1500000\",\n    \"privilege\": \"得到两个邀请名额\"\n  }],\n  \"searchEntry\": [\n    { \"name\": \"全站\", \"enabled\": true },\n    { \"name\": \"电影\", \"queryString\": \"cat401=1\", \"enabled\": false },\n    { \"name\": \"电视剧\", \"queryString\": \"cat402=1\", \"enabled\": false },\n    { \"name\": \"综艺\", \"queryString\": \"cat403=1\", \"enabled\": false },\n    { \"name\": \"纪录片\", \"queryString\": \"cat404=1\", \"enabled\": false },\n    { \"name\": \"动漫\", \"queryString\": \"cat405=1\", \"enabled\": false }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/wintersakura.net/config.json",
    "content": "{\n    \"name\": \"wintersakura\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"wintersakura\",\n    \"url\": \"https://wintersakura.net/\",\n    \"icon\": \"https://wintersakura.net/favicon.ico\",\n    \"tags\": [],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"wintersakura.net\",\n    \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1\",\n            \"seedingPoints\": \"50000\",\n            \"privilege\": \"可以查看NFO文档；可以请求续种； 可以购买/发送邀请；可以删除自己上传的字幕。可以申请友情链接；可以使用个性条。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"downloaded\": \"400GB\",\n            \"ratio\": \"1.5\",\n            \"seedingPoints\": \"120000\",\n            \"privilege\": \"可以查看种子结构；可以更新外部信息\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"downloaded\": \"800GB\",\n            \"ratio\": \"2\",\n            \"seedingPoints\": \"200000\",\n            \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"3\",\n            \"seedingPoints\": \"500000\",\n            \"privilege\": \"可以查看排行榜。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"可以查看其它用户种子历史。（只有用户的隐私等级没有设为’强‘时才生效）\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"downloaded\": \"5TB\",\n            \"ratio\": \"6\",\n            \"seedingPoints\": \"1400000\",\n            \"privilege\": \"可以更新过期的外部信息。Extreme User 及以上用户封存账号后不会被删除\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"downloaded\": \"6TB\",\n            \"ratio\": \"8\",\n            \"seedingPoints\": \"2000000\",\n            \"privilege\": \"首次到达此等级得到1个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"downloaded\": \"10TB\",\n            \"ratio\": \"9.5\",\n            \"seedingPoints\": \"2800000\",\n            \"privilege\": \"首次到达此等级得到1个邀请名额Nexus Master及以上用户会永远保留账号。\"\n        }\n    ],\n    \"selectors\": {\n        \"userSeedingTorrents\": {\n            \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n            \"fields\": {\n                \"seeding\": {\n                    \"selector\": [\n                        \"b:first\"\n                    ],\n                    \"filters\": [\n                        \"query.text()\"\n                    ]\n                },\n                \"seedingSize\": {\n                    \"selector\": \"\",\n                    \"filters\": [\n                        \"query.text().match(/总大小：(.*?)上一页/g)\",\n                        \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n                        \"(query != 0) ? query.sizeToNumber() : 0\"\n                    ]\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "resource/sites/world-in-hd.net/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"div.download-item a[href*='/torrents/download/']\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) == \"/\") {\n        siteURL = siteURL.substr(0,siteURL.length-1);\n      }\n\n      if (links.length == 0) {\n        // \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        if (link && link.substr(0, 4) != \"http\") {\n          link = siteURL + link;\n        }\n        return link;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"div.torrent-h3 > span\").text().split(\"-\")[1].trim().replace('o','B')\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      if (url.indexOf(\"download\") === -1) {\n        return \"\";\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/world-in-hd.net/config.json",
    "content": "{\n  \"name\": \"WiHD\",\n  \"timezoneOffset\": \"+0200\",\n  \"schema\": \"WiHD\",\n  \"url\": \"https://world-in-hd.net/\",\n  \"icon\": \"https://world-in-hd.net/media/cache/icon32/appicon.png\",\n  \"tags\": [\"影视\"],\n  \"host\": \"world-in-hd.net\",\n  \"collaborator\": \"luckiestone\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/torrent/view/\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"720p\",\n      \"interval\": \"5\",\n      \"uploaded\": \"250GB\",\n      \"ratio\": \"2\"\n    },\n    {\n      \"level\": \"2\",\n      \"name\": \"1080i\",\n      \"interval\": \"15\",\n      \"uploaded\": \"400GB\",\n      \"ratio\": \"3\"\n    },\n    {\n      \"level\": \"3\",\n      \"name\": \"1080p\",\n      \"interval\": \"25\",\n      \"uploaded\": \"1.2TB\",\n      \"ratio\": \"4.5\"\n    }\n  ],\n\"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"page\": \"/torrent/ajaxsearchtorrent/$key$\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"div.torrent-body\"\n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"div.fl-item\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"span.username\"\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        },\n        \"messageCount\": {\n          \"selector\": [\"li.messages li.message\", \"li.notifications li.notification\"],\n          \"filters\": [\"(query && query.length>=1)?11:0\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"i.fa-upload + strong\"]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/profils/user/$user.name$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"div.stats a.btn:contains('Upload')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').replace('o','B').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"div.stats a.btn:contains('Download')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').replace('o','B').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"levelName\": {\n          \"selector\": \"span.class\"\n        },\n        \"joinTime\": {\n          \"selector\": \"div.user-block-content:first\",\n          \"filters\": [\"query.text().trim()\", \"dateTime(query,'DD\\/MM\\/YYYY').isValid()?dateTime(query,'DD\\/MM\\/YYYY').valueOf():query\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/world-in-hd.net/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"div.download a[href*='/torrents/download/']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return `${location.origin}${url}`;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"header.panel-heading h2\").text().trim();\n      return title;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/world-in-hd.net/getSearchResult.js",
    "content": "if (!\"\".getQueryString) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/login\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(options.resultSelector);\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: 1,\n        size: 2,\n        seeders: 3,\n        leechers: 4,\n        completed: 5,\n        comments: 6,\n        author: 7,\n        category: 8,\n        title: 0\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 0; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let timeStrMatch = row.find(\"div.torrent-h3 > span\").text().split(\"-\")[0].replace('Il y a','');\n          let timeStr = timeStrMatch.trim();\n          let cells = [];\n          cells[2] = row.find(\"div.torrent-h3 > span\").text().split(\"-\")[1].trim().replace('o','B');\n          cells[3] = row.find(\"div.seeders\").text().replace('Seeders','').trim();\n          cells[4] = row.find(\"div.leechers\").text().replace('Leechers','').trim();\n          cells[5] = row.find(\"div.completed\").text().replace('Complétés','').trim();\n          cells[6] = row.find(\"div.comments\").text().replace('Commentaires','').trim();\n          cells[7] = row.find(\"div.uploader a.username\").text().trim();\n          cells[8] = row.find(\"div.category img\").attr(\"title\");\n          cells[9] = row.find(\"div.completed\").text().replace('Complétés','').trim();\n          let title = row.find(\"div.torrent-h3 h3 a\");\n          if (title.length == 0) {\n            continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          // 获取下载链接\n          let url = row.find(\"a[href*='/torrents/download/']:first\").attr(\"href\");\n          if (url && url.substr(0, 4) !== \"http\") {\n            url = `${site.url}${url}`;\n          }\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url,\n            size: cells[fieldIndex.size] || 0,\n            time: this.getTime(timeStr),\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells[fieldIndex.author] || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells[fieldIndex.seeders] || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells[fieldIndex.leechers] || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells[fieldIndex.completed] || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells[fieldIndex.comments] || 0,\n            site: site,\n            entryName: options.entry.name,\n            category:\n              fieldIndex.category == -1\n                ? \"\"\n                : cells[fieldIndex.category] || \"\",\n            tags: this.getTags(row, options.torrentTagSelectors)\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    getTime(timeStr) {\n      let timeRegex = timeStr.match(\n        /((\\d+).+?(Minute|Heure|Jour|Moi|Année)s?.*?(\\,|and))?.*?(\\d+).+?(Minute|Heure|Jour|Moi|Année)s?/\n      );\n      let milliseconds = 0;\n      if (timeRegex) {\n        if (timeRegex[1] == undefined) {\n          milliseconds = this.getMilliseconds(timeRegex[5], timeRegex[6]);\n        } else {\n          milliseconds = this.getMilliseconds(timeRegex[2], timeRegex[3]) + this.getMilliseconds(timeRegex[5], timeRegex[6]);\n        }\n      }\n      let timeStamp = Date.now() - milliseconds;\n      let date = new Date(timeStamp);\n      return date.toISOString();\n    }\n\n    getMilliseconds(num, unit) {\n      let milliseconds = 0;\n      milliseconds = num*60*1000;\n      if(unit == \"Minute\") {return milliseconds;}\n      milliseconds = milliseconds*60;\n      if(unit == \"Heure\") {return milliseconds;}\n      milliseconds = milliseconds*24;\n      if(unit == \"Jour\") {return milliseconds;}\n      milliseconds = milliseconds*30;\n      if(unit == \"Moi\") {return milliseconds;}\n      milliseconds = milliseconds*12;\n      return milliseconds;\n    }\n\n    /**\n     * 获取标签\n     * @param {*} row\n     * @param {*} selectors\n     * @return array\n     */\n    getTags(row, selectors) {\n      let tags = [];\n      if (selectors && selectors.length > 0) {\n        // 使用 some 避免错误的背景类名返回多个标签\n        selectors.some(item => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              tags.push({\n                name: \"Free\",\n                color: \"blue\"\n              });\n              return true;\n            }\n          }\n        });\n      }\n      return tags;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/www.beitai.pt/config.json",
    "content": "{\n  \"name\": \"备胎\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.beitai.pt/\",\n  \"description\": \"找不到家时，接纳无家可归的人\",\n  \"icon\": \"https://www.beitai.pt/favicon.ico\",\n  \"tags\": [\n    \"综合\"\n  ],\n  \"host\": \"www.beitai.pt\",\n  \"levelRequirements\": [{\n    \"level\": \"1\", \n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"得到一个邀请名额；直接发布种子；查看NFO文档；查看用户列表；请求续种；发送邀请；查看排行榜；查看其它用户的种子历史；删除自己上传的字幕。\"\n  },{\n    \"level\": \"2\", \n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"120GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"封存账号后不会被删除\"\n  },{\n    \"level\": \"3\", \n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"得到两个邀请名额；在做种/下载/发布的时候选择匿名模式\"\n  },{\n    \"level\": \"4\", \n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"500GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"发送邀请；查看普通日志\"\n  },{\n    \"level\": \"5\", \n    \"name\": \"Veteran User\",\n    \"interval\": \"40\",\n    \"downloaded\": \"750GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"永远保留账号；得到三个邀请名额；查看其它用户的评论、帖子历史\"\n  },{\n    \"level\": \"6\", \n    \"name\": \"Extreme User\",\n    \"interval\": \"60\",\n    \"downloaded\": \"1TB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"更新过期的外部信息；查看Extreme User论坛\"\n  },{\n    \"level\": \"7\", \n    \"name\": \"Ultimate User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"1.5TB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"得到五个邀请名额\"\n  },{\n    \"level\": \"8\", \n    \"name\": \"Nexus Master\",\n    \"interval\": \"100\",\n    \"downloaded\": \"3TB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"得到十个邀请名额\"\n  }],\n  \"collaborator\": [\"wyx1818\", \"tongyifan\"],\n  \"searchEntryConfig\": {\n    \"area\": [\n      {\n        \"name\": \"IMDB\",\n        \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n        \"appendQueryString\": \"&search_area=1\"\n      }\n    ],\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:parseFloat(query.text().split('%')[0])\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"> td:eq(8)\"\n        ],\n        \"filters\": [\n          \"query.text()==='-'?null:(query.is(\\\"[bgcolor='#44cef6']\\\")?1:(parseFloat(query.text().split('%')[0])==100?(query.is(\\\"[bgcolor='#d0d0d0']\\\")?255:2):3))\"\n        ]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    },\n    {\n      \"queryString\": \"cat401=1\",\n      \"name\": \"Movies\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat404=1\",\n      \"name\": \"Documentaries\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat405=1\",\n      \"name\": \"Animations\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat402=1\",\n      \"name\": \"TV Series\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat403=1\",\n      \"name\": \"TV Shows\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat406=1\",\n      \"name\": \"Music Videos\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat407=1\",\n      \"name\": \"Sports\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat409=1\",\n      \"name\": \"Misc\",\n      \"enabled\": false\n    },\n    {\n      \"queryString\": \"cat408=1\",\n      \"name\": \"HQ Audio\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [\n    {\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"Movies\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"Documentaries\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"Animations\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"TV Series\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"TV Shows\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"Music Videos\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"Sports\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"Misc\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"HQ Audio\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/www.cgpeers.com/config.json",
    "content": "{\n  \"name\": \"CGPeers\",\n  \"timezoneOffset\": \"+0000\",\n  \"schema\": \"GazelleJSONAPI\",\n  \"url\": \"https://www.cgpeers.com/\",\n  \"icon\": \"https://www.cgpeers.com/favicon.ico\",\n  \"tags\": [\"设计\", \"素材\"],\n  \"host\": \"www.cgpeers.com\",\n  \"collaborator\": \"bimzcy\",\n  \"supportedFeatures\": {\n    \"imdbSearch\": false,\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/www.cinematik.net/browse.js",
    "content": "(function($) {\n  console.log(\"this is browse.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\n        \"a.brolin[href*='details.php?id='][href*='hit=1']:has(b)\"\n      ).toArray();\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (links.length == 0) {\n        //  \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let urls = $.map(links, item => {\n        let url =\n          \"download.php?id=\" +\n          $(item)\n            .attr(\"href\")\n            .getQueryString(\"id\");\n        if (url) {\n          url = siteURL + url;\n        }\n        return url;\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"table[border='1']:last\").find(\n          \"td:contains('MB'),td:contains('GB'),td:contains('TB')\"\n        )\n      );\n    }\n\n    /**\n     * 获取有效的拖放地址\n     * @param {*} url\n     */\n    getDroperURL(url) {\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      if (!url.getQueryString) {\n        PTService.showNotice({\n          msg:\n            \"系统依赖函数（getQueryString）未正确加载，请尝试刷新页面或重新启用插件。\"\n        });\n        return null;\n      }\n\n      let id = url.getQueryString(\"id\");\n      if (id) {\n        url = siteURL + \"download.php?id=\" + id;\n      } else {\n        url = \"\";\n      }\n\n      return url;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/www.cinematik.net/config.json",
    "content": "{\n  \"name\": \"Cinematik\",\n  \"timezoneOffset\": \"+0000\",\n  \"schema\": \"Cinematik\",\n  \"url\": \"https://www.cinematik.net/\",\n  \"icon\": \"https://www.cinematik.net/favicon.ico\",\n  \"tags\": [\"影视\"],\n  \"host\": \"www.cinematik.net\",\n  \"collaborator\": \"DXV5\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/browse.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"browse.js\"]\n  }],\n  \"levelRequirements\": [\n    {\n      \"level\": \"1\",\n      \"name\": \"Power User\",\n      \"interval\": \"8\",\n      \"uploaded\": \"100GB\",\n      \"ratio\": \"1.1\",\n      \"privilege\": \"More download slots\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/browse.php\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"queryString\": \"search=$key$&incldead=1\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"queryString\": \"search=$key$&incldead=1&srchdtls=1\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\"\n    }]\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img[src*='freedownload.png']\"\n  }, {\n    \"name\": \"2xFree\",\n    \"selector\": \"img[src*='platinumdownload.png']\"\n  }, {\n    \"name\": \"25%\",\n    \"selector\": \"img[src*='silverdownload.png']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \"div#menu a[href*='userdetails.php']\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"name\": {\n          \"selector\": \"table.mainouter > tbody > tr > td > table.main h1\"\n        },\n        \"uploaded\": {\n          \"selector\": [\"#user-default td.rowhead:contains('Uploaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"#user-default td.rowhead:contains('Downloaded') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": \"#user-default td.rowhead:contains('Class') + td\"\n        },\n        \"bonus\": {\n          \"value\": \"N/A\"\n        },\n        \"joinTime\": {\n          \"selector\": \"#user-default td.rowhead:contains('Join') + td\",\n          \"filters\": [\"query.text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/userdetails-tab.php?SID=&id=$user.id$&mode=7&page=0\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"table:first tr:not(:eq(0))\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"table:first tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td:eq(4)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/www.cinematik.net/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a[href*='download.php?id=']\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        let id = location.href.getQueryString(\"id\");\n        url = `download.php?id=${id}`;\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return `${location.origin}/${url}`;\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"title\").text();\n      return title.replace(\"Cinematik :: \", \"\");\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.cinematik.net/getSearchResult.js",
    "content": "(function(options) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/takelogin\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n\n      options.isLogged = true;\n\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      let site = options.site;\n      let results = [];\n      // 获取种子列表行\n      let rows = options.page.find(\n        options.resultSelector || \"table[border='1']:last > tbody > tr\"\n      );\n      if (rows.length == 0) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return results;\n      }\n      // 获取表头\n      let header = rows.eq(0).find(\"th,td\");\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = {\n        time: 10,\n        size: 6,\n        seeders: 8,\n        leechers: 9,\n        completed: 7,\n        comments: -1,\n        author: -1,\n        category: 0,\n        title: 1\n      };\n\n      if (site.url.lastIndexOf(\"/\") != site.url.length - 1) {\n        site.url += \"/\";\n      }\n\n      try {\n        // 遍历数据行\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = row.find(\"a[href*='details.php?id=']\").first();\n          if (title.length == 0) {\n            continue;\n          }\n\n          let link = title.attr(\"href\");\n          if (link && link.substr(0, 4) !== \"http\") {\n            link = `${site.url}${link}`;\n          }\n\n          let id = link.getQueryString(\"id\");\n\n          // 获取下载链接\n          let url = `${site.url}download.php?id=${id}`;\n\n          let time =\n            fieldIndex.time == -1\n              ? \"\"\n              : cells\n                  .eq(fieldIndex.time)\n                  .find(\"div.addedtor\")\n                  .text() || \"\";\n\n          let data = {\n            title: title.text(),\n            subTitle: \"\",\n            link,\n            url: url,\n            size: cells.eq(fieldIndex.size).html() || 0,\n            time: time,\n            author:\n              fieldIndex.author == -1\n                ? \"\"\n                : cells.eq(fieldIndex.author).text() || \"\",\n            seeders:\n              fieldIndex.seeders == -1\n                ? \"\"\n                : cells.eq(fieldIndex.seeders).text() || 0,\n            leechers:\n              fieldIndex.leechers == -1\n                ? \"\"\n                : cells.eq(fieldIndex.leechers).text() || 0,\n            completed:\n              fieldIndex.completed == -1\n                ? \"\"\n                : cells.eq(fieldIndex.completed).text() || 0,\n            comments:\n              fieldIndex.comments == -1\n                ? \"\"\n                : cells.eq(fieldIndex.comments).text() || 0,\n            site: site,\n            entryName: options.entry.name,\n            category: this.getCategory(cells.eq(fieldIndex.category)),\n            tags: options.searcher.getRowTags(site, row)\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents; //`[${options.site.name}]没有搜索到相关的种子`;\n        }\n      } catch (error) {\n        console.error(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack; //`[${options.site.name}]获取种子信息出错: ${error.stack}`;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取分类\n     * @param {*} cell 当前列\n     */\n    getCategory(cell) {\n      let result = {\n        name: \"\",\n        link: \"\"\n      };\n      let link = cell.find(\"a:first\");\n      let img = link.find(\"img:first\");\n\n      result.link = link.attr(\"href\");\n      if (result.link.substr(0, 4) !== \"http\") {\n        result.link = options.site.url + result.link;\n      }\n\n      result.name = img.attr(\"alt\");\n      return result;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options);\n"
  },
  {
    "path": "resource/sites/www.cinematik.net/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 0\n      };\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seeding += results.seeding;\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"a[href*='type=seeding']:contains('1'):last\")\n        .attr(\"href\");\n\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.getQueryString(\"page\"));\n      } else {\n        this.pageInfo.count = 1;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */\n"
  },
  {
    "path": "resource/sites/www.empornium.sx/config.json",
    "content": "{\n  \"name\": \"Empornium\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"EMP\",\n  \"url\": \"https://www.empornium.sx/\",\n  \"tags\": [\"Adult\"],\n  \"schema\": \"Common\",\n  \"plugins\": [{\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"host\": \"www.empornium.sx\",\n  \"formerHosts\": [\n    \"www.empornium.me\",\n    \"www.empornium.is\"\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"torrents.php?searchtext=$key$\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table.torrent_table.grouping\",\n    \"dataRowSelector\": \" > tbody > tr:not(:first-child)\",\n    \"fieldIndex\": {\n\t    \"category\": 0,\n\t    \"title\": 1,\n\t    \"link\": 1,\n        \"comments\": 3,\n        \"time\": 4,\n        \"size\": 5,\n        \"author\": 9,\n        \"seeders\": 7,\n        \"leechers\": 8,\n        \"completed\": 6\n\t},\n\t\"fieldSelector\": {\n\t  \"category\": {\n\t\t\"selector\": [\"div[title]\"],\n        \"filters\": [\"query.attr('title')\"]\n\t  },\n\t  \"title\": {\n\t\t\"selector\": [\"a[onmouseout]\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"link\": {\n\t\t\"selector\": [\"a[onmouseout]\"],\n        \"filters\": [\"query.attr('href')\"]\n\t  },\n\t  \"url\": {\n\t\t\"selector\": [\"a[href*='action=download&id=']\"],\n        \"filters\": [\"query.attr('href')\"]\n\t  },\n\t  \"time\": {\n\t\t\"selector\": [\"\"],\n        \"filters\": [\"query.text()\"]\n\t  },\n\t  \"progress\": {\n        \"selector\": [\"a[title='Currently Seeding Torrent'], a[title='Previously Snatched Torrent']\", \"a[title='Previously Grabbed Torrent File']\", \"\"],\n        \"switchFilters\": [\n          [\"100\"],\n          [\"0\"],\n          [\"null\"]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"a[title='Currently Seeding Torrent']\", \"a[title='Previously Snatched Torrent']\", \"a[title='Previously Grabbed Torrent File']\"],\n        \"switchFilters\": [\n          [\"2\"],\n          [\"255\"],\n          [\"3\"]\n        ]\n      }\n\t}\n  },\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"span[title='Unlimited Freeleech']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n\t    \"id\": {\n          \"selector\": [\"a.username[href*='user.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": \"a.username\"\n        },\n        \"isLogged\": {\n          \"selector\": [\"#nav_logout\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.alertbar\"],\n          \"filters\": [\"(query && query.length>=1)?11:0\"]\n        },\n        \"seeding\": {\n\t      \"selector\": [\"#nav_seeding_r\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):null\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"td:contains('Up:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td:contains('Down:') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": [\"td:contains('Ratio:') + td\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td:contains('Credits:') + td\"],\n          \"filters\": [\"query.text()\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"joinTime\": {\n          \"selector\": [\"li:contains('Joined:') > span.time\"],\n          \"filters\": [\"dateTime(query.attr('title')).valueOf()\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"span.rank\"],\n          \"filters\":  [\"query.text()\"]\n        },\n        \"seedingSize\": {\n\t        \"selector\": [\"ul.stats.nobullet > li:contains('Seeding Size:')\"],\n\t        \"filters\": [\"query.text().replace('Seeding Size: ', '').replace(/,/g,'')\",\"query.sizeToNumber()\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/torrents.php\",\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"#content > div > div:nth-child(4)  td:contains('iB')\"],\n          \"filters\": [\"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"#thanksbutton\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"table.torrent_table a[href*='action=download&id='],a.button.blueButton\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table#torrent_table\"],\n          \"filters\": [\"query.find('td.nobr:contains(\\\\'iB\\\\')')\"]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "resource/sites/www.filept.com/config.json",
    "content": "{\n  \"name\": \"filept\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.filept.com/\",\n  \"description\": \"\",\n  \"icon\": \"https://www.filept.com/favicon.ico\",\n  \"tags\": [\n    \"影视\",\"综合\"\n  ],\n  \"host\": \"www.filept.com\",\n  \"collaborator\": \"koal\",\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.gamegamept.com/config.json",
    "content": "{\n  \"name\": \"GGPT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"gamegamept.com\",\n  \"url\": \"https://www.gamegamept.com/\",\n  \"icon\": \"https://www.gamegamept.com/favicon.ico\",\n  \"tags\": [\"游戏\"],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"www.gamegamept.com\",\n  \"collaborator\": [\n      \"IITII\",\n      \"yiyule\"\n  ],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"2\",\n      \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"100G\",\n      \"ratio\": \"2.5\",\n      \"privilege\": \"没有新权限增加\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300G\",\n      \"ratio\": \"3\",\n      \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"500G\",\n      \"ratio\": \"3.5\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"downloaded\": \"1T\",\n      \"ratio\": \"4\",\n      \"privilege\": \"可以查看其它用户的评论、帖子历史。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"2T\",\n      \"ratio\": \"4.5\",\n      \"privilege\": \"可以更新过期的外部信息。游戏大仙(Extreme User)及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"5T\",\n      \"ratio\": \"5\",\n      \"privilege\": \"这个等级会永远保留账号。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"downloaded\": \"10T\",\n      \"ratio\": \"5.5\",\n      \"privilege\": \"这个等级会永远保留账号。\"\n    }\n  ],\n  \"plugins\": [{\n    \"name\": \"9KG专区\",\n    \"pages\": [\"/special.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n  },{\n    \"name\": \"种子列表封面模式\",\n    \"pages\": [\"/torrents.php\", \"/special.php\"],\n    \"scripts\": [\"/libs/album/album.js\", \"torrents.js\"],\n    \"styles\": [\"/libs/album/style.css\"]\n  }],\n  \"categories\": [\n    {\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 401,\n          \"name\": \"PC游戏\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"索尼主机游戏\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"微软主机游戏\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"任天堂主机游戏\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"苹果游戏\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"安卓游戏\"\n        },\n        {\n          \"id\": 410,\n          \"name\": \"游戏书藉\"\n        },\n        {\n          \"id\": 411,\n          \"name\": \"其他\"\n        }\n      ]\n    },{\n      \"entry\": \"special.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [\n        {\n          \"id\": 412,\n          \"name\": \"9PC游戏\"\n        },\n        {\n          \"id\": 413,\n          \"name\": \"9索尼主机游戏\"\n        },\n        {\n          \"id\": 414,\n          \"name\": \"9KG-游戏\"\n        },\n        {\n          \"id\": 415,\n          \"name\": \"9任天堂主机游戏\"\n        },\n        {\n          \"id\": 416,\n          \"name\": \"9苹果游戏\"\n        },\n        {\n          \"id\": 417,\n          \"name\": \"9安卓游戏\"\n        },\n        {\n          \"id\": 418,\n          \"name\": \"其他\"\n        }\n      ]\n    }\n  ],\n  \"searchEntry\": [\n    {\n      \"name\": \"游戏\",\n      \"enabled\": true\n    },\n    {\n      \"entry\": \"special.php?search=$key$&notnewword=1\",\n      \"name\": \"9KG\",\n      \"enabled\": false\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\n            \"b:first\"\n          ],\n          \"filters\": [\n            \"query.text()\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.gamegamept.com/torrents.js",
    "content": "(function($, window) {\n  // 添加封面模式\n  PTService.addButton({\n    title: PTService.i18n.t(\"buttons.coverTip\"), //\"以封面的方式进行查看\",\n    icon: \"photo\",\n    label: PTService.i18n.t(\"buttons.cover\"), //\"封面模式\",\n    click: (success, error) => {\n      // 获取目标表格\n      let tables = $(\"table.torrentname\");\n      let images = [];\n      tables.each((index, item) => {\n        let img = $(\"img[src]\", item);\n        let url = img.attr(\"src\");\n        let href = $(\"a\", item).attr(\"href\");\n        let title = $(\"a\", item).find(\"b\").text();\n        images.push({\n          url: url,\n          key: href,\n          title: title, //img.parent().attr(\"title\"),\n          link: $(\"a\", item).attr(\"href\")\n        });\n      });\n\n      // 创建预览\n      new album({\n        images: images,\n        onClose: () => {\n          PTService.buttonBar.show();\n        }\n      });\n      success();\n      PTService.buttonBar.hide();\n    }\n  });\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.gaytor.rent/config.json",
    "content": "{\n  \"name\": \"GTru\",\n  \"timezoneOffset\": \"+0100\",\n  \"url\": \"https://www.gaytor.rent/\",\n  \"cdn\": [\"https://www.gaytor.rent\",\"https://www.gaytorrent.ru/\"],\n  \"icon\": \"https://www.gaytor.rent/favicon.ico\",\n  \"tags\": [\"影视\", \"成人\", \"综合\"],\n  \"schema\": \"GTru\",\n  \"host\": \"www.gaytor.rent\",\n  \"collaborator\": \"davidxuang\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"uploaded\": \"40 GB\",\n      \"ratio\": \"1.05\"\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/search.php\",\n    \"queryString\": \"search=$key$&incldead=1&inname=1&indesc=1&infn=1&orderby=added&sort=desc\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"#mysearchtable\",\n    \"fieldSelector\": {\n      \"status\": {\n        \"selector\": [\".tocolsnatched\", \".tocolloaded\"],\n        \"switchFilters\": [[\"255\"], [\"3\"]]\n      }\n    }\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"all\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"misc\",\n      \"appendQueryString\": \"&c46=1&c50=1&c48=1&c58=1&c45=1&c1=1\",\n      \"enabled\": true\n    },\n    {\n      \"name\": \"Movies\",\n      \"appendQueryString\": \"&c45=1\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"TV\",\n      \"appendQueryString\": \"&c1=1\",\n      \"enabled\": false\n    },\n    {\n      \"name\": \"Porn\",\n      \"appendQueryString\": \"&c62=1&c29=1&c30=1&c43=1&c19=1&c17=1&c44=1&c9=1&c7=1&c5=1&c67=1&c66=1&c34=1&c68=1&c27=1&c32=1&c63=1&c12=1&c33=1&c53=1&c57=1&c35=1&c36=1&c37=1&c54=1&c38=1&c39=1&c56=1&c40=1&c47=1&c41=1&c42=1&c51=1&c65=1&c28=1\",\n      \"enabled\": false\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/my.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": \".panel-title > a\",\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id') : ''\"]\n        },\n        \"name\": {\n          \"selector\": \".panel-title > a\"\n        },\n        \"uploaded\": {\n          \"selector\": \".nav:not(.navbar-right) > .navbar-text:first-of-type\",\n          \"filters\": [\"query ? query.text().replace(/.+UL:\\\\s*([\\\\d.]+ ?[A-Z]?i?B).+/gs, '$1').sizeToNumber() : null\"]\n        },\n        \"downloaded\": {\n          \"selector\": \".nav:not(.navbar-right) > .navbar-text:first-of-type\",\n          \"filters\": [\"query ? query.text().replace(/.+DL:\\\\s*([\\\\d.]+ ?[A-Z]?i?B).*/gs, '$1').sizeToNumber() : null\"]\n        },\n        \"bonus\": {\n          \"selector\": \"#bonus\"\n        },\n        \"messageCount\": {\n          \"selector\": \"#unread\"\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"levelName\": {\n          \"selector\": [\n            \"img[src*='user.gif']\",\n            \"img[src*='power.gif']\",\n            \"img[src*='vip.gif']\",\n            \"img[src*='mod.gif']\",\n            \"img[src*='sysop.gif']\",\n            \"img[src*='admin.gif']\"\n          ],\n          \"attribute\": \"src\",\n          \"switchFilters\": [[\"'User'\"], [\"'Power User'\"], [\"'VIP'\"], [\"'Moderator'\"], [\"'SysOp'\"], [\"'Admin'\"]]\n        },\n        \"joinTime\": {\n          \"selector\": \"td:contains('Join') + td\",\n          \"filters\": [\"dateTime(query.text().split(' (')[0]).valueOf()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"#SeedingTorrents > div > table > tbody > tr:not(:first-of-type) > td:nth-of-type(3)\",\n          \"filters\": [\"jQuery.map(query, (item) => {return $(item).text()})\", \"_self.getTotalSize(query)\"]\n        },\n        \"seeding\": {\n          \"selector\": \"#SeedingTorrents > div > table > tbody > tr:not(:first-of-type)\",\n          \"filters\": [\"query.length\"]\n        }\n      }\n    }\n  },\n  \"categories\": [\n    {\n      \"entry\": \"*\",\n      \"result\": \"&c$id$=1\",\n      \"category\": [\n        { \"id\": \"62\", \"name\": \"Amateur\" },\n        { \"id\": \"29\", \"name\": \"Anal\" },\n        { \"id\": \"46\", \"name\": \"Anime & Games\" },\n        { \"id\": \"30\", \"name\": \"Asian\" },\n        { \"id\": \"43\", \"name\": \"Bareback\" },\n        { \"id\": \"19\", \"name\": \"BDSM\" },\n        { \"id\": \"17\", \"name\": \"Bears\" },\n        { \"id\": \"44\", \"name\": \"Black\" },\n        { \"id\": \"50\", \"name\": \"Books & Magazines\" },\n        { \"id\": \"9\", \"name\": \"Chubbies\" },\n        { \"id\": \"7\", \"name\": \"Clips\" },\n        { \"id\": \"48\", \"name\": \"Comic & Yaoi\" },\n        { \"id\": \"5\", \"name\": \"Daddies / Sons\" },\n        { \"id\": \"67\", \"name\": \"Dildos\" },\n        { \"id\": \"66\", \"name\": \"Fan Sites\" },\n        { \"id\": \"34\", \"name\": \"Fetish\" },\n        { \"id\": \"68\", \"name\": \"Fisting\" },\n        { \"id\": \"27\", \"name\": \"Grey / Older\" },\n        { \"id\": \"32\", \"name\": \"Group-Sex\" },\n        { \"id\": \"63\", \"name\": \"Homemade\" },\n        { \"id\": \"12\", \"name\": \"Hunks\" },\n        { \"id\": \"33\", \"name\": \"Images\" },\n        { \"id\": \"53\", \"name\": \"Interracial\" },\n        { \"id\": \"57\", \"name\": \"Jocks\" },\n        { \"id\": \"35\", \"name\": \"Latino\" },\n        { \"id\": \"36\", \"name\": \"Mature\" },\n        { \"id\": \"58\", \"name\": \"Media Programs\" },\n        { \"id\": \"37\", \"name\": \"Member\" },\n        { \"id\": \"54\", \"name\": \"Middle Eastern\" },\n        { \"id\": \"38\", \"name\": \"Military\" },\n        { \"id\": \"39\", \"name\": \"Oral-Sex\" },\n        { \"id\": \"56\", \"name\": \"Softcore\" },\n        { \"id\": \"40\", \"name\": \"Solo\" },\n        { \"id\": \"45\", \"name\": \"Themed Movie\" },\n        { \"id\": \"47\", \"name\": \"Trans\" },\n        { \"id\": \"1\", \"name\": \"TV / Episodes\" },\n        { \"id\": \"41\", \"name\": \"Twinks\" },\n        { \"id\": \"42\", \"name\": \"Vintage\" },\n        { \"id\": \"51\", \"name\": \"Voyeur\" },\n        { \"id\": \"65\", \"name\": \"Wrestling and Sports\" },\n        { \"id\": \"28\", \"name\": \"Youngblood\" }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "resource/sites/www.gaytor.rent/getSearchResult.js",
    "content": "(function (options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/login\\.php/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n    }\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      let selector = options.resultSelector;\n      let table = options.page.find(selector);\n      let rows = table.find('> tbody > tr');\n      if (rows.length <= 1) {\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty; //`[${options.site.name}]没有定位到种子列表，或没有相关的种子`;\n        return [];\n      }\n      let results = [];\n      \n      try {\n        for (let index = 1; index < rows.length; index++) {\n          const row = rows.eq(index);\n\n          let title_elem = row.find('.torrent_title > .torrent_title').first();\n          if (title_elem.length == 0) {\n            continue;\n          }\n\n          let time = row.find('.tadded').first().text()\n          let peers = row.find('.hidden-xs.hidden-sm.biggerfont').first().text().match(/[\\d,]+/g)\n          let comments = row.find('.tcomments').first().text().split(/\\s/)[0];\n          let category = options.searcher.getCategoryById(\n            site,\n            options.url,\n            row.find('.browsemaincat > a').first().attr('href').split('=')[1]\n            )\n          let tags = []\n          if (row.find('.infocol > div[onmouseover] > font[color=yellow]').length > 0) {\n            tags.push({ name: 'Free', color: 'blue' })\n          }\n          if (title_elem.text().startsWith('♺')) {\n            tags.push({ name: 'Bumped', color: 'grey' })\n          }\n\n          let data = {\n            title: title_elem.text().replace(/^♺ /g, ''),\n            link: `${site.url}${title_elem.attr(\"href\")}`,\n            url: `${site.url}${row.find('.index').first().attr('href')}`,\n            size: row.find('.tsize').first().text(),\n            time: `${time.match(/\\d{4}-\\d{2}-\\d{2}/g)[0]} ${time.match(/\\d{2}:\\d{2}:\\d{2}/g)[0]}`,\n            author: '(Anonymous)',\n            seeders: peers[1],\n            leechers: peers[2],\n            completed: peers[0],\n            comments: comments ? comments : 0,\n            site: site,\n            tags: tags,\n            entryName: options.entry.name,\n            category: category.id ? category : { id: \"-1\", \"name\": \"Porn\" },\n            status: options.searcher.getFieldValue(site, row, \"status\")\n          };\n          results.push(data);\n        }\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, Searcher);\n"
  },
  {
    "path": "resource/sites/www.haidan.video/config.json",
    "content": "{\n  \"name\": \"HaiDan\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.haidan.video/\",\n  \"description\": \"海胆之家\",\n  \"icon\": \"https://www.haidan.video/public/pic/favicon.ico\",\n  \"tags\": [ \"影视\", \"综合\" ],\n  \"host\": \"www.haidan.video\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"2\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"100\",\n      \"privilege\": \"允许购买邀请码，可以直接发布种子，可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"4\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"200\",\n      \"privilege\": \"Elite User允许发送邀请码，并拥有低于该等级以下权限。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"8\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"500\",\n      \"privilege\": \"查看种子结构，并拥有低于该等级以下权限。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"16\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"1000\",\n      \"privilege\": \"发布趣味盒，并拥有低于该等级以下权限。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"28\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"2000\",\n      \"privilege\": \"Veteran User永远保留账号，并拥有低于该等级以下权限。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"32\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"5000\",\n      \"privilege\": \"查看日志权限，并拥有低于该等级以下权限。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"40\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"8000\",\n      \"privilege\": \"查看排行榜，并拥有低于该等级以下权限\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"52\",\n      \"ratio\": \"1\",\n      \"classPoints\": \"10000\",\n      \"privilege\": \"允许匿名，拥有发布主题推荐权限，并拥有低于该等级以下权限\"\n    }\n  ],\n  \"collaborator\": \"rsj\",\n  \"ver\": \"1.0\",\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/details.php\"],\n    \"scripts\": [\"common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents.php\", \"/videos.php\"],\n    \"scripts\": [\"common.js\", \"torrents.js\"]\n  }],\n  \"securityKeyFields\": [\"passkey\"],\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"title\": {\n        \"selector\": [\".video_name_str\"]\n      },\n      \"subTitle\": {\n        \"selector\": [\".torrent_name_col a\"]\n      },\n      \"seeders\": {\n        \"selector\": [\".seeder_col\"]\n      },\n      \"leechers\": {\n        \"selector\": [\".leecher_col\"]\n      },\n      \"completed\": {\n        \"selector\": [\".snatched_col\"]\n      },\n      \"size\": {\n        \"selector\": [\".video_size\"]\n      },\n      \"author\": {\n        \"selector\": [\".username-center a b\"]\n      },\n      \"time\": {\n        \"selector\": [\".time_col span[title]\"],\n        \"filters\": [\"query.attr('title')\"]\n      },\n      \"link_path\": {\n        \"selector\": [\".torrent_name_col a\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"url_path\": {\n        \"selector\": [\".fa-download\"],\n        \"filters\": [\"query.parent().attr('href')\"]\n      },\n      \"category_link_parameters\": {\n        \"selector\": [\".img_blurry a\"],\n        \"filters\": [\"query.attr('href')\"]\n      },\n      \"category_name\": {\n        \"selector\": [\".img_blurry a img\"],\n        \"filters\": [\"query.attr('title')\"]\n      },\n      \"progress\": {\n        \"selector\": [\"progress\"],\n        \"filters\": [\"query.attr('data-label')\",\"query==null?null:query.replace('%','')\"]\n      },\n      \"status\": {\n        \"selector\": [\"progress\"],\n        \"filters\": [\"query.attr('data-label')\",\"query==null?null:(query.replace('%','')==100?2:1)\"]\n      }\n    },\n    \"page\": \"torrents.php\",\n    \"queryString\": \"search=$key$\",\n    \"area\": [{\n      \"name\": \"标题\",\n      \"appendQueryString\": \"&search_area=0\"\n    }, {\n      \"name\": \"简介\",\n      \"appendQueryString\": \"&search_area=1\"\n    }, {\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"appendQueryString\": \"&search_area=4\"\n    }],\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"getSearchResult.js\"\n  },\n  \"searchEntry\": [{\n    \"name\": \"全站\",\n    \"enabled\": true\n  }],\n  \"checker\": {\n    \"isLogin\": {\n      \"page\": \"/usercp.php\",\n      \"contains\": \"logout.php\"\n    }\n  },\n  \"torrentTagSelectors\": [{\n    \"name\": \"Free\",\n    \"selector\": \"img.pro_free\"\n  }, {\n    \"name\": \"2xFree\",\n    \"selector\": \"img.pro_free2up\"\n  }, {\n    \"name\": \"2xUp\",\n    \"selector\": \"img.pro_2up\"\n  }, {\n    \"name\": \"2x50%\",\n    \"selector\": \"img.pro_50pctdown2up\"\n  }, {\n    \"name\": \"30%\",\n    \"selector\": \"img.pro_30pctdown\"\n  }, {\n    \"name\": \"50%\",\n    \"selector\": \"img.pro_50pctdown\"\n  }, {\n    \"name\": \"H&R\",\n    \"color\": \"black\",\n    \"selector\": \"img[src='public/pic/hit_run.gif']\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"classPoints\": {\n          \"selector\": [\"a[href='classpoint.php']+span\"],\n          \"filters\": [\"query.text().replace(/\\\\D/g,'')\", \"query ? parseInt(query) : 0\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.haidan.video/getSearchResult.js",
    "content": "(function (options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      if (/takelogin\\.php|<form action=\"\\?returnto=/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin; //`[${options.site.name}]需要登录后再搜索`;\n        return;\n      }\n      options.isLogged = true;\n      if (\n        /没有种子|No [Tt]orrents?|Your search did not match anything|用准确的关键字重试/.test(\n          options.responseText\n        )\n      ) {\n        options.status = ESearchResultParseStatus.noTorrents; // `[${options.site.name}]没有搜索到相关的种子`;\n        return;\n      }\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) return [];\n      let site = options.site;\n      let torrents = options.page.find(\".group_content\");\n      let results = [];\n      for (let i = 0; i < torrents.length; i++) {\n        let torrent = torrents.eq(i);\n        let items = torrent.find(\".torrent_wrap\");\n        let maintitle = Searcher.getFieldValue(site,torrent,\"title\");\n        let category_link = site.url + site.page + Searcher.getFieldValue(site,torrent,\"category_link_parameters\");\n        let category_name = Searcher.getFieldValue(site,torrent,\"category_name\");\n        for (let j = 0; j < items.length; j++) {\n          let item = items.eq(j);\n          let field = {\n            title: maintitle,\n            subTitle: Searcher.getFieldValue(site,item,\"subTitle\"),\n            link: site.url + Searcher.getFieldValue(site,item,\"link_path\"),\n            url: site.url + Searcher.getFieldValue(site,item,\"url_path\"),\n            size: Searcher.getFieldValue(site,item,\"size\"),\n            time: Searcher.getFieldValue(site,item,\"time\"),\n            author: Searcher.getFieldValue(site,item,\"author\"),\n            seeders: Searcher.getFieldValue(site,item,\"seeders\"),\n            leechers: Searcher.getFieldValue(site,item,\"leechers\"),\n            completed: Searcher.getFieldValue(site,item,\"completed\"),\n            //comments: -1,\n            site: site,\n            tags: Searcher.getRowTags(site,item),\n            entryName: options.entry.name,\n            category: {\n              link: category_link,\n              name: category_name,\n            },\n            progress: Searcher.getFieldValue(site,item,\"progress\"),\n            status: Searcher.getFieldValue(site,item,\"status\"),\n          };\n          //CustomTags\n          let listtags = item.find(\"label\");\n          for (let k = 0; k < listtags.length; k++) {\n            let tag = {\n              name: listtags.eq(k).find(\"b\").text(),\n              title: listtags.eq(k).find(\"b\").text(),\n              color: listtags.eq(k).get(0).style[\"background-color\"]\n            }\n            field.tags.push(tag);\n          }\n\n          results.push(field);\n        }\n      }\n      return results;\n    }\n  }\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n})(options, options.searcher);"
  },
  {
    "path": "resource/sites/www.hdarea.co/config.json",
    "content": "{\r\n  \"name\": \"HDArea\",\r\n  \"timezoneOffset\": \"+0800\",\r\n  \"schema\": \"NexusPHP\",\r\n  \"url\": \"https://www.hdarea.co/\",\r\n  \"description\": \"高清世界\",\r\n  \"icon\": \"https://www.hdarea.co/favicon.ico\",\r\n  \"tags\": [\r\n    \"影视\",\r\n    \"综合\"\r\n  ],\r\n  \"host\": \"www.hdarea.co\",\r\n  \"levelRequirements\": [\r\n    {\r\n      \"level\": 1,\r\n      \"name\": \"Power User\",\r\n      \"interval\": \"4\",\r\n      \"downloaded\": \"50GB\",\r\n      \"ratio\": \"1.05\",\r\n      \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；；可以请求续种； 可以发送邀请（开放邀请权限时）； 可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\r\n    },\r\n    {\r\n      \"level\": 2,\r\n      \"name\": \"Elite User\",\r\n      \"interval\": \"8\",\r\n      \"downloaded\": \"120GB\",\r\n      \"ratio\": \"3\",\r\n      \"privilege\": \"无\"\r\n    },\r\n    {\r\n      \"level\": 3,\r\n      \"name\": \"Crazy User\",\r\n      \"interval\": \"10\",\r\n      \"downloaded\": \"300GB\",\r\n      \"ratio\": \"3.5\",\r\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\r\n    },\r\n    {\r\n      \"level\": 4,\r\n      \"name\": \"Insane User\",\r\n      \"interval\": \"12\",\r\n      \"downloaded\": \"750GB\",\r\n      \"ratio\": \"4\",\r\n      \"privilege\": \"可以查看普通日志。Insane User及以上用户封存账号后不会被删除。\"\r\n    },\r\n    {\r\n      \"level\": 5,\r\n      \"name\": \"Veteran User\",\r\n      \"interval\": \"20\",\r\n      \"downloaded\": \"1024GB\",\r\n      \"ratio\": \"4.5\",\r\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\r\n    },\r\n    {\r\n      \"level\": 6,\r\n      \"name\": \"Extreme User\",\r\n      \"interval\": \"25\",\r\n      \"downloaded\": \"2TB\",\r\n      \"ratio\": \"5\",\r\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\r\n    },\r\n    {\r\n      \"level\": 7,\r\n      \"name\": \"Ultimate User\",\r\n      \"interval\": \"30\",\r\n      \"downloaded\": \"5TB\",\r\n      \"ratio\": \"5.5\",\r\n      \"privilege\": \"得到五个邀请名额。\"\r\n    },\r\n    {\r\n      \"level\": 8,\r\n      \"name\": \"Nexus Master\",\r\n      \"interval\": \"40\",\r\n      \"downloaded\": \"10TB\",\r\n      \"ratio\": \"6\",\r\n      \"privilege\": \"得到十个邀请名额。\"\r\n    }\r\n  ],\r\n  \"collaborator\": \"lzl20110\",\r\n  \"searchEntryConfig\": {\r\n    \"fieldSelector\": {\r\n      \"progress\": {\r\n        \"selector\": [\r\n          \"table[title='downloading'] > tbody > tr > td > div\",\r\n          \"table[title='seeding'] > tbody > tr > td > div\",\r\n          \"table[title='Stopped'] > tbody > tr > td > div\",\r\n          \"table[title='completed'] > tbody > tr > td > div\"\r\n        ],\r\n        \"filters\": [ \"query.attr('style')||''\", \"query.match(/width:.?(\\\\d.+)%/)\", \"(query && query.length>=2)?query[1]:null\" ]\r\n      },\r\n      \"status\": {\r\n        \"selector\": [\r\n          \"table[title='downloading']\",\r\n          \"table[title='seeding']\",\r\n          \"table[title='Stopped']\",\r\n          \"table[title='completed']\"\r\n        ],\r\n        \"switchFilters\": [\r\n          [ \"1\" ],\r\n          [ \"2\" ],\r\n          [ \"3\" ],\r\n          [ \"255\" ]\r\n        ]\r\n      }\r\n    }\r\n  },\r\n  \"searchEntry\": [\r\n    {\r\n      \"name\": \"全站\",\r\n      \"enabled\": true\r\n    },\r\n    {\r\n      \"queryString\": \"cat401=1\",\r\n      \"name\": \"Movies Blu-ray\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat415=1\",\r\n      \"name\": \"Movies REMUX\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat416=1\",\r\n      \"name\": \"Movies 3D\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat410=1\",\r\n      \"name\": \"Movies 1080p\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat411=1\",\r\n      \"name\": \"Movies 720p\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat414=1\",\r\n      \"name\": \"Movies DVD\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat412=1\",\r\n      \"name\": \"Movies WEB-DL\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat413=1\",\r\n      \"name\": \"Movies HDTV\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat417=1\",\r\n      \"name\": \"Movies iPad\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat404=1\",\r\n      \"name\": \"Documentaries\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat405=1\",\r\n      \"name\": \"Animations\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat402=1\",\r\n      \"name\": \"TV Series\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat403=1\",\r\n      \"name\": \"TV Shows\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat406=1\",\r\n      \"name\": \"Music Videos\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat407=1\",\r\n      \"name\": \"Sports\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat409=1\",\r\n      \"name\": \"Misc\",\r\n      \"enabled\": false\r\n    },\r\n    {\r\n      \"queryString\": \"cat408=1\",\r\n      \"name\": \"HQ Audio\",\r\n      \"enabled\": false\r\n    }\r\n  ],\r\n  \"categories\": [\r\n    {\r\n      \"entry\": \"torrents.php\",\r\n      \"result\": \"&cat$id$=1\",\r\n      \"category\": [\r\n        {\r\n          \"id\": 401,\r\n          \"name\": \"Movies Blu-ray\"\r\n        },\r\n        {\r\n          \"id\": 415,\r\n          \"name\": \"Movies REMUX\"\r\n        },\r\n        {\r\n          \"id\": 416,\r\n          \"name\": \"Movies 3D\"\r\n        },\r\n        {\r\n          \"id\": 410,\r\n          \"name\": \"Movies 1080p\"\r\n        },\r\n        {\r\n          \"id\": 411,\r\n          \"name\": \"Movies 720p\"\r\n        },\r\n        {\r\n          \"id\": 414,\r\n          \"name\": \"Movies DVD\"\r\n        },\r\n        {\r\n          \"id\": 412,\r\n          \"name\": \"Movies WEB-DL\"\r\n        },\r\n        {\r\n          \"id\": 413,\r\n          \"name\": \"Movies HDTV\"\r\n        },\r\n        {\r\n          \"id\": 417,\r\n          \"name\": \"Movies iPad\"\r\n        },\r\n        {\r\n          \"id\": 404,\r\n          \"name\": \"Documentaries\"\r\n        },\r\n        {\r\n          \"id\": 405,\r\n          \"name\": \"Animations\"\r\n        },\r\n        {\r\n          \"id\": 402,\r\n          \"name\": \"TV Series\"\r\n        },\r\n        {\r\n          \"id\": 403,\r\n          \"name\": \"TV Shows\"\r\n        },\r\n        {\r\n          \"id\": 406,\r\n          \"name\": \"Music Videos\"\r\n        },\r\n        {\r\n          \"id\": 407,\r\n          \"name\": \"Sports\"\r\n        },\r\n        {\r\n          \"id\": 409,\r\n          \"name\": \"Misc\"\r\n        },\r\n        {\r\n          \"id\": 408,\r\n          \"name\": \"HQ Audio\"\r\n        }\r\n      ]\r\n    }\r\n  ],\r\n  \"fieldSelector\": {\r\n    \"merge\": true,\r\n    \"title\": {\r\n      \"selector\": [ \"a.title:not(a[href*='comment']),a[onmouseover*='get_ext_info_ajax']\" ],\r\n      \"filters\": [ \"query.text()\" ]\r\n    }\r\n  },\r\n  \"selectors\": {\r\n    \"/details.php\": {\r\n      \"merge\": true,\r\n      \"fields\": {\r\n        \"downloadURL\": {\r\n          \"selector\": [ \"td.rowfollow:contains('&passkey='):last\" ],\r\n          \"filters\": [ \"query[0].childNodes[0].textContent\" ]\r\n        }\r\n      }\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "resource/sites/www.hitpt.com/config.json",
    "content": "{\n  \"name\": \"百川PT\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"校内10兆高速下载，优质高清资源共享！\",\n  \"url\": \"https://www.hitpt.com/\",\n  \"icon\": \"https://www.hitpt.com/favicon.ico\",\n  \"tags\": [\n    \"教育网\",\n    \"影视\",\n    \"综合\"\n  ],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"www.hitpt.com\",\n  \"formerHosts\": [\n    \"pt.ghtt.net\"\n  ],\n  \"collaborator\": [\"tongyifan\", \"zhuweitung\"],\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"1\",\n      \"downloaded\": \"10GB\",\n      \"ratio\": \"1.5\",\n      \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种；可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")；可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"1\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"privilege\": \"得到一个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"6\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\",\n      \"privilege\": \"得到二个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"20\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\",\n      \"privilege\": \"得到三个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"50\",\n      \"downloaded\": \"2TB\",\n      \"ratio\": \"4.05\",\n      \"privilege\": \"和Nexus Master拥有相同权限并被认为是精英成员。免除自动降级。\"\n    }\n  ],\n  \"searchEntry\": [{\n      \"entry\": \"/torrents.php?search=$key$&notnewword=1\",\n      \"name\": \"影视\",\n      \"enabled\": true\n    },\n    {\n      \"entry\": \"/music.php?search=$key$&notnewword=1\",\n      \"name\": \"综合\",\n      \"enabled\": true\n    },\n    {\n      \"entry\": \"/torrents.php?cat401=1&search=$key$&notnewword=1\",\n      \"name\": \"高清电影\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat402=1&search=$key$&notnewword=1\",\n      \"name\": \"高清剧集\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat403=1&search=$key$&notnewword=1\",\n      \"name\": \"抢鲜或标清\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat405=1&search=$key$&notnewword=1\",\n      \"name\": \"动漫\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat407=1&search=$key$&notnewword=1\",\n      \"name\": \"体育\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat413=1&search=$key$&notnewword=1\",\n      \"name\": \"纪录片\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat416=1&search=$key$&notnewword=1\",\n      \"name\": \"综艺\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/torrents.php?cat415=1&search=$key$&notnewword=1\",\n      \"name\": \"MV\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat411=1&search=$key$&notnewword=1\",\n      \"name\": \"电子文档\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat406=1&search=$key$&notnewword=1\",\n      \"name\": \"音乐\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat408=1&search=$key$&notnewword=1\",\n      \"name\": \"工程软件\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat404=1&search=$key$&notnewword=1\",\n      \"name\": \"教学视频\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat410=1&search=$key$&notnewword=1\",\n      \"name\": \"游戏\",\n      \"enabled\": false\n    },\n    {\n      \"entry\": \"/music.php?cat409=1&search=$key$&notnewword=1\",\n      \"name\": \"其他\",\n      \"enabled\": false\n    }\n  ],\n  \"categories\": [{\n      \"entry\": \"torrents.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [{\n          \"id\": 401,\n          \"name\": \"高清电影\"\n        },\n        {\n          \"id\": 402,\n          \"name\": \"高清剧集\"\n        },\n        {\n          \"id\": 403,\n          \"name\": \"抢鲜或标清\"\n        },\n        {\n          \"id\": 405,\n          \"name\": \"动漫\"\n        },\n        {\n          \"id\": 407,\n          \"name\": \"体育\"\n        },\n        {\n          \"id\": 413,\n          \"name\": \"纪录片\"\n        },\n        {\n          \"id\": 416,\n          \"name\": \"综艺\"\n        },\n        {\n          \"id\": 415,\n          \"name\": \"MV\"\n        }\n      ]\n    },\n    {\n      \"entry\": \"music.php\",\n      \"result\": \"&cat$id$=1\",\n      \"category\": [{\n          \"id\": 411,\n          \"name\": \"电子文档\"\n        },\n        {\n          \"id\": 406,\n          \"name\": \"音乐\"\n        },\n        {\n          \"id\": 408,\n          \"name\": \"工程软件\"\n        },\n        {\n          \"id\": 404,\n          \"name\": \"教学视频\"\n        },\n        {\n          \"id\": 410,\n          \"name\": \"游戏\"\n        },\n        {\n          \"id\": 409,\n          \"name\": \"其他\"\n        }\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "resource/sites/www.icc2022.com/config.json",
    "content": "{\r\n  \"name\": \"ICC\",\r\n  \"timezoneOffset\": \"+0800\",\r\n  \"schema\": \"NexusPHP\",\r\n  \"url\": \"https://www.icc2022.com/\",\r\n  \"description\": \"冰淇淋\",\r\n  \"icon\": \"https://www.icc2022.com/pic/logo.png\",\r\n  \"tags\": [\r\n    \"影视\",\r\n    \"综合\"\r\n  ],\r\n  \"host\": \"www.icc2022.com\",\r\n  \"levelRequirements\": [\r\n    {\r\n      \"level\": 1,\r\n      \"name\": \"Power User\",\r\n      \"interval\": \"4\",\r\n      \"downloaded\": \"50GB\",\r\n      \"ratio\": \"1.05\",\r\n      \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\r\n    },\r\n    {\r\n      \"level\": 2,\r\n      \"name\": \"Elite User\",\r\n      \"interval\": \"8\",\r\n      \"downloaded\": \"120GB\",\r\n      \"ratio\": \"1.55\",\r\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\r\n    },\r\n    {\r\n      \"level\": 3,\r\n      \"name\": \"Crazy User\",\r\n      \"interval\": \"15\",\r\n      \"downloaded\": \"300GB\",\r\n      \"ratio\": \"2.05\",\r\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\r\n    },\r\n    {\r\n      \"level\": 4,\r\n      \"name\": \"Insane User\",\r\n      \"interval\": \"25\",\r\n      \"downloaded\": \"500GB\",\r\n      \"ratio\": \"2.55\",\r\n      \"privilege\": \"可以查看普通日志。\"\r\n    },\r\n    {\r\n      \"level\": 5,\r\n      \"name\": \"Veteran User\",\r\n      \"interval\": \"40\",\r\n      \"downloaded\": \"750GB\",\r\n      \"ratio\": \"3.05\",\r\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\r\n    },\r\n    {\r\n      \"level\": 6,\r\n      \"name\": \"Extreme User\",\r\n      \"interval\": \"50\",\r\n      \"downloaded\": \"1TB\",\r\n      \"ratio\": \"3.55\",\r\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\r\n    },\r\n    {\r\n      \"level\": 7,\r\n      \"name\": \"Ultimate User\",\r\n      \"interval\": \"80\",\r\n      \"downloaded\": \"1.5TB\",\r\n      \"ratio\": \"4.05\",\r\n      \"privilege\": \"得到五个邀请名额。\"\r\n    },\r\n    {\r\n      \"level\": 8,\r\n      \"name\": \"Nexus Master\",\r\n      \"interval\": \"100\",\r\n      \"downloaded\": \"3TB\",\r\n      \"ratio\": \"4.55\",\r\n      \"privilege\": \"得到十个邀请名额。\"\r\n    }\r\n  ],\r\n  \"categories\": [\r\n    {\r\n      \"entry\": \"torrents.php\",\r\n      \"result\": \"&cat$id$=1\",\r\n      \"category\": [\r\n        {\r\n          \"id\": 408,\r\n          \"name\": \"音轨\"\r\n        },\r\n        {\r\n          \"id\": 409,\r\n          \"name\": \"其它\"\r\n        },\r\n        {\r\n          \"id\": 407,\r\n          \"name\": \"体育\"\r\n        },\r\n        {\r\n          \"id\": 406,\r\n          \"name\": \"MV\"\r\n        },\r\n        {\r\n          \"id\": 403,\r\n          \"name\": \"综艺\"\r\n        },\r\n        {\r\n          \"id\": 402,\r\n          \"name\": \"电视剧\"\r\n        },\r\n        {\r\n          \"id\": 405,\r\n          \"name\": \"动漫\"\r\n        },\r\n        {\r\n          \"id\": 404,\r\n          \"name\": \"纪录片\"\r\n        },\r\n        {\r\n          \"id\": 401,\r\n          \"name\": \"电影\"\r\n        } \r\n      ]\r\n    }\r\n  ],\r\n  \"selectors\": {\r\n    \"userSeedingTorrents\": {\r\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\r\n      \"fields\": {\r\n        \"seeding\": {\r\n          \"selector\": [\"b:first\"],\r\n          \"filters\": [\"query.text()\"]\r\n        },\r\n        \"seedingSize\": {\r\n          \"selector\": \"\",\r\n          \"filters\": [\r\n            \"query.text().match(/总大小：(.*?)</g)\",\r\n            \"(query && query.length>0 ) ? query[0].replace('总大小：', '').replace('<', '').trim() : 0\",\r\n            \"(query != 0) ? _self.getTotalSize([query]) : 0\"\r\n          ]\r\n        }\r\n      }\r\n    }\r\n  },\r\n  \"searchEntryConfig\": {\r\n    \"fieldSelector\": {\r\n      \"progress\": {\r\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\r\n        \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\r\n      },\r\n      \"status\": {\r\n        \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\r\n        \"filters\": [\r\n          \"query ? query.attr('title') : ''\",\r\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\r\n        ]\r\n      }\r\n    }\r\n  }\r\n}\r\n"
  },
  {
    "path": "resource/sites/www.morethantv.me/config.json",
    "content": "{\n  \"name\": \"MTV\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"\",\n  \"icon\": \"https://www.morethantv.me/favicon.ico\",\n  \"schema\": \"Common\",\n  \"tags\": [\"电视剧\", \"剧集\"],\n  \"url\": \"https://www.morethantv.me\",\n  \"collaborator\": \"luckiestone\",\n  \"host\": \"www.morethantv.me\",\n  \"formerHosts\": [\n    \"www.morethan.tv\"\n  ],\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"/torrents.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents/browse\", \"/show/(\\\\d+)/$\",\"/collages.php\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/Common/torrents.js\"]\n  }],\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"torrents/browse?searchtext=$key$\",\n    \"name\": \"全部\",\n    \"resultType\": \"html\",\n    \"resultSelector\": \"table.torrent_table\",\n    \"dataRowSelector\": \"> tbody > tr:not(:first-child)\",\n    \"fieldIndex\": {\n\t    \"title\": 1,\n\t    \"link\": 1,\n\t    \"url\": 1,\n        \"time\": 3,\n        \"size\": 4,\n        \"author\": 8,\n        \"seeders\": 6,\n        \"leechers\": 7,\n        \"completed\": 5\n    },\n    \"fieldSelector\": {\n      \"title\": {\n          \"selector\": [\"a.overlay_torrent\"],\n          \"filters\": [\"query.text()\"]\n      },\n      \"link\": {\n          \"selector\": [\"a.overlay_torrent\"],\n          \"filters\": [\"query.attr('href')\", \"'https://www.morethantv.me/'+query\"]\n      },\n      \"url\": {\n          \"selector\": [\"a[href*='torrents.php?action=download']\"],\n          \"filters\": [\"query.attr('href')\", \"'https://www.morethantv.me/'+query\"]\n      },\n      \"time\": {\n          \"selector\": [\"span.time\"],\n          \"filters\": [\"dateTime(query.attr('title')).isValid() ? query.attr('title') : query.text()\", \"dateTime(query).format()\"]\n      },\n      \"progress\": {\n        \"selector\": [\"a[title='Currently Seeding Torrent'], a[title='Previously Snatched Torrent']\", \"a[title='Previously Grabbed Torrent File']\"],\n        \"switchFilters\": [\n          [\"(query && query.length > 0)?100:null\"],\n          [\"(query && query.length > 0)?0:null\"],\n          [null]\n        ]\n      },\n      \"status\": {\n        \"selector\": [\"a[title='Currently Seeding Torrent']\", \"a[title='Previously Snatched Torrent']\", \"a[title='Previously Grabbed Torrent File']\"],\n        \"switchFilters\": [\n          [2],\n          [255],\n          [3]\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"merge\": true,\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a.username[href*='user.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a.username[href*='user.php']:first\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"div.alert-bar > a[href*='inbox.php']\", \"div.alertbar > a[href*='inbox.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"form[action='/logout']\"],\n          \"filters\": [\"query.length>0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/user.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": \"ul.stats > li:contains('Uploaded')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Uploaded.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": \"ul.stats > li:contains('Downloaded')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Downloaded.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"ul.stats > li:contains('Ratio:')\",\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Ratio.+?([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"ul.stats > li:contains('Seeding:')\"],\n          \"filters\": [ \"query.text().replace(/,/g, '').match(/Seeding:.+?([\\\\d]+)/)\", \"(query && query.length>=2)?query[1]:null\" ]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"ul.stats > li:contains('Seeding Size:')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/Seeding Size:.+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():0\"]\n        },\n        \"levelName\": {\n          \"selector\": \"ul.stats > li:contains('Class:')\",\n          \"filters\": [\"query.text().match(/Class:.+?(.+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"bonus\": {\n          \"selector\": \"#stats_credits\",\n          \"filters\": [\"query.text()\"]\n        },\n        \"bonusPerHour\": {\n          \"selector\": [\"ul.stats > li:contains('Seeding:')\"],\n          \"filters\": [ \"query.text().replace(/,/g, '').match(/Seeding:.+?([\\\\d]+)/)\", \"(query && query.length>=2)? parseInt(query[1]) : 0\", \"(query >= 300) ? 100 : Math.round((Math.sqrt(query*0.4+1)-1)*10)\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"ul.stats > li:contains('Joined:') > span\"],\n          \"filters\": [\"dateTime(query.attr('title')).isValid() ? query.attr('title') : query.text()\", \"dateTime(query).isValid() ? dateTime(query).valueOf() : query\"]\n        }\n      }\n    },\n    \"common\": {\n\t  \"page\": \"/torrents.php\",\n      \"fields\": {\n        \"downloadURL\": {\n          \"selector\": [\"tr[id*='torrentinfo'][class!='hidden']\"],\n          \"filters\": [\"query.prev().find(\\\"a[href*='action=download']\\\").attr('href')\"]\n        },\n        \"size\": {\n          \"selector\": [\"tr.group_torrent > td.nobr\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"downloadURLs\": {\n          \"selector\": [\"tr.group_torrent a[href*='action=download']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"tr.group_torrent > td.nobr\"],\n          \"filters\": [\"query.toArray()\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.myanonamouse.net/config.json",
    "content": "{\n  \"name\": \"MyAnonaMouse\",\n  \"description\": \"Friendliness, Warmth and Sharing\",\n  \"url\": \"https://www.myanonamouse.net/\",\n  \"icon\": \"https://cdn.myanonamouse.net/favicon.ico\",\n  \"tags\": [\"电子书\", \"有声书\"],\n  \"schema\": \"MyAnonaMouse\",\n  \"host\": \"www.myanonamouse.net\",\n  \"collaborator\": \"tongyifan\",\n  \"supportedFeatures\": {\n    \"search\": true,\n    \"imdbSearch\": false,\n    \"userData\": true,\n    \"sendTorrent\": true\n  },\n  \"plugins\": [\n    {\n      \"name\": \"种子详情页面\",\n      \"pages\": [\"/t/\\\\d+\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n    },\n    {\n      \"name\": \"种子列表\",\n      \"pages\": [\"/tor/browse.php\", \"/stats/top10Tor.php\"],\n      \"styles\": [\"/libs/album/style.css\"],\n      \"scripts\": [\n        \"/schemas/NexusPHP/common.js\",\n        \"/libs/album/album.js\",\n        \"torrents.js\"\n      ]\n    }\n  ],\n  \"searchEntryConfig\": {\n    \"page\": \"/tor/js/loadSearch2.php\",\n    \"resultType\": \"html\",\n    \"queryString\": \"tor%5Btext%5D=$key$&tor%5BsrchIn%5D%5Btitle%5D=true&tor%5BsrchIn%5D%5Bauthor%5D=true&tor%5BsearchType%5D=all&tor%5BsearchIn%5D=torrents&tor%5Bcat%5D%5B%5D=0&tor%5BbrowseFlagsHideVsShow%5D=0&tor%5BsortType%5D=default&tor%5BstartNumber%5D=0&thumbnail=true\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"resultSelector\": \"table.newTorTable\",\n    \"skipIMDbId\": true,\n    \"firstDataRowIndex\": 1\n  },\n  \"searchEntry\": [\n    {\n      \"name\": \"全站\",\n      \"enabled\": true\n    }\n  ],\n  \"torrentTagSelectors\": [\n    {\n      \"name\": \"Free\",\n      \"selector\": \"img[alt='freeleech']\"\n    },\n    {\n      \"name\": \"VIP\",\n      \"selector\": \"img[alt='VIP']\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"li.myInfo > a\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.match(/(\\\\d+)/)[1]:''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a#userMenu\"],\n          \"filters\": [\"query ? query.text().replace(\\\"↓\\\", \\\"\\\").trim() : ''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href='/preferences/index.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a.tmnb, a.tmn, a.tmng\"],\n          \"filters\": [\n            \"query.text().match(/(\\\\d+)/g)\",\n            \"query ? query.map(Number).reduce((sum, current) => {return sum + current;}, 0) : 0\"\n          ]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/u/$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('Uploaded'):eq(0) + td\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length==2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('Downloaded'):eq(0) + td\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length==2)?(query[1]).sizeToNumber():null\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('Class') + td\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"a#tmBP\"],\n          \"filters\": [\n            \"query.text().replace(/,/g,'').match(/Bonus: ([\\\\d.]+)/)\",\n            \"(query && query.length==2)?parseFloat(query[1]):null\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('Join'):contains('date') + td\"],\n          \"filters\": [\n            \"query.text().split(' (')[0]\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/snatch_summary.php\",\n      \"parser\": \"getUserSeedingTorrents.js\"\n    },\n    \"common\": {\n      \"fields\": {\n        \"downloadURLs\": {\n          \"selector\": [\"a[href*='/tor/download.php/']\"],\n          \"filters\": [\"query.toArray()\"]\n        },\n        \"confirmSize\": {\n          \"selector\": [\"table.newTorTable > tbody > tr > td:eq(4)\"],\n          \"filters\": [\"query\"]\n        },\n        \"downloadURL\": {\n          \"selector\": [\"a#tddl\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query\"]\n        },\n        \"size\": {\n          \"selector\": [\"div#size > div:eq(1) > span\"],\n          \"filters\": [\n            \"query.text().replace(/[, ]/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>1)?(query[1]).sizeToNumber():0\"\n          ]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"button#giveThanks\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.myanonamouse.net/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.showTorrentSize();\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let url = PTService.getFieldValue(\"downloadURL\");\n\n      return this.getFullURL(url);\n    }\n\n    /**\n     * 获取种子大小\n     */\n    showTorrentSize() {\n      let size = $(\"div#size > div:eq(1) > span\");\n      // eslint-disable-next-line no-irregular-whitespace\n      size = size.text().match(/([\\d.]+[  ]?[ZEPTGMK]?i?B)/);\n      size = (size && size.length > 1) ? size[1] : 0;\n\n      if (size) {\n        PTService.addButton({\n          title: \"当前种子大小\",\n          icon: \"attachment\",\n          label: size\n        });\n      }\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\".TorrentTitle\").text();\n    }\n  }\n\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.myanonamouse.net/getSearchResult.js",
    "content": "/**\n * 通用搜索解析脚本\n */\n(function (options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      // 判断是否已登录\n      if (\n        options.entry.loggedRegex &&\n        !new RegExp(options.entry.loggedRegex, \"\").test(options.responseText)\n      ) {\n        // 需要登录后再搜索\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let selector = options.resultSelector;\n      let dataRowSelector = options.entry.dataRowSelector || \"> tbody > tr\";\n      selector = selector.replace(dataRowSelector, \"\");\n      // 获取数据表格\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(dataRowSelector);\n      if (rows.length === 0) {\n        // 没有定位到种子列表，或没有相关的种子\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\n        return [];\n      }\n      let results = [];\n      let beginRowIndex = options.entry.firstDataRowIndex || 0;\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n\n          let torrentId = row.attr(\"id\");\n          // 跳过无种子ID的行\n          if (!torrentId) {\n            continue;\n          } else {\n            torrentId = torrentId.match(/tdr-(\\d+)/)[1];\n          }\n\n          // 下载链接，无法下载的VIP种子置为ONLY_FOR_VIP\n          let url = row.find(\".directDownload\").attr(\"href\");\n          if (!url) {\n            url = \"ONLY_FOR_VIP\"\n          }\n\n          // 解析种子大小\n          let size = row.find(\"> td:eq(4)\").text().split(\"[\")[1].replace(\"]\", \"\");\n\n          // 解析发布时间和发布者\n          let addedAndUploader = row.find(\"> td:eq(5)\").text().split(\"[\");\n          let time = addedAndUploader[0];\n          let author = addedAndUploader[1].replace(\"]\", \"\");\n\n          // 做种/下载/完成\n          let seedLeechSnatched = row.find(\"> td:eq(6) > p\");\n          let seeders = parseInt(seedLeechSnatched.eq(0).text());\n          let leechers = parseInt(seedLeechSnatched.eq(1).text());\n          let completed = parseInt(seedLeechSnatched.eq(2).text());\n\n          let data = {\n            title: row.find(\".torTitle\").text(),\n            subTitle: row.find(\".torRowDesc\").text(),\n            link: this.getFullURL(`/t/${torrentId}`),\n            url: this.getFullURL(url),\n            size: size || 0,\n            time: time,\n            author: author,\n            seeders: seeders,\n            leechers: leechers,\n            completed: completed,\n            comments: row.find(\" >td:eq(2)\").text().match(/(\\d+) comments/)[1],\n            site: this.site,\n            tags: Searcher.getRowTags(this.site, row),\n            entryName: options.entry.name\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        // 获取种子信息出错\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n\n      // 没有搜索到相关的种子\n      if (results.length === 0 && !options.errorMsg) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      let URL = PTServiceFilters.parseURL(this.site.url);\n      if (url.substr(0, 2) === \"//\") {\n        url = `${URL.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${URL.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${URL.origin}/${url}`;\n      }\n      return url;\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/www.myanonamouse.net/getUserSeedingTorrents.js",
    "content": "(function (options, User) {\n  class Parser {\n    constructor(options) {\n      this.options = options;\n      this.body = null;\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 加载当前做种数据\n     */\n    load() {\n      const types = [\n        \"seedUnsat\",\n        \"seedHnr\",\n        \"sSat\",\n        \"upAct\"\n      ]\n      let doneCount = 0\n      for (const type of types) {\n        User.getCookie(options.site, \"mam_id\").then(mamId => {\n          $.getJSON(\"https://cdn.myanonamouse.net/json/loadUserDetailsTorrents.php\", {\n            uid: options.userInfo.id,\n            iteration: 0,\n            type: type,\n            cacheTime: Math.round(Date.now() / 1000),\n            mam_id: decodeURIComponent(mamId)\n          }).done(data => {\n            doneCount++\n            data.rows.forEach(item => {\n              this.result.seeding += 1;\n              this.result.seedingSize += item.size.sizeToNumber()\n            })\n\n            if (doneCount === types.length-1) {\n              this.done();\n            }\n          }).fail(error => {\n            console.log(error);\n            this.done();\n          })\n        }).catch(err => {\n          console.log(err);\n          this.done();\n        });\n      }\n    }\n  }\n\n  new Parser(options);\n})(_options, _self);\n/**\n *\n _options 表示当前参数\n {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n _self 表示 User(/src/background/user.ts) 类实例\n */\n"
  },
  {
    "path": "resource/sites/www.myanonamouse.net/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = PTService.getFieldValue(\"downloadURLs\");\n\n      if (links.length == 0) {\n        //  \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n      if (typeof(links[0])!=\"string\"){\n        let urls = $.map(links, item => {\n          let url = $(item).attr(\"href\");\n          return this.getFullURL(url);\n        });\n        return urls;\n      }\n      return links\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(PTService.getFieldValue(\"confirmSize\"));\n    }\n\n    /**\n     * @return {number}\n     */\n    getSize(size) {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size.match(\n        /\\[(\\d*\\.?\\d+)(.*[^TGMK])?([TGMK](B|iB)?)]/i\n      );\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Ti?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B?/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/www.okpt.net/config.json",
    "content": "{\n    \"name\": \"OKPT\",\n    \"timezoneOffset\": \"+0800\",\n    \"description\": \"okpt.net\",\n    \"url\": \"https://www.okpt.net/\",\n    \"icon\": \"https://www.okpt.net/favicon.ico\",\n\n    \"tags\": [\"综合\",\"音乐\",\"写真\"],\n    \"schema\": \"NexusPHP\",\n    \"host\": \"www.okpt.net\",\n    \"collaborator\": [\"yiyule\"],\n    \"levelRequirements\": [\n      {\n        \"level\": 1,\n        \"name\": \"Power User\",\n        \"interval\": \"4\",\n        \"downloaded\": \"50GB\",\n        \"ratio\": \"2\",\n        \"privilege\": \"可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n      },\n      {\n        \"level\": 2,\n        \"name\": \"Elite User\",\n        \"interval\": \"8\",\n        \"downloaded\": \"100G\",\n        \"ratio\": \"2.5\",\n        \"privilege\": \"没有新权限增加\"\n      },\n      {\n        \"level\": 3,\n        \"name\": \"Crazy User\",\n        \"interval\": \"15\",\n        \"downloaded\": \"300G\",\n        \"ratio\": \"3\",\n        \"privilege\": \"可以在做种/下载/发布的时候选择匿名模式。\"\n      },\n      {\n        \"level\": 4,\n        \"name\": \"Insane User\",\n        \"interval\": \"25\",\n        \"downloaded\": \"500G\",\n        \"ratio\": \"3.5\",\n        \"privilege\": \"可以查看普通日志。\"\n      },\n      {\n        \"level\": 5,\n        \"name\": \"Veteran User\",\n        \"interval\": \"40\",\n        \"downloaded\": \"1T\",\n        \"ratio\": \"4\",\n        \"privilege\": \"可以查看其它用户的评论、帖子历史。\"\n      },\n      {\n        \"level\": 6,\n        \"name\": \"Extreme User\",\n        \"interval\": \"60\",\n        \"downloaded\": \"2T\",\n        \"ratio\": \"4.5\",\n        \"privilege\": \"可以更新过期的外部信息。六级烧伤(Extreme User)及以上用户会永远保留账号。\"\n      },\n      {\n        \"level\": 7,\n        \"name\": \"Ultimate User\",\n        \"interval\": \"80\",\n        \"downloaded\": \"5T\",\n        \"ratio\": \"5\",\n        \"privilege\": \"这个等级会永远保留账号。\"\n      },\n      {\n        \"level\": 8,\n        \"name\": \"Nexus Master\",\n        \"interval\": \"100\",\n        \"downloaded\": \"10T\",\n        \"ratio\": \"5.5\",\n        \"privilege\": \"这个等级会永远保留账号。\"\n      }\n    ],\n    \"plugins\": [{\n      \"name\": \"音乐写真专区\",\n      \"pages\": [\"/special.php\"],\n      \"scripts\": [\"/schemas/NexusPHP/common.js\", \"/schemas/NexusPHP/torrents.js\"]\n    },{\n      \"name\": \"种子列表封面模式\",\n      \"pages\": [\"/torrents.php\", \"/special.php\"],\n      \"scripts\": [\"/libs/album/album.js\", \"torrents.js\"],\n      \"styles\": [\"/libs/album/style.css\"]\n    }],\n    \"searchEntry\": [\n      {\n        \"name\": \"综合\",\n        \"enabled\": true\n      },\n      {\n        \"entry\": \"special.php?search=$key$&notnewword=1\",\n        \"name\": \"音乐写真\",\n        \"enabled\": true\n      }\n    ],\n    \"categories\": [\n      {\n        \"entry\": \"torrents.php\",\n        \"result\": \"&cat$id$=1\",\n        \"category\": [\n          {\n            \"id\": 401,\n            \"name\": \"电影\"\n          },\n          {\n            \"id\": 402,\n            \"name\": \"电视剧\"\n          },\n          {\n            \"id\": 403,\n            \"name\": \"综艺\"\n          },\n          {\n            \"id\": 404,\n            \"name\": \"纪录片\"\n          },\n          {\n            \"id\": 407,\n            \"name\": \"体育\"\n          },\n          {\n            \"id\": 436,\n            \"name\": \"漫画书\"\n          },\n          {\n            \"id\": 405,\n            \"name\": \"动漫\"\n          },\n          {\n            \"id\": 434,\n            \"name\": \"书刊\"\n          },\n          {\n            \"id\": 432,\n            \"name\": \"有声书\"\n          },\n          {\n            \"id\": 406,\n            \"name\": \"MV\"\n          },\n          {\n            \"id\": 413,\n            \"name\": \"游戏\"\n          },\n          {\n            \"id\": 431,\n            \"name\": \"软件\"\n          },\n          {\n            \"id\": 409,\n            \"name\": \"其他\"\n          }\n        ]\n      },\n      {\n        \"entry\": \"special.php\",\n        \"result\": \"&cat$id$=1\",\n        \"category\": [\n          {\n            \"id\": 412,\n            \"name\": \"写真图影\"\n          },\n          {\n            \"id\": 411,\n            \"name\": \"写真影片\"\n          },\n          {\n            \"id\": 410,\n            \"name\": \"写真图片\"\n          },\n          {\n            \"id\": 415,\n            \"name\": \"音乐\"\n          },\n          {\n            \"id\": 416,\n            \"name\": \"其他\"\n          }\n        ]\n      }\n    ],\n    \"searchEntryConfig\": {\n      \"fieldSelector\": {\n        \"progress\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"]\n        },\n        \"status\": {\n          \"selector\": [\"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"],\n          \"filters\": [\n            \"query ? query.attr('title') : ''\",\n            \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n          ]\n        }\n      }\n    },\n    \"selectors\": {\n      \"userSeedingTorrents\": {\n        \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n        \"fields\": {\n          \"seeding\": {\n            \"selector\": [\n              \"b:first\"\n            ],\n            \"filters\": [\n              \"query.text()\"\n            ]\n          },\n          \"seedingSize\": {\n            \"selector\": \"\",\n            \"filters\": [\n              \"query.text().match(/总大小：(.*?)上一页/g)\",\n              \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n              \"(query != 0) ? query.sizeToNumber() : 0\"\n            ]\n          }\n        }\n      }\n    }\n  }\n  "
  },
  {
    "path": "resource/sites/www.okpt.net/torrents.js",
    "content": "(function($, window) {\n  // 添加封面模式\n  PTService.addButton({\n    title: PTService.i18n.t(\"buttons.coverTip\"), //\"以封面的方式进行查看\",\n    icon: \"photo\",\n    label: PTService.i18n.t(\"buttons.cover\"), //\"封面模式\",\n    click: (success, error) => {\n      // 获取目标表格\n      let tables = $(\"table.torrentname\");\n      let images = [];\n      tables.each((index, item) => {\n        let img = $(\"img[src]\", item);\n        let url = img.attr(\"src\");\n        let href = $(\"a\", item).attr(\"href\");\n        let title = $(\"a\", item).find(\"b\").text();\n        images.push({\n          url: url,\n          key: href,\n          title: title, //img.parent().attr(\"title\"),\n          link: $(\"a\", item).attr(\"href\")\n        });\n      });\n\n      // 创建预览\n      new album({\n        images: images,\n        onClose: () => {\n          PTService.buttonBar.show();\n        }\n      });\n      success();\n      PTService.buttonBar.hide();\n    }\n  });\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.pttime.org/config.json",
    "content": "{\n  \"name\": \"pttime\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.pttime.org/\",\n  \"description\": \"PT时间\",\n  \"icon\": \"https://www.pttime.org/favicon.ico\",\n  \"tags\": [\"电影\", \"成人\"],\n  \"host\": \"www.pttime.org\",\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"Power User\",\n    \"interval\": \"4\",\n    \"downloaded\": \"512GB\",\n    \"ratio\": \"1.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"Elite User\",\n    \"interval\": \"8\",\n    \"downloaded\": \"2048GB\",\n    \"ratio\": \"1.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"Crazy User\",\n    \"interval\": \"15\",\n    \"downloaded\": \"4096GB\",\n    \"ratio\": \"2.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"Insane User\",\n    \"interval\": \"25\",\n    \"downloaded\": \"8192GB\",\n    \"ratio\": \"2.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"Veteran User\",\n    \"interval\": \"52\",\n    \"downloaded\": \"16384GB\",\n    \"ratio\": \"3.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"Extreme User\",\n    \"interval\": \"80\",\n    \"downloaded\": \"25000GB\",\n    \"ratio\": \"3.55\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"Ultimate User\",\n    \"interval\": \"104\",\n    \"downloaded\": \"45000GB\",\n    \"ratio\": \"4.05\",\n    \"privilege\": \"无\"\n  },{\n    \"level\": \"8\",\n    \"name\": \"Nexus Master\",\n    \"interval\": \"130\",\n    \"downloaded\": \"90000GB\",\n    \"ratio\": \"4.55\",\n    \"privilege\": \"无\"\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/index.php\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"a[href*='userdetails.php'][class*='Name']:first\", \"a[href*='userdetails.php']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('id'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\"a[href*='userdetails.php'][class*='Name']:first\", \"a[href*='userdetails.php']:first\"],\n          \"filters\": [\"query && query.attr('href').getQueryString('id') > 0 ? query.text(): ''\"]\n        },\n        \"isLogged\": {\n          \"selector\": [\"a[href*='usercp.php']\"],\n          \"filters\": [\"query.length>0\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"td[style*='background: red'] a[href*='messages.php']\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"font.color_active\"],\n          \"filters\": [\"query.parent().text().match(/\\\\d+(\\\\.\\\\d+)?/g)\", \"parseInt(query[0])\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\", \"td.rowfollow:contains('分享率')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(上[传傳]量|Uploaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==3)?(query[2]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"td.rowhead:contains('传输') + td\", \"td.rowhead:contains('傳送') + td\", \"td.rowhead:contains('Transfers') + td\", \"td.rowfollow:contains('分享率')\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/(下[载載]量|Downloaded).+?([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length==3)?(query[2]).sizeToNumber():null\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"td.rowhead:contains('等级')\", \"td.rowhead:contains('等級')\", \"td.rowhead:contains('Class')\"],\n          \"filters\": [\"query.next().find('img').attr('title')\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('魔力') + td\", \"td.rowhead:contains('Karma'):contains('Points') + td\", \"td.rowhead:contains('麦粒') + td\", \"td.rowfollow:contains('魔力值')\"],\n          \"filters\": [\"query.is(\\\":contains('魔力值:')\\\")?query.text().replace(/,/g,'').match(/魔力值.+?([\\\\d.]+)/)[1]:query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"td.rowhead:contains('加入日期')\", \"td.rowhead:contains('Join'):contains('date')\"],\n          \"filters\": [\"query.next().text().split(' (')[0]\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlist.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"#outer > span:nth-child(3) > b\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"/资源总大小：(.*)/.exec(query.text())[1]\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n\n        ]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"b:contains('大小'):first\"],\n          \"filters\": [\"query.parent().text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        },\n        \"imdbId\": {\n          \"selector\": [\"a[href*='www.imdb.com/title/']:first\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query.match(/(tt\\\\d+)/)\", \"(query && query.length>=2)?query[1]:null\"]\n        },\n        \"sayThanksButton\": {\n          \"selector\": [\"input#saythanks:not(:disabled)\"],\n          \"filters\": [\"query\"]\n        }\n      }\n    }\n  },\n  \"collaborator\": \"asterisk\"\n}\n"
  },
  {
    "path": "resource/sites/www.ptzone.xyz/config.json",
    "content": "{\n  \"name\": \"PTzone\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://www.ptzone.xyz/\",\n  \"description\": \"PTzone\",\n  \"icon\": \"https://www.ptzone.xyz/favicon.ico\",\n  \"tags\": [\"电影\"],\n  \"host\": \"www.ptzone.xyz\",\n  \"levelRequirements\": [\n        {\n            \"level\": 1,\n            \"name\": \"Power User\",\n            \"interval\": \"4\",\n            \"downloaded\": \"50GB\",\n            \"ratio\": \"1.05\",\n            \"seedingPoints\": \"40000\",\n            \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n        },\n        {\n            \"level\": 2,\n            \"name\": \"Elite User\",\n            \"interval\": \"8\",\n            \"downloaded\": \"120GB\",\n            \"ratio\": \"1.55\",\n            \"seedingPoints\": \"80000\",\n            \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n        },\n        {\n            \"level\": 3,\n            \"name\": \"Crazy User\",\n            \"interval\": \"15\",\n            \"downloaded\": \"300GB\",\n            \"ratio\": \"2.05\",\n            \"seedingPoints\": \"150000\",\n            \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n        },\n        {\n            \"level\": 4,\n            \"name\": \"Insane User\",\n            \"interval\": \"25\",\n            \"downloaded\": \"500GB\",\n            \"ratio\": \"2.55\",\n            \"seedingPoints\": \"250000\",\n            \"privilege\": \"可以查看普通日志。\"\n        },\n        {\n            \"level\": 5,\n            \"name\": \"Veteran User\",\n            \"interval\": \"40\",\n            \"downloaded\": \"750GB\",\n            \"ratio\": \"3.05\",\n            \"seedingPoints\": \"400000\",\n            \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n        },\n        {\n            \"level\": 6,\n            \"name\": \"Extreme User\",\n            \"interval\": \"60\",\n            \"downloaded\": \"1TB\",\n            \"ratio\": \"3.55\",\n            \"seedingPoints\": \"600000\",\n            \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n        },\n        {\n            \"level\": 7,\n            \"name\": \"Ultimate User\",\n            \"interval\": \"80\",\n            \"downloaded\": \"1.5TB\",\n            \"ratio\": \"4.05\",\n            \"seedingPoints\": \"800000\",\n            \"privilege\": \"得到五个邀请名额。\"\n        },\n        {\n            \"level\": 8,\n            \"name\": \"Nexus Master\",\n            \"interval\": \"100\",\n            \"downloaded\": \"3TB\",\n            \"ratio\": \"4.55\",\n            \"seedingPoints\": \"1000000\",\n            \"privilege\": \"得到十个邀请名额。\"\n        }\n    ],\n   \"searchEntry\": [\n    {\n      \"name\": \"全部\",\n      \"enabled\": true\n    }\n  ],\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\n            \"b:first\"\n          ],\n          \"filters\": [\n            \"query.text()\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.skyey2.com/config.json",
    "content": "{\n  \"name\": \"SkyeySnow\",\n  \"timezoneOffset\": \"+0800\",\n  \"schema\": \"Discuz\",\n  \"url\": \"https://www.skyey2.com/\",\n  \"description\": \"天雪\",\n  \"icon\": \"https://www.skyey2.com/favicon.ico\",\n  \"tags\": [\"动漫\"],\n  \"collaborator\": [\n    \"栽培者\",\n    \"MewX\",\n    \"fzlins\",\n    \"zhuweitung\"\n  ],\n  \"host\": \"www.skyey2.com\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Lv.1 白露\",\n      \"bonus\": 1000,\n      \"privilege\": \"自定义头衔；允许发短消息；允许加好友；允许设置回帖奖励；允许参与点评；\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Lv.2 秋分\",\n      \"bonus\": 3000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Lv.3 霜降\",\n      \"bonus\": 5000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Lv.4 小雪\",\n      \"bonus\": 10000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Lv.5 大雪\",\n      \"bonus\": 30000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Lv.6 小寒\",\n      \"bonus\": 100000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Lv.7 大寒\",\n      \"bonus\": 300000,\n      \"privilege\": \"\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Lv.8 立春\",\n      \"bonus\": 1000000,\n      \"privilege\": \"\"\n    }\n  ],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\".vwmy a\"],\n          \"attribute\": \"href\",\n          \"filters\": [\"query ? query.getQueryString('uid'):''\"]\n        },\n        \"name\": {\n          \"selector\": [\".vwmy a\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"a.a.showmenu.new\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/home.php?mod=space&uid=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": \"#psts li:contains('上传量')\",\n          \"filters\": [\n            \"query.text().match(/上传量.+?\\\\/\\\\s*([\\\\d\\\\.]+\\\\s*?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2) ? query[1].sizeToNumber() : 0\"\n          ]\n        },\n        \"downloaded\": {\n          \"selector\": \"#psts li:contains('下载量')\",\n          \"filters\": [\n            \"query.text().match(/下载量.+?\\\\/\\\\s*([\\\\d\\\\.]+\\\\s*?[ZEPTGMK]?i?B)/)\",\n            \"(query && query.length>=2) ? query[1].sizeToNumber() : 0\"\n          ]\n        },\n        \"ratio\": {\n          \"selector\": \"ul.bbda\",\n          \"filters\": [\n            \"query.text().match(/分享率\\\\s+?([\\\\d\\\\.]+)/)\",\n            \"(query && query.length>=2) ? query[1].trim() : 0\"\n          ]\n        },\n        \"levelName\": {\n          \"selector\": \"a[href='home.php?mod=spacecp&ac=usergroup']\",\n          \"filters\": [\"query.text().replace('用户组: ', '').trim()\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"#ratio\"],\n          \"filters\": [\n            \"query.text().replace('积分: ', '').replace(/,/g,'').trim()\",\n            \"parseFloat(query)\"\n          ]\n        },\n        \"joinTime\": {\n          \"selector\": \"#pbbs > li:contains('注册时间')\",\n          \"filters\": [\n            \"query.text().replace('注册时间', '').trim()\",\n            \"dateTime(query).isValid()?dateTime(query).valueOf():query\"\n          ]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"prerequisites\": \"!user.seeding\",\n      \"page\": \"/forum.php?&mod=torrents&cat_5up=on\",\n      \"parser\": \"getUserSeedingTorrents.js\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"table.torrents > tbody > tr:not(:eq(0))\"],\n          \"filters\": [\"query.length\"]\n        },\n        \"seedingSize\": {\n          \"selector\": [\"table.torrents > tbody > tr:not(:eq(0))\"],\n          \"filters\": [\"jQuery.map(query.find('td.rowfollow:eq(3)'), (item)=>{return $(item).text();})\", \"_self.getTotalSize(query)\"]\n        }\n      }\n    } \n  },\n  \"supportedFeatures\": {\n    \"imdbSearch\": false\n  },\n  \"searchEntryConfig\": {\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\"div.tline1, div.tline2\"],\n        \"filters\": [\"query.attr('style')||''\", \"query.match(/width:([ \\\\d.]+)%/)\", \"(query && query.length>=2)?query[1]:null\"]\n      },\n      \"status\": {\n        \"selector\": [\"> td:eq(4), > td:eq(5), > td:eq(6)\"],\n        \"filters\": [\"query.map(function(){return $(this).attr('style') || ''}).get()\", \"query[0] ? 2 : (query[2] ? 255: (query[1] ? 1 : null))\"]\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/www.skyey2.com/getUserSeedingTorrents.js",
    "content": "if (\"\".getQueryString === undefined) {\n  String.prototype.getQueryString = function(name, split) {\n    if (split == undefined) split = \"&\";\n    var reg = new RegExp(\n        \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"]*)(\" + split + \"|$)\"\n      ),\n      r;\n    if ((r = this.match(reg))) return decodeURI(r[2]);\n    return null;\n  };\n}\n\n(function(options, User) {\n  class Parser {\n    constructor(options, dataURL) {\n      this.options = options;\n      this.dataURL = dataURL;\n      this.body = null;\n      this.rawData = \"\";\n      this.pageInfo = {\n        count: 0,\n        current: 0\n      };\n      this.result = {\n        seeding: 0,\n        seedingSize: 0\n      };\n      this.load();\n    }\n\n    /**\n     * 完成\n     */\n    done() {\n      this.options.resolve(this.result);\n    }\n\n    /**\n     * 解析内容\n     */\n    parse() {\n      const doc = new DOMParser().parseFromString(this.rawData, \"text/html\");\n      // 构造 jQuery 对象\n      this.body = $(doc).find(\"body\");\n\n      this.getPageInfo();\n\n      let results = new User.InfoParser(User.service).getResult(\n        this.body,\n        this.options.rule\n      );\n\n      if (results) {\n        this.result.seeding += results.seeding;\n        this.result.seedingSize += results.seedingSize;\n      }\n\n      // 是否已到最后一页\n      if (this.pageInfo.current < this.pageInfo.count) {\n        this.pageInfo.current++;\n        this.load();\n      } else {\n        this.done();\n      }\n    }\n\n    /**\n     * 获取页面相关内容\n     */\n    getPageInfo() {\n      if (this.pageInfo.count > 0) {\n        return;\n      }\n      // 获取最大页码\n      const infos = this.body\n        .find(\"table.torrents > tbody > tr:eq(0) td:eq(1)\")\n        .text();\n      if (infos) {\n        this.pageInfo.count = parseInt(infos.split('/')[1] / 50);\n      } else {\n        this.pageInfo.count = 0;\n      }\n    }\n\n    /**\n     * 加载当前页内容\n     */\n    load() {\n      let url = this.dataURL;\n      if (this.pageInfo.current > 0) {\n        url += \"&page=\" + this.pageInfo.current;\n      }\n      $.get(url)\n        .done(result => {\n          this.rawData = result;\n          this.parse();\n        })\n        .fail(() => {\n          this.done();\n        });\n    }\n  }\n\n  let dataURL = options.site.activeURL + options.rule.page;\n  dataURL = dataURL\n    .replace(\"$user.id$\", options.userInfo.id)\n    .replace(\"$user.name$\", options.userInfo.name)\n    .replace(\"://\", \"****\")\n    .replace(/\\/\\//g, \"/\")\n    .replace(\"****\", \"://\");\n\n  new Parser(options, dataURL);\n})(_options, _self);\n/**\n * \n  _options 表示当前参数 \n  {\n    site,\n    rule,\n    userInfo,\n    resolve,\n    reject\n  }\n\n  _self 表示 User(/src/background/user.ts) 类实例 \n */"
  },
  {
    "path": "resource/sites/www.torrentday.com/config.json",
    "content": "{\n  \"name\": \"TorrentsTD\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"Torrents - TD\",\n  \"url\": \"https://www.torrentday.com/\",\n  \"icon\": \"https://www.torrentday.com/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"IPTorrents\",\n  \"host\": \"www.torrentday.com\",\n  \"searchEntry\": [{\n    \"entry\": \"/t?q=$key$\",\n    \"name\": \"全部\",\n    \"resultType\": \"html\",\n    \"parseScriptFile\": \"/schemas/IPTorrents/getSearchResult.js\",\n    \"resultSelector\": \"table#torrentTable:first\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"page\": \"/userdetails.php?id=$user.id$\",\n      \"fields\": {\n        \"uploaded\": {\n          \"selector\": [\"span.detailsInfoSpan:contains('Up: ') > span\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"span.detailsInfoSpan:contains('Down: ') > span\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>=2)?(query[1]).sizeToNumber():null\"]\n        },\n        \"ratio\": {\n          \"selector\": \"span.detailsInfoSpan:contains('Ratio: ') > span\",\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        },\n        \"levelName\": {\n          \"selector\": \"span.detailsInfoSpan:contains('Class: ') > span\"\n        },\n        \"bonus\": {\n          \"selector\": [\"a[href='/mybonus.php']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+)/)\", \"(query && query.length>=2)?query[1]:''\"]\n        },\n        \"joinTime\": {\n          \"selector\": \"span.detailsInfoSpan:contains('Joined: ') > span\",\n          \"filters\": [\"query.text()\", \"dateTime(query).isValid()?dateTime(query).valueOf():query\"]\n        },\n        \"seeding\": {\n          \"selector\": [\"a[href*='/peers?u='] > img[alt='downloads'] + span\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        },\n        \"seedingSize\": {\n          \"value\": -1\n        },\n        \"messageCount\": {\n          \"selector\": [\"a[href='/m']:contains('You have')\"],\n          \"filters\": [\"query.text().match(/(\\\\d+)/)\", \"(query && query.length>=2)?parseInt(query[1]):0\"]\n        }\n      }\n    },\n    \"/details.php\": {\n      \"fields\": {\n        \"size\": {\n          \"selector\": [\"span[title='File Size']\"],\n          \"filters\": [\"query.text().replace(/,/g,'').match(/([\\\\d.]+ ?[ZEPTGMK]?i?B)/)\", \"(query && query.length>1)?(query[1]).sizeToNumber():0\"]\n        }\n      }\n    }\n  },\n  \"supportedFeatures\": {\n    \"userData\": \"◐\"\n  }\n}"
  },
  {
    "path": "resource/sites/www.torrentleech.org/config.json",
    "content": "{\n  \"name\": \"TorrentLeech\",\n  \"timezoneOffset\": \"+0000\",\n  \"description\": \"TorrentLeech\",\n  \"url\": \"https://www.torrentleech.org/\",\n  \"icon\": \"https://www.torrentleech.org/favicon.ico\",\n  \"tags\": [\"综合\"],\n  \"schema\": \"TorrentLeech\",\n  \"host\": \"www.torrentleech.org\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"2\",\n      \"uploaded\": \"200GB\",\n      \"ratio\": \"1.1\",\n      \"privilege\": \"Increased Points: 3%, Minimum Seeding Time: 8 days\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Super User\",\n      \"interval\": \"12\",\n      \"uploaded\": \"1TB\",\n      \"ratio\": \"2.0\",\n      \"privilege\": \"Increased Points: 5%, Minimum Seeding Time: 7 days\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Extreme User\",\n      \"interval\": \"24\",\n      \"uploaded\": \"10TB\",\n      \"ratio\": \"5.0\",\n      \"privilege\": \"Increased Points: 6%, Minimum Seeding Time: 6 days\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"TL GOD\",\n      \"interval\": \"52\",\n      \"uploaded\": \"50TB\",\n      \"ratio\": \"8.0\",\n      \"privilege\": \"Increased Points: 8%, Minimum Seeding Time: 4 days\"\n    }\n  ],\n  \"plugins\": [{\n    \"name\": \"种子详情页面\",\n    \"pages\": [\"^/torrent/(\\\\d+)$\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"details.js\"]\n  }, {\n    \"name\": \"种子列表\",\n    \"pages\": [\"/torrents/top/index\", \"/torrents/browse/index\"],\n    \"scripts\": [\"/schemas/NexusPHP/common.js\", \"torrents.js\"]\n  }],\n  \"searchEntryConfig\": {\n    \"page\": \"/torrents/browse/list/query/$key$\",\n    \"resultType\": \"json\",\n    \"parseScriptFile\": \"getSearchResult.js\",\n    \"area\": [{\n      \"name\": \"IMDB\",\n      \"keyAutoMatch\": \"^(tt\\\\d+)$\",\n      \"page\": \"/torrents/browse/list/imdbID/$key$\"\n    }]\n  },\n  \"searchEntry\": [{\n    \"name\": \"all\",\n    \"enabled\": true\n  }],\n  \"selectors\": {\n    \"userBaseInfo\": {\n      \"page\": \"/\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"span.centerTopBar span[onclick*='/profile/'][onclick*='view']\"]\n        },\n        \"name\": {\n          \"selector\": [\"span.centerTopBar span[onclick*='/profile/'][onclick*='view']\"]\n        },\n        \"uploaded\": {\n          \"selector\": [\"span.centerTopBar div[title^='Uploaded'] span\"],\n          \"filters\": [\"query.text()?query.text().trim().replace(/,/g,'').sizeToNumber():null\"]\n        },\n        \"downloaded\": {\n          \"selector\": [\"span.centerTopBar div[title^='Downloaded'] span\"],\n          \"filters\": [\"query.text()?query.text().trim().replace(/,/g,'').sizeToNumber():null\"]\n        },\n        \"bonus\": {\n          \"selector\": [\"span.centerTopBar span.total-TL-points\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\"]\n        },\n        \"messageCount\": {\n          \"selector\": [\"span.link[onclick*='/notifications']\"],\n          \"filters\": [\"parseInt(query.text().trim())\"]\n        }\n      }\n    },\n    \"userExtendInfo\": {\n      \"page\": \"/profile/$user.name$\",\n      \"fields\": {\n        \"id\": {\n          \"selector\": [\"div.has-support-msg script\"],\n          \"filters\": [\"query.text().match(/var userLogUserID = '(\\\\d+)';/)[1]\"]\n        },\n        \"levelName\": {\n          \"selector\": [\"div.profile-details div.label-user-class\"]\n        },\n        \"joinTime\": {\n          \"selector\": [\"table.profileViewTable td:contains('Registration date') + td\"],\n          \"filters\": [\"dateTime(query.text().split(' ').slice(1).join(' ').replace(/th|st|nd|rd/, '')).valueOf()\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/profile/$user.name$/seeding\",\n      \"fields\": {\n        \"seedingSize\": {\n          \"selector\": \"table#profile-seedingTable > tbody > tr > td:nth-child(2)\",\n          \"filters\": [\"jQuery.map(query, (item) => {return $(item).text()})\", \"_self.getTotalSize(query)\"]\n        },\n        \"seeding\": {\n          \"selector\": \"table#profile-seedingTable > tbody > tr\",\n          \"filters\": [\"query.length\"]\n        }\n      }\n    }\n  },\n  \"categories\": [{\n    \"entry\": \"*\",\n    \"result\": \"/categories/$id$\",\n    \"category\": [{\n      \"id\": \"8\",\n      \"name\": \"Cam\"\n    }, {\n      \"id\": \"9\",\n      \"name\": \"TS/TC\"\n    }, {\n      \"id\": \"11\",\n      \"name\": \"DVDRip/DVDScreener\"\n    }, {\n      \"id\": \"37\",\n      \"name\": \"WEBRip\"\n    }, {\n      \"id\": \"43\",\n      \"name\": \"HDRip\"\n    }, {\n      \"id\": \"14\",\n      \"name\": \"BlurayRip\"\n    }, {\n      \"id\": \"12\",\n      \"name\": \"DVD-R\"\n    }, {\n      \"id\": \"13\",\n      \"name\": \"Bluray\"\n    }, {\n      \"id\": \"47\",\n      \"name\": \"4K\"\n    }, {\n      \"id\": \"15\",\n      \"name\": \"Boxsets\"\n    }, {\n      \"id\": \"29\",\n      \"name\": \"Documentaries\"\n    }, {\n      \"id\": \"26\",\n      \"name\": \"Episodes\"\n    }, {\n      \"id\": \"32\",\n      \"name\": \"Episodes HD\"\n    }, {\n      \"id\": \"27\",\n      \"name\": \"Boxsets\"\n    }, {\n      \"id\": \"17\",\n      \"name\": \"PC\"\n    }, {\n      \"id\": \"42\",\n      \"name\": \"Mac\"\n    }, {\n      \"id\": \"18\",\n      \"name\": \"XBOX\"\n    }, {\n      \"id\": \"19\",\n      \"name\": \"XBOX360\"\n    }, {\n      \"id\": \"40\",\n      \"name\": \"XBOXONE\"\n    }, {\n      \"id\": \"20\",\n      \"name\": \"PS2\"\n    }, {\n      \"id\": \"21\",\n      \"name\": \"PS3\"\n    }, {\n      \"id\": \"39\",\n      \"name\": \"PS4\"\n    }, {\n      \"id\": \"22\",\n      \"name\": \"PSP\"\n    }, {\n      \"id\": \"28\",\n      \"name\": \"Wii\"\n    }, {\n      \"id\": \"30\",\n      \"name\": \"Nintendo DS\"\n    }, {\n      \"id\": \"48\",\n      \"name\": \"Nintendo Switch\"\n    }, {\n      \"id\": \"23\",\n      \"name\": \"PC-ISO\"\n    }, {\n      \"id\": \"24\",\n      \"name\": \"Mac\"\n    }, {\n      \"id\": \"25\",\n      \"name\": \"Mobile\"\n    }, {\n      \"id\": \"33\",\n      \"name\": \"0-day\"\n    }, {\n      \"id\": \"38\",\n      \"name\": \"Education\"\n    }, {\n      \"id\": \"34\",\n      \"name\": \"Anime\"\n    }, {\n      \"id\": \"35\",\n      \"name\": \"Cartoons\"\n    }, {\n      \"id\": \"45\",\n      \"name\": \"EBooks\"\n    }, {\n      \"id\": \"46\",\n      \"name\": \"Comics\"\n    }, {\n      \"id\": \"31\",\n      \"name\": \"Audio\"\n    }, {\n      \"id\": \"16\",\n      \"name\": \"Music videos\"\n    }, {\n      \"id\": \"36\",\n      \"name\": \"Movies\"\n    }, {\n      \"id\": \"44\",\n      \"name\": \"TV Series\"\n    }]\n  }]\n}\n"
  },
  {
    "path": "resource/sites/www.torrentleech.org/details.js",
    "content": "(function($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      if (this.getDownloadURL()) {\n        this.initDetailButtons();\n      }\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let query = $(\"a#detailsDownloadButton\");\n      let url = \"\";\n      if (query.length > 0) {\n        url = query.attr(\"href\");\n      }\n\n      if (!url) {\n        return \"\";\n      }\n\n      return this.getFullURL(url);\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      let title = $(\"title\").text();\n      let datas = /\\\"(.*?)\\\"/.exec(title);\n      if (datas && datas.length > 1) {\n        return datas[1] || title;\n      }\n      return title;\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.torrentleech.org/getSearchResult.js",
    "content": "(function(options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      this.categories = {};\n      if (/login-form/.test(options.responseText)) {\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n      options.isLogged = true;\n      this.haveData = true;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n      let site = options.site;\n      if (!options.page.numFound) {\n        options.status = ESearchResultParseStatus.noTorrents;\n        return [];\n      }\n      let torrentList = options.page.torrentList;\n      let results = [];\n\n      try {\n        torrentList.forEach(item => {\n          if (item.hasOwnProperty(\"fid\")) {\n            let data = {\n              title: item.name,\n              link: `${site.url}torrent/${item.fid}`,\n              url: `${site.url}download/${item.fid}/${item.filename}`,\n              size: parseFloat(item.size),\n              time: item.addedTimestamp,\n              author: \"\",\n              seeders: item.seeders,\n              leechers: item.leechers,\n              completed: item.completed,\n              comments: item.numComments,\n              site: site,\n              tags: this.getTags(item),\n              entryName: options.entry.name,\n              category: options.searcher.getCategoryById(\n                site,\n                options.url,\n                item.categoryID\n              ),\n              imdbId: item.imdbID\n            };\n            results.push(data);\n          }\n        });\n        console.log(\"results.length\", results.length);\n        if (results.length == 0) {\n          options.status = ESearchResultParseStatus.noTorrents;\n        }\n      } catch (error) {\n        console.log(error);\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n      return results;\n    }\n    getTags(item) {\n      var tag = [{\n        name: \"Free\",\n        color: \"blue\"\n      }]\n      if(item.tags.indexOf(\"FREELEECH\")>-1)return tag;\n    }\n  }\n\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, Searcher);\n"
  },
  {
    "path": "resource/sites/www.torrentleech.org/torrents.js",
    "content": "(function($) {\n  console.log(\"this is torrents.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a.download\").toArray();\n\n      if (links.length == 0) {\n        return this.t(\"getDownloadURLsFailed\"); //\"获取下载链接失败，未能正确定位到链接\";\n      }\n\n      let urls = $.map(links, item => {\n        let link = $(item).attr(\"href\");\n        return this.getFullURL(link);\n      });\n\n      return urls;\n    }\n\n    /**\n     * 下载拖放的种子\n     * @param {*} data\n     * @param {*} callback\n     */\n    downloadFromDroper(data, callback) {\n      if (typeof data === \"string\") {\n        data = {\n          url: data,\n          title: \"\"\n        };\n      }\n\n      console.log(data);\n\n      if (!data.url) {\n        PTService.showNotice({\n          msg: this.t(\"invalidURL\") //\"无效的链接\"\n        });\n        callback();\n        return;\n      }\n\n      if (data.url.substr(0, 1) === \"/\") {\n        data.url = `${location.origin}${data.url}`;\n      } else if (data.url.substr(0, 4) !== \"http\") {\n        data.url = `${location.origin}/${data.url}`;\n      }\n\n      this.sendTorrentToDefaultClient(result)\n        .then(result => {\n          callback(result);\n        })\n        .catch(result => {\n          callback(result);\n        });\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(\n        $(\"#torrent_table\").find(\n          \"td.td-size:contains('MB'),td[align='center']:contains('GB'),td[align='center']:contains('TB')\"\n        )\n      );\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/www.torrentseeds.org/config.json",
    "content": "{\n    \"name\": \"TorrentSeeds\",\n    \"url\": \"https://www.torrentseeds.org/\",\n    \"icon\": \"https://www.torrentseeds.org/favicon.ico\",\n    \"tags\": [\"综合\"],\n    \"schema\": \"UNIT3D\",\n    \"host\": \"www.torrentseeds.org\",\n    \"collaborator\": \"ian\"\n}\n"
  },
  {
    "path": "resource/sites/www.torrentseeds.org/details.js",
    "content": "(function ($, window) {\n  console.log(\"this is details.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initDetailButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURL() {\n      let siteURL = PTService.site.url;\n      let url = $(\"td.details-text-ellipsis:eq(0) > a\").attr(\"href\");\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      return this.getFullURL(siteURL + url);\n    }\n\n    /**\n     * 获取当前种子标题\n     */\n    getTitle() {\n      return $(\"div.pull-left > h1\").text();\n    }\n  }\n  new App().init();\n})(jQuery, window);\n"
  },
  {
    "path": "resource/sites/www.torrentseeds.org/getSearchResult.js",
    "content": "/**\n * 通用搜索解析脚本\n */\n(function (options, Searcher) {\n  class Parser {\n    constructor() {\n      this.haveData = false;\n      // 判断是否已登录\n      // alert(options.responseText)\n      if (\n        options.entry.loggedRegex &&\n        !new RegExp(options.entry.loggedRegex, \"\").test(options.responseText)\n      ) {\n        // 需要登录后再搜索\n        options.status = ESearchResultParseStatus.needLogin;\n        return;\n      }\n\n      options.isLogged = true;\n\n      this.haveData = true;\n      this.site = options.site;\n    }\n\n    /**\n     * 获取搜索结果\n     */\n    getResult() {\n      if (!this.haveData) {\n        return [];\n      }\n\n\n      let results = [];\n\n      //当搜索结果只有1条时会自动重定向到种子详情页，这时直接解析页面\n      if (new RegExp(/Info\\shash/, \"\").test(options.page.html())) {\n        return [{\n          title: options.page.find(\"div > div:nth-child(3) > div.pull-left > h1\").text(),\n          subTitle: \"\",\n          link: this.getFullURL(options.page.find(\"h2.text-center > a\").attr(\"href\")),\n          url: this.getFullURL(options.page.find(\"div > div:nth-child(4) > table:nth-child(3) > tbody > tr:nth-child(1) > td.details-text-ellipsis > a\").attr(\"href\") + \"&ssl=1\"),\n          size: options.page.find(\"td.heading:contains('Size')\").next().text().split(\"(\")[0].trim().sizeToNumber(),\n          // time: dateTime(options.page.find(\"td.heading:contains('Added')\").next().text().trim()).isValid() ? dateTime(options.page.find(\"td.heading:contains('Added')\").next().text().trim()).valueOf() : options.page.find(\"td.heading:contains('Added')\").next().text().trim(),\n          time: options.page.find(\"td.heading:contains('Added')\").next().text().trim(),\n          author: \"\",\n          seeders: options.page.find(\"td.heading:contains('Peers')\").next().text().split(\"seeder\")[0].trim(),\n          leechers: options.page.find(\"td.heading:contains('Peers')\").next().text().split(\"leecher\")[0].split(\",\")[1].trim(),\n          completed: options.page.find(\"td.heading:contains('Snatched')\").next().text().split(\"time\")[0].trim(),\n          comments: 0,\n          site: this.site,\n          tags: Searcher.getRowTags(this.site, options.page.find(\"td.heading:contains('Ratio After Download')\").next()),\n          entryName: options.entry.name,\n          category: options.page.find(\"td.heading:contains('Type')\").next().text(),\n          progress: null,\n          status: null\n        }]\n      }\n\n      let selector = options.resultSelector;\n      let dataRowSelector = options.entry.dataRowSelector || \"> tbody > tr\";\n      selector = selector.replace(dataRowSelector, \"\");\n      // 获取数据表格\n      let table = options.page.find(selector);\n      // 获取种子列表行\n      let rows = table.find(dataRowSelector);\n\n      if (rows.length == 0) {\n        // 没有定位到种子列表，或没有相关的种子\n        options.status = ESearchResultParseStatus.torrentTableIsEmpty;\n        return [];\n      }\n\n      let beginRowIndex = options.entry.firstDataRowIndex || 0;\n\n      // 用于定位每个字段所列的位置\n      let fieldIndex = options.entry.fieldIndex || {\n        // 发布时间\n        time: -1,\n        // 大小\n        size: -1,\n        // 上传数量\n        seeders: -1,\n        // 下载数量\n        leechers: -1,\n        // 完成数量\n        completed: -1,\n        // 评论数量\n        comments: -1,\n        // 发布人\n        author: -1,\n        // 分类\n        category: -1\n      };\n\n      try {\n        // 遍历数据行\n        for (let index = beginRowIndex; index < rows.length; index++) {\n          const row = rows.eq(index);\n          let cells = row.find(\">td\");\n\n          let title = this.getTitle(row, cells, fieldIndex);\n\n          // 没有获取标题时，继续下一个\n          if (!title) {\n            continue;\n          }\n          let link = this.getFieldValue(row, cells, fieldIndex, \"link\");\n\n          // 获取下载链接\n          let url = this.getFieldValue(row, cells, fieldIndex, \"url\");\n\n          if (!url || !link) {\n            continue;\n          }\n\n          let data = {\n            title: title,\n            subTitle: this.getFieldValue(row, cells, fieldIndex, \"subTitle\"),\n            link: this.getFullURL(link),\n            url: this.getFullURL(url),\n            size: this.getFieldValue(row, cells, fieldIndex, \"size\") || 0,\n            time: this.getFieldValue(row, cells, fieldIndex, \"time\"),\n            author: this.getFieldValue(row, cells, fieldIndex, \"author\") || \"\",\n            seeders: this.getFieldValue(row, cells, fieldIndex, \"seeders\") || 0,\n            leechers:\n              this.getFieldValue(row, cells, fieldIndex, \"leechers\") || 0,\n            completed:\n              this.getFieldValue(row, cells, fieldIndex, \"completed\") || 0,\n            comments:\n              this.getFieldValue(row, cells, fieldIndex, \"comments\") || 0,\n            site: this.site,\n            tags: Searcher.getRowTags(this.site, row),\n            entryName: options.entry.name,\n            category: this.getFieldValue(row, cells, fieldIndex, \"category\"),\n            progress: this.getFieldValue(row, cells, fieldIndex, \"progress\"),\n            status: this.getFieldValue(row, cells, fieldIndex, \"status\")\n          };\n          results.push(data);\n        }\n      } catch (error) {\n        // 获取种子信息出错\n        options.status = ESearchResultParseStatus.parseError;\n        options.errorMsg = error.stack;\n      }\n\n      // 没有搜索到相关的种子\n      if (results.length == 0 && !options.errorMsg) {\n        options.status = ESearchResultParseStatus.noTorrents;\n      }\n\n      return results;\n    }\n\n    /**\n     * 获取指定字段内容\n     * @param {*} row\n     * @param {*} cells\n     * @param {*} fieldIndex\n     * @param {*} fieldName\n     */\n    getFieldValue(row, cells, fieldIndex, fieldName, returnCell) {\n      let parent = row;\n      let cell = null;\n      if (\n        cells &&\n        fieldIndex &&\n        fieldIndex[fieldName] !== undefined &&\n        fieldIndex[fieldName] !== -1\n      ) {\n        cell = cells.eq(fieldIndex[fieldName]);\n        parent = cell || row;\n      }\n\n      let result = Searcher.getFieldValue(this.site, parent, fieldName);\n\n      if (!result && cell) {\n        if (returnCell) {\n          return cell;\n        }\n        result = cell.text();\n      }\n\n      return result;\n    }\n\n    /**\n     * 获取完整的URL地址\n     * @param {string} url\n     */\n    getFullURL(url) {\n      let URL = PTServiceFilters.parseURL(this.site.url);\n      if (url.substr(0, 2) === \"//\") {\n        url = `${URL.protocol}${url}`;\n      } else if (url.substr(0, 1) === \"/\") {\n        url = `${URL.origin}${url}`;\n      } else if (url.substr(0, 4) !== \"http\") {\n        url = `${URL.origin}/${url}`;\n      }\n      return url;\n    }\n\n    /**\n     * 获取标题\n     */\n    getTitle(row, cells, fieldIndex) {\n      let title = this.getFieldValue(row, cells, fieldIndex, \"title\", true);\n\n      if (!title) {\n        return \"\";\n      }\n\n      if (typeof title === \"string\") {\n        return title;\n      }\n\n      // 对title进行处理，防止出现cf的email protect\n      let cfemail = title.find(\"span.__cf_email__\");\n      if (cfemail.length > 0) {\n        cfemail.each((index, el) => {\n          $(el).replaceWith(Searcher.cfDecodeEmail($(el).data(\"cfemail\")));\n        });\n      }\n\n      return title.text();\n    }\n  }\n\n  let parser = new Parser(options);\n  options.results = parser.getResult();\n  console.log(options.results);\n})(options, options.searcher);\n"
  },
  {
    "path": "resource/sites/www.torrentseeds.org/torrents.js",
    "content": "(function ($) {\n  console.log(\"this is torrent.js\");\n  class App extends window.NexusPHPCommon {\n    init() {\n      this.initButtons();\n      this.initFreeSpaceButton();\n      // 设置当前页面\n      PTService.pageApp = this;\n    }\n\n    /**\n     * 初始化按钮列表\n     */\n    initButtons() {\n      this.initListButtons();\n    }\n\n    /**\n     * 获取下载链接\n     */\n    getDownloadURLs() {\n      let links = $(\"a[href$='&ssl=1']\").toArray();\n\n      if (links.length == 0) {\n        //  \"获取下载链接失败，未能正确定位到链接\";\n        return this.t(\"getDownloadURLsFailed\");\n      }\n\n      let siteURL = PTService.site.url;\n      if (siteURL.substr(-1) != \"/\") {\n        siteURL += \"/\";\n      }\n\n      let urls = $.map(links, item => {\n        let url = $(item).attr(\"href\");\n        return this.getFullURL(siteURL + url.substr(1));\n      });\n\n      return urls;\n    }\n\n    /**\n     * 确认大小是否超限\n     */\n    confirmWhenExceedSize() {\n      return this.confirmSize(PTService.getFieldValue(\"confirmSize\"));\n    }\n  }\n  new App().init();\n})(jQuery);\n"
  },
  {
    "path": "resource/sites/xingtan.one/config.json",
    "content": "{\n  \"name\": \"杏坛\",\n  \"schema\": \"NexusPHP\",\n  \"url\": \"https://xingtan.one/\",\n  \"description\": \"杏坛 - 积少成多，聚沙成塔。\",\n  \"icon\": \"https://xingtan.one/favicon.ico\",\n  \"tags\": [\n    \"医学\"\n  ],\n  \"host\": \"xingtan.one\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"privilege\": \"得到1个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"8\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"15\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"privilege\": \"得到2个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"25\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"40\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\",\n      \"privilege\": \"得到3个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"60\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"80\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\",\n      \"privilege\": \"得到5个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"100\",\n      \"downloaded\": \"3TB\",\n      \"ratio\": \"4.55\",\n      \"privilege\": \"得到10个邀请名额。\"\n    }\n  ],\n  \"formerHosts\": [\n    \"xinglin.one\"\n  ],\n  \"collaborator\": [\"koal\",\"yum\"],\n  \"selectors\": {\n    \"userExtendInfo\": {\n      \"merge\": true,\n      \"fields\": {\n        \"bonus\": {\n          \"selector\": [\"td.rowhead:contains('杏仁值') + td\"],\n          \"filters\": [\"query.text().replace(/,/g,'')\", \"parseFloat(query)\"]\n        }\n      }\n    },\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\"b:first\"],\n          \"filters\": [\"query.text()\"]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "resource/sites/zhuque.in/config.json",
    "content": "{\n  \"name\": \"ZHUQUE\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"朱雀\",\n  \"url\": \"https://zhuque.in/\",\n  \"icon\": \"https://zhuque.in/assets/images/512.png\",\n  \"tags\": [\"影视\", \"综合\"],\n  \"schema\": \"TNode\",\n  \"host\": \"zhuque.in\",\n  \"searchEntry\": [{\n    \"name\": \"全部\",\n    \"enabled\": true\n  }],\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true\n  },\n  \"levelRequirements\": [{\n    \"level\": \"1\",\n    \"name\": \"结丹\",\n    \"interval\": \"5\",\n    \"downloaded\": \"10GB\",\n    \"ratio\": \"2\"\n  },{\n    \"level\": \"2\",\n    \"name\": \"元婴\",\n    \"interval\": \"10\",\n    \"downloaded\": \"25GB\",\n    \"ratio\": \"2\"\n  },{\n    \"level\": \"3\",\n    \"name\": \"出窍\",\n    \"interval\": \"15\",\n    \"downloaded\": \"50GB\",\n    \"ratio\": \"2\"\n  },{\n    \"level\": \"4\",\n    \"name\": \"炼虚\",\n    \"interval\": \"20\",\n    \"downloaded\": \"100GB\",\n    \"ratio\": \"2\"\n  },{\n    \"level\": \"5\",\n    \"name\": \"合体\",\n    \"interval\": \"25\",\n    \"downloaded\": \"200GB\",\n    \"ratio\": \"4\"\n  },{\n    \"level\": \"6\",\n    \"name\": \"大乘\",\n    \"interval\": \"30\",\n    \"downloaded\": \"300GB\",\n    \"ratio\": \"6\"\n  },{\n    \"level\": \"7\",\n    \"name\": \"真仙\",\n    \"interval\": \"35\",\n    \"downloaded\": \"400GB\",\n    \"ratio\": \"8\"\n  }]\n}\n"
  },
  {
    "path": "resource/sites/zmpt.cc/config.json",
    "content": "{\n  \"name\": \"ZmPT(织梦)\",\n  \"timezoneOffset\": \"+0800\",\n  \"description\": \"为爱启航，造就梦想！\",\n  \"url\": \"https://zmpt.cc/\",\n  \"icon\": \"https://zmpt.cc/favicon.ico\",\n  \"tags\": [],\n  \"schema\": \"NexusPHP\",\n  \"host\": \"zmpt.cc\",\n  \"levelRequirements\": [\n    {\n      \"level\": 1,\n      \"name\": \"Power User\",\n      \"interval\": \"4\",\n      \"downloaded\": \"50GB\",\n      \"ratio\": \"1.05\",\n      \"seedingPoints\": \"30000\",\n      \"privilege\": \"得到一个邀请名额；可以直接发布种子；可以查看NFO文档；可以查看用户列表；可以请求续种； 可以发送邀请； 可以查看排行榜；可以查看其它用户的种子历史(如果用户隐私等级未设置为\\\"强\\\")； 可以删除自己上传的字幕。\"\n    },\n    {\n      \"level\": 2,\n      \"name\": \"Elite User\",\n      \"interval\": \"7\",\n      \"downloaded\": \"120GB\",\n      \"ratio\": \"1.55\",\n      \"seedingPoints\": \"50000\",\n      \"privilege\": \"Elite User及以上用户封存账号后不会被删除。\"\n    },\n    {\n      \"level\": 3,\n      \"name\": \"Crazy User\",\n      \"interval\": \"10\",\n      \"downloaded\": \"300GB\",\n      \"ratio\": \"2.05\",\n      \"seedingPoints\": \"100000\",\n      \"privilege\": \"得到两个邀请名额；可以在做种/下载/发布的时候选择匿名模式。\"\n    },\n    {\n      \"level\": 4,\n      \"name\": \"Insane User\",\n      \"interval\": \"16\",\n      \"downloaded\": \"500GB\",\n      \"ratio\": \"2.55\",\n      \"seedingPoints\": \"200000\",\n      \"privilege\": \"可以查看普通日志。\"\n    },\n    {\n      \"level\": 5,\n      \"name\": \"Veteran User\",\n      \"interval\": \"20\",\n      \"downloaded\": \"750GB\",\n      \"ratio\": \"3.05\",\n      \"seedingPoints\": \"300000\",\n      \"privilege\": \"得到三个邀请名额；可以查看其它用户的评论、帖子历史。Veteran User及以上用户会永远保留账号。\"\n    },\n    {\n      \"level\": 6,\n      \"name\": \"Extreme User\",\n      \"interval\": \"30\",\n      \"downloaded\": \"1TB\",\n      \"ratio\": \"3.55\",\n      \"seedingPoints\": \"400000\",\n      \"privilege\": \"可以更新过期的外部信息；可以查看Extreme User论坛。\"\n      \n    },\n    {\n      \"level\": 7,\n      \"name\": \"Ultimate User\",\n      \"interval\": \"30\",\n      \"downloaded\": \"1.5TB\",\n      \"ratio\": \"4.05\",\n      \"seedingPoints\": \"500000\",\n      \"privilege\": \"得到五个邀请名额。\"\n    },\n    {\n      \"level\": 8,\n      \"name\": \"Nexus Master\",\n      \"interval\": \"30\",\n      \"downloaded\": \"3TB\",\n      \"ratio\": \"4.55\",\n      \"seedingPoints\": \"600000\",\n      \"privilege\": \"得到十个邀请名额。\"\n    }\n  ],\n  \"collaborator\": \"yeluoqiuming\",\n  \"searchEntryConfig\": {\n    \"skipIMDbId\": true,\n    \"fieldSelector\": {\n      \"progress\": {\n        \"selector\": [\n          \"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"\n        ],\n        \"filters\": [\n          \"query ? parseFloat(query.attr('title').match(/[\\\\d.]+/)) : null\"\n        ]\n      },\n      \"status\": {\n        \"selector\": [\n          \"> td.rowfollow:eq(1) td.embedded:eq(1) > div:last\"\n        ],\n        \"filters\": [\n          \"query ? query.attr('title') : ''\",\n          \"query.indexOf('seeding') != -1 ? 2 : query.indexOf('leeching') != -1 ? 1 : query.indexOf('100%') != -1 ? 255 : 3\"\n        ]\n      }\n    }\n  },\n  \"selectors\": {\n    \"userSeedingTorrents\": {\n      \"page\": \"/getusertorrentlistajax.php?userid=$user.id$&type=seeding\",\n      \"fields\": {\n        \"seeding\": {\n          \"selector\": [\n            \"b:first\"\n          ],\n          \"filters\": [\n            \"query.text()\"\n          ]\n        },\n        \"seedingSize\": {\n          \"selector\": \"\",\n          \"filters\": [\n            \"query.text().match(/总大小：(.*?)上一页/g)\",\n            \"(query && query.length>0) ? query[0].replace('总大小：', '').replace('<< 上一页', '').trim() : 0\",\n            \"(query != 0) ? query.sizeToNumber() : 0\"\n          ]\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "src/background/README.md",
    "content": "# 该目录保存 Chrome 背景脚本"
  },
  {
    "path": "src/background/collection.ts",
    "content": "import { ICollection, EConfigKey, ICollectionGroup } from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\nimport { MovieInfoService } from \"@/service/movieInfoService\";\nimport { PPF } from \"@/service/public\";\n\n/**\n * 收藏\n */\nexport default class Collection {\n  public items: ICollection[] = [];\n  public groups: ICollectionGroup[] = [];\n  public storage: localStorage = new localStorage();\n  public movieInfoService = new MovieInfoService();\n\n  private configKey = EConfigKey.collection;\n\n  constructor() {\n    this.load();\n  }\n\n  /**\n   * 获取收藏历史记录\n   */\n  public load(groupId?: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(this.configKey, (result: any) => {\n        let data = {\n          groups: [] as ICollectionGroup[],\n          items: [] as ICollection[]\n        };\n\n        if (Array.isArray(result)) {\n          data.items = result;\n        } else if (result) {\n          data = Object.assign(data, result);\n        }\n\n        this.groups = data.groups || [];\n        this.items = data.items || [];\n\n        let _result: ICollection[] = [];\n\n        if (groupId) {\n          this.items.forEach((item: ICollection) => {\n            if (item.groups && item.groups.includes(groupId)) {\n              _result.push(item);\n            }\n          });\n        } else {\n          _result = this.items;\n        }\n\n        this.updateGroupCount();\n\n        resolve(_result);\n      });\n    });\n  }\n\n  public updateGroupCount() {\n    this.groups.forEach((group: ICollectionGroup) => {\n      group.count = 0;\n      this.items.forEach((item: ICollection) => {\n        if (item.groups && group.id && item.groups.includes(group.id)) {\n          if (!group.count) {\n            group.count = 0;\n          }\n          group.count++;\n        }\n      });\n    });\n  }\n\n  /**\n   * 添加新记录\n   * @param newItem\n   */\n  public add(newItem: ICollection): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let saveData = Object.assign(\n        {\n          time: new Date().getTime(),\n          site: null\n        },\n        newItem\n      );\n\n      let movieInfo = Object.assign({}, saveData.movieInfo);\n\n      // 清理站点配置信息\n      if (saveData.site) {\n        delete saveData.site;\n      }\n\n      saveData.link = PPF.getCleaningURL(saveData.link);\n\n      if (movieInfo.imdbId || movieInfo.doubanId) {\n        // 获取影片信息\n        this.getMoviceInfo(movieInfo.imdbId, movieInfo.doubanId)\n          .then(result => {\n            saveData.movieInfo = result;\n            this.push(saveData);\n            resolve(this.items);\n          })\n          .catch(error => {\n            console.log(error);\n            this.push(saveData);\n            resolve(this.items);\n          });\n      } else {\n        this.push(saveData);\n        resolve(this.items);\n      }\n    });\n  }\n\n  private getMoviceInfo(\n    imdbId: string = \"\",\n    doubanId: string = \"\"\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let movieId = imdbId;\n      let fn = this.movieInfoService.getInfoFromIMDb;\n      if (doubanId) {\n        movieId = doubanId;\n        fn = this.movieInfoService.getInfoFromDoubanId;\n      }\n\n      // 获取影片信息\n      fn.call(this.movieInfoService, movieId)\n        .then(result => {\n          // 保留数字ID\n          let movieInfo = {\n            imdbId,\n            doubanId: result.id.toString().replace(/(\\D)/g, \"\"),\n            image:\n              result.image || (result.images ? result.images.small : undefined),\n            title: result.title,\n            link: result.mobile_link || result.share_url,\n            alt_title: result.alt_title || result.original_title,\n            year: result.year\n          };\n          if (!result.year && result.attrs) {\n            movieInfo.year = result.attrs.year[0];\n          }\n\n          resolve(movieInfo);\n        })\n        .catch(error => {\n          reject();\n        });\n    });\n  }\n\n  private push(newItem: ICollection) {\n    let index = this.items.findIndex((item: ICollection) => {\n      return item.link === newItem.link;\n    });\n    if (index === -1) {\n      this.items.push(newItem);\n      this.updateGroupCount();\n      this.storage.set(this.configKey, {\n        groups: this.groups,\n        items: this.items\n      });\n    }\n  }\n\n  /**\n   * 更新指定的记录\n   * @param item\n   */\n  public update(item: ICollection): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        item.link = PPF.getCleaningURL(item.link);\n        let index = this.items.findIndex((data: ICollection) => {\n          return data.link === item.link;\n        });\n        if (index >= 0) {\n          this.items[index] = item;\n          let movieInfo = Object.assign({}, item.movieInfo);\n\n          // 清理站点配置信息\n          if (item.site) {\n            delete item.site;\n          }\n\n          if (movieInfo.imdbId || movieInfo.doubanId) {\n            // 获取影片信息\n            this.getMoviceInfo(movieInfo.imdbId, movieInfo.doubanId)\n              .then(result => {\n                item.movieInfo = result;\n                this.items[index] = item;\n                this.updateData();\n                resolve(this.items);\n              })\n              .catch(error => {\n                console.log(error);\n                this.updateData();\n                resolve(this.items);\n              });\n            return;\n          }\n        }\n\n        this.updateData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  private updateData() {\n    this.updateGroupCount();\n    this.storage.set(this.configKey, {\n      groups: this.groups,\n      items: this.items\n    });\n  }\n\n  /**\n   * 删除单个记录\n   * @param item\n   */\n  public delete(item: ICollection): Promise<any> {\n    return this.remove([item]);\n  }\n\n  /**\n   * 删除历史记录\n   * @param items 需要删除的列表\n   */\n  public remove(items: ICollection[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        for (let index = this.items.length - 1; index >= 0; index--) {\n          let item: ICollection = this.items[index];\n          let findIndex = items.findIndex((data: ICollection) => {\n            return data.link === item.link;\n          });\n          if (findIndex >= 0) {\n            this.items.splice(index, 1);\n          }\n        }\n        this.updateGroupCount();\n        this.storage.set(this.configKey, {\n          groups: this.groups,\n          items: this.items\n        });\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 清除历史记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = [];\n      this.groups = [];\n      this.storage.set(this.configKey, {});\n      resolve({});\n    });\n  }\n\n  /**\n   * 获取指定链接的收藏\n   * @param link\n   */\n  public get(link: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      link = PPF.getCleaningURL(link);\n      this.load().then(() => {\n        let item = this.items.find((data: ICollection) => {\n          return data.link === link;\n        });\n        if (item) {\n          resolve(item);\n        } else {\n          reject(false);\n        }\n      });\n    });\n  }\n\n  /**\n   * 重置\n   * @param datas\n   */\n  public reset(datas: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!datas) {\n        reject(false);\n        return;\n      }\n      if (Array.isArray(datas)) {\n        this.items = datas;\n      } else {\n        this.groups = datas.groups || this.groups;\n        this.items = datas.items || this.items;\n      }\n\n      this.storage.set(this.configKey, {\n        groups: this.groups,\n        items: this.items\n      });\n      resolve({\n        groups: this.groups,\n        items: this.items\n      });\n    });\n  }\n\n  /**\n   * 添加分组\n   * @param newItem\n   */\n  public addGroup(newItem: ICollectionGroup): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let saveData = Object.assign(\n        {\n          id: PPF.getNewId().substr(0, 8),\n          update: new Date().getTime()\n        },\n        newItem\n      );\n\n      this.groups.push(saveData);\n      this.storage.set(this.configKey, {\n        groups: this.groups,\n        items: this.items\n      });\n      resolve(this.groups);\n    });\n  }\n\n  /**\n   * 删除分组信息\n   * @param items 需要删除的列表\n   */\n  public removeGroup(\n    datas: ICollectionGroup | ICollectionGroup[]\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let items: ICollectionGroup[] = [];\n      if (Array.isArray(datas)) {\n        items = datas;\n      } else {\n        items.push(datas);\n      }\n      this.load().then(() => {\n        for (let index = this.groups.length - 1; index >= 0; index--) {\n          let group: ICollectionGroup = this.groups[index];\n          let findIndex = items.findIndex((data: ICollectionGroup) => {\n            return data.id === group.id;\n          });\n          if (findIndex >= 0) {\n            // 清除收藏中已引用该分组的项\n            this.items.forEach((item: ICollection) => {\n              if (!item.groups) {\n                return;\n              }\n              let index = item.groups.findIndex((id: string) => {\n                return id === group.id;\n              });\n\n              if (index !== -1) {\n                item.groups.splice(index, 1);\n              }\n            });\n\n            this.groups.splice(index, 1);\n          }\n        }\n\n        this.updateGroupCount();\n        this.storage.set(this.configKey, {\n          groups: this.groups,\n          items: this.items\n        });\n        resolve(this.groups);\n      });\n    });\n  }\n\n  /**\n   * 更新指定的分组信息\n   * @param item\n   */\n  public updateGroup(item: ICollectionGroup): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        let index = this.groups.findIndex((data: ICollectionGroup) => {\n          return data.id === item.id;\n        });\n        if (index >= 0) {\n          this.groups[index] = item;\n        }\n        this.storage.set(this.configKey, {\n          groups: this.groups,\n          items: this.items\n        });\n        resolve(this.groups);\n      });\n    });\n  }\n\n  /**\n   * 获取分组列表\n   */\n  public getGroups(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        resolve(this.groups);\n      });\n    });\n  }\n\n  /**\n   * 将收藏添加到指定的分组\n   * @param item\n   * @param groupId\n   */\n  public addToGroup(item: ICollection, groupId: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!item.groups) {\n        item.groups = [];\n      }\n\n      let index = item.groups.findIndex((id: string) => {\n        return id === groupId;\n      });\n\n      if (index === -1) {\n        item.groups.push(groupId);\n        this.update(item).then(() => {\n          resolve(true);\n        });\n      } else {\n        reject(false);\n      }\n    });\n  }\n\n  /**\n   * 将指定的收藏从分组中删除\n   * @param item\n   * @param groupId\n   */\n  public removeFromGroup(item: ICollection, groupId: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (item.groups) {\n        let index = item.groups.findIndex((id: string) => {\n          return id === groupId;\n        });\n\n        if (index !== -1) {\n          item.groups.splice(index, 1);\n          this.update(item).then(() => {\n            resolve(true);\n          });\n        } else {\n          reject(false);\n        }\n      }\n    });\n  }\n\n  public getAllLinks() {\n    let links: string[] = [];\n\n    this.items.forEach((item: ICollection) => {\n      links.push(PPF.getCleaningURL(item.link));\n    });\n\n    return links;\n  }\n}\n"
  },
  {
    "path": "src/background/config.ts",
    "content": "import {\n  Options,\n  ESizeUnit,\n  EConfigKey,\n  Site,\n  DownloadClient,\n  UIOptions,\n  SearchEntry,\n  EBeforeSearchingItemSearchMode,\n  SearchSolution,\n  SearchSolutionRange,\n  IBackupServer,\n  EBackupServerType,\n  EUserDataRange,\n  EPluginPosition,\n  IBackupRawData,\n  ISiteIcon\n} from \"@/interface/common\";\nimport { API, APP } from \"@/service/api\";\nimport localStorage from \"@/service/localStorage\";\nimport { SyncStorage } from \"./syncStorage\";\nimport { PPF } from \"@/service/public\";\nimport dayjs from \"dayjs\";\nimport { OWSS } from \"./plugins/OWSS\";\nimport { WebDAV } from \"./plugins/WebDAV\";\nimport PTPlugin from \"./service\";\nimport { BackupFileParser } from \"@/service/backupFileParser\";\nimport { Favicon } from \"@/service/favicon\";\nimport FileSaver from \"file-saver\";\n\ntype Service = PTPlugin;\n\n/**\n * 配置信息类\n */\nclass Config {\n  private name: string = EConfigKey.default;\n  private localStorage: localStorage = new localStorage();\n  public syncStorage: SyncStorage = new SyncStorage();\n  public favicon: Favicon = new Favicon(this.service);\n\n  public schemas: any[] = [];\n  public sites: any[] = [];\n  public clients: any[] = [];\n  public publicSites: any[] = [];\n  public requestCount: number = 0;\n  public backupFileParser: BackupFileParser = new BackupFileParser();\n\n  constructor(public service: Service) {\n    this.reload();\n  }\n\n  public reload() {\n    APP.cache.clear();\n    // this.getSchemas();\n    // this.getSites();\n    // this.getClients();\n    this.getSystemConfig();\n  }\n\n  /**\n   * 系统参数\n   */\n  public options: Options = {\n    exceedSizeUnit: ESizeUnit.GiB,\n    sites: [],\n    clients: [],\n    backupServers: [],\n    system: {},\n    allowDropToSend: true,\n    allowSelectionTextSearch: true,\n    needConfirmWhenExceedSize: true,\n    exceedSize: 10,\n    search: {\n      rows: 50,\n      // 搜索超时\n      timeout: 30000,\n      saveKey: true\n    },\n    // 连接下载服务器超时时间（毫秒）\n    connectClientTimeout: 30000,\n    rowsPerPageItems: [\n      10,\n      20,\n      50,\n      100,\n      200,\n      { text: \"$vuetify.dataIterator.rowsPerPageAll\", value: -1 }\n    ],\n    searchSolutions: [],\n    navBarIsOpen: true,\n    showMoiveInfoCardOnSearch: true,\n    beforeSearchingOptions: {\n      getMovieInformation: true,\n      maxMovieInformationCount: 5,\n      searchModeForItem: EBeforeSearchingItemSearchMode.id\n    },\n    showToolbarOnContentPage: true,\n    // 下载失败后是否进行重试\n    downloadFailedRetry: false,\n    // 下载失败重试次数\n    downloadFailedFailedRetryCount: 3,\n    // 下载失败间隔时间（秒）\n    downloadFailedFailedRetryInterval: 5,\n    // 批量下载时间间隔（秒）\n    batchDownloadInterval: 0,\n    // 启用后台下载任务\n    enableBackgroundDownload: false,\n    // 助手工具栏显示位置\n    position: EPluginPosition.right,\n    // 是否加密存储备份数据\n    encryptBackupData: false,\n    allowSaveSnapshot: true\n  };\n\n  public uiOptions: UIOptions = {};\n\n  /**\n   * 保存配置\n   * @param options 配置信息\n   */\n  public save(options?: Options) {\n    if (options) {\n      this.options = options;\n    }\n    this.localStorage.set(this.name, this.cleaningOptions(this.options));\n  }\n\n  /**\n   * 获取站点图标并缓存\n   */\n  public getFavicons(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let urls: string[] = [];\n      this.sites.forEach((site: Site) => {\n        urls.push(site.activeURL || site.url || \"\");\n      });\n\n      if (this.options.sites) {\n        this.options.sites.forEach((site: Site) => {\n          urls.push(site.activeURL || site.url || \"\");\n        });\n      }\n\n      this.favicon\n        .gets(urls)\n        .then((results: any[]) => {\n          results.forEach((result: any) => {\n            let site = this.options.sites.find((item: Site) => {\n              let cdn = [item.url].concat(item.cdn, item.formerHosts?.map(x => `//${x}`));\n              return (\n                item.host == result.host ||\n                cdn.join(\"\").indexOf(`//${result.host}`) > -1\n              );\n            });\n\n            if (site) {\n              site.icon = result.data;\n            }\n          });\n\n          this.save();\n          this.service.options = this.options;\n          resolve(this.options);\n        })\n        .catch(error => {\n          this.service.debug(error);\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取单个站点图标\n   * @param url\n   */\n  public getFavicon(url: string, reset: boolean = false): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.favicon\n        .get(url, reset)\n        .then((result: ISiteIcon) => {\n          let site = this.options.sites.find((item: Site) => {\n            let cdn = [item.url].concat(item.cdn, item.formerHosts?.map(x => `//${x}`));\n            return (\n              item.host == result.host ||\n              cdn.join(\"\").indexOf(`//${result.host}`) > -1\n            );\n          });\n\n          if (site) {\n            site.icon = result.data;\n            this.save();\n            this.service.options = this.options;\n          }\n\n          resolve(result);\n        })\n        .catch(error => {\n          this.service.debug(error);\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 清理参数配置，一些参数只需要运行时可用即可\n   * @param options\n   */\n  public cleaningOptions(options: Options): Options {\n    // 因 Object.assign 无法进行深拷贝，会造成对原有参数的破坏\n    // 故还是采用 JSON 的方式\n    let _options = JSON.parse(JSON.stringify(options));\n    if (_options.sites) {\n      _options.sites.forEach((item: Site) => {\n        let systemSite: Site | undefined = this.sites.find((site: Site) => {\n          return site.host == item.host;\n        });\n\n        if (systemSite) {\n          // 移除运行时参数\n          [\n            \"categories\",\n            \"selectors\",\n            \"patterns\",\n            \"torrentTagSelectors\",\n            \"icon\",\n            \"activeURL\",\n            \"searchEntryConfig\",\n            \"schema\",\n            \"supportedFeatures\",\n            \"mergeSchemaTagSelectors\"\n          ].forEach((key: string) => {\n            let _item = item as any;\n            if (_item[key]) {\n              delete _item[key];\n            }\n          });\n\n          if (item.searchEntry) {\n            item.searchEntry.forEach((entry: SearchEntry, index: number) => {\n              if (!entry.isCustom) {\n                // 仅保存名称和是否启用\n                (item.searchEntry as any)[index] = {\n                  name: entry.name,\n                  enabled: entry.enabled\n                };\n              }\n            });\n          }\n        }\n\n        if (\n          PPF.isExtensionMode &&\n          item.icon &&\n          item.icon.substr(0, 10) === \"data:image\"\n        ) {\n          delete item.icon;\n        }\n      });\n    }\n\n    // 移除客户端配置中运行时内容\n    if (_options.clients) {\n      _options.clients.forEach((item: any) => {\n        [\n          \"pathDescription\",\n          \"description\",\n          \"warning\",\n          \"scripts\",\n          \"ver\",\n          \"icon\"\n        ].forEach((key: string) => {\n          if (item[key]) {\n            delete item[key];\n          }\n        });\n      });\n    }\n\n    return _options;\n  }\n\n  /**\n   * 读取配置信息\n   * @return Promise 配置信息\n   */\n  public read(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      // 加载用户界面设置\n      this.localStorage.get(EConfigKey.uiOptions, (result: any) => {\n        if (result) {\n          let defaultOptions = Object.assign({}, this.uiOptions);\n          this.uiOptions = Object.assign(defaultOptions, result);\n        }\n      });\n\n      this.loadConfig(resolve);\n    });\n  }\n\n  /**\n   * 加载配置\n   * @param success\n   */\n  private loadConfig(success: any) {\n    // 如果还有网络请求，则继续等待\n    if (this.requestCount > 0) {\n      setTimeout(() => {\n        this.loadConfig(success);\n      }, 100);\n      return;\n    }\n    this.localStorage.get(this.name, (result: any) => {\n      this.resetRunTimeOptions(result);\n      success && success(this.options);\n    });\n  }\n\n  /**\n   * 重置运行时配置\n   * @param options\n   */\n  public resetRunTimeOptions(options?: Options) {\n    if (options) {\n      if (options.system) {\n        delete options.system;\n      }\n      let defaultOptions = Object.assign({}, this.options);\n      this.options = Object.assign(defaultOptions, options);\n    }\n\n    // 如果未指定语言，则以当前浏览器默认语言为准\n    if (!this.options.locale) {\n      this.options.locale = navigator.language || \"zh-CN\";\n    }\n\n    // 覆盖站点架构\n    this.options.system = {\n      schemas: this.schemas,\n      sites: this.sites,\n      clients: this.clients,\n      publicSites: this.publicSites\n    };\n\n    this.upgradeSites();\n\n    // 升级不存在的配置项\n    this.options.sites &&\n      this.options.sites.length &&\n      this.sites.forEach((systemSite: Site) => {\n        let index = this.options.sites.findIndex((site: Site) => {\n          return site.host === systemSite.host;\n        });\n\n        if (index > -1) {\n          let _site: Site = Object.assign(\n            Object.assign({}, systemSite),\n            this.options.sites[index]\n          );\n\n          if (systemSite.categories) {\n            _site.categories = systemSite.categories;\n          }\n\n          // 网站架构以系统定义为准\n          if (systemSite.schema) {\n            _site.schema = systemSite.schema;\n          }\n\n          // 清理已移除的标签选择器\n          if (!systemSite.torrentTagSelectors && _site.torrentTagSelectors) {\n            delete _site.torrentTagSelectors;\n          } else {\n            _site.torrentTagSelectors = systemSite.torrentTagSelectors;\n          }\n\n          if (!systemSite.patterns && _site.patterns) {\n            delete _site.patterns;\n          } else {\n            _site.patterns = systemSite.patterns;\n          }\n\n          // 更新升级要求\n          if (!systemSite.levelRequirements && _site.levelRequirements) {\n            delete _site.levelRequirements;\n          } else {\n            _site.levelRequirements = systemSite.levelRequirements;\n          }\n\n          // 合并系统定义的搜索入口\n          if (_site.searchEntry && systemSite.searchEntry) {\n            systemSite.searchEntry.forEach((sysEntry: SearchEntry) => {\n              if (_site.searchEntry) {\n                let _index: number | undefined =\n                  _site.searchEntry &&\n                  _site.searchEntry.findIndex((entry: SearchEntry) => {\n                    return entry.name == sysEntry.name && !entry.isCustom;\n                  });\n\n                if (_index != undefined && _index > -1) {\n                  _site.searchEntry[_index] = Object.assign(\n                    Object.assign({}, sysEntry),\n                    {\n                      enabled: _site.searchEntry[_index].enabled\n                    }\n                  );\n                } else {\n                  _site.searchEntry.push(Object.assign({}, sysEntry));\n                }\n              }\n            });\n          } else if (systemSite.searchEntry) {\n            _site.searchEntry = systemSite.searchEntry;\n          }\n\n          // 设置默认图标\n          if (!systemSite.icon && !_site.icon) {\n            _site.icon = _site.url + \"/favicon.ico\"\n          }\n\n          this.options.sites[index] = _site;\n        }\n      });\n\n    // 设置当前需要使用的URL地址\n    this.options.sites.forEach((site: Site) => {\n      if (site.cdn && site.cdn.length > 0) {\n        site.activeURL = site.cdn[0];\n        // 去除重复的地址，由之前的Bug引起\n        site.cdn = this.arrayUnique(site.cdn);\n      } else {\n        site.activeURL = site.url;\n      }\n\n      if (site.priority == null) {\n        site.priority = 100;\n      }\n    });\n\n    // 升级不存在的配置项\n    this.options.clients &&\n      this.options.clients.length &&\n      this.options.clients.forEach((item, index) => {\n        let client = this.clients.find((c: DownloadClient) => {\n          return c.type === item.type;\n        });\n\n        if (client) {\n          this.options.clients[index] = Object.assign(\n            Object.assign({}, client),\n            this.options.clients[index]\n          );\n        }\n      });\n\n    if (PPF.isExtensionMode) {\n      this.getFavicons();\n    }\n\n    console.log(this.options);\n  }\n\n  /**\n   * 数组去重\n   * @param source 源数组\n   * @see https://www.cnblogs.com/wisewrong/p/9642264.html （性能比较）\n   */\n  private arrayUnique(source: any[]) {\n    let result: any[] = [];\n    let obj: any = {};\n\n    source.forEach((value: any) => {\n      if (!obj[value]) {\n        result.push(value);\n        obj[value] = 1;\n      }\n    });\n\n    return result;\n  }\n\n  /**\n   * 升级网站信息\n   */\n  public upgradeSites() {\n    this.sites.forEach((systemSite: Site) => {\n      if (!systemSite.host) {\n        return;\n      }\n      let formerHosts = systemSite.formerHosts;\n      let newHost = systemSite.host;\n      if (formerHosts && formerHosts.length > 0) {\n        formerHosts.forEach((host: string) => {\n          let site: Site = this.options.sites.find((site: Site) => {\n            return site.host === host;\n          });\n\n          // 更新站点基本信息\n          if (site) {\n            console.log(\"upgradeSites.site\", site, newHost);\n            site.host = newHost;\n            site.url = systemSite.url;\n            \n            // 设置默认图标\n            if (!systemSite.icon && !site.icon)\n              site.icon = site.url + \"/favicon.ico\"\n            else\n              site.icon = systemSite.icon;\n          }\n          \n          // 更新搜索方案\n          if (this.options.searchSolutions) {\n            this.options.searchSolutions.forEach(\n              (soluteion: SearchSolution) => {\n                soluteion.range.forEach((range: SearchSolutionRange) => {\n                  if (range.host == host) {\n                    console.log(\n                      \"upgradeSites.searchSolutions\",\n                      range.host,\n                      newHost\n                    );\n                    range.host = newHost;\n                  }\n                });\n              }\n            );\n          }\n\n          // 更新下载服务器路径信息\n          if (this.options.clients && this.options.clients.length > 0) {\n            this.options.clients.forEach((client: DownloadClient) => {\n              let paths = client.paths;\n              if (paths) {\n                for (const key in paths) {\n                  if (key == host && paths.hasOwnProperty(key)) {\n                    console.log(\n                      \"upgradeSites.client.paths\",\n                      client.name,\n                      key,\n                      newHost\n                    );\n                    const element = paths[key];\n                    paths[newHost] = Object.assign([], element);\n                    delete paths[key];\n                  }\n                }\n              }\n            });\n          }\n        });\n      }\n    });\n  }\n\n  public readUIOptions(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      // 加载用户界面设置\n      this.localStorage.get(EConfigKey.uiOptions, (result: any) => {\n        if (result) {\n          let defaultOptions = Object.assign({}, this.uiOptions);\n          this.uiOptions = Object.assign(defaultOptions, result);\n        }\n\n        resolve(this.uiOptions);\n      });\n    });\n  }\n\n  public saveUIOptions(options: UIOptions): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.localStorage.set(EConfigKey.uiOptions, options || this.uiOptions);\n      resolve();\n    });\n  }\n\n  /**\n   * 获取系统配置\n   */\n  public getSystemConfig() {\n    this.schemas = [];\n    this.sites = [];\n    this.clients = [];\n    this.publicSites = [];\n    this.getContentFromApi(`${API.systemConfig}`).then((result: any) => {\n      if (result) {\n        this.schemas = result.schemas;\n        this.sites = result.sites;\n        this.clients = result.clients;\n        this.publicSites = result.publicSites;\n      }\n    });\n  }\n\n  /**\n   * 获取支持的网站架构\n   */\n  public getSchemas(): any {\n    this.schemas = [];\n    this.getContentFromApi(`${API.schemas}`).then((result: any) => {\n      if (result.length) {\n        result.forEach((item: any) => {\n          if (item.type === \"dir\") {\n            this.addSchema(\n              API.schemaConfig.replace(/\\{\\$schema\\}/g, item.name)\n            );\n          }\n        });\n      }\n    });\n  }\n\n  public addSchema(path: string): void {\n    this.getContentFromApi(path).then((result: any) => {\n      if (result && result.name) {\n        this.schemas.push(result);\n      }\n    });\n  }\n\n  public getSites() {\n    this.sites = [];\n    this.getContentFromApi(API.sites).then((result: any) => {\n      if (result.length) {\n        result.forEach((item: any) => {\n          if (item.type === \"dir\") {\n            // this.schemas.push(item.name);\n            this.addSite(API.siteConfig.replace(/\\{\\$site\\}/g, item.name));\n          }\n        });\n      }\n    });\n  }\n\n  public addSite(path: string): void {\n    this.getContentFromApi(path).then((result: any) => {\n      if (result && result.name) {\n        this.sites.push(result);\n      }\n    });\n  }\n\n  public getClients() {\n    this.clients = [];\n    this.getContentFromApi(API.clients).then((result: any) => {\n      if (result.length) {\n        result.forEach((item: any) => {\n          if (item.type === \"dir\") {\n            this.addClient(\n              API.clientConfig.replace(/\\{\\$client\\}/g, item.name)\n            );\n          }\n        });\n      }\n    });\n  }\n\n  public addClient(path: string): void {\n    this.getContentFromApi(path).then((result: any) => {\n      if (result && result.name) {\n        this.clients.push(result);\n      }\n    });\n  }\n\n  /**\n   * 从远程请求指定的内容\n   * @param api\n   */\n  public getContentFromApi(api: string): Promise<any> {\n    PPF.updateBadge(++this.requestCount);\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let content = APP.cache.get(api);\n      if (content) {\n        resolve(content);\n        PPF.updateBadge(--this.requestCount);\n        return;\n      }\n      $.getJSON(api)\n        .done(result => {\n          APP.cache.set(api, result);\n          PPF.updateBadge(--this.requestCount);\n          resolve(result);\n        })\n        .fail(result => {\n          PPF.updateBadge(--this.requestCount);\n          reject && reject(result);\n        });\n    });\n  }\n\n  /**\n   * 将系统参数备份到Google\n   */\n  public backupToGoogle(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome.storage && chrome.storage.sync) {\n        let options = this.cleaningOptions(this.options);\n        if (options.system) {\n          delete options.system;\n        }\n\n        // 因Google 8K限制，固将内容拆分后保存\n        // https://developer.chrome.com/extensions/storage#type-StorageArea\n        let clients = Object.assign([], options.clients);\n        let sites = Object.assign([], options.sites);\n\n        delete options.clients;\n        delete options.sites;\n\n        // 主要配置\n        this.syncStorage\n          .set(this.name, options)\n          .then(() => {\n            // 客户端配置\n            this.syncStorage\n              .set(this.name + \".clients\", clients)\n              .then(() => {\n                // 站点配置\n                this.syncStorage\n                  .set(this.name + \".sites\", sites)\n                  .then(() => {\n                    resolve(this.options);\n                  })\n                  .catch((error: any) => {\n                    reject(APP.createErrorMessage(error));\n                  });\n              })\n              .catch((error: any) => {\n                reject(APP.createErrorMessage(error));\n              });\n          })\n          .catch((error: any) => {\n            reject(APP.createErrorMessage(error));\n          });\n      } else {\n        reject(APP.createErrorMessage(\"chrome.storage 不存在\"));\n      }\n    });\n  }\n\n  /**\n   * 从Google云端恢复系统参数\n   */\n  public restoreFromGoogle(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome.storage && chrome.storage.sync) {\n        this.syncStorage\n          .get(this.name)\n          .then((result: any) => {\n            let system = Object.assign({}, this.options.system);\n            let options = result;\n\n            options.system = system;\n\n            // 获取客户端配置\n            this.syncStorage\n              .get(this.name + \".clients\")\n              .then((result: any) => {\n                options.clients = result;\n                // 获取站点配置\n                this.syncStorage\n                  .get(this.name + \".sites\")\n                  .then((result: any) => {\n                    options.sites = result;\n                    this.resetRunTimeOptions(options);\n                    this.save();\n                    setTimeout(() => {\n                      resolve(this.options);\n                    }, 300);\n                  })\n                  .catch((error: any) => {\n                    reject(APP.createErrorMessage(error));\n                  });\n              })\n              .catch((error: any) => {\n                reject(APP.createErrorMessage(error));\n              });\n          })\n          .catch((error: any) => {\n            reject(APP.createErrorMessage(error));\n          });\n      } else {\n        reject(APP.createErrorMessage(\"chrome.storage 不存在\"));\n      }\n    });\n  }\n\n  /**\n   * 获取备份原始数据，用于插件背景页和前端传输\n   */\n  public getBackupRawData(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      try {\n        const rawUserData = this.service.userData.get(\"\", EUserDataRange.all);\n        const rawOptions = this.cleaningOptions(this.service.options);\n\n        delete rawOptions.system;\n\n        let rawData: IBackupRawData = {\n          options: rawOptions,\n          userData: rawUserData,\n          collection: {\n            items: this.service.collection.items,\n            groups: this.service.collection.groups\n          },\n          cookies: undefined,\n          searchResultSnapshot: this.service.searchResultSnapshot.items,\n          keepUploadTask: this.service.keepUploadTask.items,\n          downloadHistory: undefined\n        };\n\n        const requests: Promise<any>[] = [];\n\n        // 备份下载历史\n        requests.push(this.service.controller.downloadHistory.load());\n\n        // 是否备份站点 Cookies\n        if (\n          this.service.options.allowBackupCookies &&\n          PPF.checkOptionalPermission(\"cookies\")\n        ) {\n          requests.push(this.getAllSiteCookies());\n        }\n\n        Promise.all(requests)\n          .then(results => {\n            rawData.downloadHistory = results[0];\n            if (results.length > 1) {\n              rawData.cookies = results[1];\n            }\n\n            resolve(rawData);\n          })\n          .catch(() => {\n            resolve(rawData);\n          });\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 创建备份文件\n   * @param fileName\n   */\n  public createBackupFile(fileName?: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.getBackupFileBlob()\n        .then(blob => {\n          FileSaver.saveAs(blob, fileName || this.getNewBackupFileName());\n          resolve(true);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取备份数据\n   */\n  public getBackupFileBlob(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      try {\n        this.getBackupRawData()\n          .then((rawData: any) => {\n            this.backupFileParser\n              .createBackupFileBlob(rawData)\n              .then((blob: any) => {\n                resolve(blob);\n              });\n          })\n          .catch(error => {\n            reject(error);\n          });\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 获取所有站点Cookies\n   */\n  public getAllSiteCookies(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      PPF.checkPermissions([\"cookies\"])\n        .then(() => {\n          const sites = this.options.sites;\n\n          if (sites && sites.length > 0) {\n            const requests: any[] = [];\n            sites.forEach((site: Site) => {\n              requests.push(this.getCookiesFromSite(site));\n            });\n\n            Promise.all(requests)\n              .then(results => {\n                resolve(results);\n              })\n              .catch(error => {\n                reject(error);\n              });\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取指定站点Cookies\n   * @param site\n   */\n  public getCookiesFromSite(site: Site): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      const url = site.activeURL || site.url;\n      chrome.cookies.getAll(\n        {\n          url\n        },\n        result => {\n          if (chrome.runtime.lastError) {\n            reject(chrome.runtime.lastError.message);\n            return;\n          }\n          resolve({\n            host: site.host,\n            url,\n            cookies: result\n          });\n          console.log(result);\n        }\n      );\n    });\n  }\n\n  /**\n   * 恢复Cookies\n   * @param datas\n   */\n  public restoreCookies(datas: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let requests: any[] = [];\n\n      // 需要保留的内容\n      const keepFields = [\n        \"name\",\n        \"value\",\n        \"domain\",\n        \"path\",\n        \"secure\",\n        \"httpOnly\",\n        \"expirationDate\"\n      ];\n      datas.forEach((item: any) => {\n        item.cookies.forEach((cookie: any) => {\n          let options = PPF.clone(cookie);\n\n          // 删除不需要的键\n          for (const key in options) {\n            if (options.hasOwnProperty(key) && !keepFields.includes(key)) {\n              delete options[key];\n            }\n          }\n\n          options.url = item.url;\n\n          requests.push(this.setCookies(options, item.host));\n        });\n      });\n\n      // 不管是否成功，都返回\n      Promise.all(requests)\n        .then(() => {\n          resolve();\n        })\n        .catch(() => {\n          resolve();\n        });\n    });\n  }\n\n  /**\n   * 设置站点 Cookies\n   * @param cookie\n   * @param host\n   */\n  public setCookies(\n    cookie: chrome.cookies.SetDetails,\n    host: string\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let site: Site = PPF.getSiteFromHost(host, this.service.options);\n\n      // 尝试获取当前站点已存在的Cookie\n      chrome.cookies.get(\n        {\n          url: (site.activeURL || site.url) + \"\",\n          name: cookie.name + \"\"\n        },\n        _cookie => {\n          // 默认不对已存在相同的name的内容进行更新\n          let allowSet = false;\n          const now = new Date().getTime() / 1000;\n\n          // 如果当前站点没有这个Cookies，则允许设置\n          if (_cookie === null) {\n            allowSet = true;\n          } else if (\n            // 如果站点存在这个Cookies，但已过期，允许设置\n            _cookie.expirationDate &&\n            _cookie.expirationDate < now\n          ) {\n            allowSet = true;\n          }\n\n          if (allowSet) {\n            // 如果要导入的内容已过期，尝试按当天日期增加一天\n            if (cookie.expirationDate && cookie.expirationDate < now) {\n              cookie.expirationDate = now + 60 * 60 * 24;\n            }\n\n            chrome.cookies.set(cookie, result => {\n              if (chrome.runtime.lastError) {\n                reject(chrome.runtime.lastError.message);\n                return;\n              }\n              resolve(result);\n              console.log(result);\n            });\n          } else {\n            console.log(\"跳过 %s: %s\", host, cookie.name);\n            resolve();\n          }\n        }\n      );\n    });\n  }\n\n  private getNewBackupFileName(): string {\n    return PPF.getNewBackupFileName();\n  }\n\n  /**\n   * 备份配置到服务器\n   * @param server\n   */\n  public backupToServer(server: IBackupServer): Promise<any> {\n    console.log(\"backupToServer\", server);\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      const time = dayjs().valueOf();\n      const fileName = this.getNewBackupFileName();\n      let service: OWSS | WebDAV | null = null;\n      this.getBackupFileBlob()\n        .then(blob => {\n          const formData = new FormData();\n          formData.append(\"name\", fileName);\n          formData.append(\"data\", blob);\n\n          switch (server.type) {\n            case EBackupServerType.OWSS:\n              service = new OWSS(server);\n              break;\n\n            case EBackupServerType.WebDAV:\n              service = new WebDAV(server);\n              break;\n\n            default:\n              reject(\"暂不支持\");\n              break;\n          }\n\n          if (service) {\n            service\n              .add(formData)\n              .then(result => {\n                resolve({\n                  time,\n                  fileName\n                });\n              })\n              .catch(error => {\n                reject(error);\n              });\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 从备份服务器中恢复指定的文件\n   * @param server\n   * @param path\n   */\n  public restoreFromServer(server: IBackupServer, path: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let service: OWSS | WebDAV | null = null;\n\n      switch (server.type) {\n        case EBackupServerType.OWSS:\n          service = new OWSS(server);\n          break;\n\n        case EBackupServerType.WebDAV:\n          service = new WebDAV(server);\n          break;\n\n        default:\n          reject(\"暂不支持\");\n          break;\n      }\n\n      if (service) {\n        service\n          .get(path)\n          .then(data => {\n            this.backupFileParser\n              .loadZipData(\n                data,\n                this.service.i18n.t(\"settings.backup.enterSecretKey\"),\n                this.service.options.encryptSecretKey\n              )\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((error: any) => {\n                reject(error);\n              });\n          })\n          .catch(error => {\n            reject(error);\n          });\n      }\n    });\n  }\n\n  /**\n   * 获取备份文件列表\n   * @param server\n   * @param options\n   */\n  public getBackupListFromServer(\n    server: IBackupServer,\n    options: any = {}\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let service: OWSS | WebDAV | null = null;\n      switch (server.type) {\n        case EBackupServerType.OWSS:\n          service = new OWSS(server);\n          break;\n\n        case EBackupServerType.WebDAV:\n          service = new WebDAV(server);\n          break;\n\n        default:\n          reject(\"暂不支持\");\n          break;\n      }\n\n      if (service) {\n        service\n          .list(options)\n          .then(result => {\n            resolve(result);\n          })\n          .catch(error => {\n            reject(error);\n          });\n      }\n    });\n  }\n\n  /**\n   * 删除指定的文件\n   * @param server\n   * @param path\n   */\n  public deleteFileFromBackupServer(\n    server: IBackupServer,\n    path: string\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let service: OWSS | WebDAV | null = null;\n      switch (server.type) {\n        case EBackupServerType.OWSS:\n          service = new OWSS(server);\n          break;\n\n        case EBackupServerType.WebDAV:\n          service = new WebDAV(server);\n          break;\n\n        default:\n          reject(\"暂不支持\");\n          break;\n      }\n\n      if (service) {\n        service\n          .delete(path)\n          .then(result => {\n            resolve(result);\n          })\n          .catch(error => {\n            reject(error);\n          });\n      }\n    });\n  }\n\n  /**\n   * 测试指定的服务器是否可连接\n   * @param server\n   */\n  public testBackupServerConnectivity(server: IBackupServer): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let service: OWSS | WebDAV | null = null;\n      switch (server.type) {\n        case EBackupServerType.OWSS:\n          service = new OWSS(server);\n          break;\n\n        case EBackupServerType.WebDAV:\n          service = new WebDAV(server);\n          break;\n\n        default:\n          reject(\"暂不支持\");\n          break;\n      }\n\n      if (service) {\n        service\n          .ping()\n          .then(result => {\n            resolve(result);\n          })\n          .catch(error => {\n            reject(error);\n          });\n      }\n    });\n  }\n}\nexport default Config;\n"
  },
  {
    "path": "src/background/contextMenus.ts",
    "content": "import {\n  Options,\n  Site,\n  DownloadClient,\n  DataResult,\n  SiteSchema,\n  EAction,\n  EDataResultType,\n  DownloadOptions,\n  EModule,\n  ECommonKey,\n  SearchSolution\n} from \"@/interface/common\";\nimport PTPlugin from \"./service\";\nimport URLParse from \"url-parse\";\nimport { APP } from \"@/service/api\";\nimport { PathHandler } from \"@/service/pathHandler\";\n\ntype Service = PTPlugin;\n\nexport class ContextMenus {\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n\n  private rootId = \"PT-Plugin-Plugin-Content-Menu\";\n  private currentTabId = 0;\n\n  private siteMenus: string[] = [];\n  private pathHandler: PathHandler = new PathHandler();\n\n  constructor(public service: Service) {\n    chrome && chrome.tabs && this.initBrowserEvent();\n  }\n\n  /**\n   * 初始化浏览器事件\n   */\n  private initBrowserEvent() {\n    // 浏览器标签栏切换事件\n    chrome.tabs.onActivated.addListener(\n      (actionInfo: chrome.tabs.TabActiveInfo) => {\n        chrome.tabs.get(actionInfo.tabId, (tab: chrome.tabs.Tab) => {\n          this.clearSiteMenus();\n          if (tab.url) {\n            let host = new URLParse(tab.url).host;\n\n            this.createSiteMenus(host);\n          }\n        });\n      }\n    );\n\n    // 标签页面更新事件\n    chrome.tabs.onUpdated.addListener(\n      (\n        tabId: number,\n        changeInfo: chrome.tabs.TabChangeInfo,\n        tab: chrome.tabs.Tab\n      ) => {\n        this.clearSiteMenus();\n        if (tab.url) {\n          let host = new URLParse(tab.url).host;\n\n          this.createSiteMenus(host);\n        }\n      }\n    );\n\n    // 图标单击事件\n    // 暂时取消弹出内容，直接转到配置页\n    chrome.browserAction.onClicked.addListener(() => {\n      this.service.controller.openOptions();\n    });\n  }\n  /**\n   * 初始化上下文菜单\n   * @param options Options 参数\n   * @param service 后台服务\n   */\n  public init(options: Options) {\n    this.options = options;\n\n    // 清除原来的菜单\n    this.clear();\n    // 创建关键字搜索菜单，所有页面可用\n    this.createSearchMenus();\n    // 创建下载客户端上下文菜单，所有页面可用\n    this.createClientMenus();\n    // 创建插件图标右键菜单\n    this.createPluginIconPopupMenus();\n  }\n\n  /**\n   * 创建插件图标右键菜单\n   */\n  public createPluginIconPopupMenus() {\n    // 查看下载历史\n    this.add({\n      title: this.service.i18n.t(\"service.contextMenus.history\"),\n      contexts: [\"browser_action\"],\n      onclick: () => {\n        chrome.tabs.create({\n          url: \"index.html#/history\"\n        });\n      }\n    });\n\n    // 查看助手日志\n    this.add({\n      title: this.service.i18n.t(\"service.contextMenus.systemLog\"),\n      contexts: [\"browser_action\"],\n      onclick: () => {\n        chrome.tabs.create({\n          url: \"index.html#/system-logs\"\n        });\n      }\n    });\n\n    this.add({\n      type: \"separator\",\n      contexts: [\"browser_action\"]\n    });\n\n    // 使用问题反馈\n    this.add({\n      title: this.service.i18n.t(\"service.contextMenus.issues\"),\n      contexts: [\"browser_action\"],\n      onclick: () => {\n        chrome.tabs.create({\n          url: \"https://github.com/pt-plugins/PT-Plugin-Plus/issues/new\"\n        });\n      }\n    });\n  }\n\n  /**\n   * 清除菜单\n   */\n  public clear() {\n    chrome && chrome.contextMenus && chrome.contextMenus.removeAll();\n  }\n\n  /**\n   * 清除站点的上下文菜单\n   */\n  private clearSiteMenus() {\n    this.siteMenus.forEach((item: string) => {\n      this.remove(item);\n    });\n\n    this.siteMenus = [];\n  }\n\n  /**\n   * 获取指定站点的URL匹配规则\n   * @param site\n   */\n  private getSiteDocumentUrlPatterns(site: Site): string[] {\n    let url = site.url + \"\";\n    if (url.substr(-1) != \"/\") {\n      url += \"/\";\n    }\n    let documentUrlPatterns: string[] = [`*://${site.host}/*`, `${url}`];\n\n    if (site.cdn && site.cdn.length > 0) {\n      for (let i = 0; i < site.cdn.length; i++) {\n        const url = site.cdn[i]\n        documentUrlPatterns.push(`${url}${url.substr(-1) != '/' ? '/*' : '*'}`, url);\n      }\n    }\n\n    return documentUrlPatterns;\n  }\n\n  /**\n   * 根据指定的目录信息创建菜单\n   * @param paths\n   * @param site\n   * @param client\n   * @param parentId\n   */\n  private createPathMenus(\n    paths: string[],\n    site: Site,\n    client: DownloadClient,\n    parentId: string\n  ) {\n    paths.forEach((path: string, index: number) => {\n      let id = `${client.id}**${site.host}**${path}**${index}`;\n      this.add({\n        id,\n        title: this.pathHandler.replacePathKey(path, site),\n        parentId: parentId,\n        contexts: [\"link\"],\n        documentUrlPatterns: this.getSiteDocumentUrlPatterns(site),\n        targetUrlPatterns: this.getSiteUrlPatterns(site),\n        onclick: (\n          info: chrome.contextMenus.OnClickData,\n          tab: chrome.tabs.Tab\n        ) => {\n          let data = info.menuItemId.split(\"**\");\n          let options: DownloadOptions = {\n            clientId: data[0],\n            url: info.linkUrl as string,\n            savePath: data[2]\n          };\n\n          this.sendTorrentToClient(tab.id, options);\n        }\n      });\n    });\n  }\n\n  /**\n   * 创建站点上下文菜单\n   */\n  public createSiteMenus(host: string) {\n    let site: Site = this.options.sites.find((item: Site) => {\n      let cdn = [item.url].concat(item.cdn);\n      return item.host === host || cdn.join(\"|\").indexOf(host) > -1;\n    });\n\n    if (!site) {\n      return;\n    }\n\n    // 是否启用选择内容时搜索\n    if (this.options.allowSelectionTextSearch) {\n      let menuId: string = site.host as string;\n      this.siteMenus.push(menuId);\n      // 选中内容进行搜索\n      this.add({\n        id: menuId,\n        title: this.service.i18n.t(\n          \"service.contextMenus.searchSelectionTextOnThisSite\"\n        ),\n        contexts: [\"selection\"],\n        documentUrlPatterns: this.getSiteDocumentUrlPatterns(site),\n        onclick: (\n          info: chrome.contextMenus.OnClickData,\n          tab: chrome.tabs.Tab\n        ) => {\n          this.service.controller.searchTorrent(info.selectionText, host);\n        }\n      });\n    }\n\n    this.options.clients.forEach((client: DownloadClient) => {\n      if (client.paths) {\n        let parentId = `${client.id}-path`;\n\n        let count = 0;\n\n        // 添加以客户端名称为标题的菜单\n        this.add({\n          id: parentId,\n          title: this.service.i18n.t(\n            \"service.contextMenus.downloadClientPath\",\n            {\n              clientName: client.name\n            }\n          ),\n          contexts: [\"link\"],\n          documentUrlPatterns: this.getSiteDocumentUrlPatterns(site),\n          targetUrlPatterns: this.getSiteUrlPatterns(site)\n        });\n\n        // 根据已定义的路径创建菜单\n        for (const host in client.paths) {\n          let paths = client.paths[host];\n\n          if (host !== site.host) {\n            continue;\n          }\n\n          count++;\n          this.createPathMenus(paths, site, client, parentId);\n        }\n\n        // 最后添加当前客户端适用于所有站点的目录\n        let publicPaths = client.paths[ECommonKey.allSite];\n        if (publicPaths) {\n          count++;\n          this.createPathMenus(publicPaths, site, client, parentId);\n        }\n\n        if (count > 0) {\n          this.siteMenus.push(parentId);\n        } else {\n          this.remove(parentId);\n        }\n      }\n    });\n  }\n\n  /**\n   * 获取指定站点的配置种子链接规则\n   * @param site\n   */\n  private getSiteUrlPatterns(site: Site): string[] {\n    let result: string[] = [];\n    if (site.patterns && site.patterns[\"torrentLinks\"]) {\n      result = site.patterns[\"torrentLinks\"];\n    } else {\n      let schema = this.getSiteSchema(site);\n      if (schema && schema.patterns && schema.patterns[\"torrentLinks\"]) {\n        result = schema.patterns[\"torrentLinks\"];\n      } else {\n        result.push(\"*://*/*\");\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 根据指定的站点获取站点的架构信息\n   * @param site 站点信息\n   */\n  private getSiteSchema(site: Site): SiteSchema {\n    let schema: SiteSchema = {};\n    if (typeof site.schema === \"string\") {\n      schema =\n        this.options.system &&\n        this.options.system.schemas &&\n        this.options.system.schemas.find((item: SiteSchema) => {\n          return item.name == site.schema;\n        });\n    }\n\n    return schema;\n  }\n\n  /**\n   * 发送种子到指定的服务器\n   * @param tabid\n   * @param options\n   */\n  private sendTorrentToClient(tabid: number = 0, options: DownloadOptions) {\n    console.log(\"sendTorrentToClient\", options);\n    let site = this.getSiteFromURL(options.url);\n    if (site && options.savePath) {\n      let savePath = this.pathHandler.getSavePath(options.savePath, site);\n      if (savePath === false) {\n        // \"用户已取消\"\n        APP.showNotifications({\n          message: this.service.i18n.t(\"service.contextMenus.userCanceled\")\n        });\n        return;\n      }\n\n      options.savePath = savePath;\n    }\n\n    let notice: any;\n    try {\n      chrome.tabs.sendMessage(\n        tabid,\n        {\n          action: EAction.showMessage,\n          data: {\n            type: EDataResultType.info,\n            msg: this.service.i18n.t(\"service.contextMenus.sendingLink\"),\n            timeout: 2,\n            indeterminate: true\n          }\n        },\n        (result: any) => {\n          if (chrome.runtime.lastError) {\n            let message = chrome.runtime.lastError.message || \"\";\n            if (message.match(/Could not establish connection/)) {\n              // \"插件状态未知，当前操作可能失败，请刷新页面后再试\"\n              APP.showNotifications({\n                message: this.service.i18n.t(\n                  \"service.contextMenus.pluginStatusIsUnknown\"\n                )\n              });\n            } else {\n              APP.showNotifications({\n                message: chrome.runtime.lastError.message\n              });\n            }\n            return;\n          }\n          notice = result;\n        }\n      );\n    } catch (error) {\n      APP.showNotifications({\n        message: error\n      });\n      return;\n    }\n\n    this.service.logger.add({\n      module: EModule.background,\n      event: \"contextMenus.sendTorrentToClient.begin\",\n      msg: this.service.i18n.t(\"service.contextMenus.sendingLink\"),\n      data: options\n    });\n\n    let client = this.options.clients.find((item: DownloadClient) => {\n      return item.id === options.clientId;\n    });\n\n    if (!client) {\n      chrome.tabs.sendMessage(tabid, {\n        action: EAction.showMessage,\n        data: {\n          type: EDataResultType.error,\n          msg: this.service.i18n.t(\n            \"service.contextMenus.downloadClientGetFailed\"\n          )\n        }\n      });\n\n      this.service.logger.add({\n        module: EModule.background,\n        event: \"contextMenus.sendTorrentToClient.getClientError\",\n        msg: this.service.i18n.t(\n          \"service.contextMenus.downloadClientGetFailed\"\n        ),\n        data: options\n      });\n      this.hideNotice(tabid, notice);\n      return;\n    }\n\n    // 设置是否自动开始\n    options.autoStart = client.autoStart;\n    console.log(options);\n\n    let url = this.getParsedURL(options.url);\n    if (typeof url !== \"string\") {\n      chrome.tabs.sendMessage(tabid, {\n        action: EAction.showMessage,\n        data: url\n      });\n      this.hideNotice(tabid, notice);\n      return;\n    }\n\n    options.url = url;\n\n    this.service.controller\n      .sendTorrentToClient(options)\n      .then((result: any) => {\n        this.service.logger.add({\n          module: EModule.background,\n          event: \"contextMenus.sendTorrentToClient.done\",\n          msg: this.service.i18n.t(\n            \"service.contextMenus.sendTorrentToClientDone\"\n          ), // \"下载链接发送完成。\",\n          data: result\n        });\n        chrome.tabs.sendMessage(tabid, {\n          action: EAction.showMessage,\n          data: result\n        });\n      })\n      .catch((result: any) => {\n        this.service.logger.add({\n          module: EModule.background,\n          event: \"contextMenus.sendTorrentToClient.error\",\n          msg: this.service.i18n.t(\n            \"service.contextMenus.sendTorrentToClientError\"\n          ), // \"下载链接发送失败！\",\n          data: result\n        });\n        chrome.tabs.sendMessage(tabid, {\n          action: EAction.showMessage,\n          data: result.status == \"\" ? this.service.i18n.t(\"service.contextMenus.sendTorrentToClientError\") : result\n        });\n      })\n      .finally(() => {\n        this.hideNotice(tabid, notice);\n      });\n  }\n\n  /**\n   * 隐藏指定的 notice\n   * @param tabid\n   * @param notice\n   */\n  private hideNotice(tabid: number = 0, notice: any) {\n    if (!notice) return;\n    if (notice.id) {\n      chrome.tabs.sendMessage(tabid, {\n        action: EAction.hideMessage,\n        data: notice.id\n      });\n    } else if (notice.hide) {\n      notice.hide();\n    }\n  }\n\n  /**\n   * 创建关键字搜索菜单，所有页面可用\n   */\n  private createSearchMenus() {\n    // 是否启用选择内容时搜索\n    if (this.options.allowSelectionTextSearch) {\n      // 选中内容进行搜索\n      this.add({\n        title: this.service.i18n.t(\"service.contextMenus.searchSelectionText\"), // '搜索 \"%s\" 相关的种子',\n        contexts: [\"selection\"],\n        onclick: (\n          info: chrome.contextMenus.OnClickData,\n          tab: chrome.tabs.Tab\n        ) => {\n          this.service.controller.searchTorrent(info.selectionText);\n        }\n      });\n\n      this.pushMoreSearchMenus();\n    }\n\n    let imdbMenuId = \"searchWithIMDb\";\n    // 搜索IMDb相关种子\n    this.add({\n      id: imdbMenuId,\n      title: this.service.i18n.t(\"service.contextMenus.searchByIMDb\"), // \"搜索当前IMDb相关种子\",\n      contexts: [\"link\"],\n      targetUrlPatterns: [\"*://www.imdb.com/title/tt*\"]\n    });\n\n    // 搜索IMDb相关种子\n    this.add({\n      parentId: imdbMenuId,\n      title: this.service.i18n.t(\"service.contextMenus.searchByDefault\"), // \"搜索当前IMDb相关种子\",\n      contexts: [\"link\"],\n      targetUrlPatterns: [\"*://www.imdb.com/title/tt*\"],\n      onclick: (\n        info: chrome.contextMenus.OnClickData,\n        tab: chrome.tabs.Tab\n      ) => {\n        if (info.linkUrl) {\n          let link = info.linkUrl.match(/(tt\\d+)/);\n          if (link && link.length >= 2) {\n            this.service.controller.searchTorrent(link[1]);\n          }\n        }\n      }\n    });\n\n    this.pushMoreSearchMenus(\n      imdbMenuId,\n      [\"link\"],\n      [\"*://www.imdb.com/title/tt*\"],\n      /(tt\\d+)/\n    );\n\n    let donbanMenuId = \"searchWithDouban\";\n    // \"搜索当前豆瓣链接相关种子\"\n    this.add({\n      id: donbanMenuId,\n      title: this.service.i18n.t(\"service.contextMenus.searchByDouban\"),\n      contexts: [\"link\"],\n      targetUrlPatterns: [\"*://movie.douban.com/subject/*\"]\n    });\n\n    // \"搜索当前豆瓣链接相关种子\"\n    this.add({\n      parentId: donbanMenuId,\n      title: this.service.i18n.t(\"service.contextMenus.searchByDefault\"),\n      contexts: [\"link\"],\n      targetUrlPatterns: [\"*://movie.douban.com/subject/*\"],\n      onclick: (\n        info: chrome.contextMenus.OnClickData,\n        tab: chrome.tabs.Tab\n      ) => {\n        if (info.linkUrl) {\n          let link = info.linkUrl.match(/subject\\/(\\d+)/);\n          if (link && link.length >= 2) {\n            this.service.controller.searchTorrent(\"douban\" + link[1]);\n          }\n        }\n      }\n    });\n\n    this.pushMoreSearchMenus(\n      donbanMenuId,\n      [\"link\"],\n      [\"*://movie.douban.com/subject/*\"],\n      /subject\\/(\\d+)/,\n      \"douban\"\n    );\n  }\n\n  /**\n   * 添加更多搜索相关菜单\n   * @param _parentId\n   * @param contexts\n   * @param targetUrlPatterns\n   * @param match\n   * @param keyPrefix\n   */\n  private pushMoreSearchMenus(\n    _parentId: string | undefined = undefined,\n    contexts: string[] = [\"selection\"],\n    targetUrlPatterns: string[] | undefined = undefined,\n    match: RegExp = /(tt\\d+)/,\n    keyPrefix: string = \"\"\n  ) {\n    const sites = this.options.sites;\n    // 以指定的站点进行搜索\n    if (sites && sites.length > 0) {\n      let parentId = `${_parentId}searchInSite`;\n\n      this.add({\n        id: parentId,\n        title: this.service.i18n.t(\"service.contextMenus.searchInSite\"),\n        contexts: contexts,\n        parentId: _parentId,\n        targetUrlPatterns\n      });\n\n      // 添加站点\n      sites.forEach((site: Site) => {\n        let id = `${parentId}**${site.host}`;\n        this.add({\n          id,\n          title: `${site.name} - ${site.host}`,\n          parentId: parentId,\n          contexts: contexts,\n          targetUrlPatterns,\n          onclick: (\n            info: chrome.contextMenus.OnClickData,\n            tab: chrome.tabs.Tab\n          ) => {\n            let data = info.menuItemId.split(\"**\");\n            this.service.debug(\n              this.service.i18n.t(\"service.contextMenus.searchInSite\"),\n              info\n            );\n            if (contexts.includes(\"link\") && info.linkUrl) {\n              let link = info.linkUrl.match(match);\n              if (link && link.length >= 2) {\n                this.service.controller.searchTorrent(\n                  keyPrefix + link[1],\n                  data[1]\n                );\n              }\n            } else {\n              this.service.controller.searchTorrent(\n                info.selectionText,\n                data[1]\n              );\n            }\n          }\n        });\n      });\n    }\n\n    const solutions = this.options.searchSolutions;\n    // 以指定的方案进行搜索\n    if (solutions && solutions.length > 0) {\n      let parentId = `${_parentId}searchInSolution`;\n\n      this.add({\n        id: parentId,\n        title: this.service.i18n.t(\"service.contextMenus.searchInSolution\"),\n        contexts: contexts,\n        parentId: _parentId,\n        targetUrlPatterns\n      });\n      solutions.forEach((item: SearchSolution) => {\n        let id = `${parentId}**${item.id}`;\n        this.add({\n          id,\n          title: `${item.name}`,\n          parentId: parentId,\n          contexts: contexts,\n          targetUrlPatterns,\n          onclick: (\n            info: chrome.contextMenus.OnClickData,\n            tab: chrome.tabs.Tab\n          ) => {\n            this.service.debug(\n              this.service.i18n.t(\"service.contextMenus.searchInSolution\"),\n              info\n            );\n            let data = info.menuItemId.split(\"**\");\n            if (contexts.includes(\"link\") && info.linkUrl) {\n              let link = info.linkUrl.match(match);\n              if (link && link.length >= 2) {\n                this.service.controller.searchTorrent(\n                  keyPrefix + link[1],\n                  data[1]\n                );\n              }\n            } else {\n              this.service.controller.searchTorrent(\n                info.selectionText,\n                data[1]\n              );\n            }\n          }\n        });\n      });\n    }\n\n    // 在所有站点中搜索\n    this.add({\n      title: this.service.i18n.t(\"service.contextMenus.searchInAllSite\"),\n      parentId: _parentId,\n      contexts: contexts,\n      targetUrlPatterns,\n      onclick: (\n        info: chrome.contextMenus.OnClickData,\n        tab: chrome.tabs.Tab\n      ) => {\n        if (info.linkUrl) {\n          let link = info.linkUrl.match(match);\n          if (contexts.includes(\"link\") && link && link.length >= 2) {\n            this.service.controller.searchTorrent(keyPrefix + link[1], \"all\");\n          }\n        } else {\n          this.service.controller.searchTorrent(info.selectionText, \"all\");\n        }\n      }\n    });\n  }\n\n  /**\n   * 创建下载客户端上下文菜单，所有页面可用\n   */\n  private createClientMenus() {\n    if (this.options.defaultClientId) {\n      let client = this.options.clients.find((item: DownloadClient) => {\n        return item.id === this.options.defaultClientId;\n      });\n      if (client) {\n        this.add({\n          id: client.id,\n          title: this.service.i18n.t(\n            \"service.contextMenus.sendTorrentToDefaultClient\",\n            {\n              client\n            }\n          ), // `发送到默认服务器 ${client.name} -> ${client.address}`,\n          contexts: [\"link\"],\n          onclick: (\n            info: chrome.contextMenus.OnClickData,\n            tab: chrome.tabs.Tab\n          ) => {\n            this.sendTorrentToClient(tab.id, {\n              clientId: info.menuItemId,\n              url: info.linkUrl as string\n            });\n          }\n        });\n      }\n    }\n\n    if (this.options.clients.length > 1) {\n      this.add({\n        id: this.rootId,\n        title: this.service.i18n.t(\"service.contextMenus.sendTorrentToClient\"), // \"发送到其他服务器\",\n        contexts: [\"link\"]\n      });\n\n      // 创建可用的客户端菜单\n      this.options.clients.forEach((client: DownloadClient) => {\n        if (client.id !== this.options.defaultClientId) {\n          this.add({\n            id: client.id,\n            title: `${client.name} -> ${client.address}`,\n            parentId: this.rootId,\n            contexts: [\"link\"],\n            onclick: (\n              info: chrome.contextMenus.OnClickData,\n              tab: chrome.tabs.Tab\n            ) => {\n              this.sendTorrentToClient(tab.id, {\n                clientId: info.menuItemId,\n                url: info.linkUrl as string\n              });\n            }\n          });\n        }\n      });\n    }\n  }\n\n  /**\n   * 向浏览器添加上下文菜单\n   * @param options 菜单选项\n   * @param callback 回调\n   */\n  private add(\n    options: chrome.contextMenus.CreateProperties,\n    callback?: (() => void) | undefined\n  ) {\n    if (!options.id) {\n      options.id = this.getRandomString();\n    }\n    chrome &&\n      chrome.contextMenus &&\n      chrome.contextMenus.create(options, callback);\n  }\n\n  /**\n   * 从浏览器中删除指定的菜单\n   * @param id 菜单ID\n   * @param callback 回调\n   */\n  private remove(id: string, callback?: (() => void) | undefined) {\n    try {\n      chrome.contextMenus.remove(id, callback);\n      if (chrome.runtime.lastError) {\n        console.log(chrome.runtime.lastError);\n      }\n    } catch (error) {\n      console.log(error);\n    }\n  }\n\n  /**\n   * 获取随机字符串\n   * @param  {number} length    [长度，默认为16]\n   * @param  {boolean} noSimilar [是否包含容易混淆的字符，默认为包含]\n   * @return {string}           [返回的内容]\n   */\n  private getRandomString(\n    length: number = 16,\n    noSimilar: boolean = true\n  ): string {\n    // 是否包含容易混淆的字符[oO,Ll,9gq,Vv,Uu,I1]，默认为包含\n    let chars = noSimilar\n      ? \"abcdefhijkmnprstwxyz2345678ABCDEFGHJKMNPQRSTWXYZ\"\n      : \"abcdefghijkmnopqrstuvwxyz0123456789ABCDEFGHIJKMNOPQRSTUVWXYZ\";\n    let maxLength = chars.length;\n    let result = [];\n    for (let i = 0; i < length; i++) {\n      result.push(chars.charAt(Math.floor(Math.random() * maxLength)));\n    }\n\n    return result.join(\"\");\n  }\n\n  /**\n   * 获取解析过的地址\n   * @param source\n   */\n  private getParsedURL(source: string | any): string | DataResult {\n    let url = new URLParse(source);\n    let site: Site | null = this.getSiteFromURL(source);\n\n    if (!site) {\n      return source;\n    }\n\n    let options = {\n      url,\n      site,\n      result: source,\n      error: {} as DataResult\n    };\n\n    let parser = this.getSiteParser(site.host as string, \"downloadURL\");\n    if (parser) {\n      try {\n        eval(parser);\n      } catch (error) {\n        console.error(error);\n      }\n    }\n\n    if (options.error && options.error.msg) {\n      return options.error;\n    }\n\n    return options.result;\n  }\n\n  /**\n   * 获取指定解析器\n   * @param host\n   * @param name\n   */\n  private getSiteParser(host: string, name: string): string {\n    // 由于解析器可能会更新，所以需要从系统配置中加载\n    let site: Site =\n      this.options.system &&\n      this.options.system.sites &&\n      this.options.system.sites.find((item: Site) => {\n        return item.host === host;\n      });\n\n    if (!site) {\n      return \"\";\n    }\n\n    let result = site.parser && site.parser[name];\n    if (!result) {\n      let schema: SiteSchema =\n        this.options.system &&\n        this.options.system.schemas &&\n        this.options.system.schemas.find((item: SiteSchema) => {\n          return item.name === site.schema;\n        });\n\n      if (schema && schema.parser) {\n        result = schema.parser[name];\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 根据指定的URL获取站点信息\n   * @param source\n   */\n  private getSiteFromURL(source: string) {\n    let url = new URLParse(source);\n    if (!url.host) {\n      return null;\n    }\n    let site: Site = this.options.sites.find((item: Site) => {\n      let cdn = [item.url].concat(item.cdn);\n      return item.host == url.host || cdn.join(\"\").indexOf(url.host) > -1;\n    });\n\n    if (!site) {\n      return null;\n    }\n    return site;\n  }\n}\n"
  },
  {
    "path": "src/background/controller.ts",
    "content": "import {\n  Options,\n  EAction,\n  Site,\n  SiteSchema,\n  Dictionary,\n  DownloadClient,\n  EDownloadClientType,\n  DownloadOptions,\n  DataResult,\n  EDataResultType,\n  Request,\n  EModule,\n  ERequestMethod,\n  EUserDataRange,\n  i18nResource,\n  IBackupServer,\n  EWikiLink\n} from \"@/interface/common\";\nimport { filters as Filters } from \"@/service/filters\";\nimport { ClientController } from \"@/service/clientController\";\nimport { DownloadHistory } from \"./downloadHistory\";\nimport { Searcher } from \"./searcher\";\nimport PTPlugin from \"./service\";\nimport { FileDownloader } from \"@/service/downloader\";\nimport { APP } from \"@/service/api\";\nimport URLParse from \"url-parse\";\nimport { User } from \"./user\";\nimport { MovieInfoService } from \"@/service/movieInfoService\";\nimport parseTorrent from \"parse-torrent\";\n\ntype Service = PTPlugin;\nexport default class Controller {\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n\n  public defaultClient: any;\n  public defaultClientOptions: DownloadClient = {};\n  public siteDefaultClients: any = {};\n  public optionsTabId: number | undefined = 0;\n  public downloadHistory: DownloadHistory = new DownloadHistory();\n  public clients: any = {};\n  public searcher: Searcher = new Searcher(this.service);\n  public userService: User = new User(this.service);\n  public movieInfoService = new MovieInfoService();\n\n  public clientController: ClientController = new ClientController();\n  public isInitialized: boolean = false;\n\n  public contentPages: any[] = [];\n\n  public debuggerTabId: number | undefined = 0;\n  public debuggerPort: chrome.runtime.Port | undefined;\n\n  private imageBase64Cache: Dictionary<any> = {};\n  // 下载重试次数\n  private downloadFailedRetriesCache: Dictionary<any> = {};\n  // 种子链接对应的名称缓存\n  private torrentInfosCache: Dictionary<any> = {};\n\n  constructor(public service: Service) { }\n\n  public init(options: Options) {\n    this.reset(options);\n    this.isInitialized = true;\n  }\n\n  /**\n   * 重置并刷新配置\n   * @param options\n   */\n  public reset(options: Options) {\n    this.options = options;\n    this.clientController.init(options);\n    this.searcher.options = options;\n    this.initDefaultClient();\n    this.siteDefaultClients = {};\n    if (options.connectClientTimeout) {\n      this.movieInfoService.timeout = options.connectClientTimeout;\n    }\n\n    // 追加用户定义的apiKey\n    if (this.options.apiKey) {\n      if (this.options.apiKey.omdb && this.options.apiKey.omdb.length > 0) {\n        this.movieInfoService.appendApiKey(\"omdb\", this.options.apiKey.omdb);\n      }\n\n      if (this.options.apiKey.douban && this.options.apiKey.douban.length > 0) {\n        this.movieInfoService.appendApiKey(\n          \"douban\",\n          this.options.apiKey.douban\n        );\n      }\n    }\n  }\n\n  /**\n   * 获取搜索结果\n   * @param options\n   */\n  public getSearchResult(options: any): Promise<any> {\n    return this.searcher.searchTorrent(\n      options.site,\n      options.key,\n      options.payload\n    );\n  }\n\n  /**\n   * 取消一个正在执行的搜索请求\n   * @param options\n   */\n  public abortSearch(options: any): Promise<any> {\n    return this.searcher.abortSearch(options.site, options.key);\n  }\n\n  /**\n   * 获取下载历史记录\n   */\n  public getDownloadHistory(): Promise<any> {\n    return this.downloadHistory.load();\n  }\n\n  /**\n   * 保存下载记录\n   * @param data 下载链接信息\n   * @param host 站点域名\n   * @param clientId 下载客户端ID\n   */\n  private saveDownloadHistory(\n    data: any,\n    host: string = \"\",\n    clientId: string = \"\",\n    success: boolean = true\n  ) {\n    // 是否保存历史记录\n    if (this.options.saveDownloadHistory) {\n      this.downloadHistory.add(data, host, clientId, success);\n    }\n  }\n\n  /**\n   * 删除下载历史记录\n   * @param items 需要删除的列表\n   */\n  public removeDownloadHistory(items: any[]): Promise<any> {\n    return this.downloadHistory.remove(items);\n  }\n\n  /**\n   * 清除下载记录\n   */\n  public clearDownloadHistory(): Promise<any> {\n    return this.downloadHistory.clear();\n  }\n\n  /**\n   * 重置下载历史\n   */\n  public resetDownloadHistory(datas: any): Promise<any> {\n    return this.downloadHistory.reset(datas);\n  }\n\n  /**\n   * 发送下载信息到指定的客户端\n   * @param data\n   */\n  public sendTorrentToClient(data: DownloadOptions): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!data.url) {\n        reject({\n          msg: this.service.i18n.t(\"service.controller.invalidAddress\") //\"无效的地址\"\n        });\n        return;\n      }\n      let URL = Filters.parseURL(data.url);\n      let host = URL.host;\n      let clientConfig = this.options.clients.find((item: DownloadClient) => {\n        return item.id === data.clientId;\n      });\n      if (!clientConfig) {\n        reject({\n          msg: this.service.i18n.t(\"service.controller.invalidDownloadServer\") //\"无效的下载服务器\"\n        });\n        return;\n      }\n\n      this.getClient(clientConfig).then((result: any) => {\n        this.doDownload(result, data, host)\n          .then((result: any) => {\n            resolve(result);\n          })\n          .catch((result: any) => {\n            reject(result);\n          });\n      });\n    });\n  }\n\n  /**\n   * 发送下载链接地址到默认服务器（客户端）\n   * @param downloadOptions 下载选项\n   */\n  public sendTorrentToDefaultClient(\n    downloadOptions: DownloadOptions\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let URL = Filters.parseURL(downloadOptions.url);\n      let host = URL.host;\n      let site = this.getSiteFromHost(host);\n      // 重新指定host内容，因为站点可能定义了多域名\n      host = site.host + \"\";\n      let siteDefaultPath = this.getSiteDefaultPath(site);\n      let siteClientConfig = this.siteDefaultClients[host];\n\n      // https://github.com/pt-plugins/PT-Plugin-Plus/issues/681\n      // 在 downloadOptions 中已经有 savePath 的情况下，不覆盖 savePath\n      if (!downloadOptions.savePath && siteDefaultPath) {\n        downloadOptions.savePath = siteDefaultPath;\n      }\n      if (!siteClientConfig) {\n        this.initSiteDefaultClient(host).then((siteClientConfig: any) => {\n          this.siteDefaultClients[host] = siteClientConfig;\n\n          this.doDownload(siteClientConfig, downloadOptions, host)\n            .then((result: any) => {\n              resolve(result);\n            })\n            .catch((result: any) => {\n              reject(result);\n            });\n        });\n      } else {\n        this.doDownload(siteClientConfig, downloadOptions, host)\n          .then((result: any) => {\n            resolve(result);\n          })\n          .catch((result: any) => {\n            reject(result);\n          });\n      }\n    });\n  }\n\n  /**\n   * 执行下载操作\n   * @param clientConfig\n   * @param downloadOptions\n   * @param host\n   */\n  private doDownload(\n    clientConfig: any,\n    downloadOptions: DownloadOptions,\n    host: string = \"\"\n  ): Promise<any> {\n    // copy from sendTorrentToDefaultClient\n    let URL = Filters.parseURL(downloadOptions.url);\n    let downloadHost = URL.host;\n    let siteConfig = this.getSiteFromHost(downloadHost);\n    return new Promise((resolve?: any, reject?: any) => {\n      clientConfig.client\n        .call(EAction.addTorrentFromURL, {\n          url: downloadOptions.url,\n          savePath: downloadOptions.savePath,\n          autoStart:\n            downloadOptions.autoStart === undefined\n              ? false\n              : downloadOptions.autoStart,\n          imdbId: downloadOptions.tagIMDb ? downloadOptions.imdbId : null,\n          upLoadLimit: siteConfig !== undefined ? siteConfig.upLoadLimit : null,\n        })\n        .then((result: any) => {\n          this.service.logger.add({\n            module: EModule.background,\n            event: \"service.controller.doDownload.finished\",\n            msg: this.service.i18n.t(\"service.controller.downloadFinished\", {\n              name: clientConfig.options.name,\n              action: EAction.addTorrentFromURL\n            }), // `下载服务器${clientConfig.options.name}处理[${ EAction.addTorrentFromURL}]命令完成`,\n            data: result\n          });\n\n          // 如果未指定标题，则尝试从种子信息缓存中获取名称\n          if (\n            !downloadOptions.title &&\n            this.torrentInfosCache[downloadOptions.url]\n          ) {\n            downloadOptions.title = this.torrentInfosCache[downloadOptions.url];\n          }\n\n          if (result && (result.code === 0 || result.success === false)) {\n            if (\n              this.downloadFailedRetry(\n                clientConfig,\n                downloadOptions,\n                host,\n                result,\n                resolve,\n                reject\n              )\n            ) {\n              return;\n            }\n\n            switch (result.msg) {\n              // 连接超时\n              case \"timeout\":\n                reject({\n                  success: false,\n                  msg: this.service.i18n.t(\n                    \"service.controller.downloadTimeout\"\n                  ), //\"连接下载服务器超时，请检查网络设置或调整服务器超时时间！\",\n                  status: \"error\"\n                });\n                break;\n\n              default:\n                reject({\n                  success: false,\n                  msg: result.msg,\n                  status: \"error\"\n                });\n                break;\n            }\n\n            this.saveDownloadHistory(\n              downloadOptions,\n              host,\n              clientConfig.options.id,\n              false\n            );\n            return;\n          }\n\n          this.saveDownloadHistory(\n            downloadOptions,\n            host,\n            clientConfig.options.id,\n            true\n          );\n\n          this.formatSendResult(result, clientConfig.options, downloadOptions)\n            .then((result: any) => {\n              resolve(result);\n            })\n            .catch((result: any) => {\n              reject(result);\n            });\n\n          if (this.downloadFailedRetriesCache[downloadOptions.url]) {\n            delete this.downloadFailedRetriesCache[downloadOptions.url];\n          }\n        })\n        .catch((result: any) => {\n          if (\n            this.downloadFailedRetry(\n              clientConfig,\n              downloadOptions,\n              host,\n              result,\n              resolve,\n              reject\n            )\n          ) {\n            return;\n          }\n\n          this.service.logger.add({\n            module: EModule.background,\n            event: \"service.controller.doDownload.error\",\n            msg: this.service.i18n.t(\"service.controller.downloadError\", {\n              name: clientConfig.options.name,\n              action: EAction.addTorrentFromURL\n            }), // `下载服务器${clientConfig.options.name}处理[${EAction.addTorrentFromURL}]命令失败`,\n            data: result\n          });\n          this.saveDownloadHistory(\n            downloadOptions,\n            host,\n            clientConfig.options.id,\n            false\n          );\n          reject(result);\n        });\n    });\n  }\n\n  /**\n   * 下载失败重试\n   * @param clientConfig\n   * @param downloadOptions\n   * @param host\n   * @param failedMsg\n   * @param resolve\n   * @param reject\n   */\n  private downloadFailedRetry(\n    clientConfig: any,\n    downloadOptions: DownloadOptions,\n    host: string = \"\",\n    failedMsg: any,\n    resolve?: any,\n    reject?: any\n  ): boolean {\n    // 是否失败重试\n    if (this.options.downloadFailedRetry) {\n      let maxRetries = this.options.downloadFailedFailedRetryCount;\n      if (maxRetries === undefined) {\n        maxRetries = 0;\n      }\n      let retries = this.downloadFailedRetriesCache[downloadOptions.url];\n\n      if (retries === undefined) {\n        retries = 0;\n      }\n      if (retries < maxRetries) {\n        retries++;\n        this.service.logger.add({\n          module: EModule.background,\n          event: \"service.controller.downloadFailedRetries\",\n          msg:\n            this.service.i18n.t(\"service.controller.downloadError\", {\n              name: clientConfig.options.name,\n              action: EAction.addTorrentFromURL\n            }) +\n            \" (\" +\n            retries +\n            \")\", // `下载服务器${clientConfig.options.name}处理[${EAction.addTorrentFromURL}]命令失败`,\n          data: failedMsg\n        });\n\n        this.downloadFailedRetriesCache[downloadOptions.url] = retries;\n\n        // 是否需要延时下载\n        if (\n          this.options.downloadFailedFailedRetryInterval &&\n          this.options.downloadFailedFailedRetryInterval > 0\n        ) {\n          setTimeout(() => {\n            this.doDownload(clientConfig, downloadOptions, host)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n          }, this.options.downloadFailedFailedRetryInterval * 1000);\n        } else {\n          this.doDownload(clientConfig, downloadOptions, host)\n            .then((result: any) => {\n              resolve(result);\n            })\n            .catch((result: any) => {\n              reject(result);\n            });\n        }\n\n        return true;\n      }\n\n      delete this.downloadFailedRetriesCache[downloadOptions.url];\n    }\n    return false;\n  }\n\n  /**\n   * 根据指定的域名获取站点配置信息\n   * @param host 域名\n   */\n  public getSiteFromHost(host: string): Site {\n    return this.options.sites.find((item: Site) => {\n      let cdn = [item.url].concat(item.cdn);\n      return item.host == host || cdn.join(\"\").indexOf(host) > -1;\n    });\n  }\n\n  /**\n   * 获取当前站点的默认下载目录\n   * @param string clientId 指定客户端ID，不指定表示使用默认下载客户端\n   * @return string 目录信息，如果没有定义，则返回空字符串\n   */\n  public getSiteDefaultPath(site: Site, clientId: string = \"\"): string {\n    if (!clientId) {\n      clientId = site.defaultClientId || <string>this.options.defaultClientId;\n    }\n\n    let client = this.options.clients.find((item: any) => {\n      return item.id === clientId;\n    });\n    let path = \"\";\n    if (client && client.paths) {\n      for (const host in client.paths) {\n        if (site.host === host) {\n          path = client.paths[host][0];\n          break;\n        }\n      }\n    }\n\n    return path;\n  }\n\n  /**\n   * 格式化发送结果\n   * @param data\n   * @param clientOptions\n   * @param downloadOptions\n   */\n  private formatSendResult(\n    data: any,\n    clientOptions: any,\n    downloadOptions: DownloadOptions\n  ): Promise<any> {\n    return new Promise((resolve?: any, reject?: any) => {\n      let result: DataResult = {\n        type: EDataResultType.success,\n        msg:\n          this.service.i18n.t(\"service.controller.torrentAdded\", {\n            title: downloadOptions.title\n          }) +\n          (downloadOptions.savePath\n            ? this.service.i18n.t(\"service.controller.torrentSavePath\", {\n              path: downloadOptions.savePath,\n              interpolation: { escapeValue: false }\n            })\n            : \"\"), //`${downloadOptions.title || \"\"} 种子已添加完成。` +\n        // (downloadOptions.savePath\n        //   ? `<br/>保存至 ${downloadOptions.savePath}`\n        //   : \"\"),\n        success: true,\n        data: data\n      };\n\n      switch (clientOptions.type) {\n        // transmission\n        case EDownloadClientType.transmission:\n          if (data.id != undefined) {\n            result.msg = this.service.i18n.t(\n              \"service.controller.transmissionSuccess\",\n              {\n                data\n              }\n            ); //data.name + \" 已发送至 Transmission，编号：\" + data.id;\n            if (downloadOptions.savePath) {\n              result.msg += this.service.i18n.t(\n                \"service.controller.torrentSavePath\",\n                {\n                  path: downloadOptions.savePath,\n                  interpolation: { escapeValue: false }\n                }\n              ); //`<br/>保存至 ${downloadOptions.savePath} `;\n            }\n          } else if (data.status) {\n            switch (data.status) {\n              // 重复的种子\n              case \"duplicate\":\n                result.type = EDataResultType.error;\n                result.success = false;\n                result.msg = this.service.i18n.t(\n                  \"service.controller.transmissionDuplicate\",\n                  {\n                    name: data.torrent.name,\n                    id: data.torrent.id\n                  }\n                );\n                //data.torrent.name + \" 种子已存在！编号：\" + data.torrent.id;\n                break;\n\n              case \"error\":\n                result.type = EDataResultType.error;\n                result.success = false;\n                result.msg = this.service.i18n.t(\n                  \"service.controller.transmissionError\"\n                ); //\"链接发送失败，请检查下载服务器是否可用。\";\n                break;\n              default:\n                result.msg = data.msg;\n                break;\n            }\n          } else {\n            result.msg = data;\n          }\n\n          break;\n\n        default:\n          break;\n      }\n\n      resolve(result);\n    });\n  }\n\n  /**\n   * 根据指定客户端配置初始化客户端\n   * @param clientOptions 客户端配置\n   */\n  private getClient(clientOptions: any): Promise<any> {\n    return this.clientController.getClient(clientOptions);\n  }\n\n  /**\n   * 初始化默认客户端\n   */\n  private initDefaultClient() {\n    if (!this.options.clients) {\n      return;\n    }\n    let clientOptions: any = this.options.clients.find((item: any) => {\n      return item.id === this.options.defaultClientId;\n    });\n\n    if (clientOptions) {\n      this.getClient(clientOptions).then((result: any) => {\n        this.defaultClient = result.client;\n        this.defaultClientOptions = result.options;\n      });\n    }\n  }\n\n  /**\n   * 初始化指定站点默认客户端\n   * @param hostname 站点host名称\n   */\n  private initSiteDefaultClient(hostname: string): Promise<any> {\n    let site: any = this.options.sites.find((item: any) => {\n      return item.host == hostname;\n    });\n\n    let clientOptions: any = this.options.clients.find((item: any) => {\n      return item.id === site.defaultClientId;\n    });\n\n    if (clientOptions) {\n      return this.getClient(clientOptions);\n    }\n\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      resolve({\n        client: this.defaultClient,\n        options: this.defaultClientOptions\n      });\n    });\n  }\n\n  /**\n   * 复制指定的内容到剪切板\n   * @param text\n   */\n  public copyTextToClipboard(text: string = \"\") {\n    if (!text) {\n      return false;\n    }\n    var copyFrom = $(\"<textarea/>\");\n    copyFrom.text(text);\n    $(\"body\").append(copyFrom);\n    copyFrom.select();\n    document.execCommand(\"copy\");\n    copyFrom.remove();\n    return true;\n  }\n\n  /**\n   * 获取指定客户端的可用空间\n   * @param data\n   */\n  public getFreeSpace(data: any): Promise<any> {\n    if (!data.clientId) {\n      return this.getDefaultClientFreeSpace(data);\n    }\n\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let clientOptions: any = this.options.clients.find((item: any) => {\n        return item.id === data.clientId;\n      });\n\n      if (clientOptions) {\n        this.getClient(clientOptions).then((result: any) => {\n          result.client.call(EAction.getFreeSpace, data).then((result: any) => {\n            resolve(result);\n          });\n        });\n      }\n    });\n  }\n\n  /**\n   * 获取默认客户端的可用空间\n   * @param data\n   */\n  public getDefaultClientFreeSpace(data: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.defaultClient\n        .call(EAction.getFreeSpace, data)\n        .then((result: any) => {\n          resolve(result);\n        });\n    });\n  }\n\n  public updateOptionsTabId(id: number) {\n    this.optionsTabId = id;\n  }\n\n  /**\n   * 打开搜索种子页面\n   * @param key 关键字\n   * @param host 指定站点，默认搜索所有站\n   */\n  public searchTorrent(key: string = \"\", host: string = \"\") {\n    let url = \"\";\n    if (key) {\n      url = `search-torrent/${key}`;\n    }\n\n    if (host) {\n      url += `/${host}`;\n    }\n\n    this.openOptions(url);\n  }\n\n  /**\n   * 打开配置页\n   * @param path 要跳转的路径\n   */\n  public openOptions(path: string = \"\") {\n    let url = \"/\";\n    if (path) {\n      url += path;\n    }\n\n    if (this.optionsTabId == 0) {\n      this.openURL(url);\n    } else {\n      chrome.tabs.get(this.optionsTabId as number, tab => {\n        if (!chrome.runtime.lastError && tab) {\n          chrome.tabs.update(tab.id as number, {\n            active: true,\n            url: \"index.html#\" + url\n          });\n        } else {\n          this.openURL(url);\n        }\n      });\n    }\n  }\n\n  /**\n   * 创建配置页面选项卡\n   * @param url\n   */\n  public openURL(url: string = \"\") {\n    if (!url) {\n      return;\n    }\n    if (url.substr(0, 1) === \"/\") {\n      url = \"index.html#\" + url;\n    }\n    chrome.tabs.create(\n      {\n        url: url\n      },\n      tab => {\n        this.optionsTabId = tab.id;\n      }\n    );\n  }\n\n  /**\n   * 根据指定的站点获取站点的架构信息\n   * @param site 站点信息\n   */\n  private getSiteSchema(site: Site): SiteSchema {\n    let schema: SiteSchema = {};\n    if (typeof site.schema === \"string\") {\n      schema =\n        this.options.system &&\n        this.options.system.schemas &&\n        this.options.system.schemas.find((item: SiteSchema) => {\n          return item.name == site.schema;\n        });\n    } else {\n      let site: Site =\n        this.options.system &&\n        this.options.system.sites &&\n        this.options.system.sites.find((item: Site) => {\n          return item.host == site.host;\n        });\n      if (site && site.schema) {\n        schema = site.schema;\n        schema.siteOnly = true;\n      }\n    }\n\n    return schema;\n  }\n\n  private replaceKeys(source: string, keys: Dictionary<any>): string {\n    let result: string = source;\n\n    for (const key in keys) {\n      if (keys.hasOwnProperty(key)) {\n        const value = keys[key];\n        result = result.replace(\"$\" + key + \"$\", value);\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 接收由前台发回的指令并执行\n   * @param action 指令\n   * @param callback 回调函数\n   */\n  public call(\n    request: Request,\n    sender?: chrome.runtime.MessageSender\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let service: any = this;\n      console.log(\"contorller.call\", request.action);\n      service[request.action](request.data, sender)\n        .then((result: any) => {\n          resolve(result);\n        })\n        .catch((result: any) => {\n          reject(result);\n        });\n    });\n  }\n\n  public addContentPage(\n    data: any,\n    sender: chrome.runtime.MessageSender\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      try {\n        if (sender.tab) {\n          this.contentPages.push(sender.tab.id);\n        }\n        resolve();\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 备份系统参数至Google\n   */\n  public backupToGoogle(): Promise<any> {\n    return this.service.config.backupToGoogle();\n  }\n\n  /**\n   * 从Google恢复系统参数\n   */\n  public restoreFromGoogle(): Promise<any> {\n    return this.service.config.restoreFromGoogle();\n  }\n\n  /**\n   * 从Google中清除已备份的参数\n   */\n  public clearFromGoogle(): Promise<any> {\n    return this.service.config.syncStorage.clear();\n  }\n\n  /**\n   * 重新从网络中加载配置文件\n   */\n  public reloadConfig(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service.config.reload();\n      resolve();\n    });\n  }\n\n  /**\n   * 从指定的链接获取种子文件内容\n   * @param options\n   */\n  public getTorrentDataFromURL(options: string | any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let url = \"\";\n      if (typeof options === \"string\") {\n        url = options;\n        options = {\n          url,\n          parseTorrent: false\n        };\n      } else {\n        url = options.url;\n      }\n      let site = this.getSiteOptionsFromURL(url);\n      let requestMethod = ERequestMethod.GET;\n      if (site) {\n        requestMethod = site.downloadMethod || ERequestMethod.GET;\n      }\n      let file = new FileDownloader({\n        url,\n        getDataOnly: true,\n        timeout: this.service.options.connectClientTimeout\n      });\n\n      file.requestMethod = requestMethod;\n      file.onCompleted = () => {\n        console.log(\"getTorrentDataFromURL.completed\", url);\n        if (\n          file.content &&\n          /octet-stream|x-bittorrent/gi.test(file.content.type)\n        ) {\n          parseTorrent.remote(file.content, (err, torrent) => {\n            if (err) {\n              console.log(\"parse.error\", err);\n              // 是否解析种子文件\n              if (options.parseTorrent) {\n                reject(err);\n              } else {\n                resolve(file.content);\n              }\n            } else {\n              // 缓存种子文件名称\n              if (torrent) {\n                this.torrentInfosCache[url] = torrent.name;\n              }\n\n              // 是否解析种子文件\n              if (options.parseTorrent) {\n                resolve({\n                  url,\n                  torrent,\n                  content: file.content\n                });\n              } else {\n                resolve(file.content);\n              }\n            }\n          });\n        } else {\n          // \"无效的种子文件\"\n          reject(\n            APP.createErrorMessage(\n              this.service.i18n.t(\"service.controller.invalidTorrent\", {\n                link: EWikiLink.faq\n              })\n            )\n          );\n        }\n      };\n\n      file.onError = (e: any) => {\n        reject(APP.createErrorMessage(e));\n      };\n\n      file.start();\n    });\n  }\n\n  /**\n   * 根据指定URL获取站点配置信息\n   * @param url\n   */\n  public getSiteOptionsFromURL(url: string): Site | undefined {\n    let host = new URLParse(url).host;\n    let site: Site =\n      this.options.system &&\n      this.options.system.sites &&\n      this.options.system.sites.find((item: Site) => {\n        return item.host == host;\n      });\n\n    return site;\n  }\n\n  public getUserInfoForAllSite(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let count = 0;\n      let completed = 0;\n      let results: any[] = [];\n      this.options.sites.forEach((site: Site) => {\n        if (site.allowGetUserInfo) {\n          count++;\n\n          this.getUserInfo(site)\n            .then((result: any) => {\n              if (result) {\n                results.push({\n                  site,\n                  user: result\n                });\n              }\n\n              completed++;\n              if (completed >= count) {\n                resolve(results);\n              }\n            })\n            .catch(() => {\n              completed++;\n              if (completed >= count) {\n                resolve(results);\n              }\n            });\n        }\n      });\n\n      if (completed == count && completed == 0) {\n        // \"没有站点需要获取用户信息\"\n        reject(\n          this.service.i18n.t(\"service.controller.getUserInfoSiteIsEmpty\")\n        );\n      }\n    });\n  }\n\n  /**\n   * 获取指定站点的用户信息\n   * @param site\n   * @param callback\n   */\n  public getUserInfo(site: Site): Promise<any> {\n    return this.userService.getUserInfo(site);\n  }\n\n  public abortGetUserInfo(site: Site): Promise<any> {\n    return this.userService.abortGetUserInfo(site);\n  }\n\n  /**\n   * 根据指定的图片地址获取Base64信息\n   * @param url 图片地址\n   */\n  public getBase64FromImageUrl(url: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let data = this.imageBase64Cache[url];\n      if (data) {\n        resolve(data);\n        return;\n      }\n      let file = new FileDownloader({\n        url,\n        getDataOnly: true,\n        timeout: this.service.options.connectClientTimeout\n      });\n\n      file.onCompleted = () => {\n        console.log(\"getBase64FromImageUrl.completed\", url);\n        if (file.content && /image/gi.test(file.content.type)) {\n          var reader = new FileReader();\n          reader.onloadend = () => {\n            this.imageBase64Cache[url] = reader.result;\n            resolve(reader.result);\n          };\n          reader.readAsDataURL(file.content);\n        } else {\n          // \"无效的图片文件\"\n          reject(\n            APP.createErrorMessage(\n              this.service.i18n.t(\"service.controller.invalidImage\")\n            )\n          );\n        }\n      };\n\n      file.onError = (e: any) => {\n        reject(APP.createErrorMessage(e));\n      };\n\n      file.start();\n    });\n  }\n\n  public getUserHistoryData(host: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let data = this.service.userData.get(host, EUserDataRange.all);\n      resolve(data);\n    });\n  }\n\n  public resetUserDatas(datas: any) {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service.userData.reset(datas);\n      resolve();\n    });\n  }\n\n  /**\n   * 根据指定的关键字获取电影信息\n   * @param key\n   */\n  public getMovieInfos(key: string): Promise<any> {\n    return this.movieInfoService.getInfos(key);\n  }\n\n  /**\n   * 根据指定的 IMDbId 获取评分信息\n   * @param IMDbId\n   */\n  public getMovieRatings(IMDbId: string): Promise<any> {\n    return this.movieInfoService.getRatings(IMDbId);\n  }\n\n  /**\n   * 根据指定的 doubanId 获取 IMDbId\n   * @param doubanId\n   */\n  public getIMDbIdFromDouban(doubanId: string): Promise<any> {\n    return this.movieInfoService.getIMDbIdFromDouban(doubanId);\n  }\n\n  /**\n   * 从豆瓣查询影片信息\n   * @param key 需要查询的关键字\n   * @param count 要显示的条目数量\n   */\n  public queryMovieInfoFromDouban(options: any): Promise<any> {\n    return this.movieInfoService.queryMovieInfoFromDouban(\n      options.key,\n      options.count\n    );\n  }\n\n  /**\n   * 添加浏览器原生下载请求\n   * @param options\n   */\n  public addBrowserDownloads(options: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .checkPermissions([\"downloads\"])\n        .then(() => {\n          let items = [];\n          if (Array.isArray(options)) {\n            items = options;\n          } else {\n            items.push(options);\n          }\n\n          items.forEach(item => {\n            chrome.downloads.download(item, function (downloadId) {\n              console.log(downloadId);\n            });\n          });\n\n          resolve(items.length);\n        })\n        .catch(() => {\n          reject({\n            success: false,\n            msg: this.service.i18n.t(\"service.controller.noPermission\") //\"无权限，请前往用户授权\"\n          });\n        });\n    });\n  }\n\n  /**\n   * 获取当前语言资源\n   * @param parentKey 指定这个key下的内容\n   */\n  public getCurrentLanguageResource(parentKey: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let locale = this.service.options.locale || \"en\";\n      let resource = this.service.i18n.i18next.getResourceBundle(\n        locale,\n        \"translation\"\n      );\n\n      if (resource) {\n        if (parentKey && resource[parentKey]) {\n          resolve(resource[parentKey]);\n        } else {\n          resolve(resource);\n        }\n      } else {\n        reject();\n      }\n    });\n  }\n\n  public addLanguage(resource: i18nResource): Promise<any> {\n    return this.service.i18n.add(resource);\n  }\n\n  public replaceLanguage(resource: i18nResource): Promise<any> {\n    return this.service.i18n.replace(resource);\n  }\n\n  public backupToServer(server: IBackupServer): Promise<any> {\n    return this.service.config.backupToServer(server);\n  }\n\n  public getBackupListFromServer(options: any = {}): Promise<any> {\n    const server = options.server;\n    delete options.server;\n    return this.service.config.getBackupListFromServer(server, options);\n  }\n\n  public restoreFromServer(options: any = {}): Promise<any> {\n    return this.service.config.restoreFromServer(options.server, options.path);\n  }\n\n  public deleteFileFromBackupServer(options: any = {}): Promise<any> {\n    return this.service.config.deleteFileFromBackupServer(\n      options.server,\n      options.path\n    );\n  }\n\n  public sendTorrentsInBackground(items: any[] = []): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service.downloadQuene.add(items).run();\n      resolve(\n        this.service.i18n.t(\"service.controller.downloadTaskIsCreated\", {\n          count: items.length\n        })\n      );\n    });\n  }\n\n  public createBackupFile(fileName: string): Promise<any> {\n    return this.service.config.createBackupFile(fileName);\n  }\n\n  public addTorrentToCollection(data: any): Promise<any> {\n    if (this.options.defaultCollectionGroupId) {\n      data.groups = [this.options.defaultCollectionGroupId];\n    }\n    return this.service.collection.add(data);\n  }\n\n  public getTorrentCollections(groupId?: string): Promise<any> {\n    return this.service.collection.load(groupId);\n  }\n\n  public deleteTorrentFromCollention(data: any): Promise<any> {\n    if (Array.isArray(data)) {\n      return this.service.collection.remove(data);\n    }\n    return this.service.collection.delete(data);\n  }\n\n  public clearTorrentCollention(): Promise<any> {\n    return this.service.collection.clear();\n  }\n\n  public getTorrentCollention(link: string): Promise<any> {\n    return this.service.collection.get(link);\n  }\n\n  public getSiteSelectorConfig(options: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      const result = this.service.getSiteSelector(options.host, options.name);\n      if (result) {\n        resolve(result);\n      } else {\n        reject(false);\n      }\n    });\n  }\n\n  public resetTorrentCollections(items: any): Promise<any> {\n    return this.service.collection.reset(items);\n  }\n\n  public getTorrentCollectionGroups(): Promise<any> {\n    return this.service.collection.getGroups();\n  }\n\n  public addTorrentCollectionGroup(data: any): Promise<any> {\n    return this.service.collection.addGroup(data);\n  }\n\n  public addTorrentCollectionToGroup(options: any): Promise<any> {\n    return this.service.collection.addToGroup(options.item, options.groupId);\n  }\n\n  public updateTorrentCollectionGroup(data: any): Promise<any> {\n    return this.service.collection.updateGroup(data);\n  }\n\n  public removeTorrentCollectionFromGroup(options: any): Promise<any> {\n    return this.service.collection.removeFromGroup(\n      options.item,\n      options.groupId\n    );\n  }\n\n  public removeTorrentCollectionGroup(data: any): Promise<any> {\n    return this.service.collection.removeGroup(data);\n  }\n\n  public updateTorrentCollention(data: any): Promise<any> {\n    return this.service.collection.update(data);\n  }\n\n  public getAllTorrentCollectionLinks(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      const result = this.service.collection.getAllLinks();\n      if (result) {\n        resolve(result);\n      } else {\n        reject(false);\n      }\n    });\n  }\n\n  public restoreCookies(data: any): Promise<any> {\n    return this.service.config.restoreCookies(data);\n  }\n\n  public resetFavicons(): Promise<any> {\n    this.service.config.favicon.clear();\n    return this.service.config.getFavicons();\n  }\n\n  public resetFavicon(url: string): Promise<any> {\n    return this.service.config.getFavicon(url, true);\n  }\n\n  public getBackupRawData(): Promise<any> {\n    return this.service.config.getBackupRawData();\n  }\n\n  public testBackupServerConnectivity(options: any): Promise<any> {\n    return this.service.config.testBackupServerConnectivity(options);\n  }\n\n  public createSearchResultSnapshot(options: any): Promise<any> {\n    return this.service.searchResultSnapshot.add(options);\n  }\n\n  public getSearchResultSnapshot(id: string): Promise<any> {\n    return this.service.searchResultSnapshot.get(id);\n  }\n\n  public loadSearchResultSnapshot(): Promise<any> {\n    return this.service.searchResultSnapshot.load();\n  }\n\n  public removeSearchResultSnapshot(options: any): Promise<any> {\n    return this.service.searchResultSnapshot.remove(options);\n  }\n\n  public clearSearchResultSnapshot(): Promise<any> {\n    return this.service.searchResultSnapshot.clear();\n  }\n\n  public resetSearchResultSnapshot(datas: any): Promise<any> {\n    return this.service.searchResultSnapshot.reset(datas);\n  }\n\n  public createKeepUploadTask(options: any): Promise<any> {\n    return this.service.keepUploadTask.add(options);\n  }\n\n  public getKeepUploadTask(id: string): Promise<any> {\n    return this.service.keepUploadTask.get(id);\n  }\n\n  public loadKeepUploadTask(): Promise<any> {\n    return this.service.keepUploadTask.load();\n  }\n\n  public removeKeepUploadTask(options: any): Promise<any> {\n    return this.service.keepUploadTask.remove(options);\n  }\n\n  public clearKeepUploadTask(): Promise<any> {\n    return this.service.keepUploadTask.clear();\n  }\n\n  public resetKeepUploadTask(datas: any): Promise<any> {\n    return this.service.keepUploadTask.reset(datas);\n  }\n\n  public updateKeepUploadTask(options: any): Promise<any> {\n    return this.service.keepUploadTask.update(options);\n  }\n\n  public updateDebuggerTabId(id: number): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.debuggerTabId = id;\n      this.debuggerPort = chrome.tabs.connect(id, {\n        name: EModule.debugger\n      });\n      resolve();\n    });\n  }\n\n  public pushDebugMsg(msg: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      console.log(msg);\n      if (this.debuggerTabId) {\n        chrome.tabs.get(this.debuggerTabId, (tab: chrome.tabs.Tab) => {\n          if (tab && this.debuggerPort) {\n            this.debuggerPort.postMessage({\n              action: EAction.pushDebugMsg,\n              data: msg\n            });\n          }\n          if (chrome.runtime.lastError) {\n            console.log(chrome.runtime.lastError.message);\n            this.debuggerTabId = 0;\n            this.debuggerPort = undefined;\n          }\n        });\n      }\n      resolve();\n    });\n  }\n\n  public getTopSearches(count: number = 9): Promise<any> {\n    return this.movieInfoService.getTopSearches(count);\n  }\n}\n"
  },
  {
    "path": "src/background/downloadHistory.ts",
    "content": "import { EConfigKey } from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\n\n/**\n * 下载历史记录操作类\n */\nexport class DownloadHistory {\n  public items: any[] = [];\n  public storage: localStorage = new localStorage();\n\n  constructor() {\n    this.load();\n  }\n\n  /**\n   * 获取下载历史记录\n   */\n  public load(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(EConfigKey.downloadHistory, (result: any) => {\n        this.items = result || [];\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 保存下载记录\n   * @param data 下载链接信息\n   * @param host 站点域名\n   * @param clientId 下载客户端ID\n   */\n  public add(\n    data: any,\n    host: string = \"\",\n    clientId: string = \"\",\n    success: boolean = true\n  ) {\n    let saveData = {\n      data,\n      clientId,\n      host,\n      success,\n      time: new Date().getTime()\n    };\n    if (!this.items) {\n      this.load().then(() => {\n        this.items.push(saveData);\n        this.updateData();\n      });\n    } else {\n      let index = this.items.findIndex((item: any) => {\n        return item.data.url === data.url;\n      });\n      if (index === -1) {\n        this.items.push(saveData);\n        this.updateData();\n      }\n    }\n  }\n\n  /**\n   * 删除下载历史记录\n   * @param items 需要删除的列表\n   */\n  public remove(items: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        for (let index = this.items.length - 1; index >= 0; index--) {\n          let item = this.items[index];\n          let findIndex = items.findIndex((_data: any) => {\n            return _data.data.url === item.data.url;\n          });\n          if (findIndex >= 0) {\n            this.items.splice(index, 1);\n          }\n        }\n        this.updateData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 清除下载记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = [];\n      this.updateData();\n      resolve(this.items);\n    });\n  }\n\n  /**\n   * 重置\n   * @param datas\n   */\n  public reset(datas: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!datas) {\n        reject(false);\n        return;\n      }\n      if (!Array.isArray(datas)) {\n        reject(false);\n        return;\n      }\n\n      this.items = datas;\n      this.updateData();\n\n      resolve(this.items);\n    });\n  }\n\n  private updateData() {\n    this.storage.set(EConfigKey.downloadHistory, this.items);\n  }\n}\n"
  },
  {
    "path": "src/background/downloadQuene.ts",
    "content": "import { DownloadOptions } from \"@/interface/common\";\nimport PTPlugin from \"./service\";\nimport { PPF } from \"@/service/public\";\ntype Service = PTPlugin;\n\nexport default class DownloadQuene {\n  private queues: DownloadOptions[] = [];\n  private isRunning: boolean = false;\n  private timer: number | undefined = undefined;\n  private successCount: number = 0;\n  private failedCount: number = 0;\n\n  constructor(public service: Service) {}\n\n  /**\n   * 添加下载对列\n   * @param options\n   */\n  public add(options: DownloadOptions | DownloadOptions[]) {\n    if (Array.isArray(options)) {\n      this.queues.push(...options);\n    } else {\n      this.queues.push(options);\n    }\n\n    return this;\n  }\n\n  /**\n   * 执行下载队列\n   */\n  public run() {\n    if (this.isRunning) {\n      return this;\n    }\n\n    clearTimeout(this.timer);\n\n    const queue = this.queues.shift();\n\n    if (queue) {\n      this.isRunning = true;\n      const timout = (this.service.options.batchDownloadInterval || 0) * 1000;\n\n      const sender = queue.clientId\n        ? this.service.controller.sendTorrentToClient\n        : this.service.controller.sendTorrentToDefaultClient;\n\n      sender\n        .call(this.service.controller, queue)\n        .then(() => {\n          this.successCount++;\n        })\n        .catch(error => {\n          console.log(error);\n          this.failedCount++;\n        })\n        .finally(() => {\n          this.isRunning = false;\n          // 是否设置了时间间隔\n          if (timout > 0) {\n            this.timer = window.setTimeout(() => {\n              this.run();\n            }, timout);\n          } else {\n            this.run();\n          }\n        });\n    } else {\n      PPF.showNotifications(\n        {\n          message: this.service.i18n.t(\n            \"service.controller.downloadTaskIsCompleted\",\n            {\n              success: this.successCount,\n              failed: this.failedCount\n            }\n          )\n        },\n        10000\n      );\n\n      this.successCount = 0;\n      this.failedCount = 0;\n    }\n\n    return this;\n  }\n}\n"
  },
  {
    "path": "src/background/i18n.ts",
    "content": "import i18next from \"i18next\";\nimport PTPlugin from \"./service\";\nimport { API } from \"@/service/api\";\nimport { i18nResource } from \"@/interface/common\";\n\nexport class i18nService {\n  public loadedLanguages: Array<string> = [];\n  public i18next = i18next;\n  constructor(public service: PTPlugin) {}\n  public init() {\n    i18next.init({\n      interpolation: {\n        prefix: \"{\",\n        suffix: \"}\"\n      }\n    });\n    return this.reset(this.service.options.locale || \"en\");\n  }\n\n  /**\n   * 重设语言\n   * @param langCode 语言代码\n   */\n  public reset(langCode: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      console.log(i18next.language);\n      if (i18next.language !== langCode) {\n        if (!this.loadedLanguages.includes(langCode)) {\n          $.getJSON(`${API.host}/i18n/${langCode}.json`)\n            .done((result: any) => {\n              i18next.addResourceBundle(\n                langCode,\n                \"translation\",\n                result.words,\n                true,\n                true\n              );\n              this.loadedLanguages.push(langCode);\n              i18next.changeLanguage(langCode).then(() => {\n                resolve(langCode);\n              });\n            })\n            .fail(e => {\n              if (langCode != \"en\") {\n                this.reset(\"en\").then(() => {\n                  resolve(langCode);\n                });\n                return;\n              }\n              reject(e);\n            });\n          return;\n        }\n        i18next.changeLanguage(langCode).then(() => {\n          resolve(langCode);\n        });\n        return;\n      }\n      resolve(langCode);\n    });\n  }\n\n  public t(key: any, options?: any): string {\n    return i18next.t(key, options);\n  }\n\n  public add(resource: i18nResource): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (resource.name && resource.code) {\n        if (this.loadedLanguages.includes(resource.code)) {\n          reject();\n        } else {\n          i18next.addResourceBundle(\n            resource.code,\n            \"translation\",\n            resource.words,\n            true,\n            true\n          );\n          this.loadedLanguages.push(resource.code);\n          i18next.changeLanguage(resource.code).then(() => {\n            resolve(resource.code);\n          });\n        }\n      } else {\n        reject();\n      }\n    });\n  }\n\n  /**\n   * 替换已有语言资源\n   * @param resource 语言资源内容\n   */\n  public replace(resource: i18nResource): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (resource.name && resource.code) {\n        if (this.loadedLanguages.includes(resource.code)) {\n          i18next.addResourceBundle(\n            resource.code,\n            \"translation\",\n            resource.words,\n            true,\n            true\n          );\n          i18next.changeLanguage(resource.code).then(() => {\n            resolve(resource.code);\n          });\n        }\n      } else {\n        reject();\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/index.ts",
    "content": "import \"../interface/types.expand\";\nimport PTPlugin from \"./service\";\nimport { filters } from \"../service/filters\";\n\nconst PTService = new PTPlugin();\n\n// 暴露到 window 对象\nObject.assign(window, {\n  PTServiceFilters: filters,\n  PTBackgroundService: PTService,\n  // 用于脚本中使用多语言环境\n  i18n: PTService.i18n\n});\n"
  },
  {
    "path": "src/background/infoParser.ts",
    "content": "import { Dictionary, ERequestResultType } from \"@/interface/common\";\nimport dayjs from \"dayjs\";\nimport { PPF } from \"@/service/public\";\nimport customParseFormat from \"dayjs/plugin/customParseFormat\";\nimport advancedFormat from \"dayjs/plugin/advancedFormat\";\ndayjs.extend(customParseFormat);\ndayjs.extend(advancedFormat);\n\nexport class InfoParser {\n  constructor(public service?: any) { }\n  /**\n   * 根据指定规则和原始获取需要的数据\n   * @param content 原始内容\n   * @param rule 规则配置\n   * @return Dictionary<any>\n   */\n  getResult(content: any, rule: any): Dictionary<any> {\n    let results: Dictionary<any> = {};\n\n    if (content) {\n      for (const key in rule.fields) {\n        if (rule.fields.hasOwnProperty(key)) {\n          let config = rule.fields[key];\n\n          let result = this.getFieldData(content, config, rule);\n          if (result != null) {\n            results[key] = result;\n          }\n        }\n      }\n    }\n\n    return results;\n  }\n\n  private debug(...msg: any[]) {\n    if (this.service) {\n      this.service.debug(...msg);\n    } else {\n      PPF.debug(...msg);\n    }\n  }\n\n  /**\n   * 获取字段信息\n   * @param content 原始内容\n   * @param config 当前字段定义信息\n   * @param rule 选择器规则\n   */\n  getFieldData(content: any, config: any, rule: any) {\n    let query: any;\n    // selectorIndex 表示当前匹配了哪条选择器\n    let selectorIndex: number = 0;\n    let selectors = [];\n    // 直接表达式\n    if (typeof config.selector == \"string\") {\n      selectors.push(config.selector);\n    } else if (config.selector && config.selector.length) {\n      selectors = config.selector;\n    } else {\n      return config.value === undefined ? null : config.value;\n    }\n\n    // 遍历选择器\n    selectors.some((selector: string) => {\n      try {\n        switch (rule.dataType) {\n          case ERequestResultType.JSON:\n            if (selector == \"\") {\n              query = content;\n            } else if (selector.substr(0, 1) == \"[\") {\n              query = eval(\"content\" + selector);\n            } else {\n              query = eval(\"content.\" + selector);\n            }\n            if (query != null) {\n              return true;\n            }\n            break;\n\n          case ERequestResultType.TEXT:\n          case ERequestResultType.HTML:\n          default:\n            if (selector == \"\") {\n              query = content;\n            } else {\n              query = content.find(selector);\n              if (query.length == 0)query = content.filter(selector)\n            }\n\n            if (query.length > 0) {\n              return true;\n            }\n            break;\n        }\n\n        selectorIndex++;\n      } catch (error) {\n        this.debug(\n          \"InfoParser.getFieldData.Error\",\n          selector,\n          error.message,\n          error.stack\n        );\n        return true;\n      }\n    });\n\n    let result = null;\n    // 该变量 dateTime 用于 eval 内部执行，不可删除或改名\n    let dateTime = dayjs;\n    let _self = this;\n    if (query != null) {\n      if (config.attribute || config.filters || config.switchFilters) {\n        if (config.attribute && rule.dataType != ERequestResultType.JSON) {\n          query = query.attr(config.attribute);\n        }\n\n        let filters: any;\n\n        // 按 selectorIndex 来选择\n        if (config.switchFilters) {\n          filters = config.switchFilters[selectorIndex] || null;\n        } else {\n          filters = config.filters;\n        }\n\n        if (filters) {\n          filters.every((filter: string) => {\n            try {\n              query = eval(filter);\n            } catch (error) {\n              this.debug(\n                \"InfoParser.filter.Error\",\n                filter,\n                error.message,\n                error.stack\n              );\n              query = null;\n              return false;\n            }\n            return true;\n          });\n        }\n        result = query;\n      } else {\n        switch (rule.dataType) {\n          case ERequestResultType.JSON:\n            result = query;\n            break;\n\n          default:\n            result = query.text().trim();\n            break;\n        }\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 获取指定数组的合计尺寸\n   * @param datas 表示大小的数组\n   */\n  getTotalSize(datas: string[]) {\n    let total: number = 0.0;\n\n    datas.forEach((item: string) => {\n      let match = item.match(/^(\\d*\\.?\\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB)?)$/i);\n      if (!match) {\n        return;\n      }\n      let size = parseFloat(match[1]);\n      let unit = match[3].toLowerCase();\n      switch (true) {\n        case /ki?b/.test(unit):\n          total += size * Math.pow(2, 10);\n          break;\n\n        case /mi?b/.test(unit):\n          total += size * Math.pow(2, 20);\n          break;\n\n        case /gi?b/.test(unit):\n          total += size * Math.pow(2, 30);\n          break;\n\n        case /ti?b?/.test(unit):\n          total += size * Math.pow(2, 40);\n          break;\n\n        case /pi?b?/.test(unit):\n          total += size * Math.pow(2, 50);\n          break;\n\n        case /ei?b?/.test(unit):\n          total += size * Math.pow(2, 60);\n          break;\n\n        case /zi?b?/.test(unit):\n          total += size * Math.pow(2, 70);\n          break;\n      }\n    });\n\n    return total;\n  }\n\n  /**\n   * 获取指定数组的合计尺寸\n   * @param imdbId 表示大小的数组\n   */\n  formatIMDbId(imdbId: string) {\n    if (Number(imdbId))\n    {\n      if (imdbId.length < 7)\n        imdbId = imdbId.padStart(7, '0');\n      \n      imdbId = \"tt\" + imdbId;\n    }\n    return imdbId;\n  }\n}\n"
  },
  {
    "path": "src/background/keepUploadTask.ts",
    "content": "import { IKeepUploadTask, EConfigKey } from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\nimport { PPF } from \"@/service/public\";\n\ninterface IDetail extends IKeepUploadTask {}\n/**\n * 辅种任务\n */\nexport default class KeepUploadTask {\n  public items: IDetail[] = [];\n  public storage: localStorage = new localStorage();\n\n  private configKey = EConfigKey.keepUploadTask;\n\n  constructor() {\n    this.load();\n  }\n\n  /**\n   * 获取历史记录\n   */\n  public load(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(this.configKey, (result: any) => {\n        let data = {\n          items: [] as IDetail[]\n        };\n\n        if (Array.isArray(result)) {\n          data.items = result;\n        } else if (result) {\n          data = Object.assign(data, result);\n        }\n\n        this.items = data.items || [];\n\n        console.log(result);\n\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 添加新记录\n   * @param newItem\n   */\n  public add(newItem: IDetail): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let saveData = Object.assign(\n        {\n          time: new Date().getTime(),\n          id: PPF.getNewId()\n        },\n        newItem\n      );\n\n      this.items.push(saveData);\n      this.updateData();\n      resolve(this.items);\n    });\n  }\n\n  /**\n   * 更新指定的记录\n   * @param item\n   */\n  public update(item: IDetail): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        let index = this.items.findIndex((data: IDetail) => {\n          return data.id === item.id;\n        });\n        if (index >= 0) {\n          this.items[index] = item;\n        }\n\n        this.updateData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  private updateData() {\n    this.storage.set(this.configKey, {\n      items: this.items\n    });\n  }\n\n  /**\n   * 获取指定的内容\n   * @param id\n   */\n  public get(id: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        let item = this.items.find((data: IDetail) => {\n          return data.id === id;\n        });\n        if (item) {\n          resolve(item);\n        } else {\n          reject(false);\n        }\n      });\n    });\n  }\n\n  /**\n   * 删除单个记录\n   * @param item\n   */\n  public delete(item: IDetail): Promise<any> {\n    return this.remove([item]);\n  }\n\n  /**\n   * 删除历史记录\n   * @param items 需要删除的列表\n   */\n  public remove(items: IDetail[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        for (let index = this.items.length - 1; index >= 0; index--) {\n          let item: IDetail = this.items[index];\n          let findIndex = items.findIndex((data: IDetail) => {\n            return data.id === item.id;\n          });\n          if (findIndex >= 0) {\n            this.items.splice(index, 1);\n          }\n        }\n        this.updateData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 清除历史记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = [];\n      this.updateData();\n      resolve([]);\n    });\n  }\n\n  /**\n   * 重置\n   * @param datas\n   */\n  public reset(datas: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!datas) {\n        reject(false);\n        return;\n      }\n      if (!Array.isArray(datas)) {\n        reject(false);\n        return;\n      }\n\n      this.items = datas;\n      this.updateData();\n\n      resolve(this.items);\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/omnibox.ts",
    "content": "import PTPlugin from \"./service\";\nimport { SearchSolution } from \"@/interface/common\";\n\ntype Service = PTPlugin;\n\n/**\n * 搜索建议\n */\nexport class OmniBox {\n  private splitString = \" → \";\n  constructor(public service: Service) {\n    this.initEvents();\n  }\n\n  public init() {}\n\n  private initEvents() {\n    if (chrome && chrome.omnibox) {\n      // 当用户在浏览器搜索栏输入PT按 tab键进入时，提供前5个搜索方案建议\n      chrome.omnibox.onInputChanged.addListener((text, suggest) => {\n        if (!text) return;\n        if (this.service.options.searchSolutions) {\n          let result: any[] = [];\n          this.service.options.searchSolutions.forEach(\n            (solution: SearchSolution) => {\n              result.push({\n                content: `${solution.name}${this.splitString}${text}`,\n                description: this.service.i18n.t(\"service.omnibox.search\", {\n                  solutionName: solution.name,\n                  text\n                })\n                //  `在《${\n                //   solution.name\n                // }》方案中搜索 “${text}” 的相关种子`\n              });\n            }\n          );\n\n          if (result.length > 0) {\n            suggest(result);\n          }\n        }\n      });\n\n      // 当用户接收关键字建议时触发\n      chrome.omnibox.onInputEntered.addListener(text => {\n        let solutionName = \"\";\n        let solutionId = \"\";\n        let key = \"\";\n        if (text.indexOf(this.splitString) != -1) {\n          [solutionName, key] = text.split(this.splitString);\n          if (solutionName && this.service.options.searchSolutions) {\n            let solution = this.service.options.searchSolutions.find(\n              (item: SearchSolution) => {\n                return item.name == solutionName;\n              }\n            );\n            if (solution) {\n              solutionId = solution.id;\n            }\n          }\n        } else {\n          key = text;\n        }\n\n        // 按关键字进行搜索\n        chrome.tabs.create({\n          url:\n            \"index.html#/search-torrent/\" +\n            key +\n            (solutionId ? `/${solutionId}` : \"\")\n        });\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "src/background/pageParser.ts",
    "content": "import {\n  IPageSelector,\n  Dictionary,\n  Site,\n  ERequestResultType,\n  ERequestMethod\n} from \"@/interface/common\";\nimport { PPF } from \"@/service/public\";\nimport { APP } from \"@/service/api\";\nimport { InfoParser } from \"./infoParser\";\nimport md5 from \"blueimp-md5\";\n\n/**\n * 通用页面数据解析类\n */\nexport class PageParser {\n  private infoParserCache: Dictionary<any> = {};\n  private cacheKey = \"\";\n  private url = \"\";\n  private requestData: any;\n  private resultData: any;\n\n  /**\n   * 初始化\n   * @param options 解析配置\n   * @param site 站点\n   * @param timeout\n   * @param commonDatas 指定的通用数据\n   */\n  constructor(\n    public options: IPageSelector,\n    public site: Site,\n    public timeout: number = 30000,\n    public commonDatas?: Dictionary<any>\n  ) {\n    let url: string = site.url + \"\";\n\n    // 如果有自定义地址，则使用自定义地址\n    if (site.cdn && site.cdn.length > 0) {\n      url = site.cdn[0];\n    }\n\n    if ((url + \"\").substr(-1) != \"/\") {\n      url += \"/\";\n    }\n\n    let page = this.options.page;\n    if ((page + \"\").substr(0, 1) == \"/\") {\n      page = (page + \"\").substr(1);\n    }\n\n    this.url = (url + page)\n      .replace(\"://\", \"****\")\n      .replace(/\\/\\//g, \"/\")\n      .replace(\"****\", \"://\");\n\n    this.requestData = this.options.requestData;\n    if (this.requestData && this.commonDatas) {\n      try {\n        for (const key in this.requestData) {\n          if (this.requestData.hasOwnProperty(key)) {\n            const value = this.requestData[key];\n            for (const commonKey in this.commonDatas) {\n              if (this.commonDatas.hasOwnProperty(commonKey)) {\n                this.requestData[key] = PPF.replaceKeys(\n                  value,\n                  this.commonDatas[commonKey],\n                  commonKey\n                );\n              }\n            }\n          }\n        }\n      } catch (error) {\n        console.log(error);\n      }\n    }\n\n    this.cacheKey = md5(this.url + JSON.stringify(this.requestData || {}));\n  }\n\n  /**\n   * 获取缓存\n   */\n  private getCache() {\n    let result = window.localStorage.getItem(this.cacheKey);\n    if (result) {\n      let json = JSON.parse(result);\n      if (json.data && json.time) {\n        let time = new Date().getTime();\n\n        if (json.time < time) {\n          window.localStorage.removeItem(this.cacheKey);\n          return null;\n        }\n\n        return json.data;\n      }\n    }\n    return null;\n  }\n\n  /**\n   * 设置缓存\n   */\n  private setCache() {\n    if (this.options.dataCacheTime && this.options.dataCacheTime > 0) {\n      let cache = {\n        data: this.resultData,\n        time: new Date().getTime() + this.options.dataCacheTime * 1000\n      };\n      window.localStorage.setItem(this.cacheKey, JSON.stringify(cache));\n    }\n  }\n\n  /**\n   * 获取数据\n   */\n  public getInfos(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let cache = this.getCache();\n      if (cache) {\n        resolve(cache);\n        return;\n      }\n\n      /**\n       * 是否有脚本解析器\n       */\n      if (this.options.parser && this.site) {\n        if (this.runParser(resolve, reject)) {\n          return;\n        }\n      }\n\n      let request = $.ajax({\n        url: this.url,\n        method: this.options.requestMethod || ERequestMethod.GET,\n        dataType: \"text\",\n        headers: this.options.headers,\n        data: this.requestData,\n        timeout: this.timeout\n      })\n        .done(result => {\n          let content: any;\n          try {\n            if (this.options.dataType !== ERequestResultType.JSON) {\n              let doc = new DOMParser().parseFromString(result, \"text/html\");\n              // 构造 jQuery 对象\n              let topElement = this.options.topElement || \"body\";\n              content = $(doc).find(topElement);\n            } else {\n              content = JSON.parse(result);\n            }\n          } catch (error) {\n            reject(error);\n            return;\n          }\n\n          if (content && this.options) {\n            try {\n              let results = new InfoParser().getResult(content, this.options);\n              this.resultData = results;\n              this.setCache();\n              resolve(results);\n            } catch (error) {\n              reject(error);\n            }\n          }\n        })\n        .fail(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 执行脚本解析器\n   * @param rule\n   * @param site\n   * @param userInfo\n   * @param resolve\n   * @param reject\n   */\n  public runParser(resolve?: any, reject?: any): boolean {\n    if (!this.site || !this.options.parser) {\n      return false;\n    }\n    let siteConfigPath =\n      this.site.schema == \"publicSite\" ? \"publicSites\" : \"sites\";\n\n    if (this.site.path) {\n      siteConfigPath += `/${this.site.path}`;\n    } else {\n      siteConfigPath += `/${this.site.host}`;\n    }\n\n    let path = this.options.parser;\n    // 判断是否为相对路径\n    if (path.substr(0, 1) !== \"/\" && path.substr(0, 4) !== \"http\") {\n      path = `${siteConfigPath}/${path}`;\n    }\n\n    // 传递给解析解析的参数\n    let _options = {\n      site: this.site,\n      rule: this.options,\n      commonDatas: this.commonDatas,\n      resolve,\n      reject\n    };\n\n    // 当前对象\n    let _self = this;\n\n    let script = this.infoParserCache[path];\n    if (script) {\n      eval(script);\n    } else {\n      APP.getScriptContent(path).done(script => {\n        this.infoParserCache[path] = script;\n        eval(script);\n      });\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "src/background/plugins/OWSS.ts",
    "content": "import { IBackupServer, ERequestMethod } from \"@/interface/common\";\nimport { FileDownloader } from \"@/service/downloader\";\n\n/**\n * OWSS 客户端实现\n * @see https://github.com/ronggang/OWSS\n */\nexport class OWSS {\n  public serverURL: string = \"\";\n  constructor(public options: IBackupServer) {\n    this.serverURL = this.options.address;\n    if (this.serverURL.substr(-1) !== \"/\") {\n      this.serverURL += \"/\";\n    }\n  }\n\n  /**\n   * 发送指定的请求\n   * @param action 指令\n   * @param method 请求方法（GET，POST）\n   * @param data 请求数据\n   */\n  private request(\n    action: string,\n    method: ERequestMethod = ERequestMethod.GET,\n    data?: any\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let options: JQuery.AjaxSettings = {\n        url: this.serverURL + `${action}`,\n        method: method,\n        dataType: \"json\",\n        data\n      };\n\n      if (method === ERequestMethod.POST) {\n        options.processData = false;\n        options.contentType = false;\n      }\n\n      $.ajax(options)\n        .done(result => {\n          resolve(result);\n        })\n        .fail(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 申请资源ID\n   */\n  public create(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.request(\"create\")\n        .then(result => {\n          if (result && result.data) {\n            resolve(result.data);\n          } else {\n            reject();\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 添加文件\n   * @param formData\n   */\n  public add(formData: FormData): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.request(\n        `${this.options.authCode}/add`,\n        ERequestMethod.POST,\n        formData\n      )\n        .then(result => {\n          if (result && result.data === true) {\n            resolve(true);\n          } else {\n            reject(false);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取（下载）一个文件\n   * @param path\n   * @returns 返回一个 blob 对象\n   */\n  public get(path: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let url = `${this.serverURL}${this.options.authCode}/get/${path}`;\n      let file = new FileDownloader({\n        url,\n        getDataOnly: true\n      });\n\n      file.onCompleted = () => {\n        resolve(file.content);\n      };\n\n      file.onError = (e: any) => {\n        console.log(e);\n        reject(e);\n      };\n\n      file.start();\n    });\n  }\n\n  /**\n   * 删除一个文件\n   * @param path\n   */\n  public delete(path: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.request(\n        `${this.options.authCode}/delete/${path}`,\n        ERequestMethod.POST\n      )\n        .then(result => {\n          if (result && result.data) {\n            resolve(result.data);\n          } else {\n            reject(false);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取资源列表\n   * @param options\n   */\n  public list(options: any = {}): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.request(`${this.options.authCode}/list`, ERequestMethod.GET, options)\n        .then(result => {\n          if (result && result.data) {\n            resolve(result.data);\n          } else {\n            reject(false);\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 验证服务器可用性\n   */\n  public ping(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.request(`${this.options.authCode}/list`, ERequestMethod.GET, {\n        pageSize: 1\n      })\n        .then(() => {\n          resolve(true);\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/plugins/WebDAV.ts",
    "content": "import {\n  IBackupServer,\n  EResourceOrderBy,\n  EResourceOrderMode\n} from \"@/interface/common\";\nimport { createClient as WebDAVClient } from \"webdav\";\n\nexport class WebDAV {\n  private service: any;\n  constructor(public options: IBackupServer) {\n    this.initServer();\n  }\n\n  /**\n   * 初始化服务器\n   */\n  private initServer() {\n    this.service = WebDAVClient(this.options.address, {\n      username: this.options.loginName,\n      password: this.options.loginPwd,\n      digest: this.options.digest ? true : undefined\n    });\n  }\n\n  /**\n   * 获取资源列表\n   * @param options\n   */\n  public list(options: any = {}): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .getDirectoryContents(\"/\", { glob: \"*.zip\" })\n        .then((data: any[]) => {\n          console.log(data);\n\n          let result: any[] = [];\n          let orderMode: EResourceOrderMode =\n            options.orderMode || EResourceOrderMode.desc;\n          if (data && data.length > 0) {\n            data.forEach((item: any) => {\n              result.push({\n                name: item.basename,\n                size: item.size,\n                type: item.type,\n                time: new Date(item.lastmod).getTime()\n              });\n            });\n          }\n          resolve(\n            result.sort((a, b) => {\n              let v1, v2;\n              switch (options.orderBy) {\n                case EResourceOrderBy.name:\n                  v1 = a.name;\n                  v2 = b.name;\n                  break;\n\n                case EResourceOrderBy.size:\n                  v1 = a.size;\n                  v2 = b.size;\n                  break;\n\n                default:\n                  v1 = a.time;\n                  v2 = b.time;\n                  break;\n              }\n\n              if (orderMode === EResourceOrderMode.desc) {\n                return v1.toString().localeCompare(v2) == 1 ? -1 : 1;\n              } else {\n                return v1.toString().localeCompare(v2);\n              }\n            })\n          );\n        })\n        .catch((error: any) => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取（下载）一个文件\n   * @param path\n   * @returns 返回一个 binary 数据\n   */\n  public get(path: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .getFileContents(\"/\" + path)\n        .then((result: any) => {\n          resolve(result);\n        })\n        .catch((error: any) => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 添加文件\n   * @param formData\n   */\n  public add(formData: FormData): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .putFileContents(formData.get(\"name\"), formData.get(\"data\"))\n        .then((result: any) => {\n          if (result) {\n            resolve(true);\n          } else {\n            reject(false);\n          }\n        })\n        .catch((error: any) => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 删除一个文件\n   * @param path\n   */\n  public delete(path: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .deleteFile(\"/\" + path)\n        .then((result: any) => {\n          if (result) {\n            resolve(result.data);\n          } else {\n            reject(false);\n          }\n        })\n        .catch((error: any) => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 验证服务器可用性\n   */\n  public ping(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.service\n        .getDirectoryContents(\"/\")\n        .then(() => {\n          resolve(true);\n        })\n        .catch((error: any) => {\n          reject(error);\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/searchResultSnapshot.ts",
    "content": "import { ISearchResultSnapshot, EConfigKey } from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\nimport { PPF } from \"@/service/public\";\n\n/**\n * 搜索快照\n */\nexport default class SearchResultSnapshot {\n  public items: ISearchResultSnapshot[] = [];\n  public storage: localStorage = new localStorage();\n\n  private configKey = EConfigKey.searchResultSnapshot;\n\n  constructor() {\n    this.load();\n  }\n\n  /**\n   * 获取历史记录\n   */\n  public load(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(this.configKey, (result: any) => {\n        let data = {\n          items: [] as ISearchResultSnapshot[]\n        };\n\n        if (Array.isArray(result)) {\n          data.items = result;\n        } else if (result) {\n          data = Object.assign(data, result);\n        }\n\n        this.items = data.items || [];\n\n        console.log(result);\n\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 添加新记录\n   * @param newItem\n   */\n  public add(newItem: ISearchResultSnapshot): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let saveData = Object.assign(\n        {\n          time: new Date().getTime(),\n          id: PPF.getNewId()\n        },\n        newItem\n      );\n\n      this.items.push(saveData);\n      this.updateData();\n      resolve(this.items);\n    });\n  }\n\n  private updateData() {\n    this.storage.set(this.configKey, {\n      items: this.items\n    });\n  }\n\n  /**\n   * 获取指定的内容\n   * @param id\n   */\n  public get(id: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        let item = this.items.find((data: ISearchResultSnapshot) => {\n          return data.id === id;\n        });\n        if (item) {\n          resolve(item);\n        } else {\n          reject(false);\n        }\n      });\n    });\n  }\n\n  /**\n   * 删除单个记录\n   * @param item\n   */\n  public delete(item: ISearchResultSnapshot): Promise<any> {\n    return this.remove([item]);\n  }\n\n  /**\n   * 删除历史记录\n   * @param items 需要删除的列表\n   */\n  public remove(items: ISearchResultSnapshot[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        for (let index = this.items.length - 1; index >= 0; index--) {\n          let item: ISearchResultSnapshot = this.items[index];\n          let findIndex = items.findIndex((data: ISearchResultSnapshot) => {\n            return data.id === item.id;\n          });\n          if (findIndex >= 0) {\n            this.items.splice(index, 1);\n          }\n        }\n        this.updateData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 清除历史记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = [];\n      this.updateData();\n      resolve([]);\n    });\n  }\n\n  /**\n   * 重置\n   * @param datas\n   */\n  public reset(datas: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!datas) {\n        reject(false);\n        return;\n      }\n      if (!Array.isArray(datas)) {\n        reject(false);\n        return;\n      }\n\n      this.items = datas;\n      this.updateData();\n\n      resolve(this.items);\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/searcher.ts",
    "content": "import {\n  Site,\n  SiteSchema,\n  Dictionary,\n  Options,\n  DataResult,\n  EDataResultType,\n  SearchEntry,\n  EModule,\n  ERequestResultType,\n  SearchEntryConfigArea,\n  SearchEntryConfig,\n  ISearchPayload,\n  SiteCategories,\n  SiteCategory,\n  ERequestMethod,\n  BASE_TAG_COLORS,\n  ERequestType\n} from \"@/interface/common\";\nimport { APP } from \"@/service/api\";\nimport { SiteService } from \"./site\";\nimport PTPlugin from \"./service\";\nimport extend from \"extend\";\nimport { InfoParser } from \"./infoParser\";\nimport { PPF } from \"@/service/public\";\nimport { PageParser } from \"./pageParser\";\n\nexport type SearchConfig = {\n  site?: Site;\n  entry?: SearchEntry[];\n  rootPath?: string;\n  torrentTagSelectors?: any[];\n};\n\n/**\n * 搜索结果解析状态\n */\nexport enum ESearchResultParseStatus {\n  success = \"success\",\n  needLogin = \"needLogin\",\n  noTorrents = \"noTorrents\",\n  torrentTableIsEmpty = \"torrentTableIsEmpty\",\n  parseError = \"parseError\"\n}\n\nObject.assign(window, {\n  ESearchResultParseStatus\n});\n\n/**\n * 搜索类\n */\nexport class Searcher {\n  // 搜索入口定义缓存\n  private searchConfigs: any = {};\n  // 解析文件内容缓存\n  private parseScriptCache: any = {};\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n\n  private searchRequestQueue: Dictionary<JQueryXHR> = {};\n\n  constructor(public service: PTPlugin) { }\n\n  /**\n   * 搜索种子\n   * @param site 需要搜索的站点\n   * @param key 需要搜索的关键字\n   * @param payload 附加数据\n   */\n  public searchTorrent(\n    site: Site,\n    key: string = \"\",\n    payload?: ISearchPayload\n  ): Promise<any> {\n    this.service.debug(\"searchTorrent: start\", key, payload);\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let result: DataResult = {\n        success: false\n      };\n\n      let siteService: SiteService = new SiteService(\n        PPF.clone(site),\n        PPF.clone(this.options)\n      );\n      let searchConfig: SearchConfig = {};\n      let schema = this.getSiteSchema(site);\n      let host = site.host as string;\n      // 当前站点默认搜索页\n      let siteSearchPage = \"\";\n      // 当前站点默认搜索配置信息\n      let searchEntryConfig: SearchEntryConfig | undefined = extend(\n        true,\n        {\n          torrentTagSelectors: []\n        },\n        schema && schema.searchEntryConfig ? schema.searchEntryConfig : {},\n        siteService.options.searchEntryConfig\n      );\n      let searchEntryConfigQueryString = \"\";\n\n      if (siteService.options.searchEntry) {\n        searchConfig.rootPath = `sites/${host}/`;\n        searchConfig.entry = siteService.options.searchEntry;\n      } else if (schema && schema.searchEntry) {\n        searchConfig.rootPath = `schemas/${schema.name}/`;\n        searchConfig.entry = schema.searchEntry;\n      }\n\n      if (schema && schema.torrentTagSelectors) {\n        searchConfig.torrentTagSelectors = schema.torrentTagSelectors;\n      }\n\n      if (siteService.options.torrentTagSelectors) {\n        // 是否合并 Schema 的标签选择器\n        if (siteService.options.mergeSchemaTagSelectors) {\n          searchConfig.torrentTagSelectors = siteService.options.torrentTagSelectors.concat(\n            searchConfig.torrentTagSelectors\n          );\n        } else {\n          searchConfig.torrentTagSelectors =\n            siteService.options.torrentTagSelectors;\n        }\n      }\n\n      if (!searchConfig.entry) {\n        result.msg = this.service.i18n.t(\n          \"service.searcher.siteSearchConfigEntryIsEmpty\",\n          {\n            site\n          }\n        ); //`该站点[${site.name}]未配置搜索页面，请先配置`;\n        result.type = EDataResultType.error;\n        reject(result);\n        this.service.debug(\"searchTorrent: tip\");\n        return;\n      }\n\n      // 提取 IMDb 编号，如果带整个网址，则只取编号部分\n      let imdb = key.match(/(tt\\d+)/);\n      let autoMatched = false;\n      if (imdb && imdb.length >= 2) {\n        key = imdb[1];\n      }\n\n      // 将所有 . 替换为空格\n      key = key.replace(/\\./g, \" \");\n\n      // 是否有搜索入口配置项\n      if (searchEntryConfig && searchEntryConfig.page) {\n        siteSearchPage = searchEntryConfig.page;\n        searchEntryConfigQueryString = searchEntryConfig.queryString + \"\";\n\n        // 搜索区域\n        if (searchEntryConfig.area) {\n          searchEntryConfig.area.some((area: SearchEntryConfigArea) => {\n            // 是否有自动匹配关键字的正则\n            if (\n              area.keyAutoMatch &&\n              new RegExp(area.keyAutoMatch, \"\").test(key)\n            ) {\n              // 是否替换默认页面\n              if (area.page) {\n                siteSearchPage = area.page;\n              }\n              autoMatched = true;\n              // 如果有定义查询字符串，则替换默认的查询字符串\n              if (area.queryString) {\n                searchEntryConfigQueryString = area.queryString;\n              }\n\n              // 追加查询字符串\n              if (area.appendQueryString) {\n                searchEntryConfigQueryString += area.appendQueryString;\n              }\n\n              // 替换关键字\n              if (area.replaceKey) {\n                key = key.replace(\n                  new RegExp(area.replaceKey[0], \"g\"),\n                  area.replaceKey[1]\n                );\n              }\n\n              // 解析脚本，最终返回搜索关键字，可调用 payload 里的数据进行关键字替换\n              if (area.parseScript) {\n                try {\n                  key = eval(area.parseScript);\n                } catch (error) { }\n              }\n\n              return true;\n            }\n            return false;\n          });\n        }\n      }\n\n      this.searchConfigs[host] = searchConfig;\n\n      let results: any[] = [];\n      let entryCount = 0;\n      let doneCount = 0;\n\n      const KEY = \"$key$\";\n      // for some json post API\n      if (!searchEntryConfig.keepOriginKey) {\n        // 转换 uri\n        key = encodeURIComponent(key);\n      }\n      // 遍历需要搜索的入口\n      searchConfig.entry.forEach((entry: SearchEntry) => {\n        let searchPage = entry.entry || siteSearchPage;\n\n        // 当已自动匹配规则时，去除入口页面中已指定的关键字字段\n        if (\n          autoMatched &&\n          searchPage.indexOf(KEY) !== -1 &&\n          searchEntryConfigQueryString.indexOf(KEY) !== -1\n        ) {\n          searchPage = PPF.removeQueryStringFromValue(searchPage, KEY);\n        }\n\n        let queryString = entry.queryString;\n\n        if (searchEntryConfigQueryString) {\n          // 当前入口没有查询字符串时，尝试使用默认配置\n          if (!queryString) {\n            queryString = searchEntryConfigQueryString;\n\n            // 当前入口有查询字符串，并且不包含搜索关键字时，使用追加方式\n          } else if (queryString && queryString.indexOf(KEY) === -1) {\n            queryString = searchEntryConfigQueryString + \"&\" + queryString;\n          }\n        }\n\n        if (entry.appendQueryString) {\n          queryString += entry.appendQueryString;\n        }\n        if (searchEntryConfig) {\n          entry.parseScriptFile =\n            searchEntryConfig.parseScriptFile || entry.parseScriptFile;\n          entry.resultType = searchEntryConfig.resultType || entry.resultType;\n          entry.requestDataType = searchEntryConfig.requestDataType || entry.requestDataType;\n          entry.resultSelector =\n            searchEntryConfig.resultSelector || entry.resultSelector;\n          entry.headers = searchEntryConfig.headers || entry.headers;\n          entry.asyncParse = searchEntryConfig.asyncParse || entry.asyncParse;\n          entry.requestData = searchEntryConfig.requestData;\n        }\n\n        // 判断是否指定了搜索页和用于获取搜索结果的脚本\n        if (searchPage && entry.parseScriptFile && entry.enabled !== false) {\n          let rows: number =\n            this.options.search && this.options.search.rows\n              ? this.options.search.rows\n              : 10;\n\n          // 如果有自定义地址，则使用自定义地址\n          if (site.cdn && site.cdn.length > 0) {\n            site.url = site.cdn[0];\n          }\n\n          // 组织搜索入口\n          if ((site.url + \"\").substr(-1) != \"/\") {\n            site.url += \"/\";\n          }\n          if ((searchPage + \"\").substr(0, 1) == \"/\") {\n            searchPage = (searchPage + \"\").substr(1);\n          }\n          let url: string = site.url + searchPage;\n\n          if (queryString) {\n            if (searchPage.indexOf(\"?\") !== -1) {\n              url += \"&\" + queryString;\n            } else {\n              url += \"?\" + queryString;\n            }\n          }\n\n          // 支除重复的参数\n          url = PPF.removeDuplicateQueryString(url);\n\n          let searchKey =\n            key +\n            (entry.appendToSearchKeyString\n              ? ` ${entry.appendToSearchKeyString}`\n              : \"\");\n          url = this.replaceKeys(url, {\n            key: searchKey,\n            rows: rows,\n            passkey: site.passkey ? site.passkey : \"\"\n          });\n\n          // 替换要提交数据中包含的关键字内容\n          if (entry.requestData) {\n            try {\n              for (const key in entry.requestData) {\n                if (entry.requestData.hasOwnProperty(key)) {\n                  const value = entry.requestData[key];\n                  if (typeof value !== 'string') continue\n                  entry.requestData[key] = PPF.replaceKeys(value, {\n                    key: searchKey,\n                    passkey: site.passkey ? site.passkey : \"\"\n                  });\n\n                  if (site.user) {\n                    entry.requestData[key] = PPF.replaceKeys(\n                      entry.requestData[key],\n                      site.user,\n                      \"user\"\n                    );\n                  }\n                }\n              }\n            } catch (error) {\n              this.service.writeErrorLog(error);\n              this.service.debug(error);\n            }\n          }\n          // 替换要提交请求头中的内容\n          if (entry.headers) {\n            for (const key in entry.headers) {\n              if (entry.headers.hasOwnProperty(key)) {\n                const value = entry.headers[key];\n                entry.headers[key] = PPF.replaceKeys(value, {\n                  key: searchKey,\n                  passkey: site.passkey ? site.passkey : \"\"\n                });\n\n                if (site.user) {\n                  entry.headers[key] = PPF.replaceKeys(\n                    entry.headers[key],\n                    site.user,\n                    \"user\"\n                  );\n                }\n              }\n            }\n          }\n          // 替换用户相关信息\n          if (site.user) {\n            url = this.replaceKeys(url, site.user, \"user\");\n          }\n\n          entryCount++;\n\n          let scriptPath = entry.parseScriptFile;\n          // 判断是否为相对路径\n          if (scriptPath.substr(0, 1) !== \"/\") {\n            scriptPath = `${searchConfig.rootPath}${scriptPath}`;\n          }\n\n          entry.parseScript = this.parseScriptCache[scriptPath];\n\n          if (!entry.parseScript) {\n            this.service.debug(\"searchTorrent: getScriptContent\", scriptPath);\n            APP.getScriptContent(scriptPath)\n              .done((script: string) => {\n                this.service.debug(\n                  \"searchTorrent: getScriptContent done\",\n                  scriptPath\n                );\n                this.parseScriptCache[scriptPath] = script;\n                entry.parseScript = script;\n                this.getSearchResult(\n                  url,\n                  site,\n                  Object.assign(PPF.clone(searchEntryConfig), PPF.clone(entry)),\n                  searchConfig.torrentTagSelectors\n                )\n                  .then((result: any) => {\n                    this.service.debug(\n                      \"searchTorrent: getSearchResult done\",\n                      url\n                    );\n                    if (result && result.length) {\n                      results.push(...result);\n                    }\n                    doneCount++;\n\n                    if (doneCount === entryCount || results.length >= rows) {\n                      resolve(results.slice(0, rows));\n                    }\n                  })\n                  .catch((result: any) => {\n                    this.service.debug(\n                      \"searchTorrent: getSearchResult catch\",\n                      url,\n                      result\n                    );\n                    doneCount++;\n\n                    if (doneCount === entryCount) {\n                      if (results.length > 0) {\n                        resolve(results.slice(0, rows));\n                      } else {\n                        reject(result);\n                      }\n                    }\n                  });\n              })\n              .fail(error => {\n                this.service.debug(\n                  \"searchTorrent: getScriptContent fail\",\n                  error\n                );\n              });\n          } else {\n            this.getSearchResult(\n              url,\n              site,\n              Object.assign(PPF.clone(searchEntryConfig), PPF.clone(entry)),\n              searchConfig.torrentTagSelectors\n            )\n              .then((result: any) => {\n                if (result && result.length) {\n                  results.push(...result);\n                }\n                doneCount++;\n\n                if (doneCount === entryCount || results.length >= rows) {\n                  resolve(results.slice(0, rows));\n                }\n              })\n              .catch((result: any) => {\n                doneCount++;\n\n                if (doneCount === entryCount) {\n                  if (results.length > 0) {\n                    resolve(results.slice(0, rows));\n                  } else {\n                    reject(result);\n                  }\n                }\n              });\n          }\n        }\n      });\n\n      // 没有指定搜索入口\n      if (entryCount == 0) {\n        result.msg = this.service.i18n.t(\n          \"service.searcher.siteSearchEntryIsEmpty\",\n          {\n            site\n          }\n        ); //`该站点[${site.name}]未指定搜索页面，请先指定一个搜索入口`;\n        result.type = EDataResultType.error;\n        reject(result);\n      }\n\n      this.service.debug(\"searchTorrent: quene done\");\n    });\n  }\n\n  /**\n   * 获取搜索结果\n   * @param url\n   * @param site\n   * @param entry\n   * @param torrentTagSelectors\n   */\n  public getSearchResult(\n    url: string,\n    site: Site,\n    entry: SearchEntry,\n    torrentTagSelectors?: any[]\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      // 是否有需要搜索前处理的数据\n      if (entry.beforeSearch) {\n        let pageParser = new PageParser(\n          entry.beforeSearch,\n          site,\n          this.service.options.connectClientTimeout,\n        );\n        pageParser\n          .getInfos()\n          .then(beforeSearchData => {\n            this.addSearchRequestQueue(\n              url,\n              site,\n              entry,\n              torrentTagSelectors,\n              beforeSearchData\n            )\n              .then(result => {\n                resolve(result);\n              })\n              .catch(error => {\n                reject(error);\n              });\n          })\n          .catch(error => {\n            this.service.writeErrorLog(error);\n            this.addSearchRequestQueue(url, site, entry, torrentTagSelectors)\n              .then(result => {\n                resolve(result);\n              })\n              .catch(error => {\n                reject(error);\n              });\n          });\n      } else {\n        this.addSearchRequestQueue(url, site, entry, torrentTagSelectors)\n          .then(result => {\n            resolve(result);\n          })\n          .catch(error => {\n            reject(error);\n          });\n      }\n    });\n  }\n\n  /**\n   * 获取搜索结果\n   * @param url\n   * @param site\n   * @param entry\n   * @param torrentTagSelectors\n   */\n  public addSearchRequestQueue(\n    url: string,\n    site: Site,\n    entry: SearchEntry,\n    torrentTagSelectors?: any[],\n    beforeSearchData?: any\n  ): Promise<any> {\n    let _entry = PPF.clone(entry);\n    if (_entry.parseScript) {\n      delete _entry.parseScript;\n    }\n\n    // 是否包含搜索前处理的数据\n    if (beforeSearchData) {\n      this.service.debug(\"beforeSearchData\", beforeSearchData);\n      url = this.replaceKeys(url, beforeSearchData, \"beforeSearchData\");\n\n      // 替换要提交数据中包含的关键字内容\n      if (entry.requestData) {\n        try {\n          for (const key in entry.requestData) {\n            if (entry.requestData.hasOwnProperty(key)) {\n              const value = entry.requestData[key];\n              entry.requestData[key] = PPF.replaceKeys(\n                value,\n                beforeSearchData,\n                \"beforeSearchData\"\n              );\n            }\n          }\n        } catch (error) {\n          this.service.writeErrorLog(error);\n          this.service.debug(error);\n        }\n      }\n    }\n\n    this.service.debug(\"getSearchResult.start\", {\n      url,\n      site: site.host,\n      entry: _entry\n    });\n    let logId = \"\";\n    let contentType = 'text/plain';\n    let data: Dictionary<any> | string | undefined = entry.requestData\n    switch (entry.requestDataType) {\n      case ERequestType.JSON:\n        contentType = 'application/json';\n        if (data)\n          data = JSON.stringify(data);\n      case ERequestType.TEXT:\n      default:\n    }\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.searchRequestQueue[url] = $.ajax({\n        url: url,\n        cache: true,\n        dataType: \"text\",\n        contentType,\n        timeout: this.options.connectClientTimeout || 30000,\n        headers: entry.headers,\n        method: entry.requestMethod || ERequestMethod.GET,\n        data\n      })\n        .done((result: any) => {\n          this.service.debug(\"getSearchResult.done\", url);\n          delete this.searchRequestQueue[url];\n          if (\n            (result && typeof result == \"string\" && result.length > 100) ||\n            typeof result == \"object\"\n          ) {\n            let page: any;\n            let doc: any;\n            try {\n              switch (entry.resultType) {\n                case ERequestResultType.JSON:\n                  page = JSON.parse(result);\n                  break;\n\n                default:\n                  doc = new DOMParser().parseFromString(result, \"text/html\");\n                  // 构造 jQuery 对象\n                  page = $(doc).find(\"body\");\n                  break;\n              }\n            } catch (error) {\n              logId = this.service.logger.add({\n                module: EModule.background,\n                event:\n                  \"service.searcher.getSearchResult.siteSearchResultParseFailed\",\n                msg: error\n              });\n\n              // 数据解析失败\n              reject({\n                success: false,\n                msg: this.service.i18n.t(\n                  \"service.searcher.siteSearchResultParseFailed\",\n                  {\n                    site\n                  }\n                ),\n                data: {\n                  logId\n                },\n                type: EDataResultType.error\n              });\n              return;\n            }\n\n            let options: any = {\n              results: [],\n              responseText: result,\n              site,\n              resultSelector: entry.resultSelector,\n              page,\n              entry,\n              torrentTagSelectors: torrentTagSelectors,\n              errorMsg: \"\",\n              isLogged: false,\n              status: ESearchResultParseStatus.success,\n              searcher: this,\n              url\n            };\n\n            // 执行获取结果的脚本\n            try {\n              if (entry.parseScript) {\n                // 异步脚本，由脚本负责调用 reject 和 resolve\n                if (entry.asyncParse) {\n                  options = Object.assign(\n                    {\n                      reject,\n                      resolve\n                    },\n                    options\n                  );\n                  eval(entry.parseScript);\n                  return;\n                } else {\n                  eval(entry.parseScript);\n                }\n              }\n              if (\n                options.errorMsg ||\n                options.status != ESearchResultParseStatus.success\n              ) {\n                reject({\n                  success: false,\n                  msg: this.getErrorMessage(\n                    site,\n                    options.status,\n                    options.errorMsg\n                  ),\n                  data: {\n                    site,\n                    isLogged: options.isLogged\n                  }\n                });\n              } else {\n                resolve(PPF.clone(options.results));\n              }\n            } catch (error) {\n              console.error(error);\n              logId = this.service.logger.add({\n                module: EModule.background,\n                event: \"service.searcher.getSearchResult.siteEvalScriptFailed\",\n                msg: error\n              });\n              // 脚本执行出错\n              reject({\n                success: false,\n                msg: this.service.i18n.t(\n                  \"service.searcher.siteEvalScriptFailed\",\n                  {\n                    site\n                  }\n                ),\n                data: {\n                  logId\n                }\n              });\n            }\n          } else {\n            logId = this.service.logger.add({\n              module: EModule.background,\n              event: \"service.searcher.getSearchResult.siteSearchResultError\",\n              msg: result\n            });\n            // 没有返回预期的数据\n            reject({\n              success: false,\n              msg: this.service.i18n.t(\n                \"service.searcher.siteSearchResultError\",\n                {\n                  site\n                }\n              ),\n              data: {\n                logId\n              },\n              type: EDataResultType.error\n            });\n          }\n        })\n        .fail((jqXHR, textStatus, errorThrown) => {\n          delete this.searchRequestQueue[url];\n\n          this.service.debug({\n            title: \"getSearchResult.fail\",\n            url,\n            entry,\n            textStatus,\n            errorThrown\n          });\n          logId = this.service.logger.add({\n            module: EModule.background,\n            event: \"service.searcher.getSearchResult.fail\",\n            msg: errorThrown,\n            data: {\n              url,\n              entry,\n              code: jqXHR.status,\n              textStatus,\n              errorThrown,\n              responseText: jqXHR.responseText\n            }\n          });\n\n          // 网络请求失败\n          reject({\n            data: {\n              logId,\n              textStatus\n            },\n            msg: this.service.i18n.t(\"service.searcher.siteNetworkFailed\", {\n              site,\n              msg: `${jqXHR.status} ${errorThrown}, ${textStatus}`\n            }),\n            success: false,\n            type: EDataResultType.error\n          });\n        });\n    });\n  }\n\n  /**\n   * 根据错误代码获取错误信息\n   * @param code\n   */\n  public getErrorMessage(\n    site: Site,\n    status: ESearchResultParseStatus = ESearchResultParseStatus.success,\n    msg: string = \"\"\n  ): string {\n    if (status != ESearchResultParseStatus.success) {\n      return this.service.i18n.t(`contentPage.search.${status}`, {\n        siteName: site.name,\n        msg\n      });\n    }\n    return msg;\n  }\n\n  /**\n   * 取消正在执行的搜索请求\n   * @param site\n   * @param key\n   */\n  public abortSearch(site: Site, key: string = \"\"): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let host = site.host + \"\";\n      let searchConfig: SearchConfig = this.searchConfigs[host];\n\n      if (searchConfig.entry) {\n        this.service.logger.add({\n          module: EModule.background,\n          event: \"searcher.abortSearch\",\n          msg: this.service.i18n.t(\"service.searcher.siteAbortSearch\", {\n            site\n          }), //`正在取消[${site.host}]的搜索请求`,\n          data: {\n            site: site.host,\n            key: key\n          }\n        });\n        searchConfig.entry.forEach((entry: SearchEntry) => {\n          // 判断是否指定了搜索页和用于获取搜索结果的脚本\n          if (entry.entry && entry.parseScriptFile && entry.enabled !== false) {\n            // 如果有自定义地址，则使用自定义地址\n            if (site.cdn && site.cdn.length > 0) {\n              site.url = site.cdn[0];\n            }\n\n            let rows: number =\n              this.options.search && this.options.search.rows\n                ? this.options.search.rows\n                : 10;\n            let url: string = site.url + entry.entry;\n\n            url = this.replaceKeys(url, {\n              key: key,\n              rows: rows,\n              passkey: site.passkey ? site.passkey : \"\"\n            });\n            let queue = this.searchRequestQueue[url];\n            if (queue) {\n              try {\n                queue.abort();\n                resolve();\n              } catch (error) {\n                this.service.logger.add({\n                  module: EModule.background,\n                  event: \"searcher.abortSearch.error\",\n                  msg: this.service.i18n.t(\n                    \"service.searcher.siteAbortSearchError\",\n                    {\n                      site\n                    }\n                  ), // \"取消搜索请求失败\",\n                  data: {\n                    site: site.host,\n                    key: key,\n                    error\n                  }\n                });\n                reject(error);\n              }\n            } else {\n              resolve();\n            }\n          } else {\n            resolve();\n          }\n        });\n      }\n    });\n  }\n\n  /**\n   * 根据指定的站点获取站点的架构信息\n   * @param site 站点信息\n   */\n  getSiteSchema(site: Site): SiteSchema {\n    let schema: SiteSchema = {};\n    if (typeof site.schema === \"string\") {\n      schema =\n        this.options.system &&\n        this.options.system.schemas &&\n        this.options.system.schemas.find((item: SiteSchema) => {\n          return item.name == site.schema;\n        });\n\n      if (schema === undefined) {\n        return schema;\n      }\n    }\n\n    return PPF.clone(schema);\n  }\n\n  /**\n   * 替换指定的字符串列表\n   * @param source\n   * @param keys\n   */\n  replaceKeys(\n    source: string,\n    keys: Dictionary<any>,\n    prefix: string = \"\"\n  ): string {\n    let result: string = source;\n\n    for (const key in keys) {\n      if (keys.hasOwnProperty(key)) {\n        const value = keys[key];\n        let search = \"$\" + key + \"$\";\n        if (prefix) {\n          search = `$${prefix}.${key}$`;\n        }\n        result = result.replace(search, value);\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 从当前行中获取指定字段的值\n   * @param site 当前站点\n   * @param row 当前行\n   * @param fieldName 字段名称\n   * @return null 表示没有获取到内容\n   */\n  public getFieldValue(\n    site: Site,\n    row: JQuery<HTMLElement>,\n    fieldName: string = \"\"\n  ) {\n    let selector: any;\n    if (site.searchEntryConfig && site.searchEntryConfig.fieldSelector) {\n      selector = site.searchEntryConfig.fieldSelector[fieldName];\n\n      if (!selector) {\n        return null;\n      }\n    } else {\n      return null;\n    }\n\n    const parser = new InfoParser(this.service);\n    return parser.getFieldData(\n      row,\n      selector,\n      site.searchEntryConfig.fieldSelector\n    );\n  }\n\n  /**\n   * 根据指定信息获取分类\n   * @param site 站点\n   * @param page 当前搜索页面\n   * @param id 分类ID\n   */\n  public getCategoryById(site: Site, page: string, id: string) {\n    let result = {};\n    if (site.categories) {\n      site.categories.forEach((item: SiteCategories) => {\n        if (\n          item.category &&\n          (item.entry == \"*\" || page.indexOf(item.entry as string))\n        ) {\n          let category = item.category.find((c: SiteCategory) => {\n            return c.id == id;\n          });\n\n          if (category) {\n            result = category;\n          }\n        }\n      });\n    }\n    return result;\n  }\n\n  /**\n   * cloudflare Email 解码方法，来自 https://usamaejaz.com/cloudflare-email-decoding/\n   * @param {*} encodedString\n   */\n  public cfDecodeEmail(encodedString: string) {\n    let email = \"\",\n      r = parseInt(encodedString.substr(0, 2), 16),\n      n,\n      i;\n    for (n = 2; encodedString.length - n; n += 2) {\n      i = parseInt(encodedString.substr(n, 2), 16) ^ r;\n      email += String.fromCharCode(i);\n    }\n    return email;\n  }\n\n  /**\n   * 获取指定站点当前行标签列表\n   * @param site\n   * @param row\n   */\n  public getRowTags(site: Site, row: JQuery<HTMLElement>) {\n    let tags: {}[] = [];\n    if (site && site.host) {\n      let config = this.searchConfigs[site.host];\n      let selectors = config.torrentTagSelectors;\n\n      if (selectors && selectors.length > 0) {\n        selectors.forEach((item: any) => {\n          if (item.selector) {\n            let result = row.find(item.selector);\n            if (result.length) {\n              let color = item.color || BASE_TAG_COLORS[item.name] || \"\";\n\n              let data: Dictionary<any> = {\n                name: item.name,\n                color\n              };\n\n              if (item.title && result.attr(item.title)) {\n                data.title = result.attr(item.title);\n              }\n              tags.push(data);\n            }\n          }\n        });\n      }\n    }\n    return tags;\n  }\n}\n"
  },
  {
    "path": "src/background/service.ts",
    "content": "import {\n  EAction,\n  Request,\n  Options,\n  EModule,\n  ELogEvent,\n  Site,\n  SiteSchema,\n  Dictionary,\n  EUserDataRequestStatus,\n  LogItem\n} from \"@/interface/common\";\nimport Config from \"./config\";\nimport Controller from \"./controller\";\n\nimport { Logger } from \"@/service/logger\";\nimport { ContextMenus } from \"./contextMenus\";\nimport { UserData } from \"./userData\";\nimport { PPF } from \"@/service/public\";\nimport { OmniBox } from \"./omnibox\";\nimport { i18nService } from \"./i18n\";\nimport DownloadQuene from \"./downloadQuene\";\nimport Collection from \"./collection\";\nimport SearchResultSnapshot from \"./searchResultSnapshot\";\nimport KeepUploadTask from \"./keepUploadTask\";\n\n/**\n * PT 助手后台服务类\n */\nexport default class PTPlugin {\n  // 当前配置对象\n  public config: Config = new Config(this);\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n  // 本地模式，用于本地快速调试\n  public localMode: boolean = false;\n  // 事件处理器\n  public controller: Controller = new Controller(this);\n  // 日志处理器\n  public logger: Logger = new Logger();\n  // 上下文菜单处理器\n  public contentMenus: ContextMenus = new ContextMenus(this);\n  // 用户数据处理\n  public userData: UserData = new UserData(this);\n  public omniBox: OmniBox = new OmniBox(this);\n  public i18n: i18nService = new i18nService(this);\n  // 种子下载队列服务\n  public downloadQuene: DownloadQuene = new DownloadQuene(this);\n  // 收藏\n  public collection: Collection = new Collection();\n  // 搜索结果快照\n  public searchResultSnapshot: SearchResultSnapshot = new SearchResultSnapshot();\n  // 辅种任务\n  public keepUploadTask: KeepUploadTask = new KeepUploadTask();\n\n  private reloadCount: number = 0;\n  private autoRefreshUserDataTimer: number = 0;\n  private autoRefreshUserDataIsWorking: boolean = false;\n  private autoRefreshUserDataFailedCount: number = 0;\n\n  constructor(localMode: boolean = false) {\n    if (!localMode) {\n      this.initBrowserEvent();\n    }\n\n    this.logger.add({\n      module: EModule.background,\n      event: ELogEvent.init\n    });\n    this.localMode = localMode;\n    this.initConfig();\n  }\n\n  /**\n   * 接收由前台发回的指令并执行\n   * @param action 指令\n   * @param callback 回调函数\n   */\n  public requestMessage(request: Request, sender?: any): Promise<any> {\n    this.debug(`${ELogEvent.requestMessage}.${request.action}`);\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let result: any;\n      // if (\n      //   ![\n      //     EAction.getSystemLogs,\n      //     EAction.writeLog,\n      //     EAction.readConfig,\n      //     EAction.saveConfig,\n      //     EAction.saveUIOptions,\n      //     EAction.openOptions,\n      //     EAction.getClearedOptions,\n      //     EAction.getBase64FromImageUrl,\n      //     EAction.changeLanguage,\n      //     EAction.addLanguage,\n      //     EAction.getCurrentLanguageResource,\n      //     EAction.replaceLanguage,\n      //     EAction.readUIOptions,\n      //     EAction.addContentPage,\n      //     EAction.getDownloadHistory,\n      //     EAction.getTorrentDataFromURL\n      //   ].includes(request.action)\n      // ) {\n      //   this.logger.add({\n      //     module: EModule.background,\n      //     event: `${ELogEvent.requestMessage}.${request.action}`\n      //   });\n      // }\n\n      try {\n        switch (request.action) {\n          // 读取参数\n          case EAction.readConfig:\n            if (this.localMode) {\n              this.readConfig().then(() => {\n                resolve(this.options);\n              });\n            } else {\n              resolve(this.options);\n            }\n\n            break;\n\n          // 保存参数\n          case EAction.saveConfig:\n            if (\n              request.data.locale &&\n              request.data.locale != this.options.locale\n            ) {\n              this.i18n.reset(request.data.locale);\n            }\n            this.config.save(request.data);\n            this.options = request.data;\n            if (this.controller.isInitialized) {\n              this.controller.reset(this.options);\n            }\n            setTimeout(() => {\n              this.contentMenus.init(this.options);\n            }, 100);\n            this.resetAutoRefreshUserDataTimer();\n            resolve(this.options);\n            break;\n\n          // 获取已清理的配置\n          case EAction.getClearedOptions:\n            resolve(this.config.cleaningOptions(this.options));\n            break;\n\n          // 重置运行时配置\n          case EAction.resetRunTimeOptions:\n            this.config.resetRunTimeOptions(request.data);\n            this.options = this.config.options;\n            resolve(this.options);\n            break;\n\n          // 复制指定的内容到剪切板\n          case EAction.copyTextToClipboard:\n            result = this.controller.copyTextToClipboard(request.data);\n            if (result) {\n              resolve(result);\n            } else {\n              reject();\n            }\n            break;\n\n          // 打开选项卡\n          case EAction.openOptions:\n            this.controller.openOptions(request.data);\n            resolve(true);\n            break;\n\n          case EAction.updateOptionsTabId:\n            this.controller.updateOptionsTabId(request.data);\n            resolve(true);\n            break;\n\n          // 搜索种子\n          case EAction.searchTorrent:\n            console.log(request.data);\n            this.controller.searchTorrent(request.data);\n            resolve(true);\n            break;\n\n          // 测试客户是否可连接\n          case EAction.testClientConnectivity:\n            this.controller.clientController\n              .testClientConnectivity(request.data)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                this.logger.add({\n                  module: EModule.background,\n                  event: `${EAction.testClientConnectivity}`,\n                  msg: this.i18n.t(\"service.testClientConnectivityFailed\", {\n                    address: request.data.address\n                  }), // `测试客户连接失败[${request.data.address}]`,\n                  data: result\n                });\n                reject(result);\n              });\n            break;\n\n          case EAction.getSystemLogs:\n            this.logger\n              .load()\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n\n          case EAction.removeSystemLogs:\n            this.logger\n              .remove(request.data)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n\n          case EAction.clearSystemLogs:\n            this.logger\n              .clear()\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n\n          case EAction.writeLog:\n            this.logger.add(request.data);\n            resolve(true);\n            break;\n\n          case EAction.readUIOptions:\n            this.config\n              .readUIOptions()\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n\n          case EAction.saveUIOptions:\n            this.config\n              .saveUIOptions(request.data)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n\n          case EAction.changeLanguage:\n            return this.i18n.reset(request.data);\n            break;\n\n          // 如果没有特殊的情况默认使用处理器来处理\n          default:\n            if ((this as any)[request.action]) {\n              (this as any)\n                [request.action](request.data, sender)\n                .then((result: any) => {\n                  resolve(result);\n                })\n                .catch((result: any) => {\n                  reject(result);\n                });\n              return;\n            }\n            this.controller\n              .call(request, sender)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch((result: any) => {\n                reject(result);\n              });\n            break;\n        }\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 初始化参数\n   */\n  private initConfig() {\n    if (\n      this.reloadCount < 10 &&\n      (this.config.sites.length === 0 || this.config.clients.length === 0)\n    ) {\n      setTimeout(() => {\n        this.initConfig();\n      }, 500);\n      this.reloadCount++;\n      return;\n    }\n\n    this.readConfig().then(() => {\n      this.initI18n();\n    });\n  }\n\n  /**\n   * 初始化多语言环境\n   */\n  private initI18n() {\n    this.i18n\n      .init()\n      .then(() => {\n        this.init();\n      })\n      .catch(() => {\n        console.debug(\"i18n init error\");\n      });\n  }\n\n  /**\n   * 读取参数信息\n   */\n  private readConfig(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.config.read().then((result: any) => {\n        this.initUserData();\n        this.options = result;\n        resolve(result);\n        if (!this.localMode) {\n          this.controller.init(this.options);\n        }\n      });\n    });\n  }\n\n  /**\n   * 保存当前参数\n   */\n  public saveConfig() {\n    this.config.save(this.options);\n  }\n\n  /**\n   * 初始化用户数据\n   */\n  private initUserData() {\n    this.options.sites.forEach((site: Site) => {\n      site.user = this.userData.get(site.host as string);\n    });\n  }\n\n  /**\n   * 重设自动获取用户数据定时器\n   */\n  private resetAutoRefreshUserDataTimer(isInit: boolean = false) {\n    clearInterval(this.autoRefreshUserDataTimer);\n    if (!this.options.autoRefreshUserData) {\n      return;\n    }\n\n    // 先尝试当天\n    this.options.autoRefreshUserDataNextTime = this.getNextTime(0);\n    // 如果当前下次获取时间小于当前时间，则设置为第二天\n    if (new Date().getTime() >= this.options.autoRefreshUserDataNextTime) {\n      // 初始化时，10 秒后获取数据\n      if (isInit) {\n        // 如果当天还没有获取过，就重新获取\n        if (\n          PPF.getToDay() !=\n          PPF.getToDay(this.options.autoRefreshUserDataLastTime)\n        ) {\n          this.options.autoRefreshUserDataNextTime =\n            new Date().getTime() + 10000;\n        } else {\n          this.options.autoRefreshUserDataNextTime = this.getNextTime();\n        }\n      } else {\n        this.options.autoRefreshUserDataNextTime = this.getNextTime();\n      }\n    }\n\n    this.autoRefreshUserDataFailedCount = 0;\n    let failedRetryCount =\n      this.options.autoRefreshUserDataFailedRetryCount || 3;\n    let failedRetryInterval =\n      this.options.autoRefreshUserDataFailedRetryInterval || 5;\n\n    this.autoRefreshUserDataTimer = window.setInterval(() => {\n      let time = new Date().getTime();\n\n      if (\n        this.options.autoRefreshUserDataNextTime &&\n        time >= this.options.autoRefreshUserDataNextTime &&\n        !this.autoRefreshUserDataIsWorking\n      ) {\n        this.options.autoRefreshUserDataNextTime = this.getNextTime();\n        this.autoRefreshUserDataIsWorking = true;\n        this.controller.userService\n          .refreshUserData(this.autoRefreshUserDataFailedCount > 0)\n          .then((results: any) => {\n            this.debug(\"refreshUserData DONE.\", results);\n            this.autoRefreshUserDataIsWorking = false;\n            let haveError = false;\n            results.some((result: any) => {\n              if (!result) {\n                haveError = true;\n                return true;\n              }\n\n              if (!result.id) {\n                if (\n                  result.msg &&\n                  result.msg.status != EUserDataRequestStatus.notSupported\n                ) {\n                  haveError = true;\n                  return true;\n                }\n              }\n            });\n\n            if (haveError) {\n              // 失败重试\n              if (this.autoRefreshUserDataFailedCount < failedRetryCount) {\n                // 设置几分钟后重试\n                this.options.autoRefreshUserDataNextTime =\n                  new Date().getTime() + failedRetryInterval * 60000;\n                this.debug(\n                  \"数据刷新失败, 下次重试时间\",\n                  new Date(\n                    this.options.autoRefreshUserDataNextTime as number\n                  ).toLocaleString()\n                );\n              } else {\n                this.debug(\"数据刷新失败, 重试次数已超限制\");\n              }\n              this.autoRefreshUserDataFailedCount++;\n            } else {\n              this.debug(\"数据刷新完成\");\n              this.autoRefreshUserDataFailedCount = 0;\n            }\n          });\n      }\n    }, 1000);\n  }\n\n  /**\n   * 获取下一个时间\n   * @param addDays\n   */\n  private getNextTime(addDays: number = 1) {\n    let today = PPF.getToDay();\n    let time = new Date(\n      `${today} ${this.options.autoRefreshUserDataHours}:${this.options.autoRefreshUserDataMinutes}:00`\n    );\n\n    return new Date(time.setDate(time.getDate() + addDays)).getTime();\n  }\n\n  /**\n   * 保存用户数据\n   */\n  public saveUserData() {\n    this.initUserData();\n    this.config.save(this.options);\n  }\n\n  /**\n   * 服务初始化\n   */\n  public init() {\n    if (!this.localMode) {\n      this.contentMenus.init(this.options);\n      this.resetAutoRefreshUserDataTimer(true);\n    }\n  }\n\n  /**\n   * 输出调试信息\n   * @param msg\n   */\n  public debug(...msgs: any[]) {\n    msgs.forEach((msg: any) => {\n      this.controller.pushDebugMsg(msg);\n    });\n  }\n\n  public writeLog(msg: LogItem) {\n    this.logger.add(msg);\n  }\n\n  public writeErrorLog(msg: any) {\n    this.logger.add({\n      module: EModule.background,\n      event: \"一般错误\",\n      msg: typeof msg === \"string\" ? msg : JSON.stringify(msg)\n    });\n  }\n\n  /**\n   * 初始化浏览器事件\n   */\n  private initBrowserEvent() {\n    if (window.chrome === undefined) {\n      return;\n    }\n    if (!chrome.runtime) {\n      return;\n    }\n\n    console.log(\"service.initBrowserEvent\");\n    // 监听由活动页面发来的消息事件\n    chrome.runtime.onMessage &&\n      chrome.runtime.onMessage.addListener(\n        (message: any, sender: chrome.runtime.MessageSender, callback) => {\n          this.requestMessage(message, sender)\n            .then((result: any) => {\n              callback &&\n                callback({\n                  resolve: result\n                });\n            })\n            .catch((result: any) => {\n              callback &&\n                callback({\n                  reject: result\n                });\n            });\n          // 这句不能去掉\n          return true;\n        }\n      );\n\n    // 当扩展程序第一次安装、更新至新版本或 Chrome 浏览器更新至新版本时产生。\n    chrome.runtime.onInstalled.addListener(details => {\n      console.log(\"chrome.runtime.onInstalled\", details);\n      // 版本更新时\n      if (details.reason == \"update\") {\n        this.upgrade();\n      }\n    });\n  }\n\n  /**\n   * 升级相关内容\n   */\n  public upgrade() {\n    // 显示更新日志\n    this.controller.openURL(\"changelog.html\");\n    setTimeout(() => {\n      this.userData.upgrade();\n    }, 1000);\n  }\n\n  /**\n   * 获取指定解析器\n   * @param host\n   * @param name\n   */\n  public getSiteParser(host: string, name: string): string {\n    // 由于解析器可能会更新，所以需要从系统配置中加载\n    let site: Site =\n      this.options.system &&\n      this.options.system.sites &&\n      this.options.system.sites.find((item: Site) => {\n        return item.host === host;\n      });\n\n    if (!site) {\n      return \"\";\n    }\n\n    let result = site.parser && site.parser[name];\n    if (!result) {\n      let schema: SiteSchema =\n        this.options.system &&\n        this.options.system.schemas &&\n        this.options.system.schemas.find((item: SiteSchema) => {\n          return item.name === site.schema;\n        });\n\n      result = schema.parser && schema.parser[name];\n    }\n    return result;\n  }\n\n  /**\n   * 获取指定选择器\n   * @param hostOrSite\n   * @param name\n   */\n  public getSiteSelector(\n    hostOrSite: string | Site,\n    name: string\n  ): Dictionary<any> | null {\n    let host = typeof hostOrSite == \"string\" ? hostOrSite : hostOrSite.host;\n    let system = this.clone(this.options.system);\n    // 由于选择器可能会更新，所以优先从系统配置中加载\n    // 增加 this.options.sites 是为了兼顾自定义站点\n    let sites = system.sites.concat(this.options.sites);\n    let site: Site | undefined = sites.find((item: Site) => {\n      return item.host === host;\n    });\n\n    if (!site) {\n      if (typeof hostOrSite == \"string\") {\n        return null;\n      }\n      site = hostOrSite;\n    }\n\n    let result = site.selectors && site.selectors[name];\n    let schema: SiteSchema = system.schemas.find((item: SiteSchema) => {\n      return site != null && item.name === site.schema;\n    });\n    if (!result) {\n      if (schema && schema.selectors) {\n        result = schema.selectors[name];\n      }\n    } else {\n      // 禁用\n      if (result.disabled === true) {\n        return null;\n      }\n      if (schema && schema.selectors && schema.selectors[name]) {\n        // 合并参数\n        if (result.merge === true) {\n          result.fields = Object.assign(\n            schema.selectors[name].fields,\n            result.fields\n          );\n          result = Object.assign(schema.selectors[name], result);\n        }\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 用JSON对象模拟对象克隆\n   * @param source\n   */\n  public clone(source: any) {\n    return PPF.clone(source);\n  }\n\n  /**\n   * 检查权限\n   * @param permissions 需要检查的权限列表\n   */\n  public checkPermissions(permissions: string[]): Promise<any> {\n    return PPF.checkPermissions(permissions);\n  }\n\n  /**\n   * 申请权限\n   * @param permissions 需要申请的权限列表\n   */\n  public requestPermissions(permissions: string[]): Promise<any> {\n    return PPF.requestPermissions(permissions);\n  }\n}\n"
  },
  {
    "path": "src/background/site.ts",
    "content": "import { Site, SiteSchema, Options, SearchEntry } from \"@/interface/common\";\nimport extend from \"extend\";\n\nexport class SiteService {\n  public isLogin: boolean = false;\n\n  private _schema: SiteSchema = {};\n\n  constructor(public options: Site, public systemOptions: Options) {\n    this.mergeOptions();\n  }\n\n  public get schema(): SiteSchema {\n    if (this._schema.name) {\n      return this._schema;\n    }\n    let schema: SiteSchema = {};\n    if (typeof this.options.schema === \"string\") {\n      schema =\n        this.systemOptions.system &&\n        this.systemOptions.system.schemas &&\n        this.systemOptions.system.schemas.find((item: SiteSchema) => {\n          return item.name == this.options.schema;\n        });\n    }\n\n    this._schema = schema;\n\n    return schema;\n  }\n\n  private mergeOptions() {\n    let site: Site =\n      this.systemOptions.system &&\n      this.systemOptions.system.sites &&\n      this.systemOptions.system.sites.find((item: Site) => {\n        return item.host == this.options.host;\n      });\n\n    if (site) {\n      let customSearchEntry: SearchEntry[] = [];\n      // 单独处理自定义的搜索入口，避免参数合并后错乱\n      if (this.options.searchEntry) {\n        for (\n          let index = this.options.searchEntry.length - 1;\n          index >= 0;\n          index--\n        ) {\n          const item = this.options.searchEntry[index];\n          if (item.isCustom) {\n            customSearchEntry.push(item);\n            this.options.searchEntry.splice(index, 1);\n          }\n        }\n      }\n\n      this.options = extend(true, {}, site, this.options);\n      if (this.options.searchEntry && customSearchEntry.length > 0) {\n        this.options.searchEntry.push(...customSearchEntry);\n      }\n    }\n    console.log(this.options);\n  }\n\n  // public checkLogin(): Promise<any> {\n  //   return new Promise<any>((resolve?: any, reject?: any) => {\n  //     let site = this.options;\n  //     let schema = this.schema;\n  //     let checker: any;\n  //     if (site.checker && site.checker.isLogin) {\n  //       checker = site.checker.isLogin;\n  //     } else if (schema.checker && schema.checker.isLogin) {\n  //       checker = schema.checker.isLogin;\n  //     }\n\n  //     if (checker) {\n  //       $.get(`${site.url}/${checker.page}`)\n  //         .done((result: string) => {\n  //           resolve(new RegExp(result, \"\").test(checker.contains));\n  //         })\n  //         .fail(() => {\n  //           reject();\n  //         });\n  //     } else {\n  //       resolve(null);\n  //     }\n  //   });\n  // }\n}\n"
  },
  {
    "path": "src/background/syncStorage.ts",
    "content": "/**\n * Google云端存储类\n */\nexport class SyncStorage {\n  private arrayValues: any = {};\n  private isArray(o: any) {\n    return Object.prototype.toString.call(o) == \"[object Array]\";\n  }\n\n  /**\n   * 清除所有参数\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      try {\n        chrome.storage.sync.clear();\n        if (chrome.runtime.lastError) {\n          reject(chrome.runtime.lastError);\n        } else {\n          resolve();\n        }\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 保存一个键值\n   * @param key 键名\n   * @param value 键值\n   * @param count 当参数为数组时，数组的长度\n   * @param onSuccess 成功时回调\n   * @param onError 失败时回调\n   */\n  private _set(\n    key: string,\n    value: any,\n    count: number = 0,\n    onSuccess?: any,\n    onError?: any\n  ) {\n    let _data = value;\n    let _key = key;\n    let index = -1;\n\n    // 因Google 限制每项内容不得超出 8K，固将内容拆分后保存\n    // 限制说明：https://developer.chrome.com/extensions/storage#type-StorageArea\n    // 当内容为数组时，分开保存\n    if (this.isArray(value)) {\n      if (count == 0) {\n        count = value.length;\n        // 设置数组长度\n        this._set(\n          `${key}__count`,\n          count,\n          0,\n          () => {\n            this._set(key, value, count, onSuccess, onError);\n          },\n          onError\n        );\n        return;\n      }\n      index = value.length - 1;\n      _data = value.pop();\n      _key = `${key}_${index}`;\n    }\n\n    chrome.storage.sync.set(\n      {\n        [_key]: _data\n      },\n      () => {\n        if (chrome.runtime.lastError) {\n          onError(chrome.runtime.lastError);\n        } else {\n          // console.log(_key, \"SAVED\");\n          if (index > 0) {\n            this._set(key, value, count, onSuccess, onError);\n          } else {\n            onSuccess(value);\n          }\n        }\n      }\n    );\n  }\n\n  /**\n   * 获取指定的键值\n   * @param key 键名\n   * @param checkArray 是否检查数组\n   * @param index 当前索引\n   * @param onSuccess 成功回调\n   * @param onError 失败回调\n   */\n  private _get(\n    key: string,\n    checkArray: boolean = false,\n    index: number = 0,\n    onSuccess?: any,\n    onError?: any\n  ) {\n    if (checkArray) {\n      this._get(\n        `${key}__count`,\n        false,\n        0,\n        (result: any) => {\n          if (result > 0) {\n            this.arrayValues[key] = [];\n            this._get(key, false, result, onSuccess, onError);\n          } else {\n            this._get(key, false, 0, onSuccess, onError);\n          }\n        },\n        (error: any) => {\n          if (error == \"参数不存在\") {\n            this._get(key, false, 0, onSuccess, onError);\n          } else {\n            onError && onError(error);\n          }\n        }\n      );\n      return;\n    }\n\n    let _key = key;\n    if (index > 0) {\n      _key = `${key}_${index - 1}`;\n    }\n\n    try {\n      chrome.storage.sync.get(_key, result => {\n        let value: any = null;\n        // console.log(_key, \"GETED\", result);\n        try {\n          if (result[_key]) {\n            value = result[_key];\n          } else {\n            onError(\"参数不存在\");\n            return;\n          }\n        } catch (error) {\n          onError(error);\n          return;\n        }\n\n        index--;\n        if (index <= 0) {\n          if (this.arrayValues[key]) {\n            this.arrayValues[key].push(value);\n            onSuccess(this.arrayValues[key]);\n            delete this.arrayValues[key];\n          } else {\n            onSuccess(value);\n          }\n        } else {\n          this.arrayValues[key].push(value);\n          this._get(key, false, index, onSuccess, onError);\n        }\n      });\n    } catch (error) {\n      onError(error);\n    }\n  }\n\n  /**\n   * 保存指定的键值到Google\n   * @param key\n   * @param value\n   */\n  public set(key: string, value: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome.storage && chrome.storage.sync) {\n        try {\n          this._set(\n            key,\n            value,\n            0,\n            () => {\n              resolve(value);\n            },\n            (error: any) => {\n              reject(error);\n            }\n          );\n        } catch (error) {\n          reject(error);\n        }\n      } else {\n        reject(\"chrome.storage 不存在\");\n      }\n    });\n  }\n\n  /**\n   * 从Google中获取指定的键值\n   * @param key\n   */\n  public get(key: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome.storage && chrome.storage.sync) {\n        try {\n          this._get(\n            key,\n            true,\n            0,\n            (result: any) => {\n              resolve(result);\n            },\n            (error: any) => {\n              reject(error);\n            }\n          );\n        } catch (error) {\n          reject(error);\n        }\n      } else {\n        reject(\"chrome.storage 不存在\");\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/user.ts",
    "content": "import {\n  Site,\n  Dictionary,\n  EModule,\n  UserInfo,\n  EUserDataRequestStatus,\n  ERequestResultType,\n  ERequestMethod\n} from \"@/interface/common\";\nimport PTPlugin from \"./service\";\nimport { InfoParser } from \"./infoParser\";\nimport { APP } from \"@/service/api\";\nimport { PPF } from \"@/service/public\";\n\ntype Service = PTPlugin;\n\nexport class User {\n  private requestQueue: any = {};\n  private requestQueueCount: number = 0;\n  private infoParserCache: Dictionary<any> = {};\n\n  // 用于脚本解析器调用\n  public InfoParser = InfoParser;\n\n  constructor(public service: Service) {}\n\n  /**\n   * 刷新用户数据\n   * @param failedOnly 是否仅刷新最近状态为失败的站点\n   */\n  public refreshUserData(failedOnly: boolean = false): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let requests: any[] = [];\n      this.service.options.sites.forEach((site: Site) => {\n        if (!site.allowGetUserInfo || site.offline) {\n          return false;\n        }\n\n        if (!failedOnly) {\n          requests.push(this.getUserInfo(site, true));\n        } else if (\n          site.user &&\n          ((site.user.lastUpdateStatus &&\n            [\n              EUserDataRequestStatus.needLogin,\n              EUserDataRequestStatus.unknown\n            ].includes(site.user.lastUpdateStatus)) ||\n            !site.user.lastUpdateStatus)\n        ) {\n          requests.push(this.getUserInfo(site, true));\n        }\n      });\n\n      Promise.all(requests).then(results => {\n        resolve(results);\n      });\n    });\n  }\n\n  updateStatus(site: Site, userInfo: UserInfo) {\n    userInfo.lastUpdateTime = new Date().getTime();\n    this.service.userData.update(site, userInfo);\n  }\n\n  private getSiteURL(site: Site) {\n    if (site.cdn && site.cdn.length > 0) {\n      return site.cdn[0];\n    }\n\n    return site.url;\n  }\n\n  /**\n   * 获取指定站点的用户信息\n   * @param site\n   * @param returnResolve 指定为 true 时，失败时也调用 resolve\n   */\n  public getUserInfo(site: Site, returnResolve: boolean = false): Promise<any> {\n    this.service.options.autoRefreshUserDataLastTime = new Date().getTime();\n    this.service.saveConfig();\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let rejectFN = returnResolve ? resolve : reject;\n      if (!site) {\n        rejectFN(null);\n        return;\n      }\n\n      // 获取最近一次数据\n      let userInfo: UserInfo =\n        this.service.userData.get(site.host as string) || {};\n\n      let rule = this.service.getSiteSelector(site, \"userBaseInfo\");\n      if (!rule) {\n        userInfo.lastUpdateStatus = EUserDataRequestStatus.notSupported;\n        this.updateStatus(site, userInfo);\n        rejectFN(\n          APP.createErrorMessage({\n            status: EUserDataRequestStatus.notSupported,\n            msg: this.service.i18n.t(\"service.user.notSupported\") // \"暂不支持\"\n          })\n        );\n\n        return;\n      }\n\n      let url: string = `${this.getSiteURL(site)}${rule.page}`;\n      let host = site.host as string;\n      // 上次请求未完成时，直接返回最近的数据\n      if (this.checkQueue(host, url)) {\n        resolve(userInfo);\n        return;\n      }\n\n      // 获取用户基本信息（用户名、ID、是否登录等）\n      this.getInfos(host, url, rule)\n        .then((result: any) => {\n          console.log(\"userBaseInfo\", host, result);\n          userInfo = Object.assign({}, result);\n          // 是否已定义已登录选择器\n          if (rule && rule.fields && rule.fields.isLogged) {\n            // 如果已定义则以选择器匹配为准\n            if (userInfo.isLogged && (userInfo.name || userInfo.id)) {\n              userInfo.isLogged = true;\n            } else {\n              userInfo.isLogged = false;\n            }\n          } else if (userInfo.name || userInfo.id) {\n            userInfo.isLogged = true;\n          }\n\n          if (!userInfo.isLogged) {\n            userInfo.lastUpdateStatus = EUserDataRequestStatus.needLogin;\n            //this.updateStatus(site, userInfo);\n\n            rejectFN(\n              APP.createErrorMessage({\n                msg: this.service.i18n.t(\"service.user.notLogged\"), //\"未登录\",\n                status: EUserDataRequestStatus.needLogin\n              })\n            );\n            return;\n          }\n\n          rule = this.service.getSiteSelector(site, \"userExtendInfo\");\n\n          if (!rule) {\n            this.updateStatus(site, userInfo);\n            resolve(userInfo);\n            return;\n          }\n\n          if (userInfo.name || userInfo.id) {\n            let url = `${this.getSiteURL(site)}${rule.page\n              .replace(\"$user.id$\", userInfo.id)\n              .replace(\"$user.name$\", userInfo.name)\n              .replace(\"$user.bonusPage$\", userInfo.bonusPage)\n              .replace(\"$user.unsatisfiedsPage$\", userInfo.unsatisfiedsPage)}`;\n            // 上次请求未完成时，直接返回最近的数据\n            if (this.checkQueue(host, url)) {\n              resolve(userInfo);\n              return;\n            }\n\n            this.getInfos(host, url, rule, site, userInfo)\n              .then((result: any) => {\n                userInfo = Object.assign(userInfo, result);\n\n                userInfo.lastUpdateStatus = EUserDataRequestStatus.success;\n                this.updateStatus(site, userInfo);\n                this.getMoreInfos(site, userInfo).then(() => {\n                  resolve(userInfo);\n                });\n              })\n              .catch((error: any) => {\n                userInfo.lastUpdateStatus = EUserDataRequestStatus.unknown;\n                //this.updateStatus(site, userInfo);\n                rejectFN(APP.createErrorMessage(error));\n              });\n          } else {\n            userInfo.lastUpdateStatus = EUserDataRequestStatus.unknown;\n            //this.updateStatus(site, userInfo);\n            rejectFN(\n              APP.createErrorMessage({\n                status: EUserDataRequestStatus.unknown,\n                msg: this.service.i18n.t(\"service.user.getUserInfoFailed\") //\"获取用户名和编号失败\"\n              })\n            );\n          }\n        })\n        .catch((error: any) => {\n          userInfo.lastUpdateStatus = EUserDataRequestStatus.unknown;\n          console.log(\"getInfos Error :\",error);\n          //this.updateStatus(site, userInfo);\n          rejectFN(APP.createErrorMessage(error));\n        });\n    });\n  }\n\n  /**\n   * 获取更多用户信息（如有有定义的话）\n   * @param site\n   * @param userInfo\n   */\n  public getMoreInfos(site: Site, userInfo: UserInfo): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let requests: any[] = [];\n      let selectors = [\"userSeedingTorrents\", \"bonusExtendInfo\", \"hnrExtendInfo\", \"levelExtendInfo\", \"userUploadedTorrents\"];\n\n      selectors.forEach((name: string) => {\n        let host = site.host as string;\n        let rule = this.service.getSiteSelector(site, name);\n\n        if (rule) {\n          let url = `${this.getSiteURL(site)}${rule.page\n            .replace(\"$user.id$\", userInfo.id)\n            .replace(\"$user.name$\", userInfo.name)\n            .replace(\"$user.bonusPage$\", userInfo.bonusPage)\n            .replace(\"$user.unsatisfiedsPage$\", userInfo.unsatisfiedsPage)}`;\n          // 上次请求未完成时，跳过\n          if (this.checkQueue(host, url)) {\n            return;\n          }\n\n          // 执行该规则的前提条件（条件表达式）\n          if (rule.prerequisites) {\n            // user 用于条件中执行的内容\n            const user = userInfo;\n            try {\n              let result = eval(rule.prerequisites);\n              if (result !== true) {\n                return;\n              }\n            } catch (error) {\n              console.log(error);\n              return;\n            }\n          }\n\n          requests.push(this.getInfos(host, url, rule, site, userInfo));\n        }\n      });\n      if (requests.length) {\n        // 不管是否成功都执行 resolve 回调\n        Promise.all(requests)\n          .then((results: any[]) => {\n            results.forEach((result: any) => {\n              userInfo = Object.assign(userInfo, result);\n\n              userInfo.lastUpdateStatus = EUserDataRequestStatus.success;\n              this.updateStatus(site, userInfo);\n            });\n\n            resolve(userInfo);\n          })\n          .catch(result => {\n            resolve(userInfo);\n          });\n      } else {\n        resolve(userInfo);\n      }\n    });\n  }\n\n  /**\n   * getInfos\n   */\n  public getInfos(\n    host: string,\n    url: string,\n    rule: Dictionary<any>,\n    site?: Site,\n    userInfo?: UserInfo\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      url = url\n        .replace(\"://\", \"****\")\n        .replace(/\\/\\//g, \"/\")\n        .replace(\"****\", \"://\");\n\n      let requestData = rule.requestData;\n      if (requestData && userInfo) {\n        try {\n          for (const key in requestData) {\n            if (requestData.hasOwnProperty(key)) {\n              const value = requestData[key];\n              requestData[key] = PPF.replaceKeys(value, userInfo, \"user\");\n            }\n          }\n        } catch (error) {\n          console.log(error);\n        }\n      }\n      let headers = rule.headers;\n      if (headers && userInfo) {\n        try {\n          for (const key in headers) {\n            if (headers.hasOwnProperty(key)) {\n              const value = headers[key];\n              headers[key] = PPF.replaceKeys(value, userInfo, \"user\");\n            }\n          }\n        } catch (error) {\n          console.log(error);\n        }\n      }\n\n      /**\n       * 是否有脚本解析器\n       */\n      if (rule.parser && site) {\n        this.runParser(rule, site, userInfo, resolve, reject);\n        return;\n      }\n\n      PPF.updateBadge(++this.requestQueueCount);\n\n      let request = $.ajax({\n        url,\n        method: rule.requestMethod || ERequestMethod.GET,\n        dataType: \"text\",\n        data: requestData,\n        headers: rule.headers,\n        timeout: this.service.options.connectClientTimeout || 30000\n      })\n        .done(result => {\n          this.removeQueue(host, url);\n          PPF.updateBadge(--this.requestQueueCount);\n          let content: any;\n          try {\n            if (rule.dataType !== ERequestResultType.JSON) {\n              let doc = new DOMParser().parseFromString(result, \"text/html\");\n              // 构造 jQuery 对象\n              let topElement = rule.topElement || \"body\";\n              content = $(doc).find(topElement);\n            } else {\n              content = JSON.parse(result);\n            }\n          } catch (error) {\n            this.service.debug(\"getInfos.error\", host, url, error);\n            reject(error);\n            return;\n          }\n\n          if (content && rule) {\n            try {\n              let results = new InfoParser().getResult(content, rule);\n              resolve(results);\n            } catch (error) {\n              this.service.debug(error);\n              reject(error);\n            }\n          }\n        })\n        .fail((jqXHR, textStatus, errorThrown) => {\n          this.removeQueue(host, url);\n          PPF.updateBadge(--this.requestQueueCount);\n          let msg = this.service.i18n.t(\"service.searcher.siteNetworkFailed\", {\n            site,\n            msg: `${jqXHR.status} ${errorThrown}, ${textStatus}`\n          });\n          this.service.debug(msg, host, url, jqXHR.responseText);\n          reject(msg);\n        });\n\n      this.addQueue(host, url, request);\n    });\n  }\n\n  /**\n   * 执行脚本解析器\n   * @param rule\n   * @param site\n   * @param userInfo\n   * @param resolve\n   * @param reject\n   */\n  public runParser(\n    rule: Dictionary<any>,\n    site: Site,\n    userInfo?: UserInfo,\n    resolve?: any,\n    reject?: any\n  ) {\n    let siteConfigPath = site.schema == \"publicSite\" ? \"publicSites\" : \"sites\";\n\n    if (site.path) {\n      siteConfigPath += `/${site.path}`;\n    } else {\n      siteConfigPath += `/${site.host}`;\n    }\n\n    let path = rule.parser;\n    // 判断是否为相对路径\n    if (path.substr(0, 1) !== \"/\" && path.substr(0, 4) !== \"http\") {\n      path = `${siteConfigPath}/${path}`;\n    }\n\n    // 传递给解析解析的参数\n    let _options = {\n      site,\n      rule,\n      userInfo,\n      resolve,\n      reject\n    };\n\n    // 当前对象\n    let _self = this;\n\n    let script = this.infoParserCache[path];\n    if (script) {\n      eval(script);\n    } else {\n      APP.getScriptContent(path).done(script => {\n        this.infoParserCache[path] = script;\n        eval(script);\n      });\n    }\n  }\n\n  public addQueue(host: string, url: string, request: any) {\n    let queues = this.requestQueue[host] || {};\n    queues[url] = request;\n    this.requestQueue[host] = queues;\n  }\n\n  public checkQueue(host: string, url: string): boolean {\n    let queues = this.requestQueue[host] || {};\n    return queues[url] ? true : false;\n  }\n\n  public removeQueue(host: string, url: string) {\n    let queues = this.requestQueue[host] || {};\n    if (queues[url]) {\n      delete queues[url];\n    }\n    this.requestQueue[host] = queues;\n  }\n\n  /**\n   * 取消正在执行的搜索请求\n   * @param site\n   * @param key\n   */\n  public abortGetUserInfo(site: Site): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let host = site.host as string;\n      let queues = this.requestQueue[host];\n      let errors: any[] = [];\n\n      if (queues) {\n        for (const key in queues) {\n          if (queues.hasOwnProperty(key)) {\n            const request = queues[key];\n            try {\n              request.abort();\n            } catch (error) {\n              this.service.logger.add({\n                module: EModule.background,\n                event: \"user.abortGetUserInfo.error\",\n                msg: this.service.i18n.t(\"service.user.abortGetUserInfoFailed\"), //\"取消获取用户信息请求失败\",\n                data: {\n                  site: site.host,\n                  error\n                }\n              });\n              errors.push(error);\n            }\n          }\n        }\n        delete this.requestQueue[host];\n      }\n\n      if (errors.length > 0) {\n        reject(errors);\n      } else {\n        resolve(true);\n      }\n    });\n  }\n\n  // MAM需要在访问API时传入存于Cookies中的mam_id，构建这个辅助方法以便获取Cookie\n  public getCookie(site: Site, needle: String): Promise<any> {\n    return new Promise((resolve, reject) => {\n      PPF.checkPermissions([\"cookies\"]).then(() => {\n        this.service.config.getCookiesFromSite(site).then((result) => {\n          for (const cookie of result.cookies) {\n            if (cookie[\"name\"] === needle) {\n              resolve(cookie[\"value\"]);\n            }\n          }\n          resolve(\"\");\n        }).catch(error => {\n          reject(error);\n        });\n      }).catch(error => {\n        reject(error);\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "src/background/userData.ts",
    "content": "import {\n  EConfigKey,\n  UserInfo,\n  Site,\n  Dictionary,\n  EUserDataRange\n} from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\nimport PTPlugin from \"./service\";\nimport { PPF } from \"@/service/public\";\n\nexport class UserData {\n  public items: Dictionary<any> | null = null;\n  public storage: localStorage = new localStorage();\n  public configKey: string = EConfigKey.userDatas;\n\n  constructor(public service: PTPlugin) {\n    this.load();\n  }\n\n  /**\n   * 获取记录\n   */\n  public load(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(this.configKey, (result: any) => {\n        console.log(\"UserData.load\", result);\n        this.items = result || {};\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 获取指定站点的数据\n   * @param host\n   * @param range\n   */\n  public get(host: string, range: EUserDataRange = EUserDataRange.latest) {\n    if (!this.items) {\n      return null;\n    }\n\n    if (!host) {\n      return this.items;\n    }\n\n    let datas: Dictionary<any> = this.items[host];\n    if (!datas) {\n      return null;\n    }\n    switch (range) {\n      case EUserDataRange.all:\n        return datas;\n      case EUserDataRange.today:\n        return datas[PPF.getToDay()];\n    }\n\n    return datas[EUserDataRange.latest];\n  }\n\n  /**\n   * 更新用户数据\n   * @param site 站点信息\n   * @param data 用户数据\n   */\n  public update(site: Site, data: UserInfo) {\n    let host = site.host;\n    if (!host) {\n      return;\n    }\n    let saveData: UserInfo = Object.assign({}, data);\n    if (this.items == null) {\n      this.load().then(() => {\n        this.update(site, data);\n      });\n    } else {\n      let siteData = this.items[host];\n      let key = PPF.getToDay();\n      if (!siteData) {\n        siteData = {};\n      }\n\n      siteData[key] = saveData;\n      siteData[EUserDataRange.latest] = saveData;\n\n      this.items[host] = siteData;\n\n      this.storage.set(this.configKey, this.items).then(() => {\n        this.service.saveUserData();\n      });\n    }\n  }\n\n  /**\n   * 清除记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = {};\n      this.storage.set(this.configKey, this.items).then(() => {\n        this.service.saveUserData();\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 升级站点数据\n   */\n  public upgrade(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (\n        this.service.options &&\n        this.service.options.system &&\n        this.service.options.system.sites\n      ) {\n        let sites = this.service.options.system.sites;\n\n        this.load().then(datas => {\n          if (datas) {\n            sites.forEach((systemSite: Site) => {\n              if (!systemSite.host) {\n                return;\n              }\n              let formerHosts = systemSite.formerHosts;\n              let newHost = systemSite.host;\n              if (formerHosts && formerHosts.length > 0) {\n                formerHosts.forEach((host: string) => {\n                  for (const key in datas) {\n                    if (key == host && datas.hasOwnProperty(key)) {\n                      const element = datas[key];\n                      datas[newHost] = Object.assign({}, element);\n                      delete datas[key];\n                    }\n                  }\n                });\n              }\n            });\n\n            this.items = datas;\n            this.storage.set(this.configKey, datas);\n            this.service.saveUserData();\n            resolve(datas);\n          } else {\n            reject(null);\n          }\n        });\n      } else {\n        reject(null);\n      }\n    });\n  }\n\n  /**\n   * 重置数据\n   * @param datas 源数据\n   */\n  public reset(datas: any) {\n    this.items = datas;\n    this.storage.set(this.configKey, this.items).then(() => {\n      this.service.saveUserData();\n    });\n  }\n}\n"
  },
  {
    "path": "src/changelog/Index.vue",
    "content": "<template>\n  <v-app id=\"inspire\">\n    <div :class=\"$vuetify.breakpoint.smAndDown ? '' : 'mx-5'\">\n      <div class=\"header\">{{ version }} 更新日志</div>\n      <div v-html=\"marked(content)\" class=\"markdown-body\"></div>\n      <div class=\"footer\">\n        <div v-html=\"marked(footer)\" class=\"mt-2\"></div>\n        <div>&copy; PT 助手 {{ year }}, 版本 {{ version }}</div>\n      </div>\n    </div>\n  </v-app>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { marked } from \"marked\";\nimport { PPF } from \"@/service/public\";\n\n// 重写 version，因为使用 getVersion 获取到的是 `v1.5.1` 或者 `v1.5.1.dc988f6`，需要重写为 `v1.5.1` 否则无法获取release信息\nlet MAIN_VERSION = PPF.getVersion();\nconst mainVersionMatch = MAIN_VERSION.match(/v(\\d+\\.\\d+\\.\\d+)\\.?(.*)/)\nif (mainVersionMatch && mainVersionMatch[1]) {\n  MAIN_VERSION = `v${mainVersionMatch[1]}`\n}\n\nexport default Vue.extend({\n  data() {\n    return {\n      content:\n        \"正在加载…… <br>（如长时间未能加载成功，请前往 https://github.com/pt-plugins/PT-Plugin-Plus/releases/ 查看发布说明。）\",\n      footer:\n        \"[项目主页](https://github.com/pt-plugins/PT-Plugin-Plus) - [使用说明](https://github.com/pt-plugins/PT-Plugin-Plus/wiki) - [常见问题](https://github.com/pt-plugins/PT-Plugin-Plus/wiki/frequently-asked-questions) - [意见反馈](https://github.com/pt-plugins/PT-Plugin-Plus/issues) - [打开助手](index.html)\",\n      version: MAIN_VERSION,\n      failContent:\n        \"更新日志加载失败，请前往 https://github.com/pt-plugins/PT-Plugin-Plus/releases/ 查看发布说明\",\n      year: new Date().getFullYear()\n    };\n  },\n\n  created() {\n    fetch(`https://api.github.com/repos/pt-plugins/PT-Plugin-Plus/releases/tags/${this.version}`)\n        .then(r => r.json())\n        .then((result: any) => {\n          this.content = this.parse(result.body);\n        })\n        .catch(() => {\n          this.content = this.failContent;\n        });\n  },\n\n  methods: {\n    marked,\n    parse(content: string): string {\n      return content.replace(\n        /(#)(\\d+)/g,\n        \"[#$2](https://github.com/pt-plugins/PT-Plugin-Plus/issues/$2)\"\n      );\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\">\na {\n  text-decoration: none;\n  color: #1976d2;\n}\n\na:hover {\n  color: #008c00;\n}\n\n.header {\n  padding: 10px;\n  font-size: 30px;\n  border-bottom: 1px #ccc solid;\n}\n.markdown-body {\n  padding: 10px;\n}\n\n.footer {\n  border-top: 1px #ccc dotted;\n  margin: 10px;\n  text-align: center;\n  line-height: 30px;\n\n  p {\n    margin: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/changelog/index.ts",
    "content": "import Vue from \"vue\";\nimport App from \"./Index.vue\";\nimport vuetifyService from \"@/options/plugins/vuetify\";\nimport \"github-markdown-css/github-markdown.css\";\n\nvuetifyService.init(\"en\");\n\nnew Vue({\n  el: \"#app\",\n  render: h => h(App)\n});\n"
  },
  {
    "path": "src/content/README.md",
    "content": "# 本目录为 Chrome 扩展的需要插入到每个页面代码"
  },
  {
    "path": "src/content/index.ts",
    "content": "import Extension from \"../service/extension\";\nimport {\n  Options,\n  EAction,\n  Site,\n  SiteSchema,\n  Plugin,\n  ButtonOption,\n  NoticeOptions,\n  EDownloadClientType,\n  ESizeUnit,\n  EDataResultType,\n  Request,\n  EButtonType,\n  ECommonKey,\n  Dictionary,\n  EPluginPosition\n} from \"@/interface/common\";\nimport { APP } from \"@/service/api\";\nimport { filters } from \"@/service/filters\";\nimport { PathHandler } from \"@/service/pathHandler\";\nimport i18n from \"i18next\";\nimport { InfoParser } from \"@/background/infoParser\";\nimport { PPF } from \"@/service/public\";\n\ndeclare global {\n  interface Window {\n    Drag: any;\n  }\n}\n\n/**\n * 插件背景脚本，会插入到每个页面\n */\nclass PTPContent {\n  public extension: Extension;\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n  public site: Site = {\n    name: \"\"\n  };\n\n  public action = EAction;\n  public filters = filters;\n  public defaultClient: any;\n  public downloadClientType = EDownloadClientType;\n  public sizeUnit = ESizeUnit;\n  public buttonType = EButtonType;\n  public allSiteKey = ECommonKey.allSite;\n\n  public schema: SiteSchema = {};\n\n  private scripts: any[] = [];\n  private styles: any[] = [];\n  private messageItems: Dictionary<any> = {};\n\n  public buttonBar: JQuery = <any>null;\n  public droper: JQuery = $(\n    \"<div style='display:none;' class='pt-plugin-droper'/>\"\n  );\n  private buttons: any[] = [];\n  private logo: JQuery = <any>null;\n\n  // 插件是否被重新启用过（暂不可用），onSuspend 事件无法执行。\n  private backgroundServiceIsStoped = false;\n\n  // 用于接收页面程序\n  public pageApp: any;\n  // 当前页面地址\n  public locationURL: string = location.href;\n  // 保存路径处理器\n  public pathHandler: PathHandler = new PathHandler();\n  // 多语言处理器\n  public i18n = i18n;\n  // 页面解析器\n  public infoParser: InfoParser = new InfoParser();\n  // 当前页面选择器配置\n  public pageSelector: any = {};\n  // 自动确定工具栏位置\n  public autoPosition: boolean = true;\n\n  // 保存当前工具栏位置key\n  private positionStorageKey = \"\";\n\n  constructor() {\n    this.extension = new Extension();\n    if (this.extension.isExtensionMode) {\n      this.readConfig();\n      this.initBrowserEvent();\n    }\n  }\n\n  private readConfig() {\n    this.extension.sendRequest(EAction.readConfig, (result: any) => {\n      this.options = result;\n      this.initI18n();\n    });\n  }\n\n  private init() {\n    this.initPages();\n\n    // 由于无法直接绑定 window 相关事件，故使用定时器来监听地址变化\n    // 主要用于单页面站点，地址栏由 history.pushState 等方法来变更后可以重新创建插件图标\n    setInterval(() => {\n      this.checkLocationURL();\n    }, 1000);\n  }\n\n  /**\n   * 初始化多语言环境\n   */\n  private initI18n() {\n    this.extension\n      .sendRequest(EAction.getCurrentLanguageResource, null, \"contentPage\")\n      .then(resource => {\n        // console.log(resource);\n        let locale = this.options.locale || \"en\";\n        // 初始化\n        i18n.init({\n          lng: locale,\n          interpolation: {\n            prefix: \"{\",\n            suffix: \"}\"\n          },\n          resources: {\n            [locale]: {\n              translation: resource\n            }\n          }\n        });\n        this.init();\n      });\n  }\n\n  /**\n   * 根据指定的host获取已定义的站点信息\n   * @param host\n   */\n  public getSiteFromHost(host: string) {\n    APP.debugMode && console.log(\"getSiteFromHost\", host);\n    let sites: Site[] = [];\n    if (this.options.sites) {\n      sites.push(...this.options.sites);\n    }\n\n    if (this.options.system && this.options.system.publicSites) {\n      sites.push(...this.options.system.publicSites);\n    }\n\n    let site = sites.find((item: Site) => {\n      let cdn = [item.url].concat(item.cdn);\n      return item.host == host || cdn.join(\"\").indexOf(`//${host}`) > -1;\n    });\n\n    if (site) {\n      return JSON.parse(JSON.stringify(site));\n    }\n\n    return null;\n  }\n\n  /**\n   * 初始化符合条件的附加页面\n   */\n  private initPages() {\n    this.initSiteConfig().then(() => {\n      this.initPlugins();\n    }).catch(() => {\n      APP.debugMode && console.log(\"initPages 失败\");\n    });\n  }\n\n  /**\n   * 初始化站点配置\n   */\n  private initSiteConfig(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (!this.options.showToolbarOnContentPage) {\n        reject();\n        return;\n      }\n      // 判断当前页面的所属站点是否已经被定义\n      this.site = this.getSiteFromHost(window.location.hostname);\n\n      if (this.site) {\n        // 适应多域名\n        this.site.url = window.location.origin + \"/\";\n      }\n\n      // 如果当前站点未定义，则不再继续操作\n      if (this.site && this.site.name) {\n        if (typeof this.site.schema === \"string\") {\n          this.schema =\n            this.options.system &&\n            this.options.system.schemas &&\n            this.options.system.schemas.find((item: SiteSchema) => {\n              return item.name == this.site.schema;\n            });\n        } else {\n          let site =\n            this.options.system &&\n            this.options.system.sites &&\n            this.options.system.sites.find((item: Site) => {\n              return item.host == this.site.host;\n            });\n          if (site && site.schema && typeof site.schema !== \"string\") {\n            this.schema = site.schema;\n            this.schema.siteOnly = true;\n          }\n        }\n        // 等待页面选择器加载完成后，再加载插件内容\n        this.initPageSelector().finally(() => {\n          resolve();\n        });\n      } else {\n        reject();\n      }\n    });\n  }\n\n  /**\n   * 初始化符合条件的插件\n   */\n  private initPlugins() {\n    this.positionStorageKey = `pt-plugin-${this.site.host}-position`;\n\n    this.scripts = [];\n    this.styles = [];\n    // 初始化插件按钮列表\n    this.initButtonBar();\n    this.initDroper();\n\n    if (this.schema && this.schema.plugins) {\n      // 获取符合当前网站所需要的附加脚本\n      this.schema.plugins.forEach((plugin: Plugin) => {\n        let index = plugin.pages.findIndex((page: string) => {\n          let fullpath = window.location.href;\n          let path = window.location.pathname;\n          let indexOf = fullpath.indexOf(page);\n          // 如果页面不包含，则使用正则尝试\n          if (indexOf === -1) {\n            return new RegExp(page, \"\").test(path);\n          }\n          return true;\n          // return window.location.pathname.indexOf(page) !== -1;\n        });\n\n        if (index !== -1) {\n          plugin.scripts.forEach((script: string) => {\n            let path = script;\n            // 判断是否为相对路径\n            if (path.substr(0, 1) !== \"/\") {\n              path = this.schema.siteOnly\n                ? `sites/${this.site.host}/${script}`\n                : `schemas/${this.schema.name}/${script}`;\n            }\n            this.scripts.push({\n              type: \"file\",\n              content: path\n            });\n          });\n        }\n      });\n    }\n\n    // 获取系统定义的网站信息\n    let systemSite =\n      this.options.system &&\n      this.options.system.sites &&\n      this.options.system.sites.find((item: Site) => {\n        return item.host == this.site.host;\n      });\n\n    if (!this.site.plugins) {\n      this.site.plugins = [];\n    } else if (this.site.schema !== \"publicSite\" && systemSite) {\n      for (let index = this.site.plugins.length - 1; index >= 0; index--) {\n        const item = this.site.plugins[index];\n        // 删除非自定义的插件，从系统定义中重新获取\n        if (!item.isCustom) {\n          this.site.plugins.splice(index, 1);\n        }\n      }\n    }\n\n    if (systemSite && systemSite.plugins) {\n      // 将系统定义的内容添加到最前面，确保基本库优先加载\n      this.site.plugins = systemSite.plugins.concat(this.site.plugins);\n    }\n\n    // 网站指定的脚本\n    if (this.site.plugins) {\n      let siteConfigPath =\n        this.site.schema == \"publicSite\" ? \"publicSites\" : \"sites\";\n\n      if (this.site.path) {\n        siteConfigPath += `/${this.site.path}`;\n      } else {\n        siteConfigPath += `/${this.site.host}`;\n      }\n      this.site.plugins.forEach((plugin: Plugin) => {\n        let index = plugin.pages.findIndex((page: string) => {\n          let path = window.location.pathname;\n          let indexOf = path.indexOf(page);\n          // 如果页面不包含，则使用正则尝试\n          if (indexOf === -1) {\n            return new RegExp(page, \"\").test(path);\n          }\n          return true;\n        });\n\n        if (index !== -1) {\n          plugin.scripts &&\n            plugin.scripts.forEach((script: any) => {\n              let path = script;\n              // 判断是否为相对路径\n              if (path.substr(0, 1) !== \"/\" && path.substr(0, 4) !== \"http\") {\n                path = `${siteConfigPath}/${script}`;\n              }\n              // 文件\n              this.scripts.push({\n                type: \"file\",\n                content: path\n              });\n            });\n\n          if (plugin.script) {\n            // 代码\n            this.scripts.push({\n              type: \"code\",\n              content: plugin.script\n            });\n          }\n\n          if (plugin.styles) {\n            plugin.styles.forEach((style: string) => {\n              let path = style;\n              if (path.substr(0, 1) !== \"/\" && path.substr(0, 4) !== \"http\") {\n                path = `${siteConfigPath}/${style}`;\n              }\n              this.styles.push({\n                type: \"file\",\n                content: path\n              });\n            });\n          }\n\n          if (plugin.style) {\n            // 代码\n            this.styles.push({\n              type: \"code\",\n              content: plugin.style\n            });\n          }\n        }\n      });\n    }\n\n    if (this.styles && this.styles.length > 0) {\n      this.styles.forEach((path: string) => {\n        APP.applyStyle(path);\n      });\n    }\n\n    // 加入脚本并执行\n    if (this.scripts && this.scripts.length > 0) {\n      this.scripts.forEach((script: any) => {\n        APP.addScript(script);\n      });\n      // 按顺序执行所有脚本\n      APP.applyScripts();\n    }\n\n    // 通知后台添加了一个新页面\n    this.extension.sendRequest(EAction.addContentPage).catch(error => {\n      console.log(error);\n    });\n  }\n\n  /**\n   * 调用一个方法\n   * @param action 需要执行的命令\n   * @param data 额外需要传递的数据\n   * @return Promise\n   */\n  public call(action: EAction, data?: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.backgroundServiceIsStoped) {\n        reject({\n          msg: i18n.t(\"backgroundServiceIsStoped\") //\"插件已被禁用过重启过，请刷新页面后再重试\"\n        });\n        return;\n      }\n      try {\n        this.extension\n          .sendRequest(action, null, data)\n          .then((result: any) => {\n            if (result) {\n              resolve && resolve(result);\n            } else {\n              reject && reject();\n            }\n          })\n          .catch((result: any) => {\n            reject(result);\n          });\n      } catch (error) {\n        //`${action} 执行出错，可能后台服务不可用`\n        this.showNotice(\n          i18n.t(\"actionExecutionFailed\", {\n            action\n          })\n        );\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 初始化按钮栏\n   */\n  private initButtonBar() {\n    // 删除之前已创建的插件按钮栏\n    if ($(\".pt-plugin-body\").length) {\n      $(\".pt-plugin-body\").remove();\n    }\n    this.buttonBar = $(\"<div class='pt-plugin-body'/>\").appendTo(document.body);\n\n    // 启用拖放功能\n    if (window.Drag) {\n      let dragTitle = $(\n        \"<div class='pt-plugin-drag-title' title='\" + i18n.t(\"dragTitle\") + \"'>\"\n      ).appendTo(this.buttonBar);\n\n      new window.Drag(this.buttonBar.get(0), {\n        handle: dragTitle.get(0),\n        onStop: (result: any) => {\n          console.log(result);\n          this.saveButtonBarPosition(result);\n        }\n      });\n\n      // 双击重置位置\n      dragTitle.on(\"dblclick\", () => {\n        this.resetButtonBarPosition();\n      });\n    }\n\n    this.logo = $(\n      \"<div class='pt-plugin-logo' title='\" + i18n.t(\"pluginTitle\") + \"'/>\"\n    ).appendTo(this.buttonBar);\n    this.logo.on(\"click\", () => {\n      this.call(EAction.openOptions);\n    });\n    this.initButtonBarPosition();\n    this.buttonBar.hide();\n  }\n\n  /**\n   * 初始化工具栏位置\n   */\n  private initButtonBarPosition() {\n    let result = window.localStorage.getItem(this.positionStorageKey);\n    if (result) {\n      try {\n        let position = JSON.parse(result);\n\n        this.buttonBar.css({\n          top: position.top,\n          left: position.left\n        });\n        this.autoPosition = false;\n        return;\n      } catch (error) {\n        console.log(error);\n      }\n    }\n    this.buttonBar.css({\n      top: window.innerHeight / 2,\n      left: \"unset\"\n    });\n\n    if (this.options.position == EPluginPosition.left) {\n      this.buttonBar.css({\n        right: \"unset\",\n        left: \"5px\"\n      });\n    }\n  }\n\n  /**\n   * 重置工具栏位置\n   */\n  private resetButtonBarPosition() {\n    window.localStorage.removeItem(this.positionStorageKey);\n    this.autoPosition = true;\n    this.initButtonBarPosition();\n    this.recalculateButtonBarPosition();\n  }\n\n  /**\n   * 保存工具栏位置\n   * @param position\n   */\n  private saveButtonBarPosition(position: any) {\n    window.localStorage.setItem(\n      this.positionStorageKey,\n      JSON.stringify(position)\n    );\n    this.autoPosition = false;\n  }\n\n  /**\n   * 添加一个按钮\n   * @param options 按钮参数\n   */\n  public addButton(options: ButtonOption) {\n    options = Object.assign(\n      {\n        type: EButtonType.normal\n      },\n      options\n    );\n\n    let line = $(\"<hr/>\").appendTo(this.buttonBar);\n    let buttonType = \"<a class='pt-plugin-button'/>\";\n    if (!options.click || options.type == EButtonType.label) {\n      buttonType = \"<span class='pt-plugin-button'/>\";\n    }\n    let button = $(buttonType)\n      .attr({\n        title: options.title,\n        key: options.key\n      })\n      .data(\"line\", line);\n    let inner = $(\"<div class='pt-plugin-button-inner'/>\").appendTo(button);\n    let loading = $(\"<div class='pt-plugin-loading'/>\").appendTo(button);\n    let success = $(\"<div class='action-success'/>\")\n      .html('<div class=\"action-success-ani\"></div>')\n      .appendTo(button);\n    if (options.icon) {\n      $(\"<i class='material-icons md-36'/>\")\n        .html(options.icon)\n        .appendTo(inner);\n    }\n    $(\"<div/>\")\n      .html(options.label)\n      .appendTo(inner);\n\n    let onSuccess = (result: any) => {\n      if (options.type == EButtonType.normal) {\n        loading.hide();\n      } else {\n        inner.hide();\n      }\n\n      success.show();\n      if (result && result.msg) {\n        if (!result.type) {\n          result.type = \"success\";\n        }\n        this.showNotice(result);\n      }\n      setTimeout(() => {\n        success.hide();\n        inner.show();\n      }, 2000);\n    };\n\n    let onError = (error: any) => {\n      if (options.type == EButtonType.normal) {\n        loading.hide();\n      }\n      inner.show();\n      this.showNotice({\n        msg:\n          error ||\n          i18n.t(\"callbackFailed\", {\n            label: options.label\n          }) // `${options.label} 发生错误，请重试。`\n      });\n    };\n\n    if (options.click) {\n      button.click(event => {\n        if (options.type == EButtonType.normal) {\n          inner.hide();\n          loading.show();\n        }\n\n        (<any>options).click(onSuccess, onError, event);\n      });\n    }\n\n    button.appendTo(this.buttonBar);\n\n    // 是否指定了拖放事件\n    if (options.onDrop) {\n      this.addDroper(button, options.onDrop, onSuccess, onError);\n    }\n\n    this.buttons.push(button);\n    this.recalculateButtonBarPosition();\n  }\n\n  /**\n   * 删除指定Key的按钮\n   * @param key\n   */\n  public removeButton(key: string) {\n    let index = this.buttons.findIndex((button: JQuery) => {\n      return button.attr(\"key\") == key;\n    });\n\n    if (index != -1) {\n      let button = this.buttons[index];\n\n      let line = button.data(\"line\");\n      if (line) {\n        line.remove();\n      }\n      button.remove();\n      this.buttons.splice(index, 1);\n    }\n\n    this.recalculateButtonBarPosition();\n  }\n\n  /**\n   * 重新计算工具栏位置\n   */\n  public recalculateButtonBarPosition() {\n    if (this.buttons.length > 0) {\n      this.buttonBar.show();\n    } else {\n      this.buttonBar.hide();\n    }\n    if (!this.autoPosition) {\n      return;\n    }\n    this.buttonBar.css({\n      top: window.innerHeight / 2 - <any>this.buttonBar.outerHeight(true) / 2\n    });\n  }\n\n  /**\n   * 显示消息提示\n   * @param options 需要显示的消息选项\n   * @return DOM\n   */\n  public showNotice(options: NoticeOptions | string) {\n    APP.debugMode && console.log(options);\n    options = Object.assign(\n      {\n        type: \"error\",\n        timeout: 5,\n        position: \"bottomRight\",\n        progressBar: true,\n        width: 320,\n        indeterminate: false\n      },\n      typeof options === \"string\"\n        ? { msg: options }\n        : typeof options.msg === \"object\"\n          ? options.msg\n          : options\n    );\n\n    options.text = options.text || options.msg;\n    if (options.timeout) {\n      options.timeout = options.timeout * 10;\n    }\n    delete options.msg;\n\n    let notice = new (<any>window)[\"NoticeJs\"](options);\n\n    if (options.indeterminate === true) {\n      this.messageItems[notice.id] = notice;\n      notice.show();\n      return notice;\n    }\n\n    return $(notice.show());\n  }\n\n  /**\n   * 隐藏并关闭指定消息\n   * @param id\n   */\n  public hideMessage(id: string) {\n    if (this.messageItems[id]) {\n      this.messageItems[id].close();\n    }\n  }\n\n  /**\n   * 获取当前站点的默认下载目录\n   * @param string clientId 指定客户端ID，不指定表示使用默认下载客户端\n   * @return string 目录信息，如果没有定义，则返回空字符串\n   */\n  public getSiteDefaultPath(clientId: string = \"\"): string {\n    if (!clientId) {\n      clientId =\n        this.site.defaultClientId || <string>this.options.defaultClientId;\n    }\n\n    let client = this.options.clients.find((item: any) => {\n      return item.id === clientId;\n    });\n    let path = \"\";\n    if (client && client.paths) {\n      for (const host in client.paths) {\n        if (this.site.host === host) {\n          path = client.paths[host][0];\n          break;\n        }\n      }\n    }\n\n    // 替换目录中的关键字后再返回\n    return this.pathHandler.replacePathKey(path, this.site);\n  }\n\n  /**\n   * 获取指定客户端配置\n   * @param clientId\n   */\n  public getClientOptions(clientId: string = \"\") {\n    if (!clientId) {\n      clientId =\n        this.site.defaultClientId || <string>this.options.defaultClientId;\n    }\n\n    let client = this.options.clients.find((item: any) => {\n      return item.id === clientId;\n    });\n\n    return client;\n  }\n\n  /**\n   * 初始化拖放对象\n   */\n  public initDroper() {\n    if (!this.options.allowDropToSend) return;\n\n    // 添加文档拖放事件\n    document.addEventListener(\"dragstart\", (e: any) => {\n      if (e.target.tagName == \"A\") {\n        let data = {\n          url: e.target.getAttribute(\"href\"),\n          title: e.target.getAttribute(\"title\")\n        };\n        e.dataTransfer.setData(\"text/plain\", JSON.stringify(data));\n      }\n    });\n\n    // 拖入时\n    this.buttonBar.on(\"dragover\", (e: any) => {\n      e.stopPropagation();\n      e.preventDefault();\n      this.showDroper();\n    });\n\n    this.buttonBar.on(\"dragleave\", (e: any) => {\n      this.buttonBar.removeClass(\"pt-plugin-body-over\");\n    });\n\n    this.buttonBar.on(\"mouseleave\", (e: any) => {\n      this.buttonBar.removeClass(\"pt-plugin-body-over\");\n      this.hideDroper();\n    });\n\n    this.droper.appendTo(this.buttonBar);\n    // 拖入接收对象时\n    this.droper[0].addEventListener(\n      \"dragover\",\n      (e: any) => {\n        //console.log(e);\n        e.stopPropagation();\n        e.preventDefault();\n        // e.dataTransfer.dropEffect = \"copy\";\n        // if (e.target.tagName == \"A\") {\n        //   let data = {\n        //     url: e.target.getAttribute(\"href\"),\n        //     title: e.target.getAttribute(\"title\")\n        //   };\n        //   e.dataTransfer.setData(\"text/plain\", JSON.stringify(data));\n        // }\n        this.logo.addClass(\"pt-plugin-onLoading\");\n        this.buttonBar.addClass(\"pt-plugin-body-over\");\n      },\n      false\n    );\n\n    // 拖放事件\n    this.droper[0].addEventListener(\n      \"drop\",\n      (e: any) => {\n        //console.log(e);\n        e.stopPropagation();\n        e.preventDefault();\n        this.hideDroper();\n\n        // 获取未处理的地址\n        try {\n          let data = JSON.parse(e.dataTransfer.getData(\"text/plain\"));\n          if (data) {\n            if (data.url) {\n              // IMDb地址\n              let IMDbMatch = data.url.match(/imdb\\.com\\/title\\/(tt\\d+)/);\n              if (IMDbMatch && IMDbMatch.length > 1) {\n                this.extension.sendRequest(\n                  EAction.openOptions,\n                  null,\n                  `search-torrent/${IMDbMatch[1]}`\n                );\n                this.logo.removeClass(\"pt-plugin-onLoading\");\n                return;\n              }\n              if (this.pageApp) {\n                this.pageApp\n                  .call(EAction.downloadFromDroper, data)\n                  .then(() => {\n                    this.logo.removeClass(\"pt-plugin-onLoading\");\n                  })\n                  .catch(() => {\n                    this.logo.removeClass(\"pt-plugin-onLoading\");\n                  });\n              } else {\n                this.showNotice({\n                  type: EDataResultType.info,\n                  msg: i18n.t(\"notSupported\"), // \"当前页面不支持此操作\",\n                  timeout: 3\n                });\n                this.logo.removeClass(\"pt-plugin-onLoading\");\n              }\n            } else {\n              this.logo.removeClass(\"pt-plugin-onLoading\");\n            }\n          }\n        } catch (error) {\n          this.logo.removeClass(\"pt-plugin-onLoading\");\n        }\n      },\n      false\n    );\n\n    // 离开拖放时\n    this.droper.on(\"dragleave dragend\", (e: any) => {\n      e.stopPropagation();\n      e.preventDefault();\n      this.hideDroper();\n      this.logo.removeClass(\"pt-plugin-onLoading\");\n      this.buttonBar.removeClass(\"pt-plugin-body-over\");\n    });\n  }\n\n  /**\n   * 增加拖放对象\n   * @param parent\n   * @param onDrop\n   */\n  public addDroper(\n    parent: any,\n    onDrop: Function,\n    onSuccess: Function,\n    onError: Function\n  ) {\n    if (!onDrop) {\n      return;\n    }\n    let droper: JQuery = $(\n      \"<div style='display:none;' class='pt-plugin-droper'/>\"\n    );\n\n    droper.appendTo(this.buttonBar);\n    // 拖入接收对象时\n    droper.on(\"dragover\", (e: any) => {\n      //console.log(e);\n      e.stopPropagation();\n      e.preventDefault();\n      this.buttonBar.addClass(\"pt-plugin-body-over\");\n    });\n\n    // 拖放事件\n    droper.on(\"drop\", (e: any) => {\n      console.log(e);\n      e.stopPropagation();\n      e.preventDefault();\n      this.hideDroper();\n\n      // 获取未处理的地址\n      try {\n        let data = JSON.parse(\n          e.originalEvent.dataTransfer.getData(\"text/plain\")\n        );\n        if (data && data.url) {\n          onDrop.call(this, data, e, onSuccess, onError);\n        }\n      } catch (error) {\n        // 错误时，尝试直接使用文本内容\n        let data = e.originalEvent.dataTransfer.getData(\"text/plain\");\n        if (data) {\n          data = {\n            url: data\n          };\n\n          onDrop.call(this, data, e, onSuccess, onError);\n        }\n      }\n    });\n\n    // 离开拖放时\n    droper.on(\"dragleave dragend\", (e: any) => {\n      e.stopPropagation();\n      e.preventDefault();\n      this.hideDroper();\n      this.buttonBar.removeClass(\"pt-plugin-body-over\");\n    });\n\n    // 设置位置\n    droper.offset(parent.position());\n  }\n\n  private hideDroper() {\n    $(\".pt-plugin-droper\").hide();\n  }\n\n  private showDroper() {\n    $(\".pt-plugin-droper\").show();\n  }\n\n  private initBrowserEvent() {\n    chrome.runtime.onMessage.addListener(\n      (\n        message: Request,\n        sender: chrome.runtime.MessageSender,\n        callback: (response: any) => void\n      ) => {\n        APP.debugMode && console.log(\"content.onMessage\", message);\n        switch (message.action) {\n          case EAction.showMessage:\n            let notice = this.showNotice(message.data);\n            callback && callback(notice);\n            break;\n\n          case EAction.hideMessage:\n            this.hideMessage(message.data);\n            break;\n\n          case EAction.serviceStoped:\n            this.backgroundServiceIsStoped = true;\n            break;\n        }\n      }\n    );\n  }\n\n  /**\n   * 验证地址栏变化后重新创建插件图标\n   */\n  public checkLocationURL() {\n    if (location.href != this.locationURL) {\n      this.locationURL = location.href;\n      this.initPages();\n    }\n  }\n\n  /**\n   * 加载页面选择器\n   */\n  private initPageSelector(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.call(EAction.getSiteSelectorConfig, {\n        host: this.site.host,\n        name: location.pathname\n      })\n        .then(result => {\n          this.pageSelector = result;\n          resolve();\n        })\n        .catch(() => {\n          // 如果没有当前页面的选择器，则尝试获取通用的选择器\n          this.call(EAction.getSiteSelectorConfig, {\n            host: this.site.host,\n            name: \"common\"\n          })\n            .then(result => {\n              this.pageSelector = result;\n              resolve();\n            })\n            .catch(() => {\n              // 没有选择器\n              resolve();\n            });\n        });\n    });\n  }\n\n  /**\n   * 从当前页面或指定DOM中获取指定字段的内容\n   * @param fieldName 字段名称\n   * @param content 指定的父元素，默认为 body\n   * @return null 表示没有获取到内容\n   */\n  public getFieldValue(fieldName: string = \"\", content: any = $(\"body\")) {\n    let selector: any;\n    console.log(\"getFieldValue\", fieldName);\n    if (this.pageSelector && this.pageSelector.fields) {\n      selector = this.pageSelector.fields[fieldName];\n\n      if (!selector) {\n        return null;\n      }\n    } else {\n      return null;\n    }\n\n    return this.infoParser.getFieldData(content, selector, this.pageSelector);\n  }\n}\n\n// 暴露到 window 对象\nObject.assign(window, {\n  PTService: new PTPContent(),\n  PPF\n});\n"
  },
  {
    "path": "src/debugger/Index.vue",
    "content": "<template>\n  <v-app id=\"inspire\">\n    <v-toolbar :color=\"baseColor\" app fixed clipped-left id=\"system-topbar\">\n      <v-toolbar-title style=\"width: 220px\" class=\"hidden-md-and-down\">\n        <span>Debugger Beta</span>\n      </v-toolbar-title>\n\n      <v-spacer></v-spacer>\n      <v-toolbar-items class=\"hidden-xs-only\">\n        <v-btn\n          flat\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/issues\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon>bug_report</v-icon>\n        </v-btn>\n      </v-toolbar-items>\n    </v-toolbar>\n    <v-content>\n      <v-container fluid class=\"debugger\">\n        <table>\n          <tbody>\n            <tr v-for=\"(item, index) in items\" :key=\"index\">\n              <td class=\"id\">{{ index + 1 }}</td>\n              <td class=\"time\">{{ item.time }}</td>\n              <td class=\"msg\">\n                <div>{{ item.msg }}</div>\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </v-container>\n    </v-content>\n  </v-app>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      items: [] as any[],\n      baseColor: \"amber\"\n    };\n  },\n\n  methods: {\n    add(msg: any) {\n      this.items.push({\n        time: new Date().toLocaleString(),\n        msg: typeof msg === \"string\" ? msg : JSON.stringify(msg)\n      });\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.debugger {\n  padding: 5px;\n\n  table {\n    width: 100%;\n    td: {\n      padding: 2px;\n    }\n  }\n\n  table tbody tr:nth-child(even) {\n    background-color: #f1f1f1;\n  }\n\n  table tbody tr:nth-child(odd) {\n    background-color: #fff;\n  }\n\n  .time {\n    color: #0044ff;\n    width: 150px;\n  }\n\n  .id {\n    width: 20px;\n    text-align: center;\n  }\n\n  .msg {\n    max-width: 70%;\n    div {\n      width: 100%;\n      word-wrap: break-word;\n      word-break: break-all;\n      max-height: 120px;\n      overflow-y: auto;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "src/debugger/index.ts",
    "content": "import Vue from \"vue\";\nimport App from \"./Index.vue\";\nimport vuetifyService from \"@/options/plugins/vuetify\";\nimport { EModule, EAction } from \"@/interface/enum\";\nimport { IRequest } from \"@/interface/common\";\n\nimport Extension from \"@/service/extension\";\n\nconst extension = new Extension();\n\nclass Debugger {\n  private vm: any;\n\n  constructor() {\n    vuetifyService.init(\"en\");\n\n    this.vm = new Vue({\n      el: \"#app\",\n\n      render: h => h(App)\n    });\n\n    this.initEvents();\n  }\n\n  private initEvents() {\n    chrome.runtime.onConnect.addListener(port => {\n      console.assert(port.name == EModule.debugger);\n      port.onMessage.addListener((request: IRequest) => {\n        console.log(request);\n        if (request.action == EAction.pushDebugMsg) {\n          this.add(request.data);\n        }\n      });\n    });\n\n    chrome.tabs.getCurrent((tab: any) => {\n      console.log(\"debugTabId: %s\", tab.id);\n      extension.sendRequest(EAction.updateDebuggerTabId, null, tab.id);\n    });\n  }\n\n  private add(msg: any) {\n    this.vm.$children[0].add(msg);\n  }\n}\n\nnew Debugger();\n"
  },
  {
    "path": "src/interface/common.ts",
    "content": "export * from \"./enum\";\nimport {\n  ERequestResultType,\n  ESizeUnit,\n  EButtonType,\n  ERequestMethod,\n  EAction,\n  EDataResultType,\n  EUserDataRequestStatus,\n  EBeforeSearchingItemSearchMode,\n  EBackupServerType,\n  EPluginPosition,\n  EWorkingStatus,\n  EEncryptMode,\n  ETorrentStatus,\n  ERequestType\n} from \"./enum\";\n\n/**\n * 需要在上下文菜单显示配置\n */\nexport interface ContextMenuRules {\n  torrentDetailPages?: string[];\n  torrentListPages?: string[];\n  torrentLinks?: string[];\n}\n\nexport interface DownloadClient {\n  id?: string;\n  name?: string;\n  // oldName?: string;\n  address?: string;\n  loginName?: string;\n  loginPwd?: string;\n  paths?: any;\n  autoStart?: boolean;\n  tagIMDb?: boolean;\n  type?: string;\n}\n\n/**\n * 助手按钮\n */\nexport interface ButtonOption {\n  title: string;\n  label: string;\n  key?: string;\n  click?: Function;\n  icon?: string;\n  type?: EButtonType;\n  onDrop?: Function;\n}\n\nexport interface SystemOptions {\n  sites?: any[];\n  schemas?: any[];\n  clients?: any[];\n  publicSites?: any[];\n}\n\nexport type Dictionary<T> = { [key: string]: T };\nexport interface SearchOptions {\n  rows?: number;\n  key?: string;\n  tags?: string[];\n  timeout?: number;\n  saveKey?: boolean;\n}\n\nexport interface IApiKey {\n  omdb?: string[];\n  douban?: string[];\n}\n\n/**\n * 备份服务器\n */\nexport interface IBackupServer {\n  id: string;\n  type: EBackupServerType;\n  address: string;\n  name: string;\n  lastBackupTime?: number;\n  loginName?: string;\n  loginPwd?: string;\n  authCode?: string;\n  digest?: boolean;\n}\n\n/**\n * 参数\n */\nexport interface Options {\n  autoUpdate?: boolean;\n  allowDropToSend?: boolean;\n  defaultClient?: any;\n  defaultClientId?: string;\n  needConfirmWhenExceedSize?: boolean;\n  exceedSize?: number;\n  exceedSizeUnit?: ESizeUnit;\n  sites: any[];\n  clients: any[];\n  pluginIconShowPages?: string[];\n  contextMenuRules?: ContextMenuRules;\n  allowSelectionTextSearch?: boolean;\n  schemas?: any[];\n  system?: SystemOptions;\n  search?: SearchOptions | void;\n  saveDownloadHistory?: boolean;\n  connectClientTimeout?: number;\n  rowsPerPageItems?: any[];\n  defaultSearchSolutionId?: string;\n  searchSolutions?: SearchSolution[];\n  autoRefreshUserData?: boolean;\n  autoRefreshUserDataHours?: number | string;\n  autoRefreshUserDataMinutes?: number | string;\n  autoRefreshUserDataNextTime?: number;\n  autoRefreshUserDataLastTime?: number;\n  // 自动获取用户数据失败重试次数\n  autoRefreshUserDataFailedRetryCount?: number;\n  // 自动获取用户数据失败重试间隔时间（分钟）\n  autoRefreshUserDataFailedRetryInterval?: number;\n  // 最近搜索的关键字\n  lastSearchKey?: string;\n  // 显示的用名名称\n  displayUserName?: string;\n  // 分享寄语\n  shareMessage?: string;\n  // 搜索结果点击站点时，按站点优先级别排序\n  searchResultOrderBySitePriority?: boolean;\n  // 导航栏是否已打开\n  navBarIsOpen?: boolean;\n  // 在搜索时显示电影信息（搜索IMDb时有效）\n  showMoiveInfoCardOnSearch?: boolean;\n  // 在搜索之前一些选项配置\n  beforeSearchingOptions?: BeforeSearching;\n  // 在页面中显示工具栏\n  showToolbarOnContentPage?: boolean;\n  // 当前语言\n  locale?: string;\n  // 下载失败重试后是否重试\n  downloadFailedRetry?: boolean;\n  // 下载失败重试次数\n  downloadFailedFailedRetryCount?: number;\n  // 下载失败间隔时间（秒）\n  downloadFailedFailedRetryInterval?: number;\n  // 用户自定义 API Key 列表\n  apiKey?: IApiKey;\n  // 备份服务器列表\n  backupServers?: IBackupServer[];\n  // 批量下载时间间隔（秒）\n  batchDownloadInterval?: number;\n  // 启用后台批量下载\n  enableBackgroundDownload?: boolean;\n  // 插件默认显示位置\n  position?: EPluginPosition;\n  // 默认的收藏分组ID\n  defaultCollectionGroupId?: string;\n  // 允许备份站点 cookies\n  allowBackupCookies?: boolean;\n  // 加密备份的数据\n  encryptBackupData?: boolean;\n  // 加密密钥，本项内容备份时清除\n  encryptSecretKey?: string;\n  // 加密方式\n  encryptMode?: EEncryptMode;\n  // 允许保存搜索结果快照\n  allowSaveSnapshot?: boolean;\n}\n\n// 在搜索之前一些选项配置\nexport interface BeforeSearching {\n  // 在输入搜索关键字时加载相关信息\n  getMovieInformation?: boolean;\n  // 最多返回条目\n  maxMovieInformationCount?: number;\n  // 当点击条目时，搜索模式\n  searchModeForItem?: EBeforeSearchingItemSearchMode;\n}\n\nexport interface Plugin {\n  id?: string;\n  name?: string;\n  pages?: string[] | any;\n  scripts?: string[] | any;\n  styles?: string[] | any;\n  script?: string;\n  style?: string;\n  isCustom?: boolean;\n}\n\nexport interface SiteSchema {\n  name?: string;\n  ver?: string;\n  plugins?: Plugin[] | any;\n  siteOnly?: boolean;\n  securityKeyFields?: string[];\n  searchEntryConfig?: SearchEntryConfig;\n  searchEntry?: SearchEntry[];\n  parser?: Dictionary<any>;\n  patterns?: Dictionary<any>;\n  checker?: Dictionary<any>;\n  torrentTagSelectors?: any[];\n  selectors?: Dictionary<any>;\n}\n\n/**\n * 站点资源类别（分类）\n */\nexport interface SiteCategory {\n  id?: number | string;\n  name?: string;\n}\n\nexport interface SiteCategories {\n  entry?: string;\n  result?: string;\n  appendToSearchKey?: boolean;\n  category?: SiteCategory[];\n}\n\n/**\n * 站点配置\n */\nexport interface Site {\n  id?: string;\n  name: string;\n  url?: string;\n  // 运行时配置，当定义了cdn时，获取第一个地址，没有时使用url\n  activeURL?: string;\n  cdn?: string[];\n  icon?: string;\n  schema?: any;\n  tags?: string[];\n  passkey?: string;\n  value?: boolean;\n  description?: string;\n  host?: string;\n  defaultClientId?: string;\n  plugins?: any[];\n  allowSearch?: boolean;\n  securityKeys?: object;\n  searchEntryConfig?: SearchEntryConfig;\n  searchEntry?: SearchEntry[];\n  parser?: Dictionary<any>;\n  patterns?: Dictionary<any>;\n  checker?: Dictionary<any>;\n  torrentTagSelectors?: any[];\n  categories?: SiteCategories[];\n  downloadMethod?: ERequestMethod;\n  user?: UserInfo;\n  selectors?: Dictionary<any>;\n  allowGetUserInfo?: boolean;\n  // 站点优先级\n  priority?: number;\n  path?: string;\n  // 曾用域名列表，用于数据升级\n  formerHosts?: string[];\n  // 离线，设置为true时，不再进行搜索和个人信息获取，保存原数据统计\n  // todo: 后续可根据站点返回的状态码自动设置为离线\n  offline?: boolean;\n  // 是否为自定义\n  isCustom?: boolean;\n  // 时区偏移量，用于解决时差问题，如：+08:00, -08:00, +0800, UTC+0800, UTC+08:00\n  // @see https://zh.wikipedia.org/wiki/各國時區列表\n  // @see https://zh.wikipedia.org/wiki/时区\n  timezoneOffset?: string;\n  // 是否合并 Schema 的标签选择器\n  mergeSchemaTagSelectors?: boolean;\n  // 消息提醒开关\n  disableMessageCount?: boolean;\n  // 等级要求\n  levelRequirements?: LevelRequirement[];\n  upLoadLimit?: number;\n}\n\nexport interface LevelRequirement {\n  level?: number;\n  name?: string;\n  // 间隔要求\n  interval?: number;\n  // 日期要求\n  requiredDate?: string;\n  // 上传数要求\n  uploads?: number;\n  // 下载数要求\n  downloads?: number;\n  // 上传量要求\n  uploaded?: string | number;\n  // 下载量要求\n  downloaded?: string | number;\n  // 真实下载量\n  trueDownloaded?: string | number;\n  // 积分要求\n  bonus?: number;\n  // 做种积分要求\n  seedingPoints?: number;\n  // 做种时间要求\n  seedingTime?: number;\n  // 保种体积要求\n  seedingSize?: number;\n  // 分享率要求\n  ratio?: number;\n  // 等级积分要求\n  classPoints?: number;\n  // 独特分组要求\n  uniqueGroups?: number;\n  // “完美”FLAC要求\n  perfectFLAC?: number;\n  // 权限\n  privilege?: string;\n  // 可选要求\n  alternative?: LevelRequirement;\n}\n\nexport interface Request {\n  action: EAction;\n  data?: any;\n}\n\nexport interface IRequest extends Request {}\n\nexport interface NoticeOptions {\n  msg?: string;\n  type?: string;\n  timeout?: number;\n  position?: string;\n  text?: string;\n  indeterminate?: boolean;\n}\n\nexport interface CacheType {\n  content?: any;\n}\n\n/**\n * 下载参数\n */\nexport interface DownloadOptions {\n  // 下载地址\n  url: string;\n  title?: string;\n  savePath?: string;\n  autoStart?: boolean;\n  tagIMDb?: boolean;\n  clientId?: string;\n  // 来源链接地址\n  link?: string;\n  imdbId?: string;\n}\n\n/**\n * 调用数据返回的结果格式\n */\nexport interface DataResult {\n  // 是否成功\n  success: boolean;\n  // 成功或失败消息\n  msg?: string;\n  // 类型\n  type?: EDataResultType;\n  // 附加数据\n  data?: any;\n}\n\nexport interface LogItem {\n  module?: string;\n  event?: string;\n  data?: any;\n  id?: number | string;\n  time?: number;\n  msg?: string;\n}\n\nexport interface SearchResultItemTag {\n  color?: string;\n  name?: string;\n}\n\nexport interface SearchResultItemCategory {\n  name?: string;\n  link?: string;\n}\n\n/**\n * 搜索返回结果\n */\nexport interface SearchResultItem {\n  id?: string;\n  site: Site;\n  title: string;\n  titleHTML?: string;\n  subTitle?: string;\n  time?: number | string;\n  author?: string;\n  url?: string;\n  link?: string;\n  size?: number | string;\n  seeders?: number | string;\n  leechers?: number | string;\n  completed?: number | string;\n  comments?: number | string;\n  uid?: string;\n  tags?: SearchResultItemTag[];\n  entryName?: string;\n  category?: SearchResultItemCategory;\n  // 进度（100表示完成）\n  progress?: number;\n  // 状态\n  status?: ETorrentStatus;\n  host?: string;\n  imdbId?: string;\n}\n\n/**\n * 搜索区域\n */\nexport interface SearchEntryConfigArea {\n  name: string;\n  queryString?: string;\n  appendQueryString?: string;\n  keyAutoMatch?: string;\n  replaceKey?: string[];\n  parseScript?: string;\n  // 替换默认页面\n  page?: string;\n}\n\nexport interface ISearchFieldIndex {\n  // 发布时间\n  time?: number;\n  // 大小\n  size?: number;\n  // 上传数量\n  seeders?: number;\n  // 下载数量\n  leechers?: number;\n  // 完成数量\n  completed?: number;\n  // 评论数量\n  comments?: number;\n  // 发布人\n  author?: number;\n  // 分类\n  category?: number;\n  link?: number;\n  url?: number;\n  subTitle?: number;\n  title?: number;\n}\n\n/**\n * 通用页面解析\n */\nexport interface IPageSelector {\n  // 需要请求的页面\n  page: string;\n  // 返回的数据类型，可用值：html，json ；默认为 html\n  dataType?: ERequestResultType;\n  // 用于解析数据的脚本文件路径；当指定该内容时，则执行该解析器，由解析器处理指定页面返回的内容，可用于请求多个页面等操作；\n  parser?: string;\n  // 请求方法，默认为 GET\n  requestMethod?: ERequestMethod;\n  // 数据请求头信息\n  headers?: Dictionary<any>;\n  // 需要提交的数据\n  requestData?: Dictionary<any>;\n  // 选择器列表\n  fields?: Dictionary<any>;\n  // 执行该规则的前提条件（条件表达式），合法的 js 语句；\n  prerequisites?: string;\n  // 是否合并 schema 已定义的内容，默认为 false\n  merge?: boolean;\n  // 指定用于获取内容的顶级 DOM 对象，默认为 body\n  topElement?: string;\n  // 缓存时间，单位：秒，0 及空表示不缓存\n  dataCacheTime?: number;\n}\n\n// 搜索入口默认配置\nexport interface SearchEntryConfig {\n  page: string;\n  entry?: string;\n  resultType?: ERequestResultType;\n  // don't encode the key, for some json post API. e.g. TNode\n  keepOriginKey?: boolean\n  requestDataType?: ERequestType;\n  queryString?: string;\n  parseScriptFile?: string;\n  parseScript?: string;\n  // 是否异步解析脚本\n  asyncParse?: boolean;\n  // 数据表选择器\n  resultSelector?: string;\n  area?: SearchEntryConfigArea[];\n  // 数据请求头信息\n  headers?: Dictionary<any>;\n  // 跳过IMDb搜索\n  skipIMDbId?: boolean;\n  // 搜索解析字段索引\n  fieldIndex?: ISearchFieldIndex;\n  // 数据字段选择器\n  fieldSelector?: Dictionary<any>;\n  // 第一行数据行\n  firstDataRowIndex?: number;\n  // 数据行选择器，默认：> tbody > tr\n  dataRowSelector?: string;\n  // 验证已登录正则表达式\n  loggedRegex?: string;\n  // 在搜索前需要处理的脚本\n  beforeSearch?: IPageSelector;\n  // 请求方法，默认为 GET\n  requestMethod?: ERequestMethod;\n  // 需要提交的数据\n  requestData?: Dictionary<any>;\n}\n\n/**\n * 具体搜索入口配置\n */\nexport interface SearchEntry extends SearchEntryConfig {\n  // 搜索入口名称\n  name?: string;\n  // 是否启用\n  enabled?: boolean;\n  // 标签选择器配置\n  tagSelectors?: any[];\n  // 是否为自定义\n  isCustom?: boolean;\n  // id，自动生成\n  id?: string;\n  // 分类目录\n  categories?: string[];\n  // 追加到搜索关键字的内容\n  appendToSearchKeyString?: string;\n  // 追加到查询字符串的内容\n  appendQueryString?: string;\n}\n\nexport interface UIOptions {\n  paginations?: Dictionary<any>;\n  views?: Dictionary<any>;\n}\n\n// 搜索方案\nexport interface SearchSolution {\n  id: string;\n  name: string;\n  range: SearchSolutionRange[];\n}\n\nexport interface SearchSolutionRange {\n  host?: string;\n  siteName?: string;\n  entry?: string[];\n}\n\n/**\n * 用户信息\n */\nexport interface UserInfo {\n  // 用户ID\n  id: number | string;\n  // 用户名\n  name: string;\n  // 上传量\n  uploaded?: number;\n  // 发布数\n  uploads?: number;\n  // 下载量\n  downloaded?: number;\n  // 真实下载量\n  trueDownloaded?: string | number;\n  // 下载数\n  downloads?: number;\n  // 分享率\n  ratio?: number;\n  // 当前做种数量\n  seeding?: number;\n  // 做种体积\n  seedingSize?: number;\n  // 做种列表\n  seedingList?: string[];\n  // 当前下载数量\n  leeching?: number;\n  // 等级名称\n  levelName?: string;\n  // 魔力值/积分\n  bonus?: number;\n  // 保种积分         //add by koal 220920\n  seedingPoints?: number;\n  // 做种时间要求\n  seedingTime?: number;\n  // 时魔\n  bonusPerHour?: number;\n  // 积分页面\n  bonusPage?: string;\n  // H&R未达标页面\n  unsatisfiedsPage?: string;\n  // 入站时间\n  joinTime?: number;\n  // 等级积分\n  classPoints?: number;\n  // H&R未达标\n  unsatisfieds?: string | number;\n  // H&R预警\n  prewarn?: number;\n  // 最后更新时间\n  lastUpdateTime?: number;\n  // 最后更新状态\n  lastUpdateStatus?: EUserDataRequestStatus;\n  // 邀请数量\n  invites?: number;\n  // 头像\n  avatar?: string;\n  // 是否已登录\n  isLogged?: boolean;\n  // 正在加载\n  isLoading?: boolean;\n  // 最后错误信息\n  lastErrorMsg?: string;\n  // 消息数量\n  messageCount?: number;\n  // 独特分组\n  uniqueGroups?: number;\n  // “完美”FLAC\n  perfectFLAC?: number;\n  // 下一等级\n  nextLevels?: LevelRequirement[];\n  [key: string]: any;\n}\n\nexport type i18nResource = {\n  name: string;\n  code: string;\n  authors?: Array<string>;\n  words: Dictionary<any>;\n};\n\n// 搜索时附加数据\nexport interface ISearchPayload {\n  imdbId?: string;\n  doubanId?: string;\n  cn?: string;\n  en?: string;\n  key?: string;\n}\n\nexport interface IHashData {\n  hash: string;\n  keyMap: number[];\n  length: number;\n}\n\nexport interface IManifest {\n  checkInfo: IHashData;\n  version: string;\n  time: number;\n  hash?: string;\n  encryptMode?: EEncryptMode;\n}\n\n/**\n * 已收藏的种子\n */\nexport interface ICollection {\n  host: string;\n  title: string;\n  // 下载地址\n  url: string;\n  // 种子页面链接\n  link: string;\n  site: any;\n  size: number;\n  time?: number;\n  subTitle?: string;\n  imdbId?: string;\n  movieInfo?: {\n    title: string;\n    alt_title: string;\n    imdbId?: string;\n    doubanId?: string;\n    image?: string;\n    link?: string;\n    year?: number;\n  };\n  // 分组ID列表\n  groups?: string[];\n}\n\n/**\n * 收藏分组\n */\nexport interface ICollectionGroup {\n  id?: string;\n  name: string;\n  count?: number;\n  description?: string;\n  image?: string;\n  color?: string;\n  update?: number;\n}\n\nexport const BASE_COLORS = [\n  \"red\",\n  \"pink\",\n  \"purple\",\n  \"deep-purple\",\n  \"indigo\",\n  \"blue\",\n  \"light-blue\",\n  \"cyan\",\n  \"teal\",\n  \"green\",\n  \"light-green\",\n  \"lime\",\n  \"yellow\",\n  \"amber\",\n  \"orange\",\n  \"deep-orange\",\n  \"brown\",\n  \"blue-grey\",\n  \"grey\",\n  \"black\"\n];\n\n/**\n * 通用标签颜色\n */\nexport const BASE_TAG_COLORS: Dictionary<any> = {\n  // 免费下载\n  Free: \"blue\",\n  // 免费下载 + 2x 上传\n  \"2xFree\": \"green\",\n  // 2x 上传\n  \"2xUp\": \"lime\",\n  // 2x 上传 + 50% 下载\n  \"2x50%\": \"light-green\",\n  // 25% 下载\n  \"25%\": \"purple\",\n  // 30% 下载\n  \"30%\": \"indigo\",\n  // 35% 下载\n  \"35%\": \"indigo darken-3\",\n  // 50% 下载\n  \"50%\": \"orange\",\n  // 70% 下载\n  \"70%\": \"blue-grey\",\n  // 75% 下载\n  \"75%\": \"lime darken-3\",\n  // 仅 VIP 可下载\n  VIP: \"orange darken-2\",\n  // 禁止转载\n  \"⛔️\": \"deep-orange darken-1\"\n};\n\nexport interface ICookies {\n  host: string;\n  url: string;\n  cookies: chrome.cookies.Cookie[];\n}\n\nexport interface IURL {\n  href: string;\n  protocol: string;\n  host: string;\n  port?: number;\n  query?: string;\n  params?: string[];\n  hash?: string;\n  path: string;\n  segments: string;\n  origin: string;\n}\n\nexport interface IWorkingStatusItem {\n  key: string;\n  status?: EWorkingStatus;\n  title: string;\n}\n\nexport interface ISearchResultSnapshot {\n  id: string;\n  key: string;\n  time: number;\n  searchPayload?: ISearchPayload;\n  result: SearchResultItem[];\n}\n\nexport interface IBackupRawData {\n  options: any;\n  userData: any;\n  collection: any;\n  cookies?: any;\n  searchResultSnapshot?: any;\n  keepUploadTask?: any;\n  downloadHistory?: any;\n}\n\nexport interface IKeepUploadTask {\n  id: string;\n  time: number;\n  title: string;\n  downloadOptions: DownloadOptions;\n  items: any[];\n}\n\nexport interface ISiteIcon {\n  origin: string;\n  host: string;\n  data: string;\n}\n"
  },
  {
    "path": "src/interface/enum.ts",
    "content": "/**\n * 体积单位\n */\nexport enum ESizeUnit {\n  ZiB = \"ZiB\",\n  EiB = \"EiB\",\n  PiB = \"PiB\",\n  TiB = \"TiB\",\n  GiB = \"GiB\",\n  MiB = \"MiB\",\n  KiB = \"KiB\"\n}\n\n/**\n * 数据请求类型\n */\nexport enum ERequestType {\n  JSON = \"json\",\n  TEXT = \"urlencode\"\n}\n\n/**\n * 数据请求返回类型\n */\nexport enum ERequestResultType {\n  JSON = \"json\",\n  XML = \"xml\",\n  HTML = \"html\",\n  TEXT = \"text\"\n}\n\n/**\n * 下载服务器（客户端）类型\n */\nexport enum EDownloadClientType {\n  transmission = \"transmission\",\n  utorrent = \"utorrent\",\n  deluge = \"deluge\",\n  synologyDownloadStation = \"synologyDownloadStation\",\n  rutorrent = \"rutorrent\",\n  qbittorrent = \"qbittorrent\"\n}\n\n/**\n * 助手按钮类型\n */\nexport enum EButtonType {\n  normal = \"normal\",\n  label = \"label\",\n  spliter = \"spliter\",\n  popup = \"popup\"\n}\n\n/**\n * 请求类型\n */\nexport enum ERequestMethod {\n  POST = \"POST\",\n  GET = \"GET\"\n}\n\n/**\n * 动作列表\n */\nexport enum EAction {\n  // 读取参数\n  readConfig = \"readConfig\",\n  // 保存参数\n  saveConfig = \"saveConfig\",\n  // 从网络中重新加载配置文件\n  reloadConfig = \"reloadConfig\",\n  // 发送种子到默认的下载服务器（客户端）\n  sendTorrentToDefaultClient = \"sendTorrentToDefaultClient\",\n  // 发送种子到指定的客户端\n  sendTorrentToClient = \"sendTorrentToClient\",\n  // 搜索种子\n  searchTorrent = \"searchTorrent\",\n  // 复制文本到剪切板\n  copyTextToClipboard = \"copyTextToClipboard\",\n  // 从指定的URL添加种子\n  addTorrentFromURL = \"addTorrentFromURL\",\n  // 获取可用空间\n  getFreeSpace = \"getFreeSpace\",\n  // 下载当前拖放DOM中的地址\n  downloadFromDroper = \"downloadFromDroper\",\n  // 打开配置页面\n  openOptions = \"openOptions\",\n  // 更新配置页面TabId\n  updateOptionsTabId = \"updateOptionsTabId\",\n  // 获取搜索结果\n  getSearchResult = \"getSearchResult\",\n  // 获取下载记录\n  getDownloadHistory = \"getDownloadHistory\",\n  // 删除指定的下载记录\n  removeDownloadHistory = \"removeDownloadHistory\",\n  // 清除下载记录\n  clearDownloadHistory = \"clearDownloadHistory\",\n  // 测试下载服务器是否可连接\n  testClientConnectivity = \"testClientConnectivity\",\n  // 获取系统日志\n  getSystemLogs = \"getSystemLogs\",\n  // 删除指定的系统日志\n  removeSystemLogs = \"removeSystemLogs\",\n  // 清除系统日志\n  clearSystemLogs = \"clearSystemLogs\",\n  // 读取UI参数\n  readUIOptions = \"readUIOptions\",\n  // 保存UI参数\n  saveUIOptions = \"saveUIOptions\",\n  // 在当前选项卡显示消息\n  showMessage = \"showMessage\",\n  // 写入日志\n  writeLog = \"writeLog\",\n  // 后台服务停止\n  serviceStoped = \"serviceStoped\",\n  // 增加内容页面\n  addContentPage = \"addContentPage\",\n  // 取消搜索\n  abortSearch = \"abortSearch\",\n  // 将参数备份至Google\n  backupToGoogle = \"backupToGoogle\",\n  // 从Google恢复已备份的参数\n  restoreFromGoogle = \"restoreFromGoogle\",\n  // 从Google中清除已备份的参数\n  clearFromGoogle = \"clearFromGoogle\",\n  // 获取种子数据\n  getTorrentDataFromURL = \"getTorrentDataFromURL\",\n  // 获取用户信息\n  getUserInfo = \"getUserInfo\",\n  // 取消获取用户信息请求\n  abortGetUserInfo = \"abortGetUserInfo\",\n  // 刷新用户数据\n  refreshUserData = \"refreshUserData\",\n  // 获取已清理过的配置\n  getClearedOptions = \"getClearedOptions\",\n  // 重置运行时配置\n  resetRunTimeOptions = \"resetRunTimeOptions\",\n  // 根据指定的图片地址获取Base64信息\n  getBase64FromImageUrl = \"getBase64FromImageUrl\",\n  // 获取用户历史数据\n  getUserHistoryData = \"getUserHistoryData\",\n  // 获取电影信息\n  getMovieInfos = \"getMovieInfos\",\n  // 获取电影评分信息\n  getMovieRatings = \"getMovieRatings\",\n  // 根据指定的 doubanId 获取 IMDbId\n  getIMDbIdFromDouban = \"getIMDbIdFromDouban\",\n  // 从豆瓣查询影片信息\n  queryMovieInfoFromDouban = \"queryMovieInfoFromDouban\",\n  // 添加浏览器原生下载\n  addBrowserDownloads = \"addBrowserDownloads\",\n  // 验证权限\n  checkPermissions = \"checkPermissions\",\n  // 请求用户授权\n  requestPermissions = \"requestPermissions\",\n  // 更改语言\n  changeLanguage = \"changeLanguage\",\n  // 获取当前语言资源\n  getCurrentLanguageResource = \"getCurrentLanguageResource\",\n  // 增加新的语言\n  addLanguage = \"addLanguage\",\n  // 替换现有语言\n  replaceLanguage = \"replaceLanguage\",\n  // 隐藏指定的消息（用于前端页面）\n  hideMessage = \"hideMessage\",\n  // 重置用户数据，可用于恢复用户数据\n  resetUserDatas = \"resetUserDatas\",\n  // 备份配置到服务器\n  backupToServer = \"backupToServer\",\n  // 从服务器恢复配置\n  restoreFromServer = \"restoreFromServer\",\n  // 从服务器获取已备份的列表\n  getBackupListFromServer = \"getBackupListFromServer\",\n  // 从服务器删除指定的文件\n  deleteFileFromBackupServer = \"deleteFileFromBackupServer\",\n  // 在后台批量下载指定的链接\n  sendTorrentsInBackground = \"sendTorrentsInBackground\",\n  // 创建备份文件\n  createBackupFile = \"createBackupFile\",\n  // 验证备份数据\n  checkBackupData = \"checkBackupData\",\n  // 将种子添加到收藏\n  addTorrentToCollection = \"addTorrentToCollection\",\n  // 获取种子收藏\n  getTorrentCollections = \"getTorrentCollections\",\n  // 删除种子收藏\n  deleteTorrentFromCollention = \"deleteTorrentFromCollention\",\n  // 清除种子收藏\n  clearTorrentCollention = \"clearTorrentCollention\",\n  // 获取收藏\n  getTorrentCollention = \"getTorrentCollention\",\n  // 获取当前站点指定的选择器配置\n  getSiteSelectorConfig = \"getSiteSelectorConfig\",\n  // 重置收藏，可用于恢复收藏\n  resetTorrentCollections = \"resetTorrentCollections\",\n  // 获取收藏分组\n  getTorrentCollectionGroups = \"getTorrentCollectionGroups\",\n  // 添加收藏分组\n  addTorrentCollectionGroup = \"addTorrentCollectionGroup\",\n  // 将当前收藏添加到分组\n  addTorrentCollectionToGroup = \"addTorrentCollectionToGroup\",\n  // 更新收藏分组信息\n  updateTorrentCollectionGroup = \"updateTorrentCollectionGroup\",\n  // 将收藏从分组中删除\n  removeTorrentCollectionFromGroup = \"removeTorrentCollectionFromGroup\",\n  // 删除收藏分组\n  removeTorrentCollectionGroup = \"removeTorrentCollectionGroup\",\n  // 更新收藏信息\n  updateTorrentCollention = \"updateTorrentCollention\",\n  // 获取所有收藏的链接地址\n  getAllTorrentCollectionLinks = \"getAllTorrentCollectionLinks\",\n  // 恢复Cookies\n  restoreCookies = \"restoreCookies\",\n  // 重置所有站点图标缓存\n  resetFavicons = \"resetFavicons\",\n  // 重置单个站点图标缓存\n  resetFavicon = \"resetFavicon\",\n  // 获取备份文件原始数据\n  getBackupRawData = \"getBackupRawData\",\n  // 测试备份服务器是否可连接\n  testBackupServerConnectivity = \"testBackupServerConnectivity\",\n  // 创建搜索结果快照\n  createSearchResultSnapshot = \"createSearchResultSnapshot\",\n  // 加载搜索结果快照\n  loadSearchResultSnapshot = \"loadSearchResultSnapshot\",\n  // 获取搜索结果快照内容\n  getSearchResultSnapshot = \"getSearchResultSnapshot\",\n  // 删除搜索结果快照\n  removeSearchResultSnapshot = \"removeSearchResultSnapshot\",\n  // 清除搜索结果快照\n  clearSearchResultSnapshot = \"clearSearchResultSnapshot\",\n  // 重置搜索结果快照\n  resetSearchResultSnapshot = \"resetSearchResultSnapshot\",\n\n  // 创建辅种任务\n  createKeepUploadTask = \"createKeepUploadTask\",\n  // 加载辅种任务\n  loadKeepUploadTask = \"loadKeepUploadTask\",\n  // 获取辅种任务内容\n  getKeepUploadTask = \"getKeepUploadTask\",\n  // 删除辅种任务\n  removeKeepUploadTask = \"removeKeepUploadTask\",\n  // 清除辅种任务\n  clearKeepUploadTask = \"clearKeepUploadTask\",\n  // 重置辅种任务\n  resetKeepUploadTask = \"resetKeepUploadTask\",\n  // 更新辅种任务\n  updateKeepUploadTask = \"updateKeepUploadTask\",\n\n  // 重置下载历史\n  resetDownloadHistory = \"resetDownloadHistory\",\n\n  // 添加调试信息\n  pushDebugMsg = \"pushDebugMsg\",\n  updateDebuggerTabId = \"updateDebuggerTabId\",\n  // 获取热门搜索\n  getTopSearches = \"getTopSearches\"\n}\n\n/**\n * 数据保存类型\n */\nexport enum EStorageType {\n  text = \"TEXT\",\n  json = \"JSON\"\n}\n\n/**\n * 参数配置键值\n */\nexport enum EConfigKey {\n  default = \"PT-Plugin-Plus-Config\",\n  downloadHistory = \"PT-Plugin-Plus-downloadHistory\",\n  systemLogs = \"PT-Plugin-Plus-systemLogs\",\n  uiOptions = \"PT-Plugin-Plus-uiOptions\",\n  cache = \"PT-Plugin-Plus-Cache-Contents\",\n  userDatas = \"PT-Plugin-Plus-User-Datas\",\n  collection = \"PT-Plugin-Plus-Collection\",\n  searchResultSnapshot = \"PT-Plugin-Plus-SearchResultSnapshot\",\n  keepUploadTask = \"PT-Plugin-Plus-KeepUploadTask\"\n}\n\n/**\n * 数据返回类型\n */\nexport enum EDataResultType {\n  success = \"success\",\n  error = \"error\",\n  info = \"info\",\n  warning = \"warning\",\n  unknown = \"unknown\"\n}\n\n/**\n * 模块名称\n */\nexport enum EModule {\n  background = \"background\",\n  content = \"content\",\n  options = \"options\",\n  popup = \"popup\",\n  debugger = \"debugger\"\n}\n\n/**\n * 日志事件\n */\nexport enum ELogEvent {\n  init = \"init\",\n  requestMessage = \"requestMessage\"\n}\n\nexport enum EPaginationKey {\n  systemLogs = \"systemLogs\",\n  searchTorrent = \"searchTorrent\"\n}\n\nexport enum EViewKey {\n  home = \"home\",\n  downloadPaths = \"downloadPaths\",\n  searchTorrent = \"searchTorrent\"\n}\n\n/**\n * 用户数据范围\n */\nexport enum EUserDataRange {\n  latest = \"latest\",\n  today = \"today\",\n  all = \"all\"\n}\n\n/**\n * 用户数据请求返回状态\n */\nexport enum EUserDataRequestStatus {\n  needLogin = \"needLogin\",\n  notSupported = \"notSupported\",\n  unknown = \"unknown\",\n  success = \"success\"\n}\n\n/**\n * 公用的一些键值\n */\nexport enum ECommonKey {\n  allSite = \"__allSite__\",\n  all = \"__all__\",\n  noGroup = \"__noGroup__\"\n}\n\n/**\n * 插件安装方式\n */\nexport enum EInstallType {\n  // 相当于 zip 解压方式安装\n  development = \"development\",\n  normal = \"normal\",\n  // crx 自定义类型，官方api中无此状态\n  crx = \"crx\"\n}\n\n// 当点击预选条目时，搜索模式\nexport enum EBeforeSearchingItemSearchMode {\n  id = \"id\",\n  name = \"name\"\n}\n\n// 种子当前状态\nexport enum ETorrentStatus {\n  // 正在下载\n  downloading = 1,\n  // 正在做种\n  sending = 2,\n  // 已完成，未做种\n  completed = 255,\n  // 未活动（曾经下载过，但未完成）\n  inactive = 3\n}\n\n/**\n * 备份服务器类型\n */\nexport enum EBackupServerType {\n  OWSS = \"OWSS\",\n  WebDAV = \"WebDAV\"\n}\n\n/**\n * 插件显示位置\n */\nexport enum EPluginPosition {\n  left = \"left\",\n  right = \"right\"\n}\n\n/**\n * 相关Wiki链接\n */\nexport enum EWikiLink {\n  faq = \"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/frequently-asked-questions\"\n}\n\n/**\n * 需要恢复的内容\n */\nexport enum ERestoreContent {\n  all = \"all\",\n  options = \"options\",\n  userDatas = \"userDatas\",\n  collection = \"collection\",\n  cookies = \"cookies\",\n  searchResultSnapshot = \"searchResultSnapshot\",\n  keepUploadTask = \"keepUploadTask\",\n  downloadHistory = \"downloadHistory\"\n}\n\nexport enum EBrowserType {\n  Chrome = \"Chrome\",\n  Firefox = \"Firefox\"\n}\n\nexport enum EWorkingStatus {\n  success = \"success\",\n  error = \"error\",\n  loading = \"loading\"\n}\n\nexport enum EResourceOrderBy {\n  time = \"time\",\n  name = \"name\",\n  size = \"size\"\n}\n\nexport enum EResourceOrderMode {\n  desc = \"desc\",\n  asc = \"asc\"\n}\n\n// 加密方式\nexport enum EEncryptMode {\n  AES = \"AES\"\n}\n\nexport enum ERestoreError {\n  needSecretKey = \"needSecretKey\",\n  errorSecretKey = \"errorSecretKey\"\n}\n"
  },
  {
    "path": "src/interface/types.expand.js",
    "content": "String.prototype.getQueryString = function(name, split) {\n  if (split == undefined) split = \"&\";\n  var rule =\n    \"(^|\" + split + \"|\\\\?)\" + name + \"=([^\" + split + \"#]*)(\" + split + \"|#|$)\";\n  var reg = new RegExp(rule),\n    r;\n  if ((r = this.match(reg))) return decodeURI(r[2]);\n  return null;\n};\n\n/**\n * @return {number}\n */\nString.prototype.sizeToNumber = function() {\n  let _size_raw_match = this.match(\n    /^(\\d*\\.?\\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB))$/i\n  );\n  if (_size_raw_match) {\n    let _size_num = parseFloat(_size_raw_match[1]);\n    let _size_type = _size_raw_match[3];\n    switch (true) {\n      case /Zi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 70);\n      case /Ei?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 60);\n      case /Pi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 50);\n      case /Ti?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 40);\n      case /Gi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 30);\n      case /Mi?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 20);\n      case /Ki?B/i.test(_size_type):\n        return _size_num * Math.pow(2, 10);\n      default:\n        return _size_num;\n    }\n  }\n  return 0;\n};\n"
  },
  {
    "path": "src/options/App.vue",
    "content": "<template>\n  <v-app id=\"inspire\" :dark=\"this.darkMode\">\n    <template v-if=\"initializing\">\n      <v-progress-linear :indeterminate=\"true\" color=\"info\" height=\"5\" class=\"pa-0 ma-0\"></v-progress-linear>\n      <v-alert :value=\"true\" type=\"info\">\n        <div>\n          <div>{{ $t(\"app.initializing\", \"zh-CN\") }}</div>\n          <div>{{ $t(\"app.initializing\", \"en\") }}</div>\n        </div>\n      </v-alert>\n    </template>\n\n    <template v-else>\n      <v-alert :value=\"!$store.state.initialized\" type=\"error\">{{ $t(\"app.initError\") }}</v-alert>\n      <template v-if=\"$store.state.initialized && havePermissions\">\n        <!-- 导航栏 -->\n        <Navigation v-model=\"drawer\"></Navigation>\n        <!-- 顶部工具条 -->\n        <Topbar v-model=\"drawer\"></Topbar>\n        <!-- 内容显示区域 -->\n        <Content />\n        <!-- 页脚 -->\n        <Footer />\n      </template>\n      <Permissions v-else @update=\"reload\" />\n    </template>\n  </v-app>\n</template>\n\n<script>\nimport { EAction, Options } from \"../interface/common\";\nimport Navigation from \"./components/Navigation.vue\";\nimport Topbar from \"./components/Topbar.vue\";\nimport Footer from \"./components/Footer.vue\";\nimport Content from \"./components/Content.vue\";\nimport Permissions from \"./components/Permissions\";\nexport default {\n  name: \"App\",\n  components: {\n    Navigation,\n    Topbar,\n    Footer,\n    Content,\n    Permissions\n  },\n  data() {\n    return {\n      baseColor: \"amber\",\n      drawer: this.$store.state.options.navBarIsOpen,\n      havePermissions: false,\n      initializing: true,\n      darkMode: false\n    };\n  },\n  created() {\n    // this.init();\n    if (localStorage.getItem('DarkMode'))\n      this.darkMode = localStorage.getItem('DarkMode') == 'true';\n  },\n  mounted() {\n    this.$root.$on(\"ToggleDarkMode\",() => {\n      this.darkMode = !this.darkMode;\n      localStorage.setItem('DarkMode', this.darkMode);\n    });\n  },\n  watch: {\n    drawer() {\n      if (this.$store.state.options.navBarIsOpen != this.drawer) {\n        this.$store.dispatch(\"saveConfig\", {\n          navBarIsOpen: this.drawer\n        });\n      }\n    }\n  },\n  methods: {\n    init() {\n      console.log(\"APP init.\");\n      if (chrome && chrome.permissions) {\n        // 查询当前权限\n        chrome.permissions.contains(\n          {\n            origins: [\"http://*/*\", \"https://*/*\"]\n          },\n          result => {\n            this.havePermissions = result;\n            this.initializing = false;\n          }\n        );\n      } else {\n        this.havePermissions = true;\n        this.initializing = false;\n      }\n    },\n    reload(havePermissions) {\n      this.havePermissions = havePermissions;\n    }\n  }\n};\n</script>\n\n<style lang=\"scss\" src=\"./assets/contextMenu.scss\"></style>"
  },
  {
    "path": "src/options/assets/contextMenu.scss",
    "content": ".basicContext,\n.basicContext * {\n  box-sizing: border-box;\n}\n\n.basicContextContainer {\n  font-size: 12px;\n  position: fixed;\n  width: 100%;\n  height: 100%;\n  top: 0;\n  left: 0;\n  z-index: 1000;\n  -webkit-tap-highlight-color: transparent;\n}\n\n.basicContext {\n  position: absolute;\n  opacity: 0;\n  -moz-user-select: none;\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  padding: 6px;\n  background-color: #fff;\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.2);\n  border-radius: 3px;\n  border-style: solid;\n  border-width: 1px;\n  border-color: #ccc;\n}\n\n.basicContext__item {\n  cursor: pointer;\n  margin-bottom: 2px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n\n  &:not(.basicContext__item--disabled):hover {\n    .basicContext__data {\n      color: #fff;\n      background-color: #4393e6;\n    }\n  }\n\n  &:not(.basicContext__item--disabled):active {\n    .basicContext__data {\n      background-color: #1d79d9;\n    }\n  }\n}\n\n.basicContext__item--separator {\n  float: left;\n  width: 100%;\n  height: 1px;\n  cursor: default;\n  margin: 4px 0;\n  border-bottom: 1px solid #ccc;\n}\n\n.basicContext__item--disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n\n.basicContext__data {\n  min-width: 140px;\n  padding-right: 20px;\n  text-align: left;\n  white-space: nowrap;\n  padding: 6px 8px;\n  color: #333;\n  border-radius: 2px;\n}\n\n.basicContext__icon {\n  display: inline-block;\n  margin-right: 10px;\n  width: 12px;\n  text-align: center;\n}\n\n.basicContext--scrollable {\n  height: 100%;\n  -webkit-overflow-scrolling: touch;\n  overflow-y: auto;\n\n  .basicContext__data {\n    min-width: 160px;\n  }\n}"
  },
  {
    "path": "src/options/components/ColorSelector.vue",
    "content": "<template>\n  <v-menu offset-y v-model=\"show\">\n    <template v-slot:activator=\"{ on }\">\n      <v-btn\n        icon\n        :small=\"small\"\n        v-on=\"on\"\n        :dark=\"dark\"\n        :class=\"['ma-0',mini?'btn-mini':'']\"\n        :title=\"title\"\n      >\n        <v-icon :small=\"small\">color_lens</v-icon>\n      </v-btn>\n    </template>\n    <div v-for=\"(color, index) in colors\" :key=\"index\">\n      <template v-if=\"color!='black'\">\n        <v-btn\n          v-for=\"(value, n) in [4,3,2,1]\"\n          :key=\"`${index}.darken-${n}`\"\n          :color=\"`${color} darken-${value}`\"\n          class=\"white--text pa-0 ma-0\"\n          style=\"border-radius:0;min-width:30px;\"\n          small\n          @click.stop=\"changeColor(`${color} darken-${value}`)\"\n        ></v-btn>\n\n        <v-btn\n          :color=\"color\"\n          class=\"white--text pa-0 ma-0\"\n          style=\"border-radius:0;min-width:30px;\"\n          small\n          @click.stop=\"changeColor(color)\"\n        ></v-btn>\n\n        <v-btn\n          v-for=\"(value, n) in [1,2,3,4,5]\"\n          :key=\"`${index}.${n}`\"\n          :color=\"`${color} lighten-${value}`\"\n          class=\"white--text pa-0 ma-0\"\n          style=\"border-radius:0;min-width:30px;\"\n          small\n          @click.stop=\"changeColor(`${color} lighten-${value}`)\"\n        ></v-btn>\n      </template>\n    </div>\n  </v-menu>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\n\nimport { BASE_COLORS } from \"@/interface/common\";\nexport default Vue.extend({\n  props: {\n    dark: Boolean,\n    title: String,\n    mini: Boolean,\n    small: Boolean\n  },\n  data() {\n    return {\n      colors: BASE_COLORS,\n      show: false\n    };\n  },\n\n  methods: {\n    changeColor(color: string) {\n      this.$emit(\"change\", color);\n    }\n  },\n\n  watch: {\n    show() {\n      if (this.show) {\n        this.$emit(\"show\");\n      } else {\n        this.$emit(\"hide\");\n      }\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/components/Content.vue",
    "content": "<template>\n  <v-content>\n    <v-container\n      fluid\n      :class=\"$vuetify.breakpoint.smAndDown?['pa-0']: $vuetify.breakpoint.md?['pa-1']:['pa-3']\"\n    >\n      <keep-alive>\n        <router-view v-if=\"$route.meta.keepAlive\"></router-view>\n      </keep-alive>\n\n      <router-view v-if=\"!$route.meta.keepAlive\"></router-view>\n    </v-container>\n  </v-content>\n</template>"
  },
  {
    "path": "src/options/components/DownloadTo.vue",
    "content": "<template>\n  <v-btn\n    :flat=\"flat\"\n    :icon=\"icon\"\n    :small=\"small\"\n    :loading=\"loading\"\n    @click.stop=\"showSiteContentMenus\"\n    :class=\"[mini?'btn-mini':'']\"\n    :title=\"title||$t('searchTorrent.sendToClientTip')\"\n    :color=\"color\"\n  >\n    <v-icon v-if=\"haveSuccess\" color=\"success\" small>done</v-icon>\n    <v-icon v-else-if=\"haveError\" color=\"red\" small>close</v-icon>\n    <v-icon v-else small>{{ iconText }}</v-icon>\n    <span v-if=\"!!label\" class=\"ml-2\">{{ label }}</span>\n  </v-btn>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  Options,\n  DownloadClient,\n  ECommonKey,\n  DownloadOptions,\n  Site,\n  EAction,\n  ICollection\n} from \"@/interface/common\";\nimport { PathHandler } from \"@/service/pathHandler\";\nimport Extension from \"@/service/extension\";\nimport { PPF } from \"@/service/public\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  props: {\n    flat: Boolean,\n    icon: Boolean,\n    small: Boolean,\n    mini: Boolean,\n    color: String,\n    iconText: {\n      type: String,\n      default: \"cloud_download\"\n    },\n    downloadOptions: {\n      type: [Object, Array],\n      default: () => {\n        return {\n          host: String,\n          url: String\n        };\n      }\n    },\n    getOptionsOnly: Boolean,\n    label: String,\n    title: String,\n    payload: [Object, Array, String, Number]\n  },\n\n  data() {\n    return {\n      options: this.$store.state.options as Options,\n      contentMenus: [] as any[],\n      pathHandler: new PathHandler(),\n      loading: false,\n      site: {} as Site,\n      haveSuccess: false,\n      haveError: false,\n      allContentMenus: [] as any[]\n    };\n  },\n\n  methods: {\n    /**\n     * 根据指定的站点获取可用的下载目录及客户端信息\n     * @param site\n     */\n    getSiteContentMenus(host: string): any[] {\n      let results: any[] = [];\n      let clients: any[] = [];\n\n      if (this.contentMenus && this.contentMenus.length > 0) {\n        return this.contentMenus;\n      }\n\n      /**\n       * 增加下载目录\n       * @param paths\n       * @param client\n       */\n      function pushPath(paths: string[], client: any) {\n        paths.forEach((path: string) => {\n          results.push({\n            client,\n            path,\n            host\n          });\n        });\n      }\n\n      this.options.clients.forEach((client: DownloadClient) => {\n        clients.push({\n          client: client,\n          path: \"\",\n          host\n        });\n\n        if (client.paths) {\n          // 根据已定义的路径创建菜单\n          for (const _host in client.paths) {\n            let paths = client.paths[_host];\n\n            if (host !== _host) {\n              continue;\n            }\n\n            pushPath(paths, client);\n          }\n\n          // 最后添加当前客户端适用于所有站点的目录\n          let publicPaths = client.paths[ECommonKey.allSite];\n          if (publicPaths) {\n            if (results.length > 0) {\n              results.push({});\n            }\n\n            pushPath(publicPaths, client);\n          }\n        }\n      });\n\n      if (results.length > 0) {\n        clients.splice(0, 0, {});\n      }\n\n      results = results.concat(clients);\n\n      this.contentMenus = results;\n\n      return results;\n    },\n\n    /**\n     * 根据指定的域名获取站点配置信息\n     * @param host 域名\n     */\n    getSiteFromHost(host: string): Site {\n      return this.options.sites.find((item: Site) => {\n        let cdn = item.cdn || [];\n        item.url && cdn.push(item.url);\n        return item.host == host || cdn.join(\"\").indexOf(host) > -1;\n      });\n    },\n\n    /**\n     * 显示指定链接的下载服务器及目录菜单\n     * @param options\n     * @param event\n     */\n    showSiteContentMenus(event: any) {\n      if (Array.isArray(this.downloadOptions)) {\n        this.showAllContentMenus(event);\n        return;\n      }\n      let options = this.downloadOptions;\n      let host = options.host;\n      if (!host && options.site) {\n        host = options.site.host;\n      }\n\n      if (!host) {\n        return;\n      }\n\n      this.site = options.site || this.getSiteFromHost(host);\n      let items = this.getSiteContentMenus(host);\n      let menus: any[] = [];\n\n      items.forEach((item: any) => {\n        if (item.client && item.client.name) {\n          let title = this.$vuetify.breakpoint.xs\n            ? item.client.name\n            : this.$t(\"searchTorrent.downloadTo\", {\n                path: `${item.client.name} -> ${item.client.address}`\n              });\n\n          if (item.path) {\n            title += ` ->${this.pathHandler.replacePathKey(\n              item.path,\n              this.site\n            )}`;\n          }\n          menus.push({\n            title,\n            fn: () => {\n              if (options.url) {\n                console.log(options, item);\n                const downloadOptions = {\n                  url: options.url,\n                  title: options.title,\n                  savePath: item.path,\n                  autoStart: item.client.autoStart,\n                  tagIMDb: item.client.tagIMDb,\n                  link: options.link,\n                  clientId: item.client.id,\n                  imdbId: options.imdbId\n                };\n\n                if (this.getOptionsOnly) {\n                  downloadOptions.savePath = this.pathHandler.getSavePath(\n                    downloadOptions.savePath,\n                    this.site\n                  );\n                  this.$emit(\"itemClick\", {\n                    payload: this.payload,\n                    downloadOptions: Object.assign(\n                      {\n                        clientName: item.client.name\n                      },\n                      downloadOptions\n                    )\n                  });\n                  return;\n                }\n                this.sendToClient(downloadOptions);\n              }\n            }\n          });\n        } else {\n          menus.push({});\n        }\n      });\n\n      PPF.showContextMenu(menus, event);\n    },\n\n    /**\n     * 显示批量下载时可用下载服务器菜单\n     * @param event\n     */\n    showAllContentMenus(event: any) {\n      let clients: any[] = [];\n      let menus: any[] = [];\n      let _this = this;\n\n      function addMenu(item: any) {\n        let title = _this\n          .$t(\"searchTorrent.downloadTo\", {\n            path: `${item.client.name} -> ${item.client.address}`\n          })\n          .toString();\n        if (item.path) {\n          title += ` -> ${item.path}`;\n        }\n        menus.push({\n          title: title,\n          fn: () => {\n            _this.sendTorrentsInBackground(item);\n          }\n        });\n      }\n\n      if (this.allContentMenus.length == 0) {\n        this.options.clients.forEach((client: DownloadClient) => {\n          clients.push({\n            client: client,\n            path: \"\"\n          });\n        });\n        clients.forEach((item: any) => {\n          if (item.client && item.client.name) {\n            addMenu(item);\n\n            if (item.client.paths) {\n              // 添加适用于所有站点的目录\n              let publicPaths = item.client.paths[ECommonKey.allSite];\n              if (publicPaths) {\n                publicPaths.forEach((path: string) => {\n                  // 去除带关键字的目录\n                  if (\n                    path.indexOf(\"$site.name$\") == -1 &&\n                    path.indexOf(\"$site.host$\") == -1 &&\n                    path.indexOf(\"<...>\") == -1\n                  ) {\n                    let _item = PPF.clone(item);\n                    _item.path = path;\n                    addMenu(_item);\n                  }\n                });\n              }\n            }\n          } else {\n            menus.push({});\n          }\n        });\n        this.allContentMenus = menus;\n      } else {\n        menus = this.allContentMenus;\n      }\n\n      PPF.showContextMenu(menus, event);\n    },\n\n    /**\n     * 发送下载任务到后台\n     */\n    sendTorrentsInBackground(options: any) {\n      let items: DownloadOptions[] = [];\n      this.downloadOptions.forEach((item: ICollection | DownloadOptions) => {\n        items.push({\n          title: item.title,\n          url: item.url,\n          clientId: options.client.id,\n          savePath: options.path,\n          autoStart: options.client.autoStart,\n          tagIMDb: options.client.tagIMDb,\n          link: item.link,\n          imdbId: item.imdbId\n        });\n      });\n\n      this.loading = true;\n      extension\n        .sendRequest(EAction.sendTorrentsInBackground, null, items)\n        .then((result: any) => {\n          console.log(\"命令执行完成\", result);\n          this.haveSuccess = true;\n          this.$emit(\"success\", result);\n        })\n        .catch((result: any) => {\n          console.log(result);\n          this.haveError = true;\n          this.$emit(\"error\", result.msg || result);\n        })\n        .finally(() => {\n          this.loading = false;\n          setTimeout(() => {\n            this.clearStatus();\n          }, 5000);\n        });\n    },\n\n    sendToClient(downloadOptions: DownloadOptions) {\n      this.clearStatus();\n      let savePath = this.pathHandler.getSavePath(\n        downloadOptions.savePath,\n        this.site\n      );\n      // 取消\n      if (savePath === false) {\n        return;\n      }\n      this.loading = true;\n      downloadOptions.savePath = savePath;\n\n      extension\n        .sendRequest(EAction.sendTorrentToClient, null, downloadOptions)\n        .then((result: any) => {\n          console.log(\"命令执行完成\", result);\n          if (result.type == \"success\") {\n            this.haveSuccess = true;\n            this.$emit(\"success\", result.msg);\n          } else {\n            this.haveError = true;\n            this.$emit(\"error\", result.msg);\n          }\n        })\n        .catch((result: any) => {\n          console.log(result);\n          this.haveError = true;\n          this.$emit(\"error\", result.msg || result);\n        })\n        .finally(() => {\n          this.loading = false;\n          setTimeout(() => {\n            this.clearStatus();\n          }, 5000);\n        });\n    },\n\n    clearStatus() {\n      this.haveSuccess = false;\n      this.haveError = false;\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/components/Footer.vue",
    "content": "<template>\n  <v-footer app fixed>\n    <span class=\"pl-2 grey--text text--darken-1\">\n      &copy; {{ $t(\"app.author\") }} {{ year }}, {{ $t(\"common.version\") }}\n      {{ version }}\n      <span\n        v-if=\"isDevelopmentMode && $vuetify.breakpoint.mdAndUp\"\n        class=\"deep-orange--text\"\n        >{{ words.developmentMode }}</span\n      >\n      <v-chip\n        label\n        outline\n        color=\"orange\"\n        disabled\n        small\n        v-if=\"isDebugMode && $vuetify.breakpoint.mdAndUp\"\n        >{{ $t(\"common.debugMode\") }}</v-chip\n      >\n      <v-btn\n        outline\n        color=\"success\"\n        small\n        v-if=\"newReleases\"\n        href=\"https://github.com/pt-plugins/PT-Plugin-Plus/releases\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer nofollow\"\n        >{{ $t(\"common.haveNewReleases\") }}, {{ releasesVersion }}</v-btn\n      >\n    </span>\n    <v-spacer></v-spacer>\n    <v-btn\n      flat\n      small\n      href=\"https://t.me/joinchat/NZ9NCxPKXyby8f35rn_QTw\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer nofollow\"\n      title=\"Telegram\"\n      :icon=\"$vuetify.breakpoint.smAndDown\"\n    >\n      <v-img\n        src=\"./assets/telegram.svg\"\n        width=\"16\"\n        :style=\"$vuetify.breakpoint.smAndDown ? 'max-width:16px' : null\"\n      />\n      <span class=\"ml-1\" v-if=\"$vuetify.breakpoint.mdAndUp\">Telegram</span>\n    </v-btn>\n    <input type=\"file\" ref=\"fileLanguage\" style=\"display: none\" />\n    <v-menu top offset-y>\n      <template v-slot:activator=\"{ on }\">\n        <v-btn flat small v-on=\"on\" :icon=\"$vuetify.breakpoint.smAndDown\">\n          <v-icon small>language</v-icon>\n          <span class=\"ml-1\" v-if=\"$vuetify.breakpoint.mdAndUp\">\n            {{ $t(\"common.changeLanguage\") }}\n          </span>\n        </v-btn>\n      </template>\n\n      <v-list dense>\n        <v-list-tile @click=\"selectFile\">\n          <v-list-tile-title>{{ $t(\"common.addLanguage\") }}</v-list-tile-title>\n        </v-list-tile>\n      </v-list>\n\n      <v-divider></v-divider>\n\n      <v-list dense>\n        <v-list-tile\n          v-for=\"(item, index) in languages\"\n          :key=\"index\"\n          @click=\"changeLanguage(item)\"\n        >\n          <v-list-tile-title\n            :class=\"currentLanguage == item.code ? 'primary--text' : ''\"\n          >\n            <span>\n              <v-icon\n                small\n                class=\"mr-1 primary--text\"\n                v-if=\"currentLanguage == item.code\"\n                >check</v-icon\n              >\n              <span v-else class=\"mr-4\"></span>\n            </span>\n            {{ item.name }}\n          </v-list-tile-title>\n        </v-list-tile>\n      </v-list>\n    </v-menu>\n    <v-btn flat small to=\"/system-logs\" :icon=\"$vuetify.breakpoint.smAndDown\">\n      <v-icon small>assignment</v-icon>\n      <span class=\"ml-1\" v-if=\"$vuetify.breakpoint.mdAndUp\">\n        {{ $t(\"common.systemLog\") }}\n      </span>\n    </v-btn>\n    <v-btn @click=\"toggle_dark_mode\" flat small :icon=\"$vuetify.breakpoint.smAndDown\">\n      <v-icon small>invert_colors</v-icon>\n      <span class=\"ml-1\" v-if=\"$vuetify.breakpoint.mdAndUp\">\n        {{ $t(\"common.darkMode\") }}\n      </span>\n    </v-btn>\n    <v-btn\n      v-if=\"$vuetify.breakpoint.mdAndUp\"\n      flat\n      small\n      href=\"/debugger.html\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer nofollow\"\n      :title=\"$t('navigation.support.debugger')\"\n    >\n      <v-icon small>bug_report</v-icon>\n      <span class=\"ml-1\">{{ $t(\"navigation.support.debugger\") }}</span>\n    </v-btn>\n\n    <v-snackbar v-model=\"invalidFile\" top :timeout=\"3000\" color=\"error\">\n      {{ $t(\"footer.invalidFile\") }}\n    </v-snackbar>\n  </v-footer>\n</template>\n<script lang=\"ts\">\nimport { APP, API } from \"@/service/api\";\nimport Vue from \"vue\";\nimport { EInstallType } from \"@/interface/enum\";\nimport { i18nResource } from \"@/interface/common\";\nexport default Vue.extend({\n  data() {\n    return {\n      words: {\n        developmentMode: \"zip\"\n      },\n      version: \"\",\n      isDebugMode: APP.debugMode,\n      isDevelopmentMode: false,\n      newReleases: false,\n      releasesVersion: \"\",\n      languages: [] as Array<any>,\n      fileInput: null as any,\n      currentLanguage: \"\",\n      invalidFile: false,\n      year: new Date().getFullYear()\n    };\n  },\n  created() {\n    this.languages = window.i18nService.config;\n    this.currentLanguage = window.i18nService.currentLanguage;\n    if (APP.isExtensionMode && chrome.runtime && chrome.runtime.getManifest) {\n      let manifest = chrome.runtime.getManifest();\n      this.version = \"v\" + (manifest.version_name || manifest.version);\n    } else {\n      this.version = \"localVersion\";\n    }\n    if (this.version != \"localVersion\") {\n      this.checkUpdate();\n    }\n\n    APP.getInstallType()\n      .then(result => {\n        console.log(result, EInstallType.development);\n        this.isDevelopmentMode = [\n          EInstallType.development,\n          EInstallType.crx\n        ].includes(result);\n        if (result === EInstallType.crx) {\n          this.words.developmentMode = EInstallType.crx;\n        }\n      })\n      .catch(() => {\n        console.log(\"获取安装方式失败\");\n      });\n  },\n  mounted() {\n    this.fileInput = this.$refs.fileLanguage;\n    this.fileInput.addEventListener(\"change\", this.addLanguage);\n  },\n  beforeDestroy() {\n    this.fileInput.removeEventListener(\"change\", this.addLanguage);\n  },\n  methods: {\n    checkUpdate() {\n      $.getJSON(API.latestReleases)\n        .done((result: any) => {\n          if (result && result.tag_name) {\n            // 版本号\n            this.releasesVersion = result.tag_name;\n\n            if (this.releasesVersion > this.version) {\n              this.newReleases = true;\n            }\n          }\n        })\n        .fail((result: any) => { });\n    },\n\n    /**\n     * 更改语言\n     */\n    changeLanguage(item: any) {\n      this.currentLanguage = item.code;\n      window.i18nService.reset(item.code);\n    },\n\n    /**\n     * 手工添加一个新语言文件\n     */\n    addLanguage(event: Event) {\n      let fileInput: any = event.srcElement;\n      if (fileInput.files.length > 0 && fileInput.files[0].name.length > 0) {\n        let r = new FileReader();\n        r.onload = (e: any) => {\n          try {\n            let result = JSON.parse(e.target.result);\n            if (result.code && result.words) {\n              this.loadLanguage(result);\n              this.invalidFile = false;\n            } else {\n              this.invalidFile = true;\n            }\n            console.log(result);\n          } catch (error) {\n            console.warn(error);\n            this.invalidFile = true;\n          }\n        };\n        r.onerror = () => { };\n        r.readAsText(fileInput.files[0]);\n        fileInput.value = \"\";\n      }\n    },\n    toggle_dark_mode() {\n      this.$root.$emit('ToggleDarkMode');\n    },\n\n    /**\n     * 加载语言信息\n     */\n    loadLanguage(resource: i18nResource) {\n      // 检测指定的语言是否已存在\n      if (window.i18nService.exists(resource.code)) {\n        // 是否替换\n        if (window.confirm(this.$t(\"footer.replaceLanguageConfirm\") + \"\")) {\n          window.i18nService\n            .replace(resource)\n            .then(() => {\n              this.currentLanguage = resource.code;\n            })\n            .catch(() => { });\n        }\n      } else {\n        window.i18nService\n          .add(resource)\n          .then(() => {\n            this.currentLanguage = resource.code;\n            this.languages.push({\n              name: resource.name,\n              code: resource.code\n            });\n          })\n          .catch(() => { });\n      }\n    },\n\n    selectFile() {\n      this.fileInput.click();\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/components/MovieInfoCard.vue",
    "content": "<template>\n  <div class=\"movieInfoCard\" v-if=\"visible\">\n    <v-card color=\"blue-grey darken-2\" class=\"white--text\">\n      <!-- 标题 -->\n      <v-card-title class=\"pb-2\">\n        <div :class=\"$vuetify.breakpoint.mdAndUp?'headline': 'title'\">\n          <span>{{ info.title }}</span>\n          <span\n            :class=\"['ml-1','grey--text',$vuetify.breakpoint.mdAndUp?'title':'caption']\"\n          >({{ info.year || info.attrs.year[0] }})</span>\n        </div>\n      </v-card-title>\n      <v-img\n        :src=\"info.image || info.pic.normal\"\n        class=\"ml-3 mb-3\"\n        contain\n        :max-height=\"maxHeight\"\n        position=\"left center\"\n      >\n        <v-layout style=\"margin-left: 220px;\" v-if=\"$vuetify.breakpoint.mdAndUp\">\n          <!-- omit 格式 -->\n          <v-card-title class=\"pt-0\" v-if=\"info.updateTime\">\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.alias\") }}</span>\n              <span class=\"caption\">{{ info.aka }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.director\") }}</span>\n              <span class=\"caption\">{{ info.director }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.writer\") }}</span>\n              <span class=\"caption\">{{ info.scenarist }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.cast\") }}</span>\n              <span class=\"caption\">{{ info.cast }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.type\") }}</span>\n              <span class=\"caption\">{{ info.genre }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.pubdate\") }}</span>\n              <span class=\"caption\">{{ info.releaseDate }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.duration\") }}</span>\n              <span class=\"caption\">{{ info.runtime }}</span>\n            </v-flex>\n            <v-flex xs12 class=\"my-2\">\n              <v-divider light></v-divider>\n            </v-flex>\n            <div class=\"caption\" v-html=\"`　　${info.summary.replace(/\\n/g, '<br>')} @豆瓣`\"></div>\n          </v-card-title>\n\n          <v-card-title class=\"pt-0\" v-else-if=\"info.attrs\">\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.alias\") }}</span>\n              <span class=\"caption\">{{ info.alt_title }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.director\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.director) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.writer\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.writer) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.cast\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.cast) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.type\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.movie_type) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.pubdate\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.pubdate) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.duration\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.attrs.movie_duration) }}</span>\n            </v-flex>\n            <v-flex xs12 class=\"my-2\">\n              <v-divider light></v-divider>\n            </v-flex>\n            <div class=\"caption\" v-html=\"`${info.summary} @豆瓣`\"></div>\n          </v-card-title>\n\n          <v-card-title class=\"pt-0\" v-else>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.alias\") }}</span>\n              <span class=\"caption\">{{ info.original_title }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.director\") }}</span>\n              <span class=\"caption\">{{ getArrayValues(info.directors) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.cast\") }}</span>\n              <span class=\"caption\">{{ getArrayValues(info.actors) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.type\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.genres) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.pubdate\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.pubdate) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span>{{ $t(\"movieInfoCard.duration\") }}</span>\n              <span class=\"caption\">{{ formatArray(info.durations) }}</span>\n            </v-flex>\n            <v-flex xs12 class=\"my-2\">\n              <v-divider light></v-divider>\n            </v-flex>\n            <div class=\"caption\" v-html=\"`${info.intro} @豆瓣`\"></div>\n          </v-card-title>\n        </v-layout>\n        <v-layout v-else style=\"margin-left: 75px;\">\n          <v-card-text class=\"pt-0\">\n            <v-flex xs12>\n              <span class=\"caption\">{{ info.original_title || info.alt_title }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span class=\"caption\">{{ formatArray(info.genres || info.attrs.movie_type) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span class=\"caption\">{{ formatArray(info.pubdate || info.attrs.pubdate) }}</span>\n            </v-flex>\n            <v-flex xs12>\n              <span class=\"caption\">{{ formatArray(info.durations || info.attrs.movie_duration) }}</span>\n            </v-flex>\n          </v-card-text>\n        </v-layout>\n      </v-img>\n      <v-divider light></v-divider>\n      <v-card-actions class=\"px-3\">\n        <!-- 豆瓣评分 -->\n        <v-btn\n          color=\"success\"\n          :href=\"info.link || info.url || info.mobile_link\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >豆瓣 {{ info.average || info.rating.value || info.rating.average }}</v-btn>\n\n        <!-- IMDb评分 -->\n        <v-btn\n          color=\"amber\"\n          :href=\"`https://www.imdb.com/title/${this.IMDbId}/`\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >IMDb {{ ratings.imdbRating }}</v-btn>\n\n        <!-- 烂番茄新鲜度 -->\n        <v-btn\n          v-if=\"tomatoRating>0\"\n          color=\"red lighten-3\"\n          :href=\"ratings.tomatoURL\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-avatar size=\"20\" class=\"mr-1\">\n            <img :src=\"rottenTomatoes.fresh\" v-if=\"tomatoRating>=60\" />\n            <img :src=\"rottenTomatoes.rotten\" v-else />\n          </v-avatar>\n          {{ tomatoRating }}%\n        </v-btn>\n\n        <!-- Metacritic评分 -->\n        <v-btn\n          v-if=\"metascore>0\"\n          :color=\"metascore>60?'success':metascore>40?'warning':'error'\"\n          :href=\"`https://www.metacritic.com/search/movie/${info.title}/results`\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n          style=\"min-width: unset;\"\n        >\n          <v-avatar size=\"20\" class=\"mr-2\">\n            <img src=\"https://upload.wikimedia.org/wikipedia/commons/f/f2/Metacritic_M.png\" />\n          </v-avatar>\n          {{ metascore }}\n        </v-btn>\n\n        <v-spacer></v-spacer>\n        <v-layout v-if=\"$vuetify.breakpoint.mdAndUp\">\n          <v-flex xs6 v-if=\"rating>0\">\n            <v-rating\n              v-model=\"rating\"\n              background-color=\"white\"\n              color=\"yellow accent-4\"\n              dense\n              readonly\n              half-increments\n              size=\"30\"\n            ></v-rating>\n            <span\n              class=\"ma-2\"\n            >{{ $t(\"movieInfoCard.ratings.douban\", {average: info.average || info.rating.value || info.rating.average, numRaters: info.votes || info.rating.count || info.rating.numRaters}) }}</span>\n          </v-flex>\n          <v-flex xs6 v-if=\"imdbRating>0\">\n            <v-rating\n              v-model=\"imdbRating\"\n              background-color=\"white\"\n              color=\"yellow accent-4\"\n              dense\n              readonly\n              half-increments\n              size=\"30\"\n            ></v-rating>\n            <span\n              class=\"ma-2\"\n            >{{ $t(\"movieInfoCard.ratings.imdb\", {average: ratings.imdbRating, numRaters: ratings.imdbVotes.replace(/,/g, \"\")}) }}</span>\n          </v-flex>\n        </v-layout>\n      </v-card-actions>\n    </v-card>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\n\nimport Extension from \"@/service/extension\";\nimport { EAction } from \"@/interface/enum\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  props: {\n    IMDbId: String,\n    doubanId: String\n  },\n  data() {\n    return {\n      info: {\n        title: \"\",\n        summary: \"\",\n        image: \"\",\n        rating: {\n          average: \"\",\n          numRaters: 0,\n          value: \"\"\n        },\n        attrs: {\n          year: [],\n          director: [],\n          writer: [],\n          cast: [],\n          movie_type: [],\n          pubdate: [],\n          movie_duration: []\n        }\n      } as any,\n      ratings: {\n        imdbRating: \"\",\n        Ratings: [],\n        imdbVotes: \"\"\n      } as any,\n\n      rottenTomatoes: {\n        fresh:\n          \"https://www.rottentomatoes.com/assets/pizza-pie/images/icons/global/new-fresh.587bf3a5e47.png\",\n        rotten:\n          \"https://www.rottentomatoes.com/assets/pizza-pie/images/icons/global/new-rotten.efc30acb29c.png\"\n      },\n      visible: false\n    };\n  },\n  watch: {\n    IMDbId() {\n      this.reset();\n    }\n  },\n  created() {\n    this.reset();\n  },\n  methods: {\n    reset() {\n      this.visible = false;\n      this.ratings = {\n        imdbRating: \"\",\n        Ratings: [],\n        imdbVotes: \"\"\n      };\n      console.log(this.doubanId, this.IMDbId);\n      if (this.IMDbId) {\n        extension\n          .sendRequest(EAction.getMovieInfos, null, this.doubanId ? `douban${this.doubanId}` : this.IMDbId)\n          .then(result => {\n            console.log(result);\n            this.visible = true;\n            if (Array.isArray(result)) {\n              this.info = result[0];\n            } else {\n              this.info = result;\n            }\n          })\n          .catch(error => {\n            console.log(error);\n          });\n\n        extension\n          .sendRequest(EAction.getMovieRatings, null, this.IMDbId)\n          .then(result => {\n            console.log(result);\n            this.ratings = result;\n          })\n          .catch(error => {\n            console.log(error);\n          });\n      }\n    },\n    formatArray(\n      array: any,\n      splitChar: string = \" / \",\n      maxLength: number = 10\n    ): string {\n      if (array && array.length > 0) {\n        if (maxLength > 0 && array.length > maxLength) {\n          return array.slice(0, maxLength - 1).join(splitChar) + \" ...\";\n        }\n        return array.join(splitChar);\n      }\n      return \"\";\n    },\n    // 获取数组中指定的字段\n    getArrayValues(array: any, field: string = \"name\", splitChar: string = \" / \"): string {\n      if (array && array.length > 0) {\n        const result: string[] = [];\n        array.forEach((item: any) => {\n          result.push(item[field]);\n        });\n        return result.join(splitChar);\n      }\n      return \"\";\n    }\n  },\n  computed: {\n    rating(): number {\n      if (this.info && (this.info.rating || this.info.average)) {\n        return parseFloat(this.info.average || this.info.rating.value || this.info.rating.average) / 2;\n      }\n      return 0;\n    },\n    imdbRating(): number {\n      if (this.ratings && this.ratings.imdbRating) {\n        return parseFloat(this.ratings.imdbRating) / 2;\n      }\n      return 0;\n    },\n    tomatoRating(): number {\n      if (this.ratings && this.ratings.Ratings) {\n        let ratings = 0;\n        this.ratings.Ratings.some((item: any) => {\n          if (item.Source == \"Rotten Tomatoes\") {\n            ratings = parseInt(item.Value);\n            return true;\n          }\n        });\n        return ratings;\n      }\n      return 0;\n    },\n    metascore(): number {\n      if (this.ratings && this.ratings.Ratings) {\n        let ratings = 0;\n        this.ratings.Ratings.some((item: any) => {\n          if (item.Source == \"Metacritic\") {\n            ratings = parseInt(item.Value.split(\"/\")[0]);\n            return true;\n          }\n        });\n        return ratings;\n      }\n      return 0;\n    },\n    maxHeight(): number {\n      return this.$vuetify.breakpoint.smAndDown ? 120 : 300;\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.movieInfoCard {\n  .caption {\n    color: #ccc;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/components/Navigation.vue",
    "content": "<template>\n  <!-- 导航栏 -->\n  <v-navigation-drawer clipped fixed v-model=\"drawer\" app width=\"220\">\n    <v-list v-for=\"(group, index) in navs\" :key=\"index\" dense>\n      <v-subheader v-if=\"group.title\" class=\"grey--text text--darken-1\">{{\n        $t(group.title)\n      }}</v-subheader>\n      <template v-for=\"(item, index) in group.items\">\n        <v-list-tile\n          v-if=\"item.visible !== false\"\n          :to=\"item.key\"\n          :key=\"index\"\n          :href=\"item.url\"\n          :target=\"item.url ? '_blank' : ''\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-list-tile-action style=\"min-width: 42px;margin-left: 13px;\">\n            <v-icon>{{ item.icon }}</v-icon>\n          </v-list-tile-action>\n          <v-list-tile-content>\n            <v-list-tile-title>{{ $t(item.title) }}</v-list-tile-title>\n          </v-list-tile-content>\n        </v-list-tile>\n      </template>\n    </v-list>\n  </v-navigation-drawer>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  props: {\n    value: Boolean\n  },\n  data() {\n    return {\n      drawer: this.$store.state.options.navBarIsOpen\n    };\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    drawer() {\n      (this as any).$emit(\"change\", this.drawer);\n    },\n    value() {\n      this.drawer = this.value;\n    }\n  },\n  computed: {\n    navs() {\n      return [\n        {\n          title: \"navigation.dashboard.title\",\n          key: \"group-\",\n          items: [\n            {\n              title: \"navigation.dashboard.userData\",\n              icon: \"dashboard\",\n              key: \"/home\"\n            },\n            {\n              title: \"navigation.dashboard.searchResults\",\n              icon: \"search\",\n              key: \"/search-torrent\"\n            },\n            {\n              title: \"navigation.dashboard.searchResultSnapshot\",\n              icon: \"add_a_photo\",\n              key: \"/search-result-snapshot\",\n              visible: (this.$store as any).state.options.allowSaveSnapshot\n            },\n            {\n              title: \"navigation.dashboard.history\",\n              icon: \"history\",\n              key: \"/history\",\n              visible: (this.$store as any).state.options.saveDownloadHistory\n            },\n            {\n              title: \"navigation.dashboard.collection\",\n              icon: \"favorite\",\n              key: \"/collection\"\n            },\n            {\n              title: \"navigation.dashboard.keepUploadTask\",\n              icon: \"merge_type\",\n              key: \"/keep-upload-task\"\n            }\n          ]\n        },\n        {\n          title: \"navigation.settings.title\",\n          items: [\n            {\n              title: \"navigation.settings.downloadClients\",\n              icon: \"cloud_download\",\n              key: \"/set-download-clients\"\n            },\n            {\n              title: \"navigation.settings.base\",\n              icon: \"settings\",\n              key: \"/set-base\"\n            },\n            {\n              title: \"navigation.settings.sites\",\n              icon: \"public\",\n              key: \"/set-sites\"\n            },\n            {\n              title: \"navigation.settings.downloadPaths\",\n              icon: \"folder_open\",\n              key: \"/set-download-paths\"\n            },\n            {\n              title: \"navigation.settings.searchSolution\",\n              icon: \"widgets\",\n              key: \"/set-search-solution\"\n            },\n            {\n              title: \"navigation.settings.backup\",\n              icon: \"restore\",\n              key: \"/set-backup\"\n            },\n            {\n              title: \"navigation.settings.permissions\",\n              icon: \"verified_user\",\n              key: \"/set-permissions\"\n            }\n          ]\n        },\n        {\n          title: \"navigation.thanks.title\",\n          items: [\n            {\n              title: \"navigation.thanks.reference\",\n              icon: \"developer_board\",\n              key: \"/technology-stack\"\n            },\n            {\n              title: \"navigation.thanks.specialThanksTo\",\n              icon: \"people\",\n              key: \"/dev-team\"\n            }\n          ]\n        }\n      ];\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/components/Permissions.vue",
    "content": "<template>\n  <v-layout class=\"mt-3\">\n    <v-flex xs12 sm8 offset-sm2>\n      <v-card>\n        <v-img src=\"./assets/banner/default.jpg\" aspect-ratio=\"2.75\"></v-img>\n\n        <v-card-title class=\"pb-1\">\n          <div v-if=\"!cancelled\">\n            <h3 class=\"title mb-2\">{{ $t(\"permissions.title\") }}</h3>\n            <h3>{{ $t(\"permissions.subtitle\") }}</h3>\n\n            <v-data-table\n              v-model=\"selected\"\n              :headers=\"headers\"\n              :items=\"items\"\n              item-key=\"key\"\n              select-all\n              hide-actions\n            >\n              <template v-slot:items=\"props\">\n                <tr v-if=\"props.item.visible\">\n                  <td>\n                    <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n                  </td>\n                  <td>{{ $t(props.item.title) }}</td>\n                  <td>\n                    <v-switch\n                      true-value=\"true\"\n                      false-value=\"false\"\n                      :input-value=\"props.item.enabled?'true':'false'\"\n                      hide-details\n                      @click.stop=\"updatePermissions(props.item)\"\n                    ></v-switch>\n                  </td>\n                </tr>\n              </template>\n            </v-data-table>\n          </div>\n\n          <div v-else class=\"title mb-2\">{{ $t(\"permissions.cancelled\") }}</div>\n        </v-card-title>\n\n        <v-card-actions v-if=\"!cancelled\">\n          <v-btn\n            flat\n            color=\"success\"\n            :disabled=\"selected.length==0\"\n            @click=\"authorize\"\n          >{{ $t(\"permissions.authorize\") }}</v-btn>\n          <v-btn flat color=\"orange\" @click=\"cancel\">{{ $t(\"permissions.cancel\") }}</v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-flex>\n  </v-layout>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      permissions: [\n        {\n          origins: [\"http://*/*\", \"https://*/*\"],\n          title: \"permissions.details.allSites\",\n          visible: true,\n          isOrigin: true\n        },\n        // { key: \"tabs\", title: \"permissions.details.tabs\" },\n        {\n          key: \"downloads\",\n          visible: true,\n          title: \"permissions.details.downloads\"\n        },\n        { key: \"cookies\", visible: true, title: \"permissions.details.cookies\" }\n      ],\n      selected: [] as any,\n      cancelled: false,\n      items: [] as any\n    };\n  },\n  methods: {\n    /**\n     * 发起用户授权\n     */\n    authorize() {\n      if (chrome && chrome.permissions) {\n        let options = {\n          permissions: [] as any,\n          origins: [] as any\n        };\n        this.selected.forEach((item: any) => {\n          if (!item.visible) {\n            return;\n          }\n          if (item.isOrigin) {\n            options.origins.push(...item.origins);\n          } else {\n            options.permissions.push(item.key);\n          }\n        });\n        // 权限必须在用户操作下请求，例如按钮单击的事件处理函数。\n        chrome.permissions.request(options, granted => {\n          this.$emit(\"update\", granted);\n        });\n      } else {\n        this.$emit(\"update\", true);\n      }\n    },\n    cancel() {\n      this.cancelled = true;\n    },\n    updatePermissions(item: any) {\n      if (!item.visible) {\n        return;\n      }\n      item.enabled = !(<boolean>item.enabled);\n      let options = {};\n      if (item.isOrigin) {\n        options = {\n          origins: item.origins\n        };\n      } else {\n        options = {\n          permissions: [item.key]\n        };\n      }\n      if (item.enabled) {\n        chrome.permissions.request(options, granted => {\n          item.enabled = granted;\n        });\n      } else {\n        chrome.permissions.remove(options, granted => {\n          item.enabled = !granted;\n        });\n      }\n\n      console.log(item);\n    }\n  },\n  created() {\n    if (chrome && chrome.permissions) {\n    } else {\n      this.items = this.permissions;\n      return;\n    }\n\n    let manifest = chrome.runtime.getManifest();\n\n    this.permissions.forEach(item => {\n      let options = {};\n      if (item.isOrigin) {\n        options = {\n          origins: item.origins\n        };\n      } else {\n        if (manifest.optional_permissions && item.key) {\n          item.visible = manifest.optional_permissions.includes(item.key);\n        }\n\n        options = {\n          permissions: [item.key]\n        };\n      }\n      if (item.visible) {\n        // 查询当前权限\n        chrome.permissions.contains(options, result => {\n          this.items.push(Object.assign({ enabled: result }, item));\n        });\n      }\n    });\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"permissions.headers.title\"),\n          align: \"left\",\n          sortable: false,\n          value: \"title\"\n        },\n        {\n          text: this.$t(\"permissions.headers.enabled\"),\n          align: \"left\",\n          value: \"enabled\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.item {\n  padding: 5px;\n}\n\n.logo {\n  position: absolute;\n  right: 20px;\n  bottom: 60px;\n  opacity: 0.5;\n}\n</style>\n"
  },
  {
    "path": "src/options/components/SearchBox.vue",
    "content": "<template>\n  <v-menu v-model=\"showMenu\" offset-y nudge-right=\"32\">\n    <template v-slot:activator=\"{ on }\">\n      <v-text-field\n        flat\n        solo-inverted\n        prepend-icon=\"search\"\n        type=\"search\"\n        @click:prepend=\"searchTorrent()\"\n        :label=\"$t('searchBox.searchTip')\"\n        class=\"mt-2 mb-0\"\n        v-model=\"searchKey\"\n        clearable\n        @click.stop=\"showSelectBox\"\n        @click:clear=\"clearSearchKey\"\n        :loading=\"loadStatus\"\n        enterkeyhint=\"search\"\n        v-on:keyup.enter=\"searchTorrent()\"\n      >\n        <!-- 近期热门 -->\n        <v-menu\n          slot=\"prepend-inner\"\n          offset-y\n          class=\"top-searches\"\n          nudge-bottom=\"8\"\n          nudge-left=\"12\"\n        >\n          <v-btn slot=\"activator\" flat small color=\"grey lighten-2\">{{\n            $t(\"common.hot\")\n          }}</v-btn>\n\n          <div\n            :style=\"\n              ($vuetify.breakpoint.smAndUp ? 'width: 550px' : 'width: 300px') +\n              ';background-color: #fff;'\n            \"\n          >\n            <v-container fluid grid-list-lg class=\"pa-3\">\n              <div v-if=\"topSearches.length == 0\"> {{ $t('common.loading') }} </div>\n              <v-layout  v-else row wrap>\n                <v-flex v-for=\"(item, index) in topSearches\" :key=\"index\" xs4>\n                  <v-card\n                    @click=\"searchHotItem(item)\"\n                    style=\"cursor: pointer\"\n                    :title=\"$t('searchBox.searchThisKey', { key: item.title })\"\n                  >\n                    <v-img\n                      :src=\"item.image\"\n                      :height=\"$vuetify.breakpoint.smAndUp ? '230px' : '110px'\"\n                    >\n                      <div class=\"top-searches-item px-1\">\n                        <div>\n                          <span class=\"caption\">{{ item.title }}</span>\n                          <span\n                            v-if=\"$vuetify.breakpoint.smAndUp\"\n                            class=\"caption ml-2 grey--text\"\n                            >({{ item.year }})</span\n                          >\n\n                          <!-- 评分，点击可前往豆瓣页面 -->\n                          <a\n                            v-if=\"item.doubanRating\"\n                            class=\"caption orange--text rating\"\n                            :href=\"item.link\"\n                            rel=\"noopener noreferrer nofollow\"\n                            target=\"_blank\"\n                            :title=\"$t('searchBox.toDouban')\"\n                            @click.stop\n                            >{{ parseFloat(item.doubanRating).toFixed(1) }}</a\n                          >\n                        </div>\n                        <div\n                          v-if=\"$vuetify.breakpoint.smAndUp\"\n                          class=\"caption grey--text alt-title\"\n                        >\n                          {{ item.alt_title }}\n                        </div>\n                      </div>\n                    </v-img>\n                  </v-card>\n                </v-flex>\n              </v-layout>\n            </v-container>\n            <v-divider></v-divider>\n\n            <div class=\"pa-1 top-searches text-sm-right\">\n              <v-btn\n                flat\n                small\n                class=\"caption grey--text\"\n                :title=\"$t('common.refresh')\"\n                @click.stop=\"getTopSearches\"\n                :loading=\"topSearchesLoading\"\n              >\n                <v-icon style=\"font-size: 20px\">update</v-icon>\n                <span class=\"ml-2\">{{\n                  $t(\"common.lastUpdate\", { time: topSearchesUpdateTime })\n                }}</span>\n              </v-btn>\n            </div>\n          </div>\n        </v-menu>\n\n        <!-- 搜索方案 -->\n        <v-menu slot=\"append\" offset-y class=\"search-solution\">\n          <v-btn slot=\"activator\" flat small color=\"grey lighten-2\">{{\n            selectedSearchSolutionName\n          }}</v-btn>\n          <v-list dense>\n            <v-list-tile\n              @click=\"changeSearchSolution(null)\"\n              :title=\"$t('searchBox.defaultTip')\"\n            >\n              <v-list-tile-title>{{\n                $t(\"searchBox.default\")\n              }}</v-list-tile-title>\n            </v-list-tile>\n            <v-divider></v-divider>\n            <template\n              v-if=\"\n                $store.state.options.searchSolutions &&\n                $store.state.options.searchSolutions.length > 0\n              \"\n            >\n              <v-list-tile\n                @click=\"changeSearchSolution(item)\"\n                v-for=\"(item, index) in $store.state.options.searchSolutions\"\n                :key=\"index\"\n              >\n                <v-list-tile-title>{{ item.name }}</v-list-tile-title>\n              </v-list-tile>\n            </template>\n            <v-btn flat small v-else to=\"/set-search-solution\">{{\n              $t(\"searchBox.noSearchSolution\")\n            }}</v-btn>\n\n            <v-divider></v-divider>\n            <v-list-tile @click=\"changeSearchSolution(allSite)\">\n              <v-list-tile-title>{{ $t(\"searchBox.all\") }}</v-list-tile-title>\n            </v-list-tile>\n          </v-list>\n        </v-menu>\n      </v-text-field>\n    </template>\n    <!-- 预选结果 -->\n    <div>\n      <!-- 直接关键字跳转 -->\n      <v-list class=\"pb-0\">\n        <v-list-tile @click.stop=\"itemClick(null)\">\n          <v-list-tile-content>\n            <v-list-tile-title>\n              <v-icon>search</v-icon>\n              <span class=\"title ml-1\">{{\n                $t(\"searchBox.searchThisKey\", { key: this.searchKey })\n              }}</span>\n            </v-list-tile-title>\n          </v-list-tile-content>\n          <v-list-tile-action>\n            <v-icon>forward</v-icon>\n          </v-list-tile-action>\n        </v-list-tile>\n        <v-divider v-if=\"!isLoading\"></v-divider>\n        <v-progress-linear\n          :indeterminate=\"true\"\n          v-else\n          color=\"secondary\"\n          height=\"2\"\n        ></v-progress-linear>\n      </v-list>\n\n      <!-- 预选列表 -->\n      <v-list three-line class=\"py-0\">\n        <template v-for=\"(item, index) in items\">\n          <v-list-tile\n            :key=\"index\"\n            @click.stop=\"itemClick(item)\"\n            :title=\"$t('searchBox.searchThisKey', { key: item.title })\"\n          >\n            <v-list-tile-avatar class=\"album\" :size=\"75\">\n              <img\n                :src=\"\n                  item.image || item.img ||\n                  (item.images\n                    ? item.images.small\n                    : item.pic\n                    ? item.pic.normal\n                    : '')\n                \"\n              />\n            </v-list-tile-avatar>\n            <v-list-tile-content>\n              <v-list-tile-title class=\"mb-1\">\n                <span class=\"title\">{{ item.title }}</span>\n                <span class=\"ml-1 caption\">({{ item.year }})</span>\n              </v-list-tile-title>\n              <v-list-tile-sub-title>\n                <div>\n                  {{\n                    item.aka || item.originalTitle || item.original_title || \"\"\n                  }}\n                </div>\n                <div class=\"caption\">\n                  {{ item.genre || (item.genres && item.genres.join(\" | \")) }}\n                </div>\n              </v-list-tile-sub-title>\n            </v-list-tile-content>\n\n            <v-list-tile-action style=\"align-items: center\">\n              <a\n                class=\"grey--text text--darken-1 mt-3 title\"\n                style=\"text-decoration: none\"\n                :href=\"item.link || item.alt || item.url\"\n                rel=\"noopener noreferrer nofollow\"\n                target=\"_blank\"\n                :title=\"$t('searchBox.toDouban')\"\n                @click.stop\n              >\n                <img src=\"https://img3.doubanio.com/favicon.ico\" width=\"16\" />\n                {{\n                  parseFloat(\n                    item.average ?item.average : item.rating? (item.rating.average || item.rating.value) : null\n                  ).toFixed(1)\n                }}\n              </a>\n              <v-rating\n                :value=\"\n                  item.average\n                    ? parseFloat(item.average) / 2\n                    : item.rating? (item.rating.stars\n                    ? parseInt(item.rating.stars) / 10\n                    : item.rating.star_count): 0\n                \"\n                background-color=\"grey lighten-2\"\n                color=\"yellow accent-4\"\n                dense\n                readonly\n                half-increments\n                size=\"13\"\n                class=\"mb-3\"\n              ></v-rating>\n            </v-list-tile-action>\n          </v-list-tile>\n          <v-divider :key=\"'l' + index\"></v-divider>\n        </template>\n      </v-list>\n      <v-list class=\"py-0\">\n        <v-list-tile>\n          <v-list-tile-content>\n            <v-list-tile-title class=\"grey--text text--darken-1 caption\">{{\n              $t(\"searchBox.doubanTip\")\n            }}</v-list-tile-title>\n          </v-list-tile-content>\n        </v-list-tile>\n      </v-list>\n    </div>\n  </v-menu>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  SearchSolution,\n  Options,\n  Site,\n  EBeforeSearchingItemSearchMode,\n  EAction,\n} from \"@/interface/common\";\n\nimport Extension from \"@/service/extension\";\nimport dayjs from \"dayjs\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  data() {\n    return {\n      isLoading: false,\n      items: [] as any[],\n      selected: {} as any,\n      timer: null as any,\n      showMenu: false,\n      searchKey: \"\",\n      options: this.$store.state.options as Options,\n      selectedSearchSolutionName: \"\",\n      allSite: {\n        id: \"all\",\n        name: \"<所有站点>\",\n      },\n      initialized: false,\n      topSearches: [] as any[],\n      topSearchesUpdateTime: \"N/A\" as any,\n      topSearchesLoading: false,\n    };\n  },\n\n  methods: {\n    searchHotItem(item: any) {\n      console.log(item);\n      let key =\n        item.imdbId || `douban${item.doubanId}|${item.title}|${item.alt_title}`;\n\n      this.searchTorrent(key);\n    },\n\n    itemClick(item: any) {\n      console.log(item);\n      let key = this.searchKey;\n      let searchMode = this.options.beforeSearchingOptions\n        ? this.options.beforeSearchingOptions.searchModeForItem\n        : EBeforeSearchingItemSearchMode.id;\n      switch (searchMode) {\n        case EBeforeSearchingItemSearchMode.id:\n          if (item && item.id) {\n            key = `douban${item.id}|${item.title}|${item.originalTitle || item.aka || item.original_title}|${key}`;\n          }\n          break;\n\n        case EBeforeSearchingItemSearchMode.name:\n          if (item && item.title) {\n            key = item.title;\n          }\n          break;\n      }\n\n      this.searchTorrent(key);\n    },\n    showSelectBox() {\n      if (\n        this.$store &&\n        !this.$store.state.options.beforeSearchingOptions.getMovieInformation\n      )\n        return;\n      if (this.items.length > 0) {\n        this.showMenu = true;\n      } else if (this.searchKey) {\n        this.timer = setTimeout(() => {\n          this.getDoubanInfos(this.searchKey);\n        }, 750);\n      }\n    },\n    /**\n     * 从豆瓣获取相关信息\n     */\n    getDoubanInfos(key: string) {\n      if (\n        this.$store &&\n        !this.$store.state.options.beforeSearchingOptions.getMovieInformation\n      )\n        return;\n      if (this.isLoading || !key) return;\n\n      this.isLoading = true;\n      this.items = [];\n\n      // 本地调试时\n      if (window.location.hostname == \"localhost\") {\n        $.ajax(\"http://localhost:8001/test/beforeSearching.json\")\n          .done((result) => {\n            console.log(result);\n            if (result && result.subjects) {\n              this.items = result.subjects;\n              this.isLoading = false;\n              this.showMenu = true;\n            }\n          })\n          .fail((err) => {\n            console.log(err);\n          })\n          .always(() => (this.isLoading = false));\n        return;\n      }\n\n      extension\n        .sendRequest(EAction.queryMovieInfoFromDouban, null, {\n          key,\n          count: this.$store.state.options.beforeSearchingOptions\n            .maxMovieInformationCount,\n        })\n        .then((result) => {\n          this.isLoading = false;\n          if (result) {\n            if (result.subjects) {\n              // issue 615: frodo 接口不止返回电影类型，而模板只考虑了电影类型的展示，把其他都筛掉\n              this.items = result.subjects.filter((x: { type: string; }) => x.type == 'movie');\n              this.showMenu = this.items.length > 0;\n            } else if (result.title && result.updateTime) {\n              this.items = [result];\n              this.showMenu = true;\n            }\n          }\n        })\n        .catch((error) => {\n          console.log(error);\n        })\n        .finally(() => {\n          this.isLoading = false;\n        });\n    },\n\n    searchTorrent(key?: string) {\n      key = key || this.searchKey;\n      if (!key) {\n        return;\n      }\n\n      this.showMenu = false;\n      clearTimeout(this.timer);\n\n      this.$store.dispatch(\"saveConfig\", {\n        lastSearchKey: this.searchKey,\n      });\n\n      this.$router.push({\n        name: \"search-torrent\",\n        params: {\n          key: key,\n        },\n      });\n    },\n    changeSearchSolution(solution?: SearchSolution) {\n      let defaultSearchSolutionId = \"\";\n      if (solution) {\n        this.selectedSearchSolutionName = solution.name;\n        defaultSearchSolutionId = solution.id;\n      } else {\n        this.selectedSearchSolutionName = this.$t(\n          \"searchBox.default\"\n        ).toString();\n      }\n\n      this.$store.dispatch(\"saveConfig\", {\n        defaultSearchSolutionId: defaultSearchSolutionId,\n      });\n    },\n    clearSearchKey() {\n      this.$store.dispatch(\"saveConfig\", {\n        lastSearchKey: \"\",\n      });\n    },\n    getTopSearches() {\n      this.topSearchesLoading = true;\n      extension\n        .sendRequest(EAction.getTopSearches)\n        .then((result) => {\n          this.topSearches = result;\n          this.topSearchesUpdateTime = dayjs().format(\"YYYY-MM-DD HH:mm:ss\");\n        })\n        .catch((error) => {\n          console.log(error);\n        })\n        .finally(() => {\n          this.topSearchesLoading = false;\n        });\n    },\n  },\n  watch: {\n    /**\n     * 当搜索关键字变更时，尝试从豆瓣获取相关信息\n     */\n    searchKey() {\n      if (\n        this.$store &&\n        !this.$store.state.options.beforeSearchingOptions.getMovieInformation\n      )\n        return;\n      clearTimeout(this.timer);\n      if (!this.initialized) return;\n      if (!this.searchKey) {\n        this.showMenu = false;\n        return;\n      }\n\n      this.timer = setTimeout(() => {\n        this.getDoubanInfos(this.searchKey);\n      }, 750);\n    },\n    /**\n     * 监控最后的搜索关键字，前显示当前搜索框\n     */\n    \"$store.state.options.lastSearchKey\"() {\n      if (this.searchKey != this.$store.state.options.lastSearchKey) {\n        console.log(\"key change: %s\", this.$store.state.options.lastSearchKey);\n        this.searchKey = this.$store.state.options.lastSearchKey;\n        if (this.searchKey) {\n          this.isLoading = true;\n          this.items = [];\n          setTimeout(() => {\n            this.isLoading = false;\n          }, 1000);\n        }\n      }\n    },\n  },\n  created() {\n    this.selectedSearchSolutionName = this.$t(\"searchBox.default\").toString();\n    if (this.options.defaultSearchSolutionId == this.allSite.id) {\n      this.selectedSearchSolutionName = this.allSite.name;\n    } else if (\n      this.options.defaultSearchSolutionId &&\n      this.options.searchSolutions\n    ) {\n      let searchSolution:\n        | SearchSolution\n        | undefined = this.options.searchSolutions.find(\n          (item: SearchSolution) => {\n            return item.id === this.options.defaultSearchSolutionId;\n          }\n        );\n\n      if (searchSolution) {\n        this.selectedSearchSolutionName = searchSolution.name;\n      }\n    }\n    if (this.options.sites) {\n      let count = 0;\n      this.options.sites.forEach((item: Site) => {\n        if (item.allowSearch) {\n          count++;\n        }\n      });\n    }\n    if (this.options.search && this.options.search.saveKey) {\n      this.searchKey = this.options.lastSearchKey || \"\";\n    }\n    this.getTopSearches();\n    // 防止初始化时进行信息获取\n    setTimeout(() => {\n      this.initialized = true;\n    }, 500);\n  },\n  computed: {\n    loadStatus(): boolean {\n      if (this.$store && this.$store.state) {\n        return this.$store.state.searching || this.isLoading;\n      } else {\n        return this.isLoading;\n      }\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.search-solution {\n  margin-right: -10px;\n}\n\n.album {\n  img {\n    margin-top: 18px;\n    border-radius: 0px;\n    margin-right: 10px;\n  }\n}\n\n.top-searches {\n  button {\n    min-width: unset;\n    margin: 0;\n  }\n}\n\n.top-searches-item {\n  position: absolute;\n  bottom: 0px;\n  width: 100%;\n  max-height: 50px;\n  background-color: #fff;\n  opacity: 0.85;\n  padding: 2px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n\n  .alt-title {\n    display: inline;\n  }\n\n  .rating {\n    position: absolute;\n    text-decoration: none;\n    right: 0px;\n    background-color: #fff;\n    padding: 1px 3px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/components/Topbar.vue",
    "content": "<template>\n  <v-toolbar :color=\"baseColor\" app fixed clipped-left id=\"system-topbar\">\n    <v-toolbar-side-icon\n      @click.stop=\"drawer = !drawer\"\n      :title=\"$t('topbar.navBarTip')\"\n    ></v-toolbar-side-icon>\n    <v-toolbar-title style=\"width: 220px\" class=\"hidden-md-and-down\">\n      <span>{{ $t(\"topbar.title\") }}</span>\n    </v-toolbar-title>\n    <SearchBox />\n    <v-btn\n      flat\n      to=\"/search-torrent/__LatestTorrents__\"\n      class=\"grey--text text--darken-2 hidden-xs-only\"\n      :title=\"$t('topbar.showNewTorrentsTip')\"\n    >\n      <v-icon>fiber_new</v-icon>\n      <span class=\"ml-2 hidden-md-and-down\">{{\n        $t(\"topbar.showNewTorrents\")\n      }}</span>\n    </v-btn>\n\n    <v-spacer></v-spacer>\n    <v-toolbar-items class=\"hidden-xs-only\">\n      <v-btn\n        flat\n        href=\"https://github.com/pt-plugins/PT-Plugin-Plus\"\n        target=\"_blank\"\n        class=\"grey--text text--darken-2\"\n        rel=\"noopener noreferrer nofollow\"\n        :title=\"$t('topbar.github')\"\n      >\n        <v-icon>home</v-icon>\n        <span class=\"ml-1\">{{ $t(\"topbar.github\") }}</span>\n      </v-btn>\n      <v-btn\n        flat\n        href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki\"\n        target=\"_blank\"\n        class=\"grey--text text--darken-2\"\n        :title=\"$t('topbar.help')\"\n        rel=\"noopener noreferrer nofollow\"\n      >\n        <v-icon>help</v-icon>\n        <span class=\"ml-1\">{{ $t(\"topbar.help\") }}</span>\n      </v-btn>\n    </v-toolbar-items>\n  </v-toolbar>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport SearchBox from \"./SearchBox.vue\";\nexport default Vue.extend({\n  props: {\n    value: Boolean\n  },\n  components: {\n    SearchBox\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    drawer() {\n      this.$emit(\"change\", this.drawer);\n    },\n    value() {\n      this.drawer = this.value;\n    }\n  },\n  data() {\n    return {\n      words: {\n        title: \"PT 助手\",\n        navBarTip: \"点击显示/隐藏导航栏\",\n        help: \"使用帮助\",\n        github: \"访问 Github\",\n        showNewTorrents: \"浏览各站首页种子\",\n        showNewTorrentsTip: \"根据当前方案，搜索各站的首页种子\"\n      },\n      drawer: this.$store.state.options.navBarIsOpen,\n      baseColor: \"amber\"\n    };\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/components/TorrentProgress.vue",
    "content": "<template>\n  <v-layout row wrap>\n    <v-flex xs2 class=\"mt-1\">\n      <v-icon :size=\"10\" :color=\"color\" :title=\"statusTip\">{{icon}}</v-icon>\n    </v-flex>\n    <v-flex xs10>\n      <v-progress-linear\n        style=\"margin-left: 1px;\"\n        :color=\"color\"\n        height=\"4\"\n        :value=\"progress\"\n        :title=\"`${progress}%`\"\n      ></v-progress-linear>\n    </v-flex>\n  </v-layout>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { ETorrentStatus } from \"@/interface/enum\";\n\nexport default Vue.extend({\n  props: {\n    progress: Number,\n    status: Number\n  },\n  mounted() {},\n  computed: {\n    color(): string {\n      let result = \"success\";\n      switch (this.status) {\n        case ETorrentStatus.downloading:\n          result = \"info\";\n          break;\n\n        case ETorrentStatus.completed:\n        case ETorrentStatus.inactive:\n          result = \"grey\";\n          break;\n\n        case ETorrentStatus.sending:\n        default:\n          break;\n      }\n      return result;\n    },\n    icon(): string {\n      let result = \"arrow_upward\";\n      switch (this.status) {\n        case ETorrentStatus.downloading:\n          result = \"arrow_downward\";\n          break;\n\n        case ETorrentStatus.completed:\n          result = \"done\";\n          break;\n\n        case ETorrentStatus.inactive:\n          result = \"wifi_off\";\n          break;\n\n        case ETorrentStatus.sending:\n        default:\n          break;\n      }\n      return result;\n    },\n    statusTip(): string {\n      let result = this.$t(\"searchTorrent.torrentStatus.sending\").toString();\n      switch (this.status) {\n        case ETorrentStatus.downloading:\n          result = this.$t(\n            \"searchTorrent.torrentStatus.downloading\"\n          ).toString();\n          break;\n\n        case ETorrentStatus.completed:\n          result = this.$t(\"searchTorrent.torrentStatus.completed\").toString();\n          break;\n\n        case ETorrentStatus.inactive:\n          result = this.$t(\"searchTorrent.torrentStatus.inactive\").toString();\n          break;\n\n        case ETorrentStatus.sending:\n        default:\n          break;\n      }\n      return result;\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/components/WorkingStatus.vue",
    "content": "<template>\n  <div v-if=\"working\">\n    <v-list>\n      <v-list-tile v-for=\"(item, index) in items\" :key=\"index\">\n        <v-list-tile-action>\n          <v-btn flat icon :loading=\"item.status=='loading'\">\n            <v-icon :color=\"getColor(item)\">{{getIcon(item)}}</v-icon>\n          </v-btn>\n        </v-list-tile-action>\n\n        <v-list-tile-content>\n          <v-list-tile-title v-text=\"item.title\"></v-list-tile-title>\n        </v-list-tile-content>\n      </v-list-tile>\n    </v-list>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { IWorkingStatusItem, EWorkingStatus } from \"@/interface/common\";\n\nexport default Vue.extend({\n  props: {\n    timeout: {\n      type: Number,\n      default: 5000\n    }\n  },\n\n  data() {\n    return {\n      items: [] as IWorkingStatusItem[],\n      working: true\n    };\n  },\n\n  watch: {\n    items: {\n      handler() {\n        this.change();\n      },\n      deep: true\n    }\n  },\n\n  methods: {\n    add(item: IWorkingStatusItem) {\n      item.status = EWorkingStatus.loading;\n      this.items.push(item);\n    },\n\n    update(key: string, status: EWorkingStatus) {\n      let index = this.items.findIndex((_: IWorkingStatusItem) => {\n        return _.key === key;\n      });\n\n      if (index != -1) {\n        this.items[index].status = status;\n      }\n    },\n\n    clear() {\n      this.items = [];\n    },\n\n    change() {\n      if (!this.items) {\n        this.working = false;\n        return;\n      }\n\n      if (this.items.length == 0) {\n        this.working = false;\n        return;\n      }\n      let workingCount = 0;\n      this.items.forEach((item: IWorkingStatusItem) => {\n        if (item.status === EWorkingStatus.loading) {\n          workingCount++;\n        }\n      });\n\n      if (workingCount > 0) {\n        this.working = true;\n      } else {\n        setTimeout(() => {\n          this.working = false;\n        }, this.timeout);\n      }\n    },\n\n    getColor(item: IWorkingStatusItem) {\n      switch (item.status) {\n        case EWorkingStatus.success:\n        case EWorkingStatus.error:\n          return item.status;\n\n        default:\n          return \"info\";\n      }\n    },\n\n    getIcon(item: IWorkingStatusItem) {\n      switch (item.status) {\n        case EWorkingStatus.success:\n          return \"check\";\n\n        case EWorkingStatus.error:\n          return \"close\";\n\n        default:\n          return \"refresh\";\n      }\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/i18n.ts",
    "content": "import Vue from \"vue\";\nimport VueI18n from \"vue-i18n\";\nimport { API } from \"@/service/api\";\nimport { i18nResource } from \"@/interface/common\";\n\nVue.use(VueI18n);\n\nexport const i18n = new VueI18n({\n  locale: \"\",\n  fallbackLocale: \"en\"\n});\n\nexport class i18nService {\n  // 已加载的语言文件\n  public loadedLanguages: Array<string> = [];\n  // 已支持的语言文件列表\n  public config: Array<any> = [];\n  // 当前语言\n  public currentLanguage: string = \"\";\n\n  public onChanged: Function = () => {};\n  public onAdded: Function = () => {};\n  public onReplaced: Function = () => {};\n\n  private initialized = false;\n  public vuei18n = i18n;\n\n  constructor() {\n    // 加载所有语言包\n    this.loadLangResource(\"en\");\n    this.loadLangResource(\"zh-CN\");\n  }\n\n  /**\n   * 初始化语言环境\n   * @param langCode\n   */\n  public init(langCode: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.loadConfig()\n        .then(() => {\n          this.reset(langCode)\n            .then(() => {\n              this.initialized = true;\n              resolve(i18n);\n            })\n            .catch(() => {\n              reject();\n            });\n        })\n        .catch(() => {\n          console.error(\n            \"Loading language configuration file failed. (加载语言配置文件失败)\"\n          );\n        });\n    });\n  }\n\n  /**\n   * 加载语言配置信息\n   */\n  public loadConfig(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      $.getJSON(`${API.host}/i18n.json`)\n        .done((result: any) => {\n          this.config = result;\n          resolve(this.config);\n        })\n        .fail(e => {\n          reject(e);\n        });\n    });\n  }\n\n  /**\n   * 加载语言资源文件\n   * @param langCode\n   */\n  public loadLangResource(langCode: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      $.getJSON(`${API.host}/i18n/${langCode}.json`)\n        .done((result: any) => {\n          this.push(result);\n          resolve(result);\n        })\n        .fail(e => {\n          reject(e);\n        });\n    });\n  }\n\n  /**\n   * push 语言资源\n   * @param resource\n   */\n  public push(resource: i18nResource) {\n    if (resource.name && resource.code) {\n      if (!this.exists(resource.code)) {\n        i18n.setLocaleMessage(resource.code, resource.words);\n        this.loadedLanguages.push(resource.code);\n      }\n    }\n  }\n\n  /**\n   * 变更语言\n   * @param langCode\n   */\n  public change(langCode: string) {\n    if (this.currentLanguage !== langCode) {\n      i18n.locale = langCode;\n      this.currentLanguage = langCode;\n      this.initialized && this.onChanged.call(this, langCode);\n    }\n  }\n\n  /**\n   * 重设语言\n   * @param langCode 语言代码\n   */\n  public reset(langCode: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.currentLanguage !== langCode) {\n        if (!this.exists(langCode)) {\n          this.loadLangResource(langCode)\n            .then(() => {\n              this.change(langCode);\n              resolve(langCode);\n            })\n            .catch(e => {\n              if (langCode != \"en\") {\n                this.reset(\"en\").then(() => {\n                  resolve(langCode);\n                });\n                return;\n              }\n              reject(e);\n            });\n          return;\n        }\n        this.change(langCode);\n      }\n      resolve(langCode);\n    });\n  }\n\n  /**\n   * 从指定的资源加载新的语言\n   * @param resource\n   */\n  public add(resource: i18nResource): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (resource.name && resource.code) {\n        if (this.exists(resource.code)) {\n          reject();\n        } else {\n          this.push(resource);\n          i18n.locale = resource.code;\n          this.currentLanguage = resource.code;\n          this.initialized && this.onAdded.call(this, resource);\n          resolve(resource.code);\n        }\n      } else {\n        reject();\n      }\n    });\n  }\n\n  /**\n   * 替换已有语言资源\n   * @param resource 语言资源内容\n   */\n  public replace(resource: i18nResource): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (resource.name && resource.code) {\n        if (this.exists(resource.code)) {\n          i18n.setLocaleMessage(resource.code, resource.words);\n          i18n.locale = resource.code;\n          this.currentLanguage = resource.code;\n          this.initialized && this.onReplaced.call(this, resource);\n          resolve(resource.code);\n        }\n      } else {\n        reject();\n      }\n    });\n  }\n\n  public exists(code: string): boolean {\n    return this.loadedLanguages.includes(code);\n  }\n}\n"
  },
  {
    "path": "src/options/main.ts",
    "content": "import Vue from \"vue\";\nimport vuetifyService from \"./plugins/vuetify\";\nimport App from \"./App.vue\";\nimport router from \"./router\";\nimport store from \"./store\";\nimport { filters } from \"@/service/filters\";\nimport dayjs from \"dayjs\";\nimport { i18nService } from \"./i18n\";\nimport { Options, i18nResource } from \"@/interface/common\";\n\nclass Main {\n  private options: Options = {\n    sites: [],\n    clients: []\n  };\n\n  private vm: any;\n  private i18n = new i18nService();\n\n  private loadCount = 0;\n\n  constructor() {\n    this.initI18n();\n    this.initVueConfig();\n    this.initMainVM();\n    this.init();\n  }\n\n  /**\n   * 初始化Vue全局相关参数\n   */\n  private initVueConfig() {\n    Vue.config.productionTip = false;\n\n    // 初始化全局过滤器\n    for (const key in filters) {\n      if (filters.hasOwnProperty(key)) {\n        Vue.filter(key, (...result: any) => {\n          return filters[key].call(filters, result);\n        });\n      }\n    }\n\n    // 初始化时间格式化过滤器\n    Vue.filter(\n      \"formatDate\",\n      (val: any, format: string = \"YYYY-MM-DD HH:mm\") => {\n        if (!val) {\n          return \"\";\n        }\n        if (dayjs(val).isValid()) {\n          // 标准时间戳需要 * 1000\n          if (/^(\\d){10}$/.test(val)) {\n            val = parseInt(val) * 1000;\n          }\n          return dayjs(val).format(format);\n        }\n        return val;\n      }\n    );\n\n    /**\n     * 将时间格式化为 xxx 前\n     * 修改自 NexusPHP\n     */\n    Vue.filter(\"timeAgo\", (source: any, weekOnly: boolean = false) => {\n      if (!source) {\n        return \"\";\n      }\n      let unit = {\n        year: this.i18n.vuei18n.t(\"timeline.time.year\", this.i18n.currentLanguage).toString(),\n        month: this.i18n.vuei18n.t(\"timeline.time.month\", this.i18n.currentLanguage).toString(),\n        day: this.i18n.vuei18n.t(\"timeline.time.day\", this.i18n.currentLanguage).toString(),\n        hour: this.i18n.vuei18n.t(\"timeline.time.hour\", this.i18n.currentLanguage).toString(),\n        mins: this.i18n.vuei18n.t(\"timeline.time.mins\", this.i18n.currentLanguage).toString(),\n        week: this.i18n.vuei18n.t(\"timeline.time.week\", this.i18n.currentLanguage).toString()\n      };\n\n      let now = new Date().getTime();\n\n      let mins = Math.floor(Math.abs(now - source) / 1000 / 60);\n      let hours = Math.floor(mins / 60);\n      mins -= hours * 60;\n      let days = Math.floor(hours / 24);\n      hours -= days * 24;\n\n      if (weekOnly) {\n        let week = Math.floor(days / 7);\n        if (week < 1) {\n          return this.i18n.vuei18n.t(\"timeline.time.lessThanAWeek\", this.i18n.currentLanguage).toString();\n        }\n        return `${week}${unit.week}`;\n      }\n\n      let months = Math.floor(days / 30);\n      let days2 = days - months * 30;\n      let years = Math.floor(days / 365);\n      months -= years * 12;\n      while (months > 12) {\n        years++;\n        months -= 12;\n      }\n      let result = \"\";\n\n      switch (true) {\n        case years > 0:\n          result = years + unit[\"year\"] + months + unit[\"month\"];\n          break;\n\n        case months > 0:\n          result = months + unit[\"month\"] + days2 + unit[\"day\"];\n          break;\n\n        case days > 0:\n          result = days + unit[\"day\"] + hours + unit[\"hour\"];\n          break;\n\n        case hours > 0:\n          result = hours + unit[\"hour\"] + mins + unit[\"mins\"];\n          break;\n\n        case mins > 0:\n          result = mins + unit[\"mins\"];\n          break;\n\n        default:\n          result = \"< 1\" + unit[\"mins\"];\n      }\n\n      return result + this.i18n.vuei18n.t(\"timeline.time.ago\", this.i18n.currentLanguage).toString();\n    });\n  }\n\n  /**\n   * 初始化多语言环境\n   */\n  private initI18n() {\n    this.i18n.onChanged = (locale: string) => {\n      this.options.locale = locale;\n      store.dispatch(\"saveConfig\", {\n        locale\n      });\n      if (!this.vm) {\n        return;\n      }\n\n      // 非简体中文时，暂时切换到英文\n      // TODO 考虑添加其他语言动态支持\n      if (locale != \"zh-CN\") {\n        this.vm.$vuetify.lang.current = \"en\";\n      } else {\n        this.vm.$vuetify.lang.current = \"zh-Hans\";\n      }\n    };\n\n    this.i18n.onAdded = (resource: i18nResource) => {\n      store.dispatch(\"addLanguage\", resource);\n    };\n\n    this.i18n.onReplaced = (resource: i18nResource) => {\n      store.dispatch(\"replaceLanguage\", resource);\n    };\n\n    // 全局挂载 i18nService 对象\n    window.i18nService = this.i18n;\n    vuetifyService.init(\"en\");\n  }\n\n  /**\n   * 初始AppVM\n   */\n  private initMainVM() {\n    this.vm = new Vue({\n      router,\n      store,\n      i18n: this.i18n.vuei18n,\n      render: h => h(App)\n    });\n    this.vm.$mount(\"#app\");\n  }\n\n  public init() {\n    const requests: any[] = [];\n\n    // 读取配置信息\n    requests.push(store.dispatch(\"readConfig\"));\n    requests.push(store.dispatch(\"readUIOptions\"));\n\n    Promise.all(requests)\n      .then((results: any) => {\n        this.options = results[0];\n\n        // 设置语言信息\n        this.i18n.init(this.options.locale || \"zh-CN\").then((i18n: any) => {\n          if (this.options.locale != \"zh-CN\") {\n            this.vm.$vuetify.lang.current = \"en\";\n          } else {\n            this.vm.$vuetify.lang.current = \"zh-Hans\";\n          }\n\n          this.vm.$children[0].init();\n        });\n      })\n      .catch(error => {\n        // alert(\"加载配置失败？？\" + error);\n        if (++this.loadCount < 5) {\n          setTimeout(() => {\n            this.init();\n          }, 1000);\n        } else {\n          alert(\"加载配置失败？？\" + error);\n        }\n        console.log(error);\n      });\n  }\n}\n(function main() {\n  new Main();\n})();\n\n/**\n * 定义 window 中需要挂载的对象\n */\ndeclare global {\n  interface Window {\n    i18nService: i18nService;\n  }\n}\n"
  },
  {
    "path": "src/options/plugins/vuetify.ts",
    "content": "import Vue from \"vue\";\nimport Vuetify from \"vuetify\";\n// Translation provided by Vuetify (typescript)\nimport zhHans from \"vuetify/src/locale/zh-Hans\";\nimport en from \"vuetify/src/locale/en\";\nimport \"vuetify/src/stylus/app.styl\";\n\nclass VuetifyService {\n  public init(lang: string = \"zh-Hans\") {\n    Vue.use(Vuetify, {\n      iconfont: \"md\",\n      lang: {\n        locales: { \"zh-Hans\": zhHans, en },\n        current: lang\n      }\n    });\n  }\n}\n\nexport default new VuetifyService();\n"
  },
  {
    "path": "src/options/router.ts",
    "content": "import Vue from \"vue\";\nimport Router from \"vue-router\";\nimport Home from \"./views/Home.vue\";\n\nVue.use(Router);\n\nexport default new Router({\n  routes: [\n    {\n      path: \"/\",\n      name: \"home\",\n      component: Home,\n      alias: \"/home\",\n      meta: {\n        // 需要被缓存\n        keepAlive: true\n      }\n    },\n    // {\n    //   path: \"/about\",\n    //   name: \"about\",\n    //   // route level code-splitting\n    //   // this generates a separate chunk (about.[hash].js) for this route\n    //   // which is lazy-loaded when the route is visited.\n    //   component: () =>\n    //     import(/* webpackChunkName: \"about\" */ \"./views/About.vue\")\n    // },\n    {\n      path: \"/set-sites\",\n      name: \"set-sites\",\n      component: () => import(\"./views/settings/Sites/Index.vue\"),\n      meta: {\n        // 需要被缓存\n        keepAlive: true\n      }\n    },\n    {\n      path: \"/set-support-schema\",\n      name: \"set-support-schema\",\n      component: () => import(\"./views/settings/SupportSchema.vue\")\n    },\n    {\n      path: \"/set-download-clients\",\n      name: \"set-download-clients\",\n      component: () => import(\"./views/settings/DownloadClients/Index.vue\")\n    },\n    {\n      path: \"/set-base\",\n      name: \"set-base\",\n      component: () => import(\"./views/settings/Base/Index.vue\")\n    },\n    {\n      path: \"/set-download-paths\",\n      name: \"set-download-paths\",\n      component: () => import(\"./views/settings/DownloadPaths/Index.vue\")\n    },\n    {\n      path: \"/set-backup\",\n      name: \"set-backup\",\n      component: () => import(\"./views/settings/Backup/Index.vue\")\n    },\n    {\n      path: \"/technology-stack\",\n      name: \"technology-stack\",\n      component: () => import(\"./views/TechnologyStack.vue\")\n    },\n    {\n      path: \"/set-language\",\n      name: \"set-language\",\n      component: () => import(\"./views/settings/Language/Index.vue\")\n    },\n    {\n      path: \"/set-search-solution\",\n      name: \"set-search-solution\",\n      component: () => import(\"./views/settings/SearchSolution/Index.vue\")\n    },\n    {\n      path: \"/donate\",\n      name: \"donate\",\n      component: () => import(\"./views/Donate.vue\")\n    },\n    {\n      path: \"/set-site-plugins/:host\",\n      name: \"set-site-plugins\",\n      component: () => import(\"./views/settings/SitePlugins/Index.vue\"),\n      props: true\n    },\n    {\n      path: \"/search-torrent/:key?/:host?\",\n      name: \"search-torrent\",\n      component: () => import(\"./views/search/SearchTorrent.vue\"),\n      props: true,\n      meta: {\n        // 需要被缓存\n        keepAlive: true\n      }\n    },\n    {\n      path: \"/history\",\n      name: \"history\",\n      component: () => import(\"./views/History.vue\")\n    },\n    {\n      path: \"/system-logs\",\n      name: \"system-logs\",\n      component: () => import(\"./views/SystemLogs.vue\")\n    },\n    {\n      path: \"/set-site-search-entry/:host\",\n      name: \"set-site-search-entry\",\n      component: () => import(\"./views/settings/SiteSearchEntry/Index.vue\"),\n      props: true\n    },\n    {\n      path: \"/dev-team\",\n      name: \"dev-team\",\n      component: () => import(\"./views/Teams.vue\")\n    },\n    {\n      path: \"/user-data-timeline\",\n      name: \"user-data-timeline\",\n      component: () => import(\"./views/UserDataTimeline.vue\")\n    },\n    {\n      path: \"/statistic/:host?\",\n      name: \"statistic\",\n      component: () => import(\"./views/statisticCharts/SiteBase.vue\")\n    },\n    {\n      path: \"/set-permissions\",\n      name: \"set-permissions\",\n      component: () => import(\"./components/Permissions.vue\")\n    },\n    {\n      path: \"/collection\",\n      name: \"collection\",\n      component: () => import(\"./views/collection/Index.vue\"),\n      meta: {\n        // 需要被缓存\n        keepAlive: true\n      }\n    },\n    {\n      path: \"/search-result-snapshot\",\n      name: \"search-result-snapshot\",\n      component: () => import(\"./views/search/SearchResultSnapshot.vue\")\n    },\n    {\n      path: \"/keep-upload-task\",\n      name: \"keep-upload-task\",\n      component: () => import(\"./views/keepUpload/KeepUploadTasks.vue\")\n    }\n  ]\n});\n"
  },
  {
    "path": "src/options/shims-tsx.d.ts",
    "content": "import Vue, { VNode } from \"vue\";\n\ndeclare global {\n  namespace JSX {\n    // tslint:disable no-empty-interface\n    interface Element extends VNode {}\n    // tslint:disable no-empty-interface\n    interface ElementClass extends Vue {}\n    interface IntrinsicElements {\n      [elem: string]: any;\n    }\n  }\n}\n"
  },
  {
    "path": "src/options/shims-vue.d.ts",
    "content": "declare module '*.vue' {\n  import Vue from 'vue'\n  export default Vue\n}\n"
  },
  {
    "path": "src/options/store.ts",
    "content": "import Vue from \"vue\";\nimport Vuex from \"vuex\";\n\nimport md5 from \"blueimp-md5\";\nimport {\n  Options,\n  EAction,\n  Site,\n  UIOptions,\n  EModule,\n  SearchSolution,\n  SearchEntry,\n  ECommonKey,\n  IBackupServer\n} from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\n\nclass ExtensionWorker extends Extension {\n  constructor() {\n    super();\n  }\n\n  save(options: Options) {\n    return this.sendRequest(EAction.saveConfig, null, options);\n  }\n}\n\nconst extension = new ExtensionWorker();\n\nVue.use(Vuex);\nexport default new Vuex.Store({\n  /**\n   * 状态\n   */\n  state: {\n    options: {\n      sites: [],\n      clients: []\n    } as Options,\n    schemas: [],\n    uiOptions: {} as UIOptions,\n    initialized: false,\n    searching: false\n  },\n\n  /**\n   * 方法\n   */\n  mutations: {\n    readConfig(state) {\n      extension.sendRequest(EAction.readConfig, (options: Options) => {\n        state.options = options;\n      });\n    },\n\n    resetConfig(state, options) {\n      let system = state.options.system;\n      state.options = Object.assign({}, options);\n      state.options.system = system;\n      extension.sendRequest(EAction.saveConfig, null, state.options);\n    },\n\n    updateConfig(state, options) {\n      Object.assign(state.options, options);\n      extension.sendRequest(EAction.saveConfig, null, state.options);\n    },\n\n    /**\n     * 添加一个站点\n     * @param state 状态\n     * @param site 站点配置\n     */\n    addSite(state, site) {\n      state.options.sites.push(site);\n\n      extension.sendRequest(EAction.saveConfig, null, state.options);\n    },\n\n    /**\n     * 更新站点配置\n     * @param state\n     * @param site\n     */\n    updateSite(state, site) {\n      let index = state.options.sites.findIndex(item => {\n        return item.host === site.host;\n      });\n\n      if (index !== -1) {\n        state.options.sites[index] = site;\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 删除指定的站点\n     * @param state\n     * @param site\n     */\n    removeSite(state, site) {\n      let index = state.options.sites.findIndex(item => {\n        return item.host === site.host;\n      });\n\n      if (index !== -1) {\n        state.options.sites.splice(index, 1);\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 添加下载服务器\n     * @param state\n     * @param item\n     */\n    addClient(state, item) {\n      item.id = md5(new Date().toString());\n      state.options.clients.push(item);\n\n      extension.sendRequest(EAction.saveConfig, null, state.options);\n    },\n\n    /**\n     * 更新下载服务器配置\n     * @param state\n     * @param item\n     */\n    updateClient(state, item) {\n      let index = state.options.clients.findIndex(data => {\n        return item.id === data.id;\n      });\n\n      if (index !== -1) {\n        state.options.clients[index] = item;\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 删除指定下载服务器\n     * @param state\n     * @param item\n     */\n    removeClient(state, item) {\n      let index = state.options.clients.findIndex(data => {\n        return data.id === item.id;\n      });\n\n      if (index !== -1) {\n        let client = state.options.clients[index];\n        if (state.options.defaultClientId == client.id) {\n          state.options.defaultClientId = \"\";\n        }\n        state.options.clients.splice(index, 1);\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    clearClients(state) {\n      state.options.clients = [];\n      state.options.defaultClientId = \"\";\n      extension.sendRequest(EAction.saveConfig, null, state.options);\n    },\n\n    /**\n     * 更新指定客户端的保存目录信息\n     * @param state\n     * @param options\n     */\n    updatePathsOfClient(state, options) {\n      let client = state.options.clients.find(data => {\n        return options.clientId === data.id;\n      });\n      if (client) {\n        if (!client.paths) {\n          client.paths = {};\n        }\n\n        if (options.site && options.site.host) {\n          client.paths[options.site.host] = options.paths;\n        } else {\n          // 如果未指定网站，则用于所有站点\n          client.paths[ECommonKey.allSite] = options.paths;\n        }\n\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 移除下载服务器已配置的保存目录\n     * @param state\n     * @param options\n     */\n    removePathsOfClient(state, options) {\n      let client = state.options.clients.find(data => {\n        return options.clientId === data.id;\n      });\n      if (client && client.paths) {\n        let key = \"\";\n        if (options.site) {\n          key = options.site.host;\n        } else {\n          key = ECommonKey.allSite;\n        }\n        delete client.paths[key];\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 添加插件\n     * @param state\n     * @param options\n     */\n    addPlugin(state, options) {\n      let site: any = state.options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n\n      if (site) {\n        if (!site.plugins) {\n          site.plugins = [];\n        }\n        options.plugin.id = md5(new Date().toString());\n        site.plugins.push(options.plugin);\n        extension.sendRequest(EAction.saveConfig, null, state.options);\n      }\n    },\n\n    /**\n     * 更新插件\n     * @param state\n     * @param options\n     */\n    updatePlugin(state, options) {\n      let site: any = state.options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n      if (site) {\n        let index: any = site.plugins.findIndex((item: any) => {\n          return item.id === options.plugin.id;\n        });\n        if (index !== -1) {\n          site.plugins[index] = options.plugin;\n\n          extension.sendRequest(EAction.saveConfig, null, state.options);\n        }\n      }\n    },\n\n    /**\n     * 删除插件\n     * @param state\n     * @param options\n     */\n    removePlugin(state, options) {\n      let site: any = state.options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n      if (site) {\n        let index: any = site.plugins.findIndex((item: any) => {\n          return item.id === options.plugin.id;\n        });\n        if (index !== -1) {\n          site.plugins.splice(index, 1);\n          extension.sendRequest(EAction.saveConfig, null, state.options);\n        }\n      }\n    },\n\n    updateOptions(state, options: Options) {\n      state.options = options;\n    },\n\n    updateUIOptions(state, options: UIOptions) {\n      state.uiOptions = options;\n    },\n\n    updateSearchStatus(state, searching: boolean) {\n      state.searching = searching;\n    }\n  },\n  actions: {\n    /**\n     * 重置运行时配置\n     * @param param0\n     */\n    resetRunTimeOptions({ commit, state }, options: Options) {\n      extension\n        .sendRequest(EAction.resetRunTimeOptions, null, options)\n        .then(result => {\n          commit(\"updateOptions\", result);\n        })\n        .catch(error => {\n          console.log(\"store.resetRunTimeOptions.error\", error);\n        });\n    },\n\n    readConfig({ commit, state }): Promise<any> {\n      return new Promise<any>((resolve?: any, reject?: any) => {\n        extension.sendRequest(EAction.writeLog, null, {\n          module: EModule.options,\n          event: \"Options.readConfig\",\n          msg: \"开始加载配置信息\"\n        });\n        extension\n          .sendRequest(EAction.readConfig)\n          .then((options: Options) => {\n            commit(\"updateOptions\", options);\n            if (!options.system) {\n              extension.sendRequest(EAction.writeLog, null, {\n                module: EModule.options,\n                event: \"Options.readConfig.Error\",\n                msg: \"配置信息加载失败，没有获取到系统定义信息\"\n              });\n              reject(\"Options.readConfig.Error\");\n              return;\n            }\n\n            if (!options.system.clients || !options.system.schemas) {\n              extension.sendRequest(EAction.writeLog, null, {\n                module: EModule.options,\n                event: \"Options.readConfig.Error\",\n                msg: \"配置信息加载失败，没有获取到下载服务器或站点架构信息\"\n              });\n              reject(\"Options.readConfig.Error\");\n              return;\n            }\n\n            extension.sendRequest(EAction.writeLog, null, {\n              module: EModule.options,\n              event: \"Options.readConfig.Finished\",\n              msg: \"配置加载完成\"\n            });\n\n            state.initialized = true;\n            resolve(options);\n          })\n          .catch(e => {\n            reject(e);\n          });\n      });\n    },\n\n    saveConfig({ commit, state }, options: Options) {\n      let _options: Options = Object.assign({}, state.options);\n      Object.assign(_options, options);\n\n      extension\n        .sendRequest(EAction.saveConfig, null, _options)\n        .then(() => {\n          commit(\"updateOptions\", _options);\n        })\n        .catch(error => {\n          console.log(\"store.saveConfig.error\", error);\n        });\n    },\n\n    readUIOptions({ commit }): Promise<any> {\n      return new Promise<any>((resolve?: any, reject?: any) => {\n        extension\n          .sendRequest(EAction.readUIOptions)\n          .then((options: UIOptions) => {\n            commit(\"updateUIOptions\", options);\n            resolve(options);\n          })\n          .catch(error => {\n            console.log(\"store.saveConfig.error\", error);\n            reject(error);\n          });\n      });\n    },\n\n    saveUIOptions({ commit, state }, options: UIOptions) {\n      let _options: UIOptions = Object.assign({}, state.uiOptions);\n      Object.assign(_options, options);\n      extension\n        .sendRequest(EAction.saveUIOptions, null, _options)\n        .then(() => {\n          commit(\"updateUIOptions\", _options);\n        })\n        .catch();\n    },\n\n    updatePagination({ commit, state }, data: any) {\n      let paginations = state.uiOptions.paginations || {};\n\n      paginations[data.key] = data.options;\n      state.uiOptions.paginations = paginations;\n      extension\n        .sendRequest(EAction.saveUIOptions, null, state.uiOptions)\n        .then(() => {\n          commit(\"updateUIOptions\", state.uiOptions);\n        })\n        .catch();\n    },\n\n    /**\n     * 更新视图相关参数\n     * @param param0\n     * @param data\n     */\n    updateViewOptions({ commit, state }, data: any) {\n      let views = state.uiOptions.views || {};\n\n      views[data.key] = data.options;\n      state.uiOptions.views = views;\n      extension\n        .sendRequest(EAction.saveUIOptions, null, state.uiOptions)\n        .then(() => {\n          commit(\"updateUIOptions\", state.uiOptions);\n        })\n        .catch();\n    },\n\n    /**\n     * 添加/更新搜索方案\n     * @param state\n     * @param options\n     */\n    updateSearchSolution({ commit, state }, options: SearchSolution) {\n      let index: number = -1;\n      if (state.options.searchSolutions) {\n        index = state.options.searchSolutions.findIndex((e: SearchSolution) => {\n          return e.id === options.id;\n        });\n      }\n\n      let _options: Options = Object.assign({}, state.options);\n\n      if (!_options.searchSolutions) {\n        _options.searchSolutions = [];\n      }\n      if (index == -1) {\n        options.id = md5(new Date().toString());\n        _options.searchSolutions.push(options);\n      } else {\n        _options.searchSolutions[index] = options;\n      }\n\n      extension\n        .save(_options)\n        .then(() => {\n          commit(\"updateOptions\", _options);\n        })\n        .catch();\n    },\n\n    /**\n     * 删除搜索方案\n     * @param state\n     * @param options\n     */\n    removeSearchSolution({ commit, state }, options) {\n      let index: number = -1;\n      if (state.options.searchSolutions) {\n        index = state.options.searchSolutions.findIndex((e: SearchSolution) => {\n          return e.id === options.id;\n        });\n      }\n\n      let _options: Options = Object.assign({}, state.options);\n\n      if (!_options.searchSolutions) {\n        return;\n      }\n\n      if (index > -1) {\n        if (_options.defaultSearchSolutionId == options.id) {\n          _options.defaultSearchSolutionId = \"\";\n        }\n        _options.searchSolutions.splice(index, 1);\n        extension\n          .save(_options)\n          .then(() => {\n            commit(\"updateOptions\", _options);\n          })\n          .catch();\n      }\n    },\n\n    /**\n     * 添加搜索入口\n     * @param state\n     * @param options\n     */\n    addSiteSearchEntry({ commit, state }, options) {\n      let _options: Options = Object.assign({}, state.options);\n\n      let site: any = _options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n\n      if (site) {\n        if (!site.searchEntry) {\n          site.searchEntry = [];\n        }\n        options.item.id = md5(new Date().toString());\n        site.searchEntry.push(options.item);\n\n        commit(\"updateOptions\", _options);\n        extension.save(_options);\n      }\n    },\n\n    /**\n     * 更新插件\n     * @param state\n     * @param options\n     */\n    updateSiteSearchEntry({ commit, state }, options) {\n      let _options: Options = Object.assign({}, state.options);\n\n      let site: any = _options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n\n      if (site) {\n        let index: any = site.searchEntry.findIndex((item: SearchEntry) => {\n          return item.isCustom && item.id === options.item.id;\n        });\n        if (index !== -1) {\n          site.searchEntry[index] = options.item;\n\n          commit(\"updateOptions\", _options);\n          extension.save(_options);\n        }\n      }\n    },\n\n    /**\n     * 删除插件\n     * @param state\n     * @param options\n     */\n    removeSiteSearchEntry({ commit, state }, options) {\n      let _options: Options = Object.assign({}, state.options);\n\n      let site: any = _options.sites.find((item: any) => {\n        return item.host === options.host;\n      });\n      if (site) {\n        let index: any = site.searchEntry.findIndex((item: any) => {\n          return item.isCustom && item.id === options.item.id;\n        });\n        if (index !== -1) {\n          site.searchEntry.splice(index, 1);\n          commit(\"updateOptions\", _options);\n          extension.save(_options);\n        }\n      }\n    },\n\n    addLanguage({ commit, state }, options) {\n      extension\n        .sendRequest(EAction.addLanguage, null, options)\n        .then(() => {})\n        .catch(error => {});\n    },\n\n    replaceLanguage({ commit, state }, options) {\n      extension\n        .sendRequest(EAction.replaceLanguage, null, options)\n        .then(() => {})\n        .catch(error => {});\n    },\n\n    /**\n     * 添加备份服务器\n     * @param param0\n     * @param server\n     */\n    addBackupServer({ commit, state }, server: IBackupServer) {\n      let _options: Options = Object.assign({}, state.options);\n\n      if (!_options.backupServers) {\n        _options.backupServers = [];\n      }\n\n      server.id = md5(new Date().toString());\n      _options.backupServers.push(server);\n\n      commit(\"updateOptions\", _options);\n      extension.save(_options);\n    },\n\n    /**\n     * 更新备份服务器\n     * @param param0\n     * @param newData\n     */\n    updateBackupServer({ commit, state }, newData: IBackupServer) {\n      let _options: Options = Object.assign({}, state.options);\n\n      if (_options.backupServers) {\n        let index = _options.backupServers.findIndex((item: IBackupServer) => {\n          return item.id === newData.id;\n        });\n\n        if (index !== -1) {\n          _options.backupServers[index] = newData;\n\n          commit(\"updateOptions\", _options);\n          extension.save(_options);\n        }\n      }\n    },\n\n    /**\n     * 删除备份服务器\n     * @param param0\n     * @param newData\n     */\n    removeBackupServer({ commit, state }, newData: IBackupServer) {\n      let _options: Options = Object.assign({}, state.options);\n\n      if (_options.backupServers) {\n        let index = _options.backupServers.findIndex((item: IBackupServer) => {\n          return item.id === newData.id;\n        });\n\n        if (index !== -1) {\n          _options.backupServers.splice(index, 1);\n\n          commit(\"updateOptions\", _options);\n          extension.save(_options);\n        }\n      }\n    }\n  },\n  getters: {\n    sites: state => {\n      return (\n        state.options.system &&\n        state.options.system.sites &&\n        state.options.system.sites.filter((site: Site) => {\n          if (state.options.sites) {\n            return (\n              state.options.sites.findIndex(item => {\n                return item.host === site.host;\n              }) === -1\n            );\n          } else {\n            return true;\n          }\n        })\n      );\n    },\n    clients: state => {\n      return (\n        state.options.system &&\n        state.options.system.clients &&\n        state.options.system.clients.filter((systemItem: any) => {\n          if (state.options.clients) {\n            return (\n              state.options.clients.findIndex(item => {\n                return item.name === systemItem.name;\n              }) === -1\n            );\n          } else {\n            return true;\n          }\n        })\n      );\n    },\n    defaultClient: state => {\n      if (!state.options.defaultClientId) {\n        return null;\n      }\n      return state.options.clients.find(data => {\n        return state.options.defaultClientId === data.id;\n      });\n    },\n    /**\n     * 获取指定客户端配置\n     * @param clientId\n     */\n    clientOptions: state => (site: Site, clientId: string = \"\") => {\n      if (!clientId) {\n        clientId =\n          site.defaultClientId || <string>state.options.defaultClientId;\n      }\n\n      let client = state.options.clients.find((item: any) => {\n        return item.id === clientId;\n      });\n\n      return client;\n    },\n\n    /**\n     * 获取当前站点的默认下载目录\n     * @param string clientId 指定客户端ID，不指定表示使用默认下载客户端\n     * @return string 目录信息，如果没有定义，则返回空字符串\n     */\n    siteDefaultPath: state => (site: Site, clientId: string = \"\"): string => {\n      if (!clientId) {\n        clientId =\n          site.defaultClientId || <string>state.options.defaultClientId;\n      }\n\n      let client = state.options.clients.find((item: any) => {\n        return item.id === clientId;\n      });\n      let path = \"\";\n      if (client && client.paths) {\n        for (const host in client.paths) {\n          if (site.host === host) {\n            path = client.paths[host][0];\n            break;\n          }\n        }\n      }\n\n      return path;\n    },\n\n    pagination: state => (key: string, defalutValue: any) => {\n      if (state.uiOptions && state.uiOptions.paginations) {\n        return state.uiOptions.paginations[key] || defalutValue;\n      }\n      return defalutValue;\n    },\n\n    viewsOptions: state => (key: string, defalutValue: any) => {\n      if (state.uiOptions && state.uiOptions.views) {\n        return state.uiOptions.views[key] || defalutValue;\n      }\n      return defalutValue;\n    }\n  }\n});\n\n// 用于本地调试\nwindow.chrome = window.chrome || {};\n\n// 更新当前TabId\nif (chrome && chrome.tabs) {\n  chrome.tabs.getCurrent((tab: any) => {\n    extension.sendRequest(EAction.updateOptionsTabId, null, tab.id);\n  });\n}\n"
  },
  {
    "path": "src/options/typings.d.ts",
    "content": "declare module \"basiccontext\";\ndeclare module \"highcharts-vue\";\ndeclare module \"webdav\";\n"
  },
  {
    "path": "src/options/views/About.vue",
    "content": "<template>\n  <div class=\"about\">\n    <h1>This is an about page</h1>\n  </div>\n</template>\n"
  },
  {
    "path": "src/options/views/AutoSignWarning.vue",
    "content": "<template>\n  <v-dialog\n    v-model=\"dialog\"\n    persistent\n    scrollable\n    max-width=\"700\"\n    :fullscreen=\"$vuetify.breakpoint.smAndDown\"\n  >\n    <template v-slot:activator=\"{ on }\">\n      <v-btn\n        v-if=\"showButton\"\n        dark\n        v-on=\"on\"\n        title=\"一键签到？\"\n        color=\"warning\"\n      >\n        <v-icon>how_to_reg</v-icon>\n        <span class=\"ml-2\">一键签到？</span>\n      </v-btn>\n    </template>\n    <v-card>\n      <v-toolbar dark color=\"blue-grey darken-2\">\n        <v-toolbar-title>关于自动签到</v-toolbar-title>\n        <v-spacer></v-spacer>\n        <v-btn\n          icon\n          flat\n          color=\"success\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/frequently-asked-questions#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%8A%A9%E6%89%8B%E6%B2%A1%E6%9C%89%E8%87%AA%E5%8A%A8%E7%AD%BE%E5%88%B0%E5%8A%9F%E8%83%BD\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n          :title=\"$t('common.help')\"\n        >\n          <v-icon>help</v-icon>\n        </v-btn>\n      </v-toolbar>\n      <v-card-text style=\"max-height: 80vh\" class=\"pa-0\">\n        <v-alert :value=\"true\" color=\"warning\" class=\"ma-0\">\n          <div>\n            感谢您使用助手，抱歉这里没有自动签到功能，如果您愿意，请阅读以下内容：\n            <br />\n            <br />- 首先，自动签到对站点来说属于“作弊”行为； <br />-\n            其次，签到功能对站点来说目的是用来活跃人气，如果自动签到了，对站点来说没有任何作用；\n            <br />-\n            再次，签到功能一般具有奖励作用，如签到后给予一定的积分（魔力）；\n            <br />- 最后，本人痛恨任何“薅羊毛”行为； <br />-\n            综上所述，助手不会添加任何可以自动获取奖励的功能，现在不会有，将来也不会有；\n            <br />\n            <br />PS：如果您喜欢一个站点，请用行动表示支持，而不是把一切交给自动化脚本；\n            <br />\n            <br />那为什么有这个按钮？因为很多人问，不得已加上这个“功能”。\n          </div>\n        </v-alert>\n      </v-card-text>\n\n      <v-divider></v-divider>\n      <v-card-actions>\n        <v-spacer></v-spacer>\n        <v-btn color=\"success\" flat @click=\"hideButton\">不再显示该按钮</v-btn>\n        <v-btn color=\"error\" flat @click=\"dialog = false\">{{\n          $t(\"common.close\")\n        }}</v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      dialog: false,\n      showButton: true\n    };\n  },\n\n  methods: {\n    hideButton() {\n      this.showButton = false;\n      this.$store.dispatch(\"updateViewOptions\", {\n        key: \"AutoSignWarning\",\n        options: {\n          showButton: false\n        }\n      });\n      this.dialog = false;\n    }\n  },\n\n  created() {\n    let viewOptions = this.$store.getters.viewsOptions(\"AutoSignWarning\", {\n      showButton: true\n    });\n    Object.assign(this, viewOptions);\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/Donate.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{ words.title }}</v-alert>\n    <v-card>\n      <v-card-title primary-title>\n        <div>\n          <div>本项目由作者在业余时间完成，如果您喜欢本项目，可以通过捐助来支持作者继续开发。</div>\n          <div class=\"mt-3\">支付宝和微信</div>\n          <v-img src=\"./assets/donate.png\" max-width=\"300\"></v-img>\n          <div\n            class=\"mt-3\"\n          >This project is completed by the author in his spare time. You can support the author's continued development through donations.</div>\n        </div>\n      </v-card-title>\n      <v-card-actions>\n        <v-btn\n          flat\n          color=\"orange\"\n          href=\"https://www.paypal.me/ronggang\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon>attach_money</v-icon>Paypal\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      words: {\n        title: \"感谢您的关注和支持\"\n      }\n    };\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/History.vue",
    "content": "<template>\n  <div class=\"history\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t(\"history.title\") }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"history.remove\") }}\n        </v-btn>\n\n        <v-btn color=\"error\" @click=\"clear\" :disabled=\"items.length == 0\">\n          <v-icon class=\"mr-2\">clear</v-icon>\n          {{ $t(\"history.clear\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n          v-model=\"filterKey\"\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        :search=\"filterKey\"\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"data.url\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width:20px;\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n            ></v-checkbox>\n          </td>\n          <!-- 站点 -->\n          <td style=\"text-align: center;\">\n            <div v-if=\"!!props.item.site\">\n              <v-avatar size=\"18\">\n                <img :src=\"props.item.site.icon\" />\n              </v-avatar>\n              <br />\n              <span class=\"captionText\">{{ props.item.site.name }}</span>\n            </div>\n          </td>\n          <td>\n            <a\n              v-if=\"props.item.data.link\"\n              :href=\"props.item.data.link\"\n              target=\"_blank\"\n              :title=\"props.item.data.title\"\n              rel=\"noopener noreferrer nofollow\"\n              >{{ props.item.data.title || props.item.data.link }}</a\n            >\n            <span v-else :title=\"props.item.data.url\">{{\n              props.item.data.title || props.item.data.url\n            }}</span>\n            <br />\n            <span class=\"sub-title\"\n              >[\n              {{\n                getClientName(props.item.data.clientId || props.item.clientId)\n              }}\n              ] ->\n              {{ props.item.data.savePath || $t(\"history.defaultPath\") }}</span\n            >\n          </td>\n          <td>\n            <v-icon\n              v-if=\"props.item.success === false\"\n              color=\"error\"\n              :title=\"$t('history.fail')\"\n              >close</v-icon\n            >\n            <v-icon v-else color=\"success\" :title=\"$t('history.success')\"\n              >done</v-icon\n            >\n          </td>\n          <td>{{ props.item.time | formatDate }}</td>\n          <td>\n            <!-- 重新下载 -->\n            <v-btn\n              icon\n              flat\n              small\n              @click=\"download(props.item)\"\n              :title=\"$t('history.download')\"\n            >\n              <v-icon small>save_alt</v-icon>\n            </v-btn>\n\n            <!-- 下载到 -->\n            <DownloadTo\n              :downloadOptions=\"{\n                host: props.item.host,\n                url: props.item.data.url\n              }\"\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              @error=\"onError\"\n              @success=\"onSuccess\"\n            />\n\n            <v-btn\n              icon\n              flat\n              small\n              color=\"error\"\n              @click=\"removeConfirm(props.item)\"\n              :title=\"$t('common.remove')\"\n            >\n              <v-icon small>delete</v-icon>\n            </v-btn>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"history.removeConfirmTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{ $t(\"history.removeConfirm\") }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"history.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove(null)\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"history.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      errorMsg\n    }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{\n      successMsg\n    }}</v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { EAction, DownloadOptions, Site, Dictionary } from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\n\nconst extension = new Extension();\nexport default Vue.extend({\n  components: {\n    DownloadTo\n  },\n  data() {\n    return {\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      },\n      items: [] as any[],\n      dialogRemoveConfirm: false,\n      options: this.$store.state.options,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      siteCache: {} as Dictionary<any>,\n      filterKey: \"\"\n    };\n  },\n\n  methods: {\n    clear() {\n      if (confirm(this.$t(\"history.clearConfirm\").toString())) {\n        extension\n          .sendRequest(EAction.clearDownloadHistory)\n          .then((result: any) => {\n            this.getDownloadHistory();\n          });\n      }\n    },\n\n    removeSelected() {\n      if (this.selected && this.selected.length > 0) {\n        if (\n          confirm(\n            this.$t(\"common.removeSelectedConfirm\", {\n              count: this.selected.length\n            }).toString()\n          )\n        ) {\n          this.remove(this.selected);\n        }\n      }\n    },\n\n    remove(items?: any) {\n      if (!items) {\n        items = [this.selectedItem];\n      }\n\n      extension\n        .sendRequest(EAction.removeDownloadHistory, null, items)\n        .then((result: any) => {\n          this.getDownloadHistory();\n        });\n      this.dialogRemoveConfirm = false;\n    },\n\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    getDownloadHistory() {\n      extension.sendRequest(EAction.getDownloadHistory).then((result: any) => {\n        console.log(\"downloadHistory\", result);\n        this.items = [];\n        result.forEach((item: any) => {\n          let site = this.siteCache[item.host];\n          if (!site) {\n            site = this.options.sites.find((site: Site) => {\n              return site.host === item.host;\n            });\n            this.siteCache[item.host] = site;\n          }\n\n          item.site = site;\n\n          this.items.push(item);\n        });\n      });\n    },\n\n    getClientName(clientId: string): string {\n      let client = this.options.clients.find((item: any) => {\n        return item.id === clientId;\n      });\n      if (client) {\n        return client.name;\n      }\n      return \"\";\n    },\n    download(options: any) {\n      console.log(options);\n\n      this.haveSuccess = true;\n      this.successMsg = this.$t(\"history.seedingTorrent\").toString();\n\n      let data = Object.assign({}, options.data);\n      if (!data.clientId) {\n        data.clientId = options.clientId;\n      }\n\n      extension\n        .sendRequest(EAction.sendTorrentToClient, null, data)\n        .then((result: any) => {\n          console.log(\"命令执行完成\", result);\n\n          if (result.success) {\n            this.haveSuccess = true;\n            this.successMsg = result.msg;\n          } else {\n            this.haveError = true;\n            this.errorMsg = result.msg;\n          }\n        });\n    },\n\n    onError(msg: string) {\n      this.errorMsg = msg;\n    },\n\n    onSuccess(msg: string) {\n      this.successMsg = msg;\n    }\n  },\n\n  created() {\n    this.getDownloadHistory();\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"history.headers.site\"),\n          align: \"center\",\n          value: \"data.host\",\n          width: \"140px\"\n        },\n        {\n          text: this.$t(\"history.headers.title\"),\n          align: \"left\",\n          value: \"data.title\"\n        },\n        {\n          text: this.$t(\"history.headers.status\"),\n          align: \"left\",\n          value: \"data.success\"\n        },\n        { text: this.$t(\"history.headers.time\"), align: \"left\", value: \"time\" },\n        {\n          text: this.$t(\"history.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.history {\n  .sub-title {\n    color: #aaaaaa;\n    font-size: 12px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/Home.vue",
    "content": "<template>\n  <div class=\"home\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t(\"home.title\") }}</v-alert>\n    <v-card>\n      <v-card-title v-if=\"sites && sites.length > 0\">\n        <v-btn color=\"success\" @click=\"getInfos\" :loading=\"loading\" :title=\"$t('home.getInfos')\">\n          <v-icon class=\"mr-2\">cached</v-icon>\n          {{ $t(\"home.getInfos\") }}\n        </v-btn>\n        <v-btn to=\"/user-data-timeline\" color=\"success\" :title=\"$t('home.timeline')\">\n          <v-icon>timeline</v-icon>\n        </v-btn>\n\n        <v-btn to=\"/statistic\" color=\"success\" :title=\"$t('home.statistic')\">\n          <v-icon>equalizer</v-icon>\n        </v-btn>\n\n        <v-menu :close-on-content-click=\"false\" offset-y>\n          <template v-slot:activator=\"{ on }\">\n            <v-btn color=\"blue\" dark v-on=\"on\" :title=\"$t('home.settings')\">\n              <v-icon>settings</v-icon>\n            </v-btn>\n          </template>\n\n          <v-card>\n            <v-container grid-list-xs>\n              <v-switch color=\"success\" v-model=\"showSiteName\" :label=\"$t('home.siteName')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showUserName\" :label=\"$t('home.userName')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showUserLevel\" :label=\"$t('home.userLevel')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showLevelRequirements\" :label=\"$t('home.levelRequirements')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showWeek\" :label=\"$t('home.week')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showSeedingPoints\" :label=\"$t('home.seedingPoints')\"\n                @change=\"updateViewOptions\"></v-switch>\n              <v-switch color=\"success\" v-model=\"showHnR\" :label=\"$t('home.showHnR')\"\n                @change=\"updateViewOptions\"></v-switch>\n            </v-container>\n          </v-card>\n        </v-menu>\n        <v-select v-model=\"selectedHeaders\" class=\"select\" :items=\"headers\" :label=\"$t('home.selectColumns')\"\n          @change=\"updateViewOptions\" multiple outlined return-object>\n          <template v-slot:selection=\"{ item, index }\">\n            <v-chip v-if=\"index === 0\">\n              <span>{{ item.text }}</span>\n            </v-chip>\n            <span v-if=\"index === 1\" class=\"grey--text caption\">(+{{ selectedHeaders.length - 1 }} others)</span>\n          </template>\n        </v-select>\n\n        <!-- <AutoSignWarning /> -->\n        <v-spacer></v-spacer>\n\n        <v-text-field class=\"search\" v-model=\"filterKey\" append-icon=\"search\" label=\"Search\" single-line hide-details\n          enterkeyhint=\"search\"></v-text-field>\n      </v-card-title>\n\n      <v-data-table :search=\"filterKey\" :headers=\"showHeaders\" :items=\"sites\" :pagination.sync=\"pagination\"\n        item-key=\"host\" class=\"elevation-1\" ref=\"userDataTable\" :no-data-text=\"$t('home.nodata')\">\n        <template slot=\"items\" slot-scope=\"props\">\n          <!-- 站点 -->\n          <td v-if=\"showColumn('name')\" class=\"center\">\n            <v-badge color=\"red messageCount\" overlap>\n              <template v-slot:badge v-if=\"\n                !props.item.disableMessageCount &&\n                props.item.user.messageCount > 0\n              \" :title=\"$t('home.newMessage')\">\n                {{\n                    props.item.user.messageCount > 10\n                      ? \"\"\n                      : props.item.user.messageCount\n                }}\n              </template>\n              <v-btn flat icon class=\"siteIcon\" :title=\"$t('home.getInfos')\" :disabled=\"props.item.offline\"\n                @click.stop=\"getSiteUserInfo(props.item)\">\n                <v-avatar :size=\"showSiteName ? 18 : 24\">\n                  <img :src=\"props.item.icon\" />\n                </v-avatar>\n              </v-btn>\n            </v-badge>\n\n            <br />\n            <a :href=\"props.item.activeURL\" target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"nodecoration\"\n              v-if=\"showSiteName\">\n              <span class=\"caption\">{{ props.item.name }}</span>\n            </a>\n          </td>\n          <td v-if=\"showColumn('user.name')\" :title=\"props.item.user.id\">\n            <template v-if=\"showUserName\">\n              {{ props.item.user.name }}\n            </template>\n            <template v-else> **** </template>\n          </td>\n          <td v-if=\"showColumn('user.levelName')\">\n            <v-icon v-if=\"showLevelRequirements\" small>military_tech</v-icon>\n            {{ showUserLevel ? props.item.user.levelName : \"****\" }}\n            <template v-if=\"showLevelRequirements\">\n              <template v-if=\"props.item.levelRequirements\">\n                <template v-if=\"props.item.user.nextLevels && props.item.user.nextLevels.length > 0\">\n                  <template v-for=\"nextLevel in props.item.user.nextLevels\">\n                    <div>\n                      <v-icon small>keyboard_tab</v-icon>\n                      <template v-if=\"nextLevel.requiredDate\">\n                        {{ nextLevel.requiredDate }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.uploaded\">\n                        <v-icon small color=\"green darken-4\">expand_less</v-icon>{{\n                            nextLevel.uploaded | formatSize\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.downloaded\">\n                        <v-icon small color=\"red darken-4\">expand_more</v-icon>{{\n                            nextLevel.downloaded | formatSize\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.trueDownloaded\">\n                        {{ $t(\"home.levelRequirement.trueDownloaded\") }}\n                        {{\n                            nextLevel.trueDownloaded | formatSize\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.bonus\">\n                        <v-icon small color=\"green darken-4\">attach_money</v-icon>{{\n                            nextLevel.bonus | formatNumber\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.seedingPoints\">\n                        <v-icon small color=\"green darken-4\">energy_savings_leaf</v-icon>{{\n                            nextLevel.seedingPoints | formatNumber\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.seedingTime\">\n                        <v-icon small color=\"green darken-4\">timer</v-icon>{{\n                            nextLevel.seedingTime | formatNumber\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.uploads\">\n                        <v-icon small color=\"green darken-4\">file_upload</v-icon>{{ nextLevel.uploads\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.downloads\">\n                        <v-icon small color=\"red darken-4\">file_download</v-icon>{{ nextLevel.downloads\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.uniqueGroups\">\n                        <v-icon small color=\"green darken-4\">library_music</v-icon>{{ nextLevel.uniqueGroups\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.perfectFLAC\">\n                        <v-icon small color=\"green darken-4\">diamond</v-icon>{{ nextLevel.perfectFLAC\n                        }}&nbsp;\n                      </template>\n                      <template v-if=\"nextLevel.classPoints\">\n                        <v-icon small color=\"yellow darken-4\">energy_savings_leaf</v-icon>{{\n                            nextLevel.classPoints | formatNumber\n                        }}&nbsp;\n                      </template>\n                    </div>\n                  </template>\n                </template>\n                <template v-else-if=\"props.item.user.name\">\n                  <v-icon small color=\"green darken-4\">done</v-icon>\n                </template>\n                <v-card class=\"levelRequirement\">\n                  <template v-for=\"levelRequirement of props.item.levelRequirements\">\n                    <div>\n                      <v-icon v-if=\"!(props.item.user.nextLevels && props.item.user.nextLevels.length > 0) || Number(props.item.user.nextLevels[0].level) > Number(levelRequirement.level)\"\n                        small color=\"green darken-4\">done</v-icon>\n                      <v-icon v-else small color=\"red darken-4\">block</v-icon>\n                      <template v-if=\"levelRequirement.requiredDate\">\n                        {{ levelRequirement.requiredDate }} </template>({{ levelRequirement.name }}):\n                      <template v-if=\"levelRequirement.uploaded\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uploaded')\">expand_less</v-icon>{{ levelRequirement.uploaded }};\n                      </template>\n                      <template v-if=\"levelRequirement.uploads\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uploads')\">file_upload</v-icon>{{ levelRequirement.uploads }};\n                      </template>\n                      <template v-if=\"levelRequirement.downloaded\">\n                        <v-icon small color=\"red darken-4\" :title=\"$t('home.levelRequirement.downloaded')\">expand_more</v-icon>{{ levelRequirement.downloaded }};\n                      </template>\n                      <template v-if=\"levelRequirement.trueDownloaded\">\n                        {{ $t(\"home.levelRequirement.trueDownloaded\") }}\n                        {{ levelRequirement.trueDownloaded }};\n                      </template>\n                      <template v-if=\"levelRequirement.downloads\">\n                        <v-icon small color=\"red darken-4\" :title=\"$t('home.levelRequirement.downloads')\">file_download</v-icon>{{ levelRequirement.downloads }};\n                      </template>\n                      <template v-if=\"levelRequirement.ratio\">\n                        <v-icon small color=\"orange darken-4\" :title=\"$t('home.levelRequirement.ratio')\">balance</v-icon>{{ levelRequirement.ratio }};\n                      </template>\n                      <template v-if=\"levelRequirement.bonus\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.bonus')\">attach_money</v-icon>{{ levelRequirement.bonus |\n                            formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.seedingPoints\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.seedingPoints')\">energy_savings_leaf</v-icon>{{ levelRequirement.seedingPoints\n                            | formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.seedingTime\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.seedingTime')\">timer</v-icon>{{ levelRequirement.seedingTime\n                            | formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.classPoints\">\n                        <v-icon small color=\"yellow darken-4\" :title=\"$t('home.levelRequirement.classPoints')\">energy_savings_leaf</v-icon>{{ levelRequirement.classPoints\n                            | formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.uniqueGroups\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uniqueGroups')\">library_music</v-icon>{{ levelRequirement.uniqueGroups\n                            | formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.perfectFLAC\">\n                        <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.perfectFLAC')\">diamond</v-icon>{{ levelRequirement.perfectFLAC\n                            | formatInteger\n                        }};\n                      </template>\n                      <template v-if=\"levelRequirement.alternative\">\n                        <v-icon small :title=\"$t('home.levelRequirement.alternative')\">filter_1</v-icon>(\n                        <template v-if=\"levelRequirement.alternative.requiredDate\">\n                          {{ levelRequirement.alternative.requiredDate }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.uploaded\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uploaded')\">expand_less</v-icon>{{ levelRequirement.alternative.uploaded }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.uploads\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uploads')\">file_upload</v-icon>{{ levelRequirement.alternative.uploads }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.downloaded\">\n                          <v-icon small color=\"red darken-4\" :title=\"$t('home.levelRequirement.downloaded')\">expand_more</v-icon>{{ levelRequirement.alternative.downloaded }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.trueDownloaded\">\n                          {{ $t(\"home.levelRequirement.trueDownloaded\") }}\n                          {{ levelRequirement.alternative.trueDownloaded }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.downloads\">\n                          <v-icon small color=\"red darken-4\" :title=\"$t('home.levelRequirement.downloads')\">file_download</v-icon>{{ levelRequirement.alternative.downloads }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.ratio\">\n                          <v-icon small color=\"orange darken-4\" :title=\"$t('home.levelRequirement.ratio')\">balance</v-icon>{{ levelRequirement.alternative.ratio }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.bonus\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.bonus')\">attach_money</v-icon>{{ levelRequirement.alternative.bonus |\n                              formatInteger\n                          }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.seedingPoints\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.seedingPoints')\">energy_savings_leaf</v-icon>{{ levelRequirement.alternative.seedingPoints\n                              | formatInteger\n                          }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.seedingTime\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.seedingTime')\">timer</v-icon>{{ levelRequirement.alternative.seedingTime\n                              | formatInteger\n                          }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.classPoints\">\n                          <v-icon small color=\"yellow darken-4\" :title=\"$t('home.levelRequirement.classPoints')\">energy_savings_leaf</v-icon>{{ levelRequirement.alternative.classPoints\n                              | formatInteger\n                          }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.uniqueGroups\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.uniqueGroups')\">library_music</v-icon>{{ levelRequirement.alternative.uniqueGroups\n                              | formatInteger\n                          }};\n                        </template>\n                        <template v-if=\"levelRequirement.alternative.perfectFLAC\">\n                          <v-icon small color=\"green darken-4\" :title=\"$t('home.levelRequirement.perfectFLAC')\">diamond</v-icon>{{ levelRequirement.alternative.perfectFLAC\n                              | formatInteger\n                          }};\n                        </template>);\n                      </template>\n                      {{ levelRequirement.privilege }}\n                    </div>\n                  </template>\n                </v-card>\n              </template>\n            </template>\n          </td>\n          <td v-if=\"showColumn('user.uploaded')\" class=\"number\">\n            <div>\n              {{ props.item.user.uploaded | formatSize }}\n              <v-icon small color=\"green darken-4\">expand_less</v-icon>\n            </div>\n            <div>\n              {{ props.item.user.downloaded | formatSize }}\n              <v-icon small color=\"red darken-4\">expand_more</v-icon>\n            </div>\n          </td>\n          <td v-if=\"showColumn('user.ratio')\" class=\"number\">\n            {{ props.item.user.ratio | formatRatio }}\n          </td>\n          <td v-if=\"showColumn('user.seeding')\" class=\"number\">\n            <div>{{ props.item.user.seeding }}</div>\n            <div v-if=\"showHnR && props.item.user.unsatisfieds && props.item.user.unsatisfieds != 0\" :title=\"$t('home.headers.unsatisfieds')\" ><v-icon small color=\"yellow darken-4\">warning</v-icon>{{props.item.user.unsatisfieds}}</div>\n          </td>\n          <td v-if=\"showColumn('user.seedingSize')\" class=\"number\">\n            {{ props.item.user.seedingSize | formatSize }}\n          </td>\n          <td v-if=\"showColumn('user.bonus')\" class=\"number\">\n            <template v-if=\"showSeedingPoints && props.item.user.seedingPoints\">\n              <div>\n                <v-icon small color=\"green darken-4\">attach_money</v-icon>{{ props.item.user.bonus | formatNumber }}\n              </div>\n              <div>\n                <v-icon small color=\"green darken-4\">energy_savings_leaf</v-icon>{{ props.item.user.seedingPoints |\n                    formatNumber\n                }}\n              </div>\n            </template>\n            <template v-else-if=\"showSeedingPoints && props.item.user.classPoints\">\n              <div>\n                <v-icon small color=\"green darken-4\">attach_money</v-icon>{{ props.item.user.bonus | formatNumber }}\n              </div>\n              <div>\n                <v-icon small color=\"yellow darken-4\">energy_savings_leaf</v-icon>{{ props.item.user.classPoints |\n                    formatNumber\n                }}\n              </div>\n            </template>\n            <template v-else>\n              {{ props.item.user.bonus | formatNumber }}\n            </template>\n          </td>\n          <td v-if=\"showColumn('user.bonusPerHour')\" class=\"number\">\n            <template v-if=\"props.item.user.bonusPerHour\">\n              {{ props.item.user.bonusPerHour | formatNumber }}\n            </template>\n          </td>\n          <td v-if=\"showColumn('user.joinTime')\" class=\"number\" :title=\"props.item.user.joinDateTime\">\n            {{ props.item.user.joinTime | timeAgo(showWeek) }}\n          </td>\n          <td v-if=\"showColumn('user.lastUpdateTime')\" class=\"number\">\n            <v-btn depressed small :to=\"`statistic/${props.item.host}`\" :title=\"$t('home.statistic')\">{{\n                props.item.user.lastUpdateTime |\n                formatDate(\"YYYY-MM-DD HH:mm:ss\")\n            }}</v-btn>\n          </td>\n          <td v-if=\"showColumn('user.lastUpdateStatus')\" class=\"center\">\n            <v-progress-circular indeterminate :width=\"3\" size=\"30\" color=\"green\" v-if=\"props.item.user.isLoading\">\n              <v-icon v-if=\"props.item.user.isLoading\" @click=\"abortRequest(props.item)\" color=\"red\" small\n                :title=\"$t('home.cancelRequest')\">cancel</v-icon>\n            </v-progress-circular>\n            <span v-else>\n              <span v-if=\"props.item.offline\">{{ $t(\"home.offline\" )}}</span>\n              <a :href=\"props.item.activeURL\" v-else-if=\"!props.item.user.isLogged\" target=\"_blank\"\n                rel=\"noopener noreferrer nofollow\" class=\"nodecoration\">{{ formatError(props.item.user) }}</a>\n              <span v-else>{{ formatError(props.item.user) }}</span>\n            </span>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <v-alert :value=\"true\" color=\"grey\">\n      <div v-html=\"$t('home.tip')\"></div>\n    </v-alert>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport {\n  EAction,\n  Site,\n  LogItem,\n  EModule,\n  EUserDataRequestStatus,\n  Options,\n  UserInfo,\n  EViewKey,\n  LevelRequirement,\n} from \"@/interface/common\";\nimport dayjs from \"dayjs\";\n\nimport AutoSignWarning from \"./AutoSignWarning.vue\";\nimport { PPF } from \"@/service/public\";\n\ninterface UserInfoEx extends UserInfo {\n  joinDateTime?: string;\n}\n\nconst extension = new Extension();\nexport default Vue.extend({\n  components: {\n    AutoSignWarning,\n  },\n  data() {\n    return {\n      loading: false,\n      items: [] as any[],\n      selectedHeaders: [] as any[],\n      headers: [\n        {\n          text: this.$t(\"home.headers.site\"),\n          align: \"center\",\n          value: \"name\",\n          width: \"110px\",\n        },\n        {\n          text: this.$t(\"home.headers.userName\"),\n          align: \"left\",\n          value: \"user.name\",\n        },\n        {\n          text: this.$t(\"home.headers.levelName\"),\n          align: \"left\",\n          value: \"user.levelName\",\n        },\n        {\n          text: this.$t(\"home.headers.activitiyData\"),\n          align: \"right\",\n          value: \"user.uploaded\",\n          width: \"120px\",\n        },\n        {\n          text: this.$t(\"home.headers.ratio\"),\n          align: \"right\",\n          value: \"user.ratio\",\n        },\n        {\n          text: this.$t(\"home.headers.seeding\"),\n          align: \"right\",\n          value: \"user.seeding\",\n        },\n        {\n          text: this.$t(\"home.headers.seedingSize\"),\n          align: \"right\",\n          value: \"user.seedingSize\",\n        },\n        {\n          text: this.$t(\"home.headers.bonus\"),\n          align: \"right\",\n          value: \"user.bonus\",\n        },\n        {\n          text: this.$t(\"home.headers.bonusPerHour\"),\n          align: \"right\",\n          value: \"user.bonusPerHour\",\n        },\n        {\n          text: this.$t(\"home.headers.joinTime\"),\n          align: \"right\",\n          value: \"user.joinTime\",\n        },\n        {\n          text: this.$t(\"home.headers.lastUpdateTime\"),\n          align: \"right\",\n          value: \"user.lastUpdateTime\",\n        },\n        {\n          text: this.$t(\"home.headers.status\"),\n          align: \"center\",\n          value: \"user.lastUpdateStatus\",\n        },\n      ],\n      pagination: {\n        rowsPerPage: -1,\n      },\n      options: this.$store.state.options,\n      beginTime: null as any,\n      reloadCount: 0,\n      requestQueue: [] as any[],\n      requestTimer: 0,\n      requestMsg: \"\",\n      sites: [] as Site[],\n      filterKey: \"\",\n      isSecret: false,\n      showUserName: true,\n      showSiteName: true,\n      showUserLevel: true,\n      showLevelRequirements: true,\n      showSeedingPoints: true,\n      showHnR: true,\n      showWeek: false,\n    };\n  },\n  created() {\n    this.init();\n  },\n  computed: {\n    //Done to get the ordered headers\n    showHeaders(): any[] {\n      return this.headers.filter((s) =>\n        this.selectedHeaders.map((sh) => sh.value).includes(s.value)\n      );\n    },\n  },\n\n  /**\n   * 当前组件激活时触发\n   * 因为启用了缓存，所以需要重新加载数据\n   */\n  activated() {\n    if (!this.loading) {\n      this.init();\n    }\n  },\n\n  methods: {\n    showColumn(val: string) {\n      for (var header of this.headers.filter((s) =>\n        this.selectedHeaders.includes(s)\n      )) {\n        if (header.value === val) return true;\n      }\n      return false;\n    },\n    resetSites() {\n      this.sites = [];\n      this.options.sites.forEach((site: Site) => {\n        let _site: Site = this.clone(site);\n        if (_site.allowGetUserInfo) {\n          if (!_site.user) {\n            _site.user = {\n              id: \"\",\n              name: \"\",\n              isLogged: false,\n              isLoading: false,\n            };\n          } else {\n            if (_site.user.isLoading === undefined) {\n              _site.user.isLoading = false;\n            }\n\n            if (_site.user.isLogged === undefined) {\n              _site.user.isLogged = false;\n            }\n            this.formatUserInfo(_site.user, _site);\n          }\n          this.sites.push(_site);\n        }\n      });\n    },\n\n    init() {\n      extension\n        .sendRequest(EAction.readConfig)\n        .then((options: Options) => {\n          this.options = this.clone(options);\n          this.resetSites();\n        })\n        .catch();\n\n      let viewOptions = this.$store.getters.viewsOptions(EViewKey.home, {\n        showUserName: true,\n        showSiteName: true,\n        showUserLevel: true,\n        showLevelRequirements: true,\n        showSeedingPoints: true,\n        showHnR: true,\n        showWeek: false,\n        selectedHeaders: this.selectedHeaders,\n      });\n      Object.assign(this, viewOptions);\n      this.selectedHeaders = this.headers.filter((s) =>\n        this.selectedHeaders.map((sh) => sh.value).includes(s.value)\n      );\n      if (this.selectedHeaders.length == 0) {\n        this.selectedHeaders = Object.assign([], this.headers);\n      }\n    },\n    getInfos() {\n      this.loading = true;\n      this.beginTime = dayjs();\n      this.writeLog({\n        event: `Home.getUserInfo.Start`,\n        msg: this.$t(\"home.startGetting\").toString(),\n      });\n\n      this.sites.forEach((site: Site, index: number) => {\n        this.writeLog({\n          event: `Home.getUserInfo.Processing`,\n          msg: this.$t(\"home.gettingForSite\", {\n            siteName: site.name,\n          }).toString(),\n          data: {\n            host: site.host,\n            name: site.name,\n          },\n        });\n\n        this.getSiteUserInfo(site);\n      });\n    },\n    /**\n     * 记录日志\n     * @param options\n     */\n    writeLog(options: LogItem) {\n      extension.sendRequest(EAction.writeLog, null, {\n        module: EModule.options,\n        event: options.event,\n        msg: options.msg,\n        data: options.data,\n      });\n    },\n\n    /**\n     * 移除搜索队列\n     */\n    removeQueue(site: Site) {\n      let index = this.requestQueue.findIndex((item: any) => {\n        return item.host === site.host;\n      });\n\n      (site.user as any).isLoading = false;\n      if (index !== -1) {\n        this.requestQueue.splice(index, 1);\n        if (this.requestQueue.length == 0) {\n          this.requestMsg = this.$t(\"home.requestCompleted\", {\n            second: dayjs().diff(this.beginTime, \"second\", true),\n          }).toString();\n          this.loading = false;\n          this.writeLog({\n            event: `Home.getUserInfo.Finished`,\n            msg: this.requestMsg,\n          });\n          // 重置站点信息，因为有时候加载完成后，某些行还显示正在加载，暂时未明是哪里问题\n          this.sites = this.clone(this.sites);\n        }\n      }\n    },\n    /**\n     * 格式化一些用户信息\n     */\n    formatUserInfo(user: UserInfoEx, site: Site) {\n      let downloaded = user.downloaded as number;\n      let uploaded = user.uploaded as number;\n      // 没有下载量时设置分享率为无限\n      if (downloaded == 0 && uploaded > 0) {\n        user.ratio = -1;\n      }\n      // 重新以 上传量 / 下载量计算分享率\n      else if (downloaded > 0) {\n        user.ratio = uploaded / downloaded;\n      }\n\n      // 如果设置了时区，则进行转换\n      user.joinTime = PPF.transformTime(user.joinTime, site.timezoneOffset);\n\n      user.joinDateTime = dayjs(user.joinTime).format(\"YYYY-MM-DD HH:mm:ss\");\n\n      // 设置升级条件\n      try {\n        if (site.levelRequirements) {\n          for (var levelRequirement of site.levelRequirements) {\n            if (levelRequirement.requiredDate) break;\n\n            if (levelRequirement.interval && user.joinDateTime) {\n              levelRequirement.requiredDate = dayjs(user.joinDateTime).add(levelRequirement.interval as number, \"week\").format(\"YYYY-MM-DD\");\n            } else break;\n          }\n\n          user.nextLevels = [] as LevelRequirement[];\n          if (site.levelRequirements[0].alternative) {\n            for(var key of Object.keys(site.levelRequirements[0].alternative) as Array<keyof LevelRequirement>) {\n              for (var levelRequirement of site.levelRequirements) {\n                var newLevelRequirement = Object.assign({}, levelRequirement)\n                newLevelRequirement[key] = levelRequirement.alternative ? levelRequirement.alternative[key] as any : undefined;\n                var nextLevel = this.calculateNextLeve(user, newLevelRequirement);\n                if (nextLevel) {\n                  console.log(newLevelRequirement)\n                  console.log(nextLevel)\n                  if (user.nextLevels.length == 0 || Number(nextLevel.level) == Number(user.nextLevels[0].level))              \n                    user.nextLevels.push(nextLevel);\n                  else if (Number(nextLevel.level) > Number(user.nextLevels[0].level))\n                  {\n                    user.nextLevels = [] as LevelRequirement[];\n                    user.nextLevels.push(nextLevel);\n                  }\n                    \n                  break;\n                }\n              }\n            };\n          }\n          else {\n            for (var levelRequirement of site.levelRequirements) {\n              let nextLevel = this.calculateNextLeve(user, levelRequirement);\n              if (nextLevel) {\n                if (user.nextLevels.length) {\n                  continue\n                }\n                user.nextLevels.push(nextLevel);\n              } else {\n                user.nextLevels = []\n              }\n            }\n          }\n        }\n      } catch(error) {\n        console.log(error);\n      }\n    },\n    /**\n     * @return {LevelRequirement}\n     */\n    calculateNextLeve(user: UserInfoEx, levelRequirement: LevelRequirement): LevelRequirement | undefined {\n      let nextLevel = {} as LevelRequirement;\n      nextLevel.level = -1;\n\n      let downloaded = user.downloaded ?? 0;\n      let uploaded = user.uploaded ?? 0;\n      \n      if (user.levelName == levelRequirement.name) {\n        return undefined;\n      }\n\n      if (levelRequirement.interval && user.joinDateTime) {\n        let weeks = levelRequirement.interval as number;\n        let requiredDate = dayjs(user.joinDateTime).add(weeks, \"week\");\n        if (dayjs(new Date()).isBefore(requiredDate)) {\n          nextLevel.requiredDate = requiredDate.format(\"YYYY-MM-DD\");\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.uploaded || levelRequirement.ratio) {\n        let levelRequirementUploaded = levelRequirement.uploaded ? this.fileSizetoLength(levelRequirement.uploaded as string) : 0;\n        let requiredDownloaded = levelRequirement.downloaded ? this.fileSizetoLength(levelRequirement.downloaded as string) : 0;\n        let requiredUploadedbyRatio = Math.max(downloaded, requiredDownloaded) * (levelRequirement.ratio ?? 0);\n        let requiredUploaded = Math.max(levelRequirementUploaded, requiredUploadedbyRatio);\n        if (uploaded < requiredUploaded) {\n          nextLevel.uploaded = requiredUploaded - uploaded;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.downloaded) {\n        let requiredDownloaded = this.fileSizetoLength(levelRequirement.downloaded as string);\n        if (downloaded < requiredDownloaded) {\n          nextLevel.downloaded = requiredDownloaded - downloaded;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.ratio) {\n        let userRatio = user.ratio as number;\n        let requiredRatio = levelRequirement.ratio as number;\n        if (userRatio != -1 && userRatio < requiredRatio) {\n          nextLevel.ratio = levelRequirement.ratio;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.bonus) {\n        let userBonus = user.bonus as number;\n        let requiredBonus = levelRequirement.bonus as number;\n\n        if (userBonus < requiredBonus) {\n          nextLevel.bonus = requiredBonus - userBonus;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.seedingPoints) {\n        let userSeedingPoints = user.seedingPoints as number;\n        let requiredSeedingPoints = levelRequirement.seedingPoints as number;\n        if (userSeedingPoints < requiredSeedingPoints) {\n          nextLevel.seedingPoints = requiredSeedingPoints - userSeedingPoints;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.seedingTime) {\n        let userSeedingTime = user.seedingTime as number;\n        let requiredSeedingTime = levelRequirement.seedingTime as number;\n        if (userSeedingTime < requiredSeedingTime) {\n          nextLevel.seedingTime = requiredSeedingTime - userSeedingTime;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.uploads) {\n        let userUploads = user.uploads ? user.uploads as number : 0;\n        let requiredUploads = levelRequirement.uploads as number;\n        if (userUploads < requiredUploads) {\n          nextLevel.uploads = requiredUploads - userUploads;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.downloads) {\n        let userDownloads = user.downloads ? user.downloads as number : 0;\n        let requiredDownloads = levelRequirement.downloads as number;\n        if (userDownloads < requiredDownloads) {\n          nextLevel.downloads = requiredDownloads - userDownloads;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.trueDownloaded) {\n        let userTrueDownloaded = user.trueDownloaded ? (user.trueDownloaded as number) : 0;\n        let requiredTrueDownloaded = this.fileSizetoLength(\n          levelRequirement.trueDownloaded as string\n        );\n        if (userTrueDownloaded < requiredTrueDownloaded) {\n          nextLevel.trueDownloaded = requiredTrueDownloaded - userTrueDownloaded;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.classPoints) {\n        let userClassPoints = user.classPoints as number;\n        let requiredClassPoints = levelRequirement.classPoints as number;\n        if (userClassPoints < requiredClassPoints) {\n          nextLevel.classPoints = requiredClassPoints - userClassPoints;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.uniqueGroups) {\n        let userUniqueGroups = user.uniqueGroups as number;\n        let requiredUniqueGroups = levelRequirement.uniqueGroups as number;\n        if (userUniqueGroups < requiredUniqueGroups) {\n          nextLevel.uniqueGroups = requiredUniqueGroups - userUniqueGroups;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if (levelRequirement.perfectFLAC) {\n        let userPerfectFLAC = user.perfectFLAC as number;\n        let requiredPerfectFLAC = levelRequirement.perfectFLAC as number;\n        if (userPerfectFLAC < requiredPerfectFLAC) {\n          nextLevel.perfectFLAC = requiredPerfectFLAC - userPerfectFLAC;\n          nextLevel.level = levelRequirement.level;\n        }\n      }\n\n      if ((nextLevel.level as number) > 0)\n      {\n        nextLevel.name = levelRequirement.name;\n        return nextLevel;\n      } else\n        return undefined;\n    },\n    /**\n     * @return {number}\n     */\n    fileSizetoLength(size: string | number): number {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size\n        .replace(/,/g, \"\")\n        .trim()\n        .match(/^(\\d*\\.?\\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB))$/i);\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Zi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 70);\n          case /Ei?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 60);\n          case /Pi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 50);\n          case /Ti?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    },\n    /**\n     * 获取站点用户信息\n     */\n    getSiteUserInfo(site: Site) {\n      // fix issue #1065, 注意此处直接return在无站点更新时会导致刷新按钮一直处于loading状态，暂不修\n      if (!site.user || site.offline) {\n        return;\n      }\n\n      let user = site.user;\n      user.isLoading = true;\n      user.isLogged = false;\n      user.lastErrorMsg = \"\";\n\n      this.requestQueue.push(Object.assign({}, site));\n      extension\n        .sendRequest(EAction.getUserInfo, null, site)\n        .then((result: any) => {\n          console.log(result);\n          if (result && result.name) {\n            user = Object.assign(user, result);\n            this.formatUserInfo(user, site);\n          }\n        })\n        .catch((result) => {\n          console.log(\"error\", result);\n          if (result.msg && result.msg.status) {\n            user.lastErrorMsg = result.msg.msg;\n          } else {\n            user.lastErrorMsg = this.$t(\"home.getUserInfoError\").toString();\n          }\n        })\n        .finally(() => {\n          this.removeQueue(site);\n          // 重新加载配置信息\n          this.$store.commit(\"readConfig\");\n        });\n    },\n\n    abortRequest(site: Site) {\n      extension\n        .sendRequest(EAction.abortGetUserInfo, null, site)\n        .then(() => {\n          this.writeLog({\n            event: `Home.getUserInfo.Abort`,\n            msg: this.$t(\"home.getUserInfoAbort\", {\n              siteName: site.name,\n            }).toString(),\n          });\n        })\n        .catch(() => {\n          this.writeLog({\n            event: `Home.getUserInfo.Abort.Error`,\n            msg: this.$t(\"home.getUserInfoAbortError\", {\n              siteName: site.name,\n            }).toString(),\n          });\n          this.removeQueue(site);\n        });\n    },\n\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */\n    clone(source: any) {\n      return JSON.parse(JSON.stringify(source));\n    },\n\n    updateViewOptions() {\n      this.$store.dispatch(\"updateViewOptions\", {\n        key: EViewKey.home,\n        options: {\n          showUserName: this.showUserName,\n          showSiteName: this.showSiteName,\n          showUserLevel: this.showUserLevel,\n          showLevelRequirements: this.showLevelRequirements,\n          showSeedingPoints: this.showSeedingPoints,\n          showHnR: this.showHnR,\n          showWeek: this.showWeek,\n          selectedHeaders: this.selectedHeaders,\n        },\n      });\n    },\n\n    formatError(user: any) {\n      if (user.lastErrorMsg) {\n        return user.lastErrorMsg;\n      }\n      if (\n        user.lastUpdateStatus &&\n        user.lastUpdateStatus !== EUserDataRequestStatus.success\n      ) {\n        return this.$t(`service.user.${user.lastUpdateStatus}`);\n      }\n      return \"\";\n    },\n  },\n\n  filters: {\n    formatRatio(v: any) {\n      if (v > 10000 || v == -1) {\n        return \"∞\";\n      }\n      let number = parseFloat(v);\n      if (isNaN(number)) {\n        return \"\";\n      }\n      return number.toFixed(2);\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\">\n.home {\n\n  table.v-table thead tr:not(.v-datatable__progress) th,\n  table.v-table tbody tr td {\n    padding: 5px !important;\n    font-size: 12px;\n  }\n\n  .center {\n    text-align: center;\n  }\n\n  .number {\n    text-align: right;\n  }\n\n  .nodecoration {\n    text-decoration: none;\n  }\n\n  .messageCount {\n    font-size: 9px;\n    height: 16px;\n    width: 16px;\n    top: -2px;\n    right: -8px;\n  }\n\n  .siteIcon {\n    margin: 0;\n    height: 30px;\n    width: 30px;\n  }\n\n  td:hover div.levelRequirement {\n    display: block;\n  }\n\n  .levelRequirement {\n    position: absolute;\n    display: none;\n    z-index: 999;\n    border: 1px solid;\n  }\n\n  .select {\n    max-width: 180px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/SystemLogs.vue",
    "content": "<template>\n  <div class=\"system-logs\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t('systemLog.title') }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"error\" :disabled=\"selected.length==0\">\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t('common.remove') }}\n        </v-btn>\n\n        <v-btn color=\"error\" @click=\"clear\" :disabled=\"items.length==0\">\n          <v-icon class=\"mr-2\">clear</v-icon>\n          {{ $t('common.clear') }}\n        </v-btn>\n\n        <v-btn color=\"success\" @click=\"save\">\n          <v-icon>save</v-icon>\n          <span class=\"ml-1\">{{ $t('systemLog.save') }}</span>\n        </v-btn>\n        <v-spacer></v-spacer>\n\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n          v-model=\"filterKey\"\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        :search=\"filterKey\"\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"time\"\n        select-all\n        class=\"elevation-1\"\n        :rows-per-page-items=\"options.rowsPerPageItems\"\n        @update:pagination=\"updatePagination\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <tr @click=\"props.expanded = !props.expanded\">\n            <td style=\"width:20px;\">\n              <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n            </td>\n            <td>{{ props.item.module }}</td>\n            <td>{{ props.item.event }}</td>\n            <td>{{ props.item.time | formatDate(\"YYYY-MM-DD HH:mm:ss\") }}</td>\n            <td>{{ props.item.msg }}</td>\n            <td>\n              <v-icon small color=\"error\" @click=\"removeConfirm(props.item)\">delete</v-icon>\n            </td>\n          </tr>\n        </template>\n        <template slot=\"expand\" slot-scope=\"props\">\n          <v-card flat>\n            <v-card-text>{{getErrorDetail(props.item.data)}}</v-card-text>\n          </v-card>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{ $t('common.removeConfirmTitle') }}</v-card-title>\n\n        <v-card-text>{{ $t('common.removeConfirm') }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm=false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('common.cancel') }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { EAction, DownloadOptions, EPaginationKey } from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nimport FileSaver from \"file-saver\";\n\nconst extension = new Extension();\nexport default Vue.extend({\n  data() {\n    return {\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      },\n      items: [],\n      dialogRemoveConfirm: false,\n      options: this.$store.state.options,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      filterKey: \"\"\n    };\n  },\n\n  methods: {\n    clear() {\n      if (confirm(this.$t(\"common.clearConfirm\").toString())) {\n        extension.sendRequest(EAction.clearSystemLogs).then((result: any) => {\n          this.items = result;\n        });\n      }\n    },\n    remove() {\n      extension\n        .sendRequest(EAction.removeSystemLogs, null, [this.selectedItem])\n        .then((result: any) => {\n          console.log(\"removeSystemLogs\", result);\n          this.items = result;\n        });\n      this.dialogRemoveConfirm = false;\n    },\n\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    getSystemLogs() {\n      extension.sendRequest(EAction.getSystemLogs).then((result: any) => {\n        this.items = result;\n      });\n    },\n    save() {\n      const Blob = window.Blob;\n      const data = new Blob([JSON.stringify(this.items)], {\n        type: \"text/plain\"\n      });\n      FileSaver.saveAs(data, \"PT-Plugin-Plus-System-Logs.json\");\n    },\n    updatePagination(value: any) {\n      console.log(value);\n      this.$store.dispatch(\"updatePagination\", {\n        key: EPaginationKey.systemLogs,\n        options: value\n      });\n    },\n    getErrorDetail(data: any): string {\n      let result = \"\";\n      if (data) {\n        try {\n          result = JSON.stringify(data);\n        } catch (error) {\n          result = \"\";\n        }\n      }\n      return result;\n    }\n  },\n\n  created() {\n    this.getSystemLogs();\n    this.pagination = this.$store.getters.pagination(\n      EPaginationKey.systemLogs,\n      {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      }\n    );\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"systemLog.headers.module\"),\n          align: \"left\",\n          value: \"module\"\n        },\n        {\n          text: this.$t(\"systemLog.headers.event\"),\n          align: \"left\",\n          value: \"event\"\n        },\n        {\n          text: this.$t(\"systemLog.headers.time\"),\n          align: \"left\",\n          value: \"time\"\n        },\n        { text: this.$t(\"systemLog.headers.msg\"), align: \"left\", value: \"msg\" },\n        {\n          text: this.$t(\"systemLog.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.system-logs {\n  .sub-title {\n    color: #aaaaaa;\n    font-size: 12px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/Teams.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{ $t(\"team.title\") }}</v-alert>\n    <v-card>\n      <div v-for=\"(item, index) in peoples\" :key=\"index\">\n        <v-avatar>\n          <v-icon>account_circle</v-icon>\n        </v-avatar>\n        {{ item }}\n        <v-divider :key=\"index\"></v-divider>\n      </div>\n    </v-card>\n    <v-alert :value=\"true\" color=\"grey\">\n      在项目的开发和测试中，他们给予了很多帮助和支持（或开发，或测试，或发💊，或建议，或鼓励，或鞭策），在此表示感谢。\n      <br />列表中未能一一列出所有给予帮助的同学，也对他们表示感谢，如有遗漏敬请谅解。\n      <br />同时一并感谢这些\n      <br />\n      {{ $t(\"team.contributors\") }}\n      <a\n        target=\"_blank\"\n        rel=\"noopener noreferrer nofollow\"\n        href=\"https://github.com/pt-plugins/PT-Plugin-Plus/graphs/contributors\"\n        >https://github.com/pt-plugins/PT-Plugin-Plus/graphs/contributors</a\n      >\n      <br />\n      {{ $t(\"team.issues\") }}\n      <a\n        target=\"_blank\"\n        rel=\"noopener noreferrer nofollow\"\n        href=\"https://github.com/pt-plugins/PT-Plugin-Plus/issues\"\n        >https://github.com/pt-plugins/PT-Plugin-Plus/issues</a\n      >\n    </v-alert>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      items: [\n        \"Rhilip (R酱)\",\n        \"bimzcy (白鸽男孩)\",\n        \"DXV5 (贝壳)\",\n        \"An\",\n        \"Abel袁\",\n        \"Мало\",\n        \"tongyifan (杯杯杯杯具)\",\n        \"the chosen one (三哥)\",\n        \"橙子\",\n        \"frank777777777 (杀死那个异教徒)\"\n      ]\n    };\n  },\n  computed: {\n    peoples(): string[] {\n      return this.items.sort();\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/TechnologyStack.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{ $t('reference.title') }}</v-alert>\n    <v-card>\n      <v-data-table\n          :headers=\"headers\"\n          :items=\"items\"\n          :pagination.sync=\"pagination\"\n          class=\"elevation-1\"\n          hide-actions\n          item-key=\"name\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td>{{ props.item.name }}</td>\n          <td>{{ props.item.ver }}</td>\n          <td>\n            <a\n                :href=\"props.item.url\"\n                rel=\"noopener noreferrer nofollow\"\n                target=\"_blank\"\n            >{{ props.item.url }}</a>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n    <v-alert :value=\"true\" color=\"grey\">{{ $t(\"reference.thanks\") }}</v-alert>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport axios from 'axios';\n\nconst rawDependencies = require('@/../package.json').dependencies;\nconst dependencies = Object.entries(rawDependencies).map(value => {\n  const [name, version] = value\n  return {\n    name: name,\n    ver: (version as string),\n    url: `https://www.npmjs.com/package/${name}`\n  }\n})\n\nexport default Vue.extend({\n  data() {\n    return {\n      pagination: {\n        rowsPerPage: -1\n      },\n      items: [\n        ...dependencies,\n        // 其他一些不属于NPM依赖的参考项目\n        {\n          name: \"PT-Plugin Rhilip修改版\",\n          ver: \"0.0.9\",\n          url: \"https://github.com/Rhilip/PT-Plugin\"\n        },\n        {\n          name: \"Jackett\",\n          ver: \"latest\",\n          url: \"https://github.com/Jackett/Jackett\"\n        }\n      ]\n    };\n  },\n\n  created() {\n    const cacheDependMetaData = JSON.parse(localStorage.getItem('depend-metadata') || '{}')\n\n    // 延迟替换依赖的url项\n    setTimeout(async () => {\n      for (let i = 0; i < this.items.length; i++) {\n        let {name, url} = this.items[i]\n        if (url.match(/npmjs/)) {\n          if (cacheDependMetaData[name]) {\n            url = cacheDependMetaData[name]\n          } else {\n            try {\n              const req = await axios.get(`https://registry.npm.taobao.org/${name}`)\n              if (req.data?.homepage) {\n                url = cacheDependMetaData[name] = req.data?.homepage\n              }\n            } catch (e) {\n              //\n            }\n          }\n          this.items[i].url = url\n        }\n      }\n      localStorage.setItem('depend-metadata', JSON.stringify(cacheDependMetaData))\n    }, 1e3);\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {text: this.$t(\"reference.headers.name\"), align: \"left\", value: \"name\"},\n        {text: this.$t(\"reference.headers.ver\"), align: \"left\", value: \"ver\"},\n        {text: this.$t(\"reference.headers.url\"), align: \"left\", value: \"url\"}\n      ];\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/UserDataTimeline.vue",
    "content": "<template>\n  <div class=\"userDataTimeline\">\n    <div class=\"card\" ref=\"userDataCard\">\n      <v-card color=\"blue-grey darken-2\" class=\"white--text\">\n        <v-card-actions>\n          <v-chip\n            color=\"blue-grey darken-2\"\n            text-color=\"white\"\n            label\n            outline\n            @click.stop=\"changeDisplayUserName\"\n          >\n            <v-avatar>\n              <v-icon>account_circle</v-icon>\n            </v-avatar>\n            <div class=\"title\">{{ displayUserName || infos.nameInfo.name }}</div>\n          </v-chip>\n          <v-spacer></v-spacer>\n          <v-btn\n            flat\n            icon\n            class=\"white--text\"\n            @click=\"share\"\n            v-if=\"!shareing\"\n            :title=\"$t('timeline.share')\"\n          >\n            <v-icon>share</v-icon>\n          </v-btn>\n          <v-btn\n            flat\n            icon\n            class=\"white--text\"\n            to=\"/home\"\n            v-if=\"!shareing\"\n            :title=\"$t('timeline.close')\"\n          >\n            <v-icon>close</v-icon>\n          </v-btn>\n          <v-progress-circular indeterminate :width=\"3\" size=\"30\" color=\"green\" v-if=\"shareing\" class=\"by_pass_canvas\"></v-progress-circular>\n        </v-card-actions>\n\n        <v-card-title primary-title>\n          <div class=\"headline font-weight-bold\">\n            <div>{{ $t('timeline.total.uploaded') }}{{ infos.total.uploaded | formatSize }}</div>\n            <div>{{ $t('timeline.total.downloaded') }}{{ infos.total.downloaded | formatSize }}</div>\n            <div>{{ $t('timeline.total.seedingSize') }}{{ infos.total.seedingSize | formatSize }} ({{ infos.total.seeding }})</div>\n            <div>{{ $t('timeline.total.ratio') }}{{ infos.total.ratio | formatRatio }}</div>\n            <div>{{ $t('timeline.total.years', {year: infos.joinTimeInfo.years}) }}</div>\n          </div>\n        </v-card-title>\n        <v-card-text>\n          <v-divider></v-divider>\n          <div style=\"text-align:center;\">\n            <div\n              class=\"headline font-weight-bold mt-2\"\n              @click.stop=\"changeShareMessage\"\n            >... {{ shareMessage }} ...</div>\n            <div\n              style=\"color:#b5b5b5;\"\n            >({{ $t('timeline.updateat') }}{{ options.autoRefreshUserDataLastTime | formatDate('YYYY-MM-DD HH:mm:ss') }})</div>\n          </div>\n\n          <v-timeline class=\"my-2\">\n            <v-timeline-item v-for=\"(site, i) in datas\" :key=\"i\" color=\"transparent\" large>\n              <template v-slot:icon>\n                <v-avatar size=\"38\">\n                  <img v-if=\"site.icon\" :src=\"site.icon\" :class=\"{'icon-blur': blurSiteIcon}\"/>\n                </v-avatar>\n              </template>\n              <template v-slot:opposite>\n                <div class=\"headline font-weight-bold\">{{ site.user.joinTime | timeAgo }}</div>\n                <div class=\"caption\">\n                  <span v-if=\"showUserName\" class=\"mr-2\">{{ site.user.name }}</span>\n                  <span v-if=\"showUserLevel\">&lt;{{ site.user.levelName }}&gt;</span>\n                  <span v-if=\"site.user.id && site.user.id.length > 0 && showUid\">&lt;{{ site.user.id }}&gt;</span>\n                </div>\n              </template>\n              <div>\n                <v-divider v-if=\"i>0\" class=\"mb-2\"></v-divider>\n                <div class=\"headline font-weight-light mb-2\" v-if=\"showSiteName\">{{ site.name }}</div>\n                <div>{{ $t('timeline.user.uploaded') }}{{ site.user.uploaded | formatSize}}</div>\n                <div>{{ $t('timeline.user.downloaded') }}{{ site.user.downloaded | formatSize }}</div>\n                <div>{{ $t('timeline.user.ratio') }}{{ site.user.ratio | formatRatio }}</div>\n                <div>{{ $t('timeline.user.seedingSize') }}{{ site.user.seedingSize | formatSize }} ({{ site.user.seeding }})</div>\n                <div>{{ $t('timeline.user.bonus') }}{{ site.user.bonus | formatNumber }}</div>\n                <div v-if=\"site.user.bonusPerHour && site.user.bonusPerHour != 'N/A'\">{{ $t('timeline.user.bonusPerHour') }}{{ site.user.bonusPerHour | formatNumber }}</div>\n              </div>\n            </v-timeline-item>\n          </v-timeline>\n        </v-card-text>\n        <v-divider></v-divider>\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <span>{{ shareTime | formatDate('YYYY-MM-DD HH:mm:ss') }}</span>\n          <span class=\"ml-1\">Created By {{ $t('app.name') }} {{ version }}</span>\n        </v-card-actions>\n      </v-card>\n    </div>\n\n    <div class=\"toolbar\">\n      <v-switch\n        color=\"success\"\n        v-model=\"showSiteName\"\n        :label=\"$t('timeline.siteName')\"\n        class=\"my-0\"\n      ></v-switch>\n      <v-switch\n          color=\"success\"\n          v-model=\"blurSiteIcon\"\n          :label=\"$t('timeline.blurSiteIcon')\"\n          class=\"my-0\"\n      ></v-switch>\n      <v-switch\n        color=\"success\"\n        v-model=\"showUserName\"\n        :label=\"$t('timeline.userName')\"\n        class=\"my-0\"\n      ></v-switch>\n      <v-switch\n        color=\"success\"\n        v-model=\"showUserLevel\"\n        :label=\"$t('timeline.userLevel')\"\n        class=\"my-0\"\n      ></v-switch>\n      <v-switch\n        color=\"success\"\n        v-model=\"showUid\"\n        :label=\"$t('timeline.userId')\"\n        class=\"my-0\"\n      ></v-switch>\n      <v-divider />\n      <h1 style=\"padding: 5px;\">{{ $t('timeline.showSites') }}</h1>\n      <v-layout justify-start row wrap>\n        <v-flex v-for=\"(site, i) in sites\" :key=\"i\" xs3>\n          <v-switch\n                  color=\"success\"\n                  v-model=\"showSites\"\n                  :label=\"site.name\"\n                  :value=\"site.name\"\n                  class=\"my-0\"\n                  :disabled=\"!site.allowGetUserInfo\"\n                  @change=\"formatData\"\n          ></v-switch>\n        </v-flex>\n      </v-layout>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Site, Dictionary, EAction, Options } from \"@/interface/common\";\nimport FileSaver from \"file-saver\";\nimport domtoimage from 'dom-to-image';\nimport Extension from \"@/service/extension\";\nimport dayjs from \"dayjs\";\nimport { PPF } from \"@/service/public\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  data() {\n    return {\n      shareMessage: this.$t(\"timeline.shareMessage\").toString(),\n      displayUserName: \"\",\n      sites: [] as Site[],\n      showSites: [] as string[],\n      infos: {\n        nameInfo: { name: \"test\", maxCount: 0 },\n        joinTimeInfo: {\n          site: {} as Site,\n          time: new Date().getTime(),\n          years: 0 as number | string\n        },\n        maxUploadedInfo: {\n          site: {} as Site,\n          maxValue: 0\n        },\n        maxSeedingInfo: {\n          site: {} as Site,\n          maxValue: 0\n        },\n        total: {\n          uploaded: 0,\n          downloaded: 0,\n          seedingSize: 0,\n          ratio: -1,\n          seeding: 0\n        }\n      },\n      options: this.$store.state.options as Options,\n      version: \"\",\n      datas: [] as Site[],\n      shareTime: new Date(),\n      shareing: false,\n      showUserName: true,\n      showSiteName: false,\n      showUserLevel: true,\n      showUid: true,\n      blurSiteIcon: true,\n      iconCache: {} as Dictionary<any>\n    };\n  },\n  created() {\n    if (chrome && chrome.runtime) {\n      let manifest = chrome.runtime.getManifest();\n      this.version = \"v\" + (manifest.version_name || manifest.version);\n    }\n    this.init();\n  },\n  mounted() {\n    this.replaceImageToBase64();\n  },\n  methods: {\n    init() {\n      extension\n        .sendRequest(EAction.readConfig)\n        .then((options: Options) => {\n          this.options = this.clone(options);\n          this.sites = this.options.sites;\n          if (this.options.shareMessage) {\n            this.shareMessage = this.options.shareMessage;\n          }\n          if (this.options.displayUserName) {\n            this.displayUserName = this.options.displayUserName;\n          }\n          this.showSites = this.sites\n                  .filter((site: Site) => {return site.allowGetUserInfo})\n                  .map((site: Site) => {return site.name});  //  只提取站点名称\n          this.formatData();\n        })\n        .catch();\n    },\n    formatData() {\n      let userNames: Dictionary<any> = {};\n      let result = this.infos;\n      result.total = {\n        uploaded: 0,\n        downloaded: 0,\n        seedingSize: 0,\n        ratio: -1,\n        seeding: 0\n      };\n\n      let sites: Site[] = [];\n      this.sites.forEach((site: Site) => {\n        // 站点设置不获取用户信息\n        if (!site.allowGetUserInfo) {\n          return;\n        }\n\n        // 展示时不显示该站点信息\n        if (!this.showSites.includes(site.name)) {\n          return;\n        }\n \n        let user = site.user;\n        if (user && user.name && user.joinTime) {\n          user.joinTime = PPF.transformTime(user.joinTime, site.timezoneOffset);  //add by pxwang for gpw jointime error\n          \n          sites.push(site);\n          if (!userNames[user.name]) {\n            userNames[user.name] = 0;\n          }\n          userNames[user.name]++;\n\n          // 获取使用最多的用户名\n          if (userNames[user.name] > result.nameInfo.maxCount) {\n            result.nameInfo.name = user.name;\n            result.nameInfo.maxCount = userNames[user.name];\n          }\n\n          // 获取加入时间最久的站点\n          if (user.joinTime && user.joinTime < result.joinTimeInfo.time) {\n            result.joinTimeInfo.time = Math.round(user.joinTime);\n            result.joinTimeInfo.site = site;\n          }\n\n          if (user.uploaded && user.uploaded > 0) {\n            result.total.uploaded += user.uploaded;\n            // 获取上传最多的站点\n            if (user.uploaded > result.maxUploadedInfo.maxValue) {\n              result.maxUploadedInfo.maxValue = user.uploaded;\n              result.maxUploadedInfo.site = site;\n            }\n          }\n\n          if (user.downloaded && user.downloaded > 0) {\n            result.total.downloaded += user.downloaded;\n          }\n\n          if (user.seeding && user.seeding > 0) {\n            result.total.seeding += Math.round(user.seeding);\n          }\n\n          if (user.seedingSize && user.seedingSize > 0) {\n            result.total.seedingSize += user.seedingSize;\n            // 获取上传最多的站点\n            if (user.seedingSize > result.maxSeedingInfo.maxValue) {\n              result.maxSeedingInfo.maxValue = user.seedingSize;\n              result.maxSeedingInfo.site = site;\n            }\n          }\n          user.ratio = this.getRatio(user);\n        }\n      });\n\n      if (result.joinTimeInfo.time > 0) {\n        // 计算P龄，带小数\n        result.joinTimeInfo.years = dayjs(new Date())\n          .diff(result.joinTimeInfo.time, \"year\", true)\n          .toFixed(2);\n      }\n\n      this.infos = result;\n\n      // 按加入时间排序\n      this.datas = sites.sort((a, b) => {\n        \n        if (!a.user || !b.user) {\n          return 0;\n        }\n        const sortA = a.user.joinTime || 0;\n        const sortB = b.user.joinTime || 0;\n\n        if (sortA < sortB) return -1;\n        if (sortA > sortB) return 1;\n        return 0;\n      });\n\n      this.infos.total.ratio = this.getRatio(this.infos.total);\n      setTimeout(() => {\n        this.replaceImageToBase64();\n      }, 200);\n    },\n    getRatio(info: any): number {\n      let downloaded = info.downloaded as number;\n      let uploaded = info.uploaded as number;\n      // 没有下载量时设置分享率为无限\n      if (downloaded == 0 && uploaded > 0) {\n        return -1;\n      }\n      // 没有分享率时，重新以 上传量 / 下载量计算\n      else if (downloaded > 0) {\n        return uploaded / downloaded;\n      }\n      return -1;\n    },\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */ clone(source: any) {\n      return JSON.parse(JSON.stringify(source));\n    },\n    share() {\n      this.shareing = true;\n      this.shareTime = new Date();\n      this.formatData();\n      setTimeout(() => {\n        let div = this.$refs.userDataCard as HTMLDivElement;\n        domtoimage.toBlob(div, {\n          filter: (node) => {\n            if (node.nodeType === 1) {\n              return !(node as Element).classList.contains('by_pass_canvas')\n            }\n\n            return true\n          }\n        }).then((blob: any) => {\n          if (blob) {\n            FileSaver.saveAs(blob, \"PT-Plugin-Plus-UserData.png\");\n          }\n          this.shareing = false;\n        });\n      }, 500);\n    },\n\n    /**\n     * 替换网络图片，用于生成图片\n     * 因为网络图片会存在跨域问题，无法正常生成图片\n     */\n    replaceImageToBase64() {\n      let div = this.$refs.userDataCard as HTMLDivElement;\n      let imgs = $(\"img\", div);\n      imgs.each((index, el) => {\n        let src = $(el).attr(\"src\") + \"\";\n        if (src.indexOf(\"http\") > -1) {\n          if (this.iconCache[src]) {\n            $(el).attr(\"src\", this.iconCache[src]);\n            return;\n          }\n          extension\n            .sendRequest(EAction.getBase64FromImageUrl, null, src)\n            .then(result => {\n              this.iconCache[src] = result;\n              $(el).attr(\"src\", result);\n            })\n            .catch(e => {\n              console.log(e);\n            });\n        }\n      });\n    },\n\n    /**\n     * 修改需要显示的用户名\n     */\n    changeDisplayUserName() {\n      let result = prompt(\n        this.$t(\"timeline.inputDisplayName\").toString(),\n        this.displayUserName\n      );\n      if (result != null) {\n        this.displayUserName = result;\n        this.$store.dispatch(\"saveConfig\", {\n          displayUserName: result\n        });\n      }\n    },\n\n    /**\n     * 修复需要分享的寄语\n     */\n    changeShareMessage() {\n      let result = prompt(\n        this.$t(\"timeline.inputShareMessage\").toString(),\n        this.shareMessage\n      );\n      if (result != null) {\n        this.shareMessage = result;\n        this.$store.dispatch(\"saveConfig\", {\n          shareMessage: result\n        });\n      }\n    }\n  },\n  filters: {\n    formatRatio(v: any) {\n      if (v > 10000 || v == -1) {\n        return \"∞\";\n      }\n      let number = parseFloat(v);\n      if (isNaN(number)) {\n        return \"-\";\n      }\n      return number.toFixed(2);\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.userDataTimeline {\n  position: relative;\n  .card {\n    width: 650px;\n  }\n\n  .toolbar {\n    position: absolute;\n    left: 660px;\n    top: 0;\n  }\n\n  .icon-blur {\n    filter: blur(4px);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/collection/AddToGroup.vue",
    "content": "<template>\n  <v-btn\n    :flat=\"flat\"\n    :icon=\"icon\"\n    :small=\"small\"\n    :loading=\"loading\"\n    :color=\"color\"\n    @click.stop=\"showContentMenus\"\n  >\n    <v-icon v-if=\"haveSuccess\" color=\"success\" small>done</v-icon>\n    <v-icon v-else-if=\"haveError\" color=\"red\" small>close</v-icon>\n    <v-icon v-else small :title=\"$t('collection.addToGroup')\">{{ iconText }}</v-icon>\n    {{ label }}\n  </v-btn>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  Options,\n  EAction,\n  ICollection,\n  ICollectionGroup,\n  ECommonKey\n} from \"@/interface/common\";\n\nimport { PPF } from \"@/service/public\";\n\nexport default Vue.extend({\n  props: {\n    flat: Boolean,\n    icon: Boolean,\n    small: Boolean,\n    iconText: {\n      type: String,\n      default: \"add\"\n    },\n    item: {\n      type: Object,\n      default: () => {\n        return {} as ICollection;\n      }\n    },\n    groups: Array,\n    color: {\n      type: String,\n      default: \"success\"\n    },\n    label: {\n      type: String,\n      default: \"\"\n    }\n  },\n\n  data() {\n    return {\n      options: this.$store.state.options as Options,\n      contentMenus: [] as any[],\n      loading: false,\n      haveSuccess: false,\n      haveError: false\n    };\n  },\n\n  methods: {\n    /**\n     * 显示上下文菜单\n     * @param options\n     * @param event\n     */\n    showContentMenus(event?: any) {\n      let menus: any[] = [];\n      let groups: string[] = PPF.clone(this.item.groups || []);\n      groups.push(ECommonKey.all);\n      groups.push(ECommonKey.noGroup);\n      this.groups.forEach((group: any) => {\n        if (group.id && group.name && !groups.includes(group.id)) {\n          menus.push({\n            title: group.name,\n            fn: () => {\n              this.$emit(\"add\", this.item, group);\n            }\n          });\n        }\n      });\n\n      if (menus.length == 0) {\n        return;\n      }\n\n      PPF.showContextMenu(menus, event);\n    },\n\n    clearStatus() {\n      this.haveSuccess = false;\n      this.haveError = false;\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/views/collection/GroupCard.vue",
    "content": "<template>\n  <v-hover>\n    <v-card\n      :color=\"color\"\n      slot-scope=\"{ hover }\"\n      :class=\"`elevation-${hover||active ? 5 : 1} mr-2`\"\n      :style=\"styles\"\n      @click=\"click\"\n      :dark=\"dark\"\n    >\n      <v-card-title class=\"ma-0 pa-2\">\n        <div>\n          <span class=\"subheading\">{{ name }}</span>\n          <span class=\"ml-2 caption\">({{ count }})</span>\n        </div>\n        <div>{{ description }}</div>\n      </v-card-title>\n\n      <v-card-actions class=\"toolbar\">\n        <span class=\"ma-1 caption\" v-if=\"count>0\">{{ size | formatSize}}</span>\n        <v-spacer></v-spacer>\n\n        <template v-if=\"hover && count>0\">\n          <!-- 下载到 -->\n          <DownloadTo\n            :downloadOptions=\"items\"\n            flat\n            icon\n            class=\"mx-0 btn-mini\"\n            @error=\"onDownloadError\"\n            @success=\"onDownloadSuccess\"\n          />\n\n          <!-- 复制下载链接 -->\n          <v-btn\n            icon\n            flat\n            :title=\"$t('searchTorrent.copyToClipboardTip')\"\n            @click.stop=\"copyLinksToClipboard\"\n            class=\"mx-0 btn-mini\"\n          >\n            <v-icon>file_copy</v-icon>\n          </v-btn>\n        </template>\n\n        <template v-if=\"!readOnly\">\n          <template v-if=\"hover||colorBoxIsOpen\">\n            <!-- 编辑 -->\n            <v-btn icon @click.stop=\"rename\" class=\"ma-0 btn-mini\" :title=\"$t('common.edit')\">\n              <v-icon>edit</v-icon>\n            </v-btn>\n\n            <!-- 删除 -->\n            <v-btn icon @click.stop=\"remove\" class=\"ma-0 btn-mini\" :title=\"$t('common.remove')\">\n              <v-icon>delete</v-icon>\n            </v-btn>\n\n            <!-- 颜色选择 -->\n            <ColorSelector\n              @change=\"changeColor\"\n              :dark=\"dark\"\n              class=\"ma-0\"\n              mini\n              @show=\"colorBoxIsOpen=true\"\n              @hide=\"colorBoxIsOpen=false\"\n              :title=\"$t('common.color')\"\n            />\n\n            <v-btn\n              v-if=\"!isDefault\"\n              icon\n              @click.stop=\"setDefault\"\n              class=\"ma-0 btn-mini\"\n              :title=\"$t('common.setDefault')\"\n            >\n              <v-icon>favorite_border</v-icon>\n            </v-btn>\n          </template>\n\n          <v-btn\n            v-if=\"isDefault\"\n            icon\n            @click.stop=\"cancelDefault\"\n            class=\"ma-0 btn-mini\"\n            :title=\"$t('common.cancelDefault')\"\n          >\n            <v-icon>favorite</v-icon>\n          </v-btn>\n        </template>\n      </v-card-actions>\n    </v-card>\n  </v-hover>\n</template>\n<style lang=\"scss\" scoped>\n.toolbar {\n  position: absolute;\n  bottom: 0;\n  height: 35px;\n  width: 100%;\n  padding: 5px;\n}\n</style>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { isNumber } from \"util\";\nimport ColorSelector from \"@/options/components/ColorSelector.vue\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\nimport { ICollection, EAction } from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nconst extension = new Extension();\n\nexport default Vue.extend({\n  components: {\n    ColorSelector,\n    DownloadTo\n  },\n  props: {\n    width: {\n      type: [String, Number],\n      default: \"205px\"\n    },\n    height: {\n      type: [String, Number],\n      default: \"90px\"\n    },\n    name: String,\n    description: String,\n    count: {\n      type: Number,\n      default: 0\n    },\n    color: {\n      type: String,\n      default: \"grey\"\n    },\n    group: {\n      type: Object\n    },\n    active: Boolean,\n    readOnly: Boolean,\n    isDefault: Boolean,\n    items: {\n      type: Array,\n      default: () => {\n        return [] as ICollection[];\n      }\n    }\n  },\n  data() {\n    return {\n      dark: true,\n      colorBoxIsOpen: false\n    };\n  },\n\n  watch: {\n    color() {\n      if (this.color.indexOf(\"lighten\") > 0) {\n        this.dark = false;\n      } else {\n        this.dark = true;\n      }\n    }\n  },\n\n  methods: {\n    remove() {\n      this.$emit(\"remove\", this.group);\n    },\n\n    changeColor(color: string) {\n      this.$emit(\"changeColor\", color, this.group);\n    },\n\n    click() {\n      this.$emit(\"click\", this.group);\n    },\n\n    rename() {\n      let newValue = window.prompt(\n        this.$t(\"collection.changeGroupName\").toString(),\n        this.name\n      );\n      if (newValue && newValue !== this.name) {\n        this.$emit(\"rename\", newValue, this.group);\n      }\n    },\n\n    setDefault() {\n      this.$emit(\"setDefault\", this.group);\n    },\n\n    cancelDefault() {\n      this.$emit(\"cancelDefault\", this.group);\n    },\n\n    onDownloadSuccess(msg: any) {\n      console.log(\"onDownloadSuccess\");\n      this.$emit(\"downloadSuccess\", msg);\n    },\n\n    onDownloadError(msg: any) {\n      this.$emit(\"downloadError\", msg);\n    },\n\n    copyLinksToClipboard() {\n      let urls: string[] = [];\n\n      this.items.forEach((item: any) => {\n        urls.push(item.url);\n      });\n\n      extension\n        .sendRequest(EAction.copyTextToClipboard, null, urls.join(\"\\n\"))\n        .then(result => {\n          let msg = this.$t(\"searchTorrent.copySelectedToClipboardSuccess\", {\n            count: urls.length\n          }).toString();\n          this.$emit(\"downloadSuccess\", msg);\n        })\n        .catch(() => {\n          let msg = this.$t(\n            \"searchTorrent.copyLinkToClipboardError\"\n          ).toString();\n          this.$emit(\"downloadError\", msg);\n        });\n    }\n  },\n\n  computed: {\n    styles() {\n      let result = {\n        width: this.width,\n        height: this.height\n      };\n\n      if (isNumber(this.width)) {\n        result.width = this.width.toString() + \"px\";\n      } else {\n        result.width = this.width;\n      }\n      if (isNumber(this.height)) {\n        result.height = this.height.toString() + \"px\";\n      } else {\n        result.height = this.height;\n      }\n\n      return result;\n    },\n\n    size() {\n      let size = 0;\n      if (this.items && this.items.length > 0) {\n        (this.items as any).forEach((item: ICollection) => {\n          if (item.size && item.size > 0) {\n            size += item.size;\n          }\n        });\n      }\n\n      return size;\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/views/collection/Index.vue",
    "content": "<template>\n  <div class=\"collection\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t(\"collection.title\") }}</v-alert>\n    <v-card>\n      <div\n        style=\"height: 120px; overflow-x: auto; display: -webkit-box\"\n        class=\"ma-2 pt-2\"\n        v-if=\"groups.length > 1\"\n      >\n        <GroupCard\n          :color=\"group.color\"\n          v-for=\"(group, index) in groups\"\n          :key=\"index\"\n          :name=\"group.name\"\n          :description=\"group.description\"\n          :count=\"group.count\"\n          :group=\"group\"\n          :active=\"group.id === activeGroupId\"\n          :readOnly=\"group.readOnly\"\n          :width=\"group.width\"\n          :isDefault=\"group.id === defaultGroupId\"\n          :items=\"getItemsFromGroup(group.id)\"\n          @changeColor=\"changeGroupColor\"\n          @remove=\"removeGroup\"\n          @rename=\"changeGroupName\"\n          @click=\"setGroupActive\"\n          @setDefault=\"setDefaultGroup\"\n          @cancelDefault=\"cancelDefaultGroup\"\n          @downloadSuccess=\"onSuccess\"\n          @downloadError=\"onError\"\n        ></GroupCard>\n      </div>\n\n      <!-- 分隔线 -->\n      <v-divider></v-divider>\n\n      <v-card-title>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n\n        <v-btn color=\"error\" @click=\"clear\" :disabled=\"items.length == 0\">\n          <v-icon class=\"mr-2\">clear</v-icon>\n          {{ $t(\"common.clear\") }}\n        </v-btn>\n\n        <v-divider class=\"mx-3 mt-0\" vertical></v-divider>\n\n        <v-btn color=\"success\" @click=\"addGroup\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t(\"collection.addGroup\") }}\n        </v-btn>\n\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/my-collection\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.searchSolution.index.help\") }}\n        </v-btn>\n\n        <v-spacer></v-spacer>\n\n        <v-text-field\n          v-model=\"filterKey\"\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n          clearable\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :search=\"filterKey\"\n        :custom-filter=\"searchResultFilter\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"link\"\n        select-all\n        class=\"dataList\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width: 50px\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n            ></v-checkbox>\n          </td>\n          <td>{{ props.index + 1 }}</td>\n          <td>\n            <v-img\n              :src=\"\n                props.item.movieInfo && props.item.movieInfo.image\n                  ? props.item.movieInfo.image\n                  : './assets/movie.png'\n              \"\n              class=\"mx-0 my-2\"\n              contain\n              :max-height=\"\n                props.item.movieInfo && props.item.movieInfo.image ? 100 : 80\n              \"\n              position=\"left center\"\n            >\n              <v-layout style=\"margin-left: 90px\" row wrap>\n                <template\n                  v-if=\"props.item.movieInfo && props.item.movieInfo.title\"\n                >\n                  <v-flex xs12>\n                    <a\n                      :href=\"props.item.movieInfo.link\"\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer nofollow\"\n                    >\n                      <img\n                        src=\"https://img3.doubanio.com/favicon.ico\"\n                        class=\"mr-2 mt-0\"\n                        width=\"16\"\n                      />\n                    </a>\n                    <span class=\"title\">{{ props.item.movieInfo.title }}</span>\n\n                    <span class=\"caption ml-2\"\n                      >({{ props.item.movieInfo.year }})</span\n                    >\n                  </v-flex>\n\n                  <v-flex xs12 class=\"mb-1\">\n                    <span class=\"sub-title\">{{\n                      props.item.movieInfo.alt_title\n                    }}</span>\n                  </v-flex>\n                </template>\n                <template>\n                  <v-flex xs12>\n                    <a\n                      :href=\"props.item.link\"\n                      target=\"_blank\"\n                      :title=\"props.item.title\"\n                      rel=\"noopener noreferrer nofollow\"\n                    >\n                      <span>{{ props.item.title }}</span>\n                    </a>\n                  </v-flex>\n\n                  <v-flex xs12>\n                    <span class=\"sub-title\">{{ props.item.subTitle }}</span>\n                  </v-flex>\n                </template>\n              </v-layout>\n            </v-img>\n\n            <!-- 分组列表 -->\n            <template>\n              <div style=\"margin-left: 90px\">\n                <v-hover\n                  v-for=\"(group, index) in getGroupList(props.item)\"\n                  :key=\"index\"\n                >\n                  <v-chip\n                    slot-scope=\"{ hover }\"\n                    :close=\"hover && group.id != null\"\n                    label\n                    :color=\"group.color || 'grey'\"\n                    :dark=\"\n                      group.color && group.color.indexOf('lighten') > 0\n                        ? false\n                        : true\n                    \"\n                    small\n                    @input=\"removeFromGroup(props.item, group)\"\n                    >{{ group.name }}</v-chip\n                  >\n                </v-hover>\n\n                <AddToGroup\n                  v-if=\"groups && groups.length > 1\"\n                  icon\n                  small\n                  flat\n                  @add=\"addToGroup\"\n                  :item=\"props.item\"\n                  :groups=\"groups\"\n                ></AddToGroup>\n              </div>\n            </template>\n          </td>\n          <td>\n            <v-layout row wrap v-if=\"!!props.item.site\">\n              <a\n                :href=\"props.item.site.activeURL\"\n                target=\"_blank\"\n                rel=\"noopener noreferrer nofollow\"\n                class=\"nodecoration\"\n              >\n                <v-avatar :size=\"15\">\n                  <img :src=\"props.item.site.icon\" />\n                </v-avatar>\n                <span class=\"caption ml-1 site-name\">{{\n                  props.item.site.name\n                }}</span>\n              </a>\n            </v-layout>\n          </td>\n          <td class=\"text-xs-right\">{{ props.item.size | formatSize }}</td>\n          <td class=\"text-xs-right\">{{ props.item.time | formatDate }}</td>\n          <td class=\"text-xs-center\">\n            <template v-if=\"props.item.movieInfo\">\n              <!-- IMDb Id -->\n              <v-btn\n                v-if=\"!!props.item.movieInfo.imdbId\"\n                flat\n                icon\n                small\n                class=\"mx-0\"\n                :title=\"$t('common.search')\"\n                :to=\"`/search-torrent/${props.item.movieInfo.imdbId}`\"\n              >\n                <v-icon small>search</v-icon>\n              </v-btn>\n              <!-- 豆瓣ID -->\n              <v-btn\n                v-else-if=\"!!props.item.movieInfo.doubanId\"\n                flat\n                icon\n                small\n                class=\"mx-0\"\n                :title=\"$t('common.search')\"\n                :to=\"`/search-torrent/douban${props.item.movieInfo.doubanId}|${props.item.movieInfo.title}|${props.item.movieInfo.alt_title}`\"\n              >\n                <v-icon small>search</v-icon>\n              </v-btn>\n\n              <v-btn\n                v-else\n                flat\n                icon\n                small\n                class=\"mx-0\"\n                :title=\"$t('collection.setMovieId')\"\n                @click=\"setMovieId(props.item)\"\n              >\n                <v-icon small>edit</v-icon>\n              </v-btn>\n            </template>\n\n            <!-- 下载到 -->\n            <DownloadTo\n              :downloadOptions=\"props.item\"\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              @error=\"onError\"\n              @success=\"onSuccess\"\n            />\n\n            <!-- 下载种子文件 -->\n            <v-btn\n              v-if=\"props.item.site.downloadMethod == 'POST'\"\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              @click.stop=\"saveTorrentFile(props.item)\"\n            >\n              <v-icon small :title=\"$t('searchTorrent.saveTip')\">save</v-icon>\n            </v-btn>\n\n            <v-btn\n              v-else\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              :href=\"props.item.url\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer nofollow\"\n              :title=\"$t('searchTorrent.saveTip')\"\n            >\n              <v-icon small>save</v-icon>\n            </v-btn>\n\n            <!-- 删除 -->\n            <v-btn\n              flat\n              icon\n              small\n              @click=\"removeConfirm(props.item)\"\n              color=\"error\"\n              class=\"mx-0\"\n              :title=\"$t('common.remove')\"\n            >\n              <v-icon small>delete</v-icon>\n            </v-btn>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <v-snackbar\n      v-model=\"haveError\"\n      top\n      :timeout=\"3000\"\n      color=\"error\"\n      multi-line\n    >\n      <span v-html=\"errorMsg\"></span>\n    </v-snackbar>\n    <v-snackbar\n      v-model=\"haveSuccess\"\n      bottom\n      :timeout=\"3000\"\n      color=\"success\"\n      multi-line\n    >\n      <span v-html=\"successMsg\"></span>\n    </v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  EAction,\n  DownloadOptions,\n  Site,\n  Dictionary,\n  ICollection,\n  ICollectionGroup,\n  BASE_COLORS,\n  ECommonKey,\n  Options,\n  ERequestMethod\n} from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\nimport GroupCard from \"./GroupCard.vue\";\nimport AddToGroup from \"./AddToGroup.vue\";\nimport { PPF } from \"@/service/public\";\nimport { FileDownloader } from \"@/service/downloader\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  components: {\n    DownloadTo,\n    GroupCard,\n    AddToGroup\n  },\n  data() {\n    return {\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      },\n      items: [] as ICollection[],\n      allItems: [] as ICollection[],\n      groups: [] as ICollectionGroup[],\n      options: this.$store.state.options as Options,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      siteCache: {} as Dictionary<any>,\n      activeGroupId: ECommonKey.all as any,\n      defaultGroupId: \"\" as any,\n      filterKey: \"\",\n      loading: false\n    };\n  },\n  /**\n   * 当前组件激活时触发\n   * 因为启用了搜索结果缓存，所以需要在这里处理关键字\n   */\n  activated() {\n    this.getTorrentCollections();\n  },\n  methods: {\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n\n    clear() {\n      if (confirm(this.$t(\"common.clearConfirm\").toString())) {\n        extension\n          .sendRequest(EAction.clearTorrentCollention)\n          .then((result: any) => {\n            this.allItems = [];\n            this.items = [];\n            this.groups = [];\n          });\n      }\n    },\n    remove(item: ICollection | ICollection[]) {\n      extension\n        .sendRequest(EAction.deleteTorrentFromCollention, null, item)\n        .then((result: any) => {\n          this.getTorrentCollections();\n        });\n    },\n\n    removeConfirm(item: ICollection) {\n      if (confirm(this.$t(\"common.removeConfirm\").toString())) {\n        this.remove(item);\n      }\n    },\n\n    getTorrentCollections() {\n      if (this.loading) {\n        return;\n      }\n      const requests: any[] = [];\n\n      requests.push(extension.sendRequest(EAction.getTorrentCollectionGroups));\n      requests.push(extension.sendRequest(EAction.getTorrentCollections));\n      this.loading = true;\n      return Promise.all(requests).then(results => {\n        console.log(\"getTorrentCollections\", results);\n        this.items = [];\n        this.groups = [];\n        let noGroup = {\n          id: ECommonKey.noGroup,\n          name: this.$t(\"collection.noGroup\").toString(),\n          count: 0,\n          readOnly: true,\n          width: 120\n        };\n\n        results[1].forEach((item: any) => {\n          let site = this.siteCache[item.host];\n          if (!site) {\n            site = PPF.getSiteFromHost(item.host, this.options);\n            this.siteCache[item.host] = site;\n          }\n\n          item.site = site;\n          if (!item.groups || item.groups.length == 0) {\n            noGroup.count++;\n          }\n\n          this.items.push(item);\n        });\n\n        this.allItems = PPF.clone(this.items);\n\n        let allGroup = {\n          name: this.$t(\"common.all\").toString(),\n          id: ECommonKey.all,\n          count: this.allItems.length,\n          color: \"grey darken-2\",\n          readOnly: true,\n          width: 120\n        };\n\n        this.groups.push(allGroup);\n        if (noGroup.count !== allGroup.count && noGroup.count > 0) {\n          this.groups.push(noGroup);\n        }\n\n        this.groups.push(...results[0]);\n\n        if (this.activeGroupId !== ECommonKey.all) {\n          this.filterCollections();\n        }\n        this.loading = false;\n      });\n    },\n\n    removeSelected() {\n      if (this.selected && this.selected.length > 0) {\n        if (confirm(this.$t(\"common.actionConfirm\").toString())) {\n          this.remove(this.selected);\n        }\n      }\n    },\n\n    onError(msg: string) {\n      this.errorMsg = msg;\n    },\n\n    onSuccess(msg: string) {\n      this.successMsg = msg;\n    },\n\n    addGroup() {\n      let name = window.prompt(this.$t(\"collection.inputGroupName\").toString());\n      if (name) {\n        extension\n          .sendRequest(EAction.addTorrentCollectionGroup, null, {\n            name,\n            color: BASE_COLORS[Math.floor(Math.random() * BASE_COLORS.length)]\n          })\n          .then(() => {\n            this.getTorrentCollections();\n          });\n      }\n    },\n\n    getGroupList(item: ICollection) {\n      let result: ICollectionGroup[] = [];\n      if (item.groups) {\n        item.groups.forEach(id => {\n          this.groups.forEach(group => {\n            if (group.id === id) {\n              result.push(group);\n            }\n          });\n        });\n      }\n\n      if (result.length == 0) {\n        result.push({\n          name: this.$t(\"collection.noGroup\").toString()\n        });\n      }\n\n      return result;\n    },\n\n    removeGroup(group: ICollectionGroup) {\n      if (group.count && group.count > 0) {\n        if (\n          !confirm(\n            this.$t(\"collection.removeGroupConfirm\", {\n              count: group.count\n            }).toString()\n          )\n        ) {\n          return;\n        }\n      }\n      extension\n        .sendRequest(EAction.removeTorrentCollectionGroup, null, group)\n        .then(() => {\n          this.getTorrentCollections();\n        });\n\n      this.cancelDefaultGroup(group);\n    },\n\n    changeGroupColor(color: string, group: ICollectionGroup) {\n      group.color = color;\n      console.log(color, group);\n      extension\n        .sendRequest(EAction.updateTorrentCollectionGroup, null, group)\n        .then(() => {\n          this.getTorrentCollections();\n        });\n    },\n\n    addToGroup(item: ICollection, group: ICollectionGroup) {\n      console.log(item, group);\n      extension\n        .sendRequest(EAction.addTorrentCollectionToGroup, null, {\n          item,\n          groupId: group.id\n        })\n        .then(() => {\n          this.getTorrentCollections();\n        });\n    },\n\n    changeGroupName(name: string, group: ICollectionGroup) {\n      group.name = name;\n      extension\n        .sendRequest(EAction.updateTorrentCollectionGroup, null, group)\n        .then(() => {\n          this.getTorrentCollections();\n        });\n    },\n\n    removeFromGroup(item: ICollection, group: ICollectionGroup) {\n      extension\n        .sendRequest(EAction.removeTorrentCollectionFromGroup, null, {\n          item,\n          groupId: group.id\n        })\n        .then(() => {\n          this.getTorrentCollections();\n        });\n    },\n\n    setGroupActive(group: ICollectionGroup) {\n      this.activeGroupId = group.id;\n      if (this.activeGroupId === ECommonKey.all) {\n        this.getTorrentCollections();\n        return;\n      }\n      this.filterCollections();\n    },\n\n    filterCollections() {\n      let groupId = this.activeGroupId;\n\n      this.items = this.getItemsFromGroup(groupId);\n    },\n\n    getItemsFromGroup(groupId: string) {\n      if (groupId === ECommonKey.all) {\n        return this.allItems;\n      }\n\n      let result = [];\n      for (let index = 0; index < this.allItems.length; index++) {\n        const item = this.allItems[index];\n        if (item.groups && item.groups.includes(groupId)) {\n          result.push(item);\n        } else if (\n          groupId === ECommonKey.noGroup &&\n          (!item.groups || item.groups.length === 0)\n        ) {\n          result.push(item);\n        }\n      }\n\n      return result;\n    },\n\n    setDefaultGroup(group: ICollectionGroup) {\n      this.defaultGroupId = group.id;\n      this.$store.dispatch(\"saveConfig\", {\n        defaultCollectionGroupId: group.id\n      });\n    },\n\n    cancelDefaultGroup(group: ICollectionGroup) {\n      if (this.defaultGroupId === group.id) {\n        this.defaultGroupId = \"\";\n        this.$store.dispatch(\"saveConfig\", {\n          defaultCollectionGroupId: \"\"\n        });\n      }\n    },\n\n    setMovieId(item: ICollection) {\n      let id = prompt(this.$t(\"collection.setMovieId\").toString());\n\n      if (!id) {\n        return;\n      }\n\n      let doubanId = \"\";\n      let imdbId = \"\";\n\n      if (/^(tt\\d+)$/.test(id)) {\n        imdbId = id;\n      } else if (/^(\\d+)$/.test(id)) {\n        doubanId = id;\n      }\n\n      if (imdbId || doubanId) {\n        let data = PPF.clone(item);\n        delete data.site;\n        data.movieInfo = {\n          imdbId,\n          doubanId\n        };\n        extension\n          .sendRequest(EAction.updateTorrentCollention, null, data)\n          .then(() => {\n            this.getTorrentCollections();\n          });\n      }\n    },\n\n    /**\n     * 搜索结果过滤器，用于用户二次过滤\n     * @param items\n     * @param search\n     */\n    searchResultFilter(items: any[], search: string) {\n      search = search.toString().toLowerCase();\n      if (search.trim() === \"\") return items;\n\n      // 以空格分隔要过滤的关键字\n      let searchs = search.split(\" \");\n\n      return items.filter((item: ICollection) => {\n        let texts: string[] = [];\n        texts.push(item.title);\n\n        item.subTitle && texts.push(item.title);\n        if (item.movieInfo) {\n          item.movieInfo.title && texts.push(item.movieInfo.title);\n          item.movieInfo.alt_title && texts.push(item.movieInfo.alt_title);\n        }\n\n        let source = texts.join(\"\").toLowerCase();\n        let result = true;\n        searchs.forEach(key => {\n          if (key.trim() != \"\") {\n            result = result && source.indexOf(key) > -1;\n          }\n        });\n        return result;\n      });\n    },\n\n    /**\n     * 保存种子文件\n     * @param item\n     */\n    saveTorrentFile(item: any) {\n      let requestMethod = ERequestMethod.GET;\n      if (item.site) {\n        requestMethod = item.site.downloadMethod || ERequestMethod.GET;\n      }\n      let url = item.url + \"\";\n      let file = new FileDownloader({\n        url,\n        timeout: this.options.connectClientTimeout,\n        fileName: `[${item.site.name}][${item.title}].torrent`\n      });\n\n      file.requestMethod = requestMethod;\n      file.onError = (error: any) => { };\n      file.start();\n    }\n  },\n\n  created() {\n    this.getTorrentCollections();\n    this.defaultGroupId = this.options.defaultCollectionGroupId;\n  },\n\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: \"№\",\n          align: \"left\",\n          sortable: false,\n          value: \"title\",\n          width: 30\n        },\n        {\n          text: this.$t(\"collection.headers.title\"),\n          align: \"left\",\n          value: \"title\"\n        },\n        {\n          text: this.$t(\"collection.headers.site\"),\n          align: \"left\",\n          value: \"site.host\",\n          width: 150\n        },\n        {\n          text: this.$t(\"collection.headers.size\"),\n          align: \"right\",\n          value: \"size\",\n          width: 100\n        },\n        {\n          text: this.$t(\"collection.headers.time\"),\n          align: \"right\",\n          value: \"time\",\n          width: 130\n        },\n        {\n          text: this.$t(\"collection.headers.action\"),\n          value: \"title\",\n          align: \"center\",\n          sortable: false,\n          width: 150\n        }\n      ];\n    }\n  }\n});\n</script>\n<style lang=\"scss\" >\n.collection {\n  .sub-title {\n    color: #aaaaaa;\n    font-size: 12px;\n  }\n\n  .dataList {\n    table.v-table thead tr:not(.v-datatable__progress) th,\n    table.v-table tbody tr td {\n      padding: 8px !important;\n      font-size: 12px;\n    }\n\n    table.v-table tbody tr:nth-child(even) {\n      background-color: #f1f1f1;\n    }\n\n    table.v-table.theme--dark tbody tr:nth-child(even) {\n      background-color: #1f1f1f;\n    }\n  }\n\n  .nodecoration {\n    text-decoration: none;\n  }\n\n  .site-name {\n    vertical-align: middle;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/keepUpload/KeepUploadTasks.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{\n      $t(\"keepUploadTask.title\")\n    }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n\n        <v-btn color=\"error\" @click=\"clear\" :disabled=\"items.length == 0\">\n          <v-icon class=\"mr-2\">clear</v-icon>\n          {{ $t(\"common.clear\") }}\n        </v-btn>\n\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/keep-upload-task\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.searchSolution.index.help\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n\n        <v-text-field\n          v-model=\"filterKey\"\n          class=\"search\"\n          append-icon=\"search\"\n          :label=\"$t('keepUploadTask.filterSearchResults')\"\n          single-line\n          hide-details\n          enterkeyhint=\"search\"\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :search=\"filterKey\"\n        :custom-filter=\"searchResultFilter\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"id\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <tr @click=\"props.expanded = !props.expanded\">\n            <td style=\"width: 20px\">\n              <v-checkbox\n                v-model=\"props.selected\"\n                primary\n                hide-details\n              ></v-checkbox>\n            </td>\n            <td class=\"text-xs-center\">\n              <v-avatar size=\"18\">\n                <img :src=\"props.item.site.icon\" />\n              </v-avatar>\n              <br />\n              <span class=\"caption\">{{ props.item.site.name }}</span>\n            </td>\n            <!-- 标题 -->\n            <td class=\"py-2\">\n              <a\n                class=\"subheading\"\n                :href=\"props.item.items[0].link\"\n                target=\"_blank\"\n                v-html=\"props.item.title\"\n                :title=\"props.item.title\"\n                rel=\"noopener noreferrer nofollow\"\n              ></a>\n              <div class=\"body-1\">\n                <span v-if=\"props.item.items[0].subTitle\">{{\n                  props.item.items[0].subTitle\n                }}</span>\n              </div>\n              <div class=\"caption\">\n                {{ $t(\"keepUploadTask.savePath\")\n                }}{{ props.item.downloadOptions.clientName }} ->\n                {{\n                  props.item.downloadOptions.savePath ||\n                  $t(\"keepUploadTask.defaultPath\")\n                }}\n                <DownloadTo\n                  flat\n                  icon\n                  small\n                  :title=\"$t('keepUploadTask.setSavePath')\"\n                  iconText=\"edit\"\n                  get-options-only\n                  @itemClick=\"setDownloadOptions\"\n                  :payload=\"props.item\"\n                  :downloadOptions=\"props.item.items[0]\"\n                />\n              </div>\n              <div class=\"caption\">\n                {{ $t(\"keepUploadTask.torrentCount\")\n                }}{{ props.item.items.length }}\n              </div>\n            </td>\n            <!-- 大小 -->\n            <td class=\"text-xs-right\">{{ props.item.size | formatSize }}</td>\n            <td>{{ props.item.time | formatDate }}</td>\n            <td>\n              <v-btn\n                small\n                color=\"success\"\n                icon\n                flat\n                :title=\"$t('keepUploadTask.sendBaseTorrent')\"\n                class=\"mx-0\"\n                @click.stop=\"sendBaseTorrent(props.item)\"\n              >\n                <v-icon small>filter_1</v-icon>\n              </v-btn>\n              <v-btn\n                small\n                color=\"info\"\n                icon\n                flat\n                :title=\"$t('keepUploadTask.sendOtherTorrents')\"\n                class=\"mx-0\"\n                @click.stop=\"sendOtherTorrents(props.item)\"\n              >\n                <v-icon small>filter_2</v-icon>\n              </v-btn>\n\n              <v-btn\n                small\n                color=\"primary\"\n                icon\n                flat\n                :title=\"$t('keepUploadTask.sendAllTorrents')\"\n                class=\"mx-0\"\n                @click.stop=\"sendAllTorrents(props.item)\"\n              >\n                <v-icon small>save_alt</v-icon>\n              </v-btn>\n\n              <!-- 复制下载链接 -->\n              <v-btn\n                color=\"info\"\n                small\n                icon\n                flat\n                :title=\"$t('searchTorrent.copyToClipboardTip')\"\n                @click.stop=\"copyLinksToClipboard(props.item)\"\n                class=\"mx-0\"\n              >\n                <v-icon small>file_copy</v-icon>\n              </v-btn>\n\n              <v-btn\n                small\n                color=\"error\"\n                icon\n                flat\n                @click.stop=\"removeConfirm(props.item)\"\n                class=\"mx-0\"\n                :title=\"$t('common.remove')\"\n              >\n                <v-icon small>delete</v-icon>\n              </v-btn>\n            </td>\n          </tr>\n        </template>\n\n        <template slot=\"expand\" slot-scope=\"props\">\n          <v-list subheader dense class=\"ml-5\">\n            <template v-for=\"item in props.item.items\">\n              <v-list-tile :key=\"item.link\" class=\"ml-5\">\n                <v-list-tile-avatar>\n                  <v-avatar size=\"18\">\n                    <img :src=\"item.site.icon\" />\n                  </v-avatar>\n                </v-list-tile-avatar>\n\n                <v-list-tile-content>\n                  <v-list-tile-title>\n                    <a\n                      :href=\"item.link\"\n                      target=\"_blank\"\n                      v-html=\"item.title\"\n                      :title=\"item.title\"\n                      rel=\"noopener noreferrer nofollow\"\n                    ></a>\n                  </v-list-tile-title>\n                  <v-list-tile-sub-title>{{\n                    item.subTitle\n                  }}</v-list-tile-sub-title>\n                </v-list-tile-content>\n              </v-list-tile>\n            </template>\n          </v-list>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <v-alert :value=\"true\" color=\"warning\">\n      <div>\n        警告：\n        <ul>\n          <li>\n            辅种前请确认下载服务器已关闭类似于 “自动开始下载” 的选项（如果有）。\n          </li>\n          <li>\n            助手仅对种子文件做简单验证，不保证辅种成功，请自行斟酌是否要使用辅种功能！\n          </li>\n          <li>\n            如出现因辅种失败造成的爆仓，由用户自行负责！别找我，别找我，别找我。\n          </li>\n        </ul>\n      </div>\n    </v-alert>\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"keepUploadTask.removeConfirmTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{ $t(\"keepUploadTask.removeConfirm\") }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove()\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      errorMsg\n    }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{\n      successMsg\n    }}</v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  EAction,\n  DownloadOptions,\n  Site,\n  Dictionary,\n  IKeepUploadTask\n} from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nimport { PPF } from \"@/service/public\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\n\nconst extension = new Extension();\nexport default Vue.extend({\n  components: {\n    DownloadTo\n  },\n  data() {\n    return {\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      },\n      items: [] as any[],\n      dialogRemoveConfirm: false,\n      options: this.$store.state.options,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      siteCache: {} as Dictionary<any>,\n      filterKey: \"\",\n      // 已过滤的数据\n      filteredDatas: [] as any\n    };\n  },\n\n  methods: {\n    setDownloadOptions(options: any) {\n      console.log(options);\n      options.payload.downloadOptions = options.downloadOptions;\n\n      extension.sendRequest(\n        EAction.updateKeepUploadTask,\n        null,\n        options.payload\n      );\n    },\n    clear() {\n      if (confirm(this.$t(\"keepUploadTask.clearConfirm\").toString())) {\n        extension\n          .sendRequest(EAction.clearKeepUploadTask)\n          .then((result: any) => {\n            console.log(\"clearKeepUploadTask\", result);\n            this.items = [];\n          });\n      }\n    },\n\n    removeSelected() {\n      if (this.selected && this.selected.length > 0) {\n        if (\n          confirm(\n            this.$t(\"common.removeSelectedConfirm\", {\n              count: this.selected.length\n            }).toString()\n          )\n        ) {\n          this.remove(this.selected);\n        }\n      }\n    },\n\n    remove(items: any) {\n      if (!items) {\n        items = [this.selectedItem];\n      }\n\n      extension\n        .sendRequest(EAction.removeKeepUploadTask, null, items)\n        .then((result: any) => {\n          console.log(\"removeKeepUploadTask\", result);\n          this.resetItems(result);\n        });\n      this.dialogRemoveConfirm = false;\n    },\n\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    resetItems(result: any[]) {\n      this.items = [];\n      result.forEach((data: any) => {\n        data.items.forEach((item: any) => {\n          item.site = PPF.getSiteFromHost(item.host, this.options);\n        });\n\n        data.site = PPF.getSiteFromHost(data.items[0].host, this.options);\n\n        this.items.push(data);\n      });\n    },\n    loadKeepUploadTask() {\n      extension\n        .sendRequest(EAction.loadKeepUploadTask)\n        .then((result: any[]) => {\n          console.log(result);\n          this.resetItems(result);\n        });\n    },\n    getInfos(item: IKeepUploadTask) {\n      let result = \"\";\n\n      return result;\n    },\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n    sendBaseTorrent(source: any) {\n      this.sendTorrentsInBackground(source.downloadOptions);\n    },\n    sendOtherTorrents(source: any) {\n      let items: DownloadOptions[] = [];\n      source.items.slice(1).forEach((item: any) => {\n        let downloadOptions = PPF.clone(source.downloadOptions);\n        downloadOptions = Object.assign(downloadOptions, {\n          title: item.title,\n          url: item.url,\n          link: item.link,\n          imdbId: item.imdbId\n        });\n        items.push(downloadOptions);\n      });\n\n      this.sendTorrentsInBackground(items);\n    },\n    sendAllTorrents(source: any) {\n      let items: DownloadOptions[] = [];\n      source.items.forEach((item: any) => {\n        let downloadOptions = PPF.clone(source.downloadOptions);\n        downloadOptions = Object.assign(downloadOptions, {\n          title: item.title,\n          url: item.url,\n          link: item.link,\n          imdbId: item.imdbId\n        });\n        items.push(downloadOptions);\n      });\n\n      this.sendTorrentsInBackground(items);\n    },\n    /**\n     * 发送下载任务到后台\n     */\n    sendTorrentsInBackground(items: DownloadOptions[]) {\n      console.log(items);\n\n      if (items.length > 1) {\n        if (\n          !confirm(\n            this.$t(\"keepUploadTask.sendConfirm\", {\n              count: items.length\n            }).toString()\n          )\n        ) {\n          return;\n        }\n      }\n\n      extension\n        .sendRequest(EAction.sendTorrentsInBackground, null, items)\n        .then((result: any) => {\n          this.successMsg = this.$t(\"keepUploadTask.sendSuccess\").toString();\n          console.log(\"命令执行完成\", result);\n\n          this.$emit(\"success\", result);\n        })\n        .catch((result: any) => {\n          console.log(result);\n          this.errorMsg = this.$t(\"keepUploadTask.sendError\").toString();\n        })\n        .finally(() => { });\n    },\n\n    copyLinksToClipboard(source: any) {\n      let urls: string[] = [];\n\n      source.items.forEach((item: any) => {\n        urls.push(item.url);\n      });\n\n      this.clearMessage();\n      extension\n        .sendRequest(EAction.copyTextToClipboard, null, urls.join(\"\\n\"))\n        .then(result => {\n          this.successMsg = this.$t(\n            \"searchTorrent.copySelectedToClipboardSuccess\",\n            {\n              count: urls.length\n            }\n          ).toString();\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\n            \"searchTorrent.copyLinkToClipboardError\"\n          ).toString();\n        });\n    },\n    /**\n     * 搜索结果过滤器，用于用户二次过滤\n     * @param items\n     * @param search\n     */\n     searchResultFilter(items: any[], search: string) {\n      search = search.toString().toLowerCase();\n      this.filteredDatas = [];\n      if (search.trim() === \"\") return items;\n\n      // 以空格分隔要过滤的关键字\n      let searchs = search.split(\" \");\n\n      this.filteredDatas = items.filter((item: IKeepUploadTask) => {\n        // 过滤标题和副标题\n        let source = (item.title + (item.items[0].subTitle || \"\")).toLowerCase();\n        let result = true;\n        searchs.forEach((key) => {\n          if (key.trim() != \"\") {\n            result = result && source.indexOf(key) > -1;\n          }\n        });\n        return result;\n      });\n      return this.filteredDatas;\n    },\n  },\n\n  created() {\n    this.loadKeepUploadTask();\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"keepUploadTask.headers.site\"),\n          align: \"center\",\n          width: \"60px\",\n          value: \"site.name\"\n        },\n        {\n          text: this.$t(\"keepUploadTask.headers.title\"),\n          align: \"left\",\n          value: \"title\"\n        },\n        {\n          text: this.$t(\"keepUploadTask.headers.size\"),\n          align: \"right\",\n          value: \"size\"\n        },\n        {\n          text: this.$t(\"keepUploadTask.headers.time\"),\n          align: \"left\",\n          value: \"time\"\n        },\n        {\n          text: this.$t(\"history.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  },\n\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  }\n});\n</script>\n<style lang=\"scss\" >\n.v-datatable .caption {\n  line-height: 1px!important;\n}\n</style>"
  },
  {
    "path": "src/options/views/search/Actions.vue",
    "content": "<template>\n  <div class=\"torrent-actions\">\n    <!-- 下载到 -->\n    <DownloadTo\n      :downloadOptions=\"item\"\n      flat\n      icon\n      small\n      :mini=\"$vuetify.breakpoint.smAndDown\"\n      class=\"mx-0\"\n      color=\"grey darken-1\"\n      @error=\"downloadError\"\n      @success=\"downloadSuccess\"\n    />\n\n    <!-- 复制下载链接 -->\n    <v-btn\n      flat\n      icon\n      small\n      :class=\"$vuetify.breakpoint.mdAndUp? 'mx-0': 'mx-0 btn-mini'\"\n      color=\"grey darken-1\"\n    >\n      <v-icon\n        small\n        @click=\"copyLinkToClipboard\"\n        :title=\"$t('searchTorrent.copyToClipboardTip')\"\n      >file_copy</v-icon>\n    </v-btn>\n\n    <!-- 下载种子文件 -->\n    <v-btn\n      v-if=\"downloadMethod=='POST'\"\n      flat\n      icon\n      small\n      :class=\"$vuetify.breakpoint.mdAndUp? 'mx-0': 'mx-0 btn-mini'\"\n      color=\"grey darken-1\"\n    >\n      <v-icon @click.stop=\"saveTorrentFile\" small :title=\"$t('searchTorrent.saveTip')\">save</v-icon>\n    </v-btn>\n\n    <v-btn\n      v-else\n      flat\n      icon\n      small\n      :class=\"$vuetify.breakpoint.mdAndUp? 'mx-0': 'mx-0 btn-mini'\"\n      :href=\"url\"\n      target=\"_blank\"\n      rel=\"noopener noreferrer nofollow\"\n      :title=\"$t('searchTorrent.saveTip')\"\n      color=\"grey darken-1\"\n    >\n      <v-icon small>save</v-icon>\n    </v-btn>\n\n    <!-- 收藏 -->\n    <v-btn\n      v-if=\"!isCollectioned\"\n      flat\n      icon\n      small\n      :class=\"$vuetify.breakpoint.mdAndUp? 'mx-0': 'mx-0 btn-mini'\"\n      color=\"grey darken-1\"\n      @click=\"addToCollection\"\n      :title=\"$t('collection.add')\"\n    >\n      <v-icon small>favorite_border</v-icon>\n    </v-btn>\n\n    <v-btn\n      v-else\n      flat\n      icon\n      small\n      :class=\"$vuetify.breakpoint.mdAndUp? 'mx-0': 'mx-0 btn-mini'\"\n      color=\"pink\"\n      @click=\"deleteCollection\"\n      :title=\"$t('collection.remove')\"\n    >\n      <v-icon small>favorite</v-icon>\n    </v-btn>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\nexport default Vue.extend({\n  components: {\n    DownloadTo\n  },\n  props: {\n    url: String,\n    downloadMethod: String,\n    isCollectioned: Boolean,\n    item: Object\n  },\n  methods: {\n    copyLinkToClipboard() {\n      this.$emit(\"copyLinkToClipboard\");\n    },\n    showSiteContentMenus(event: any) {\n      this.$emit(\"showSiteContentMenus\", this.item, event);\n    },\n    saveTorrentFile() {\n      this.$emit(\"saveTorrentFile\", this.item);\n    },\n    addToCollection() {\n      this.$emit(\"addToCollection\", this.item);\n    },\n    deleteCollection() {\n      this.$emit(\"deleteCollection\", this.item);\n    },\n    downloadSuccess(msg: any) {\n      this.$emit(\"downloadSuccess\", msg);\n    },\n    downloadError(msg: any) {\n      this.$emit(\"downloadError\", msg);\n    }\n  }\n});\n</script>\n<style lang=\"scss\" >\n.torrent-actions {\n  display: inline-flex;\n}\n</style>"
  },
  {
    "path": "src/options/views/search/AddToCollectionGroup.vue",
    "content": "<template>\n  <v-btn\n    :flat=\"flat\"\n    :icon=\"icon\"\n    :small=\"small\"\n    :loading=\"loading\"\n    :color=\"color\"\n    :disabled=\"disabled\"\n    @click.stop=\"showContentMenus\"\n    :class=\"$vuetify.breakpoint.smAndUp?'':'mini'\"\n    :title=\"$t('collection.add')\"\n    :dark=\"dark\"\n  >\n    <v-icon v-if=\"haveSuccess\" color=\"success\" small>done</v-icon>\n    <v-icon v-else-if=\"haveError\" color=\"red\" small>close</v-icon>\n    <v-icon v-else small>{{ iconText }}</v-icon>\n    <span class=\"ml-2\">{{ label }}</span>\n  </v-btn>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  Options,\n  EAction,\n  ICollection,\n  ICollectionGroup,\n  ECommonKey,\n  BASE_COLORS\n} from \"@/interface/common\";\n\nimport { PPF } from \"@/service/public\";\nimport Extension from \"@/service/extension\";\nconst extension = new Extension();\n\nexport default Vue.extend({\n  props: {\n    flat: Boolean,\n    icon: Boolean,\n    small: Boolean,\n    dark: Boolean,\n    iconText: {\n      type: String,\n      default: \"favorite_border\"\n    },\n\n    color: {\n      type: String,\n      default: \"success\"\n    },\n    label: {\n      type: String,\n      default: \"\"\n    },\n\n    disabled: Boolean\n  },\n\n  data() {\n    return {\n      options: this.$store.state.options as Options,\n      contentMenus: [] as any[],\n      loading: false,\n      haveSuccess: false,\n      haveError: false,\n      groups: [] as ICollectionGroup[]\n    };\n  },\n\n  methods: {\n    /**\n     * 显示上下文菜单\n     * @param options\n     * @param event\n     */\n    showContentMenus(event?: any) {\n      extension.sendRequest(EAction.getTorrentCollectionGroups).then(result => {\n        this.groups = result;\n        let menus: any[] = [];\n\n        this.groups.forEach((group: any) => {\n          menus.push({\n            title: group.name,\n            fn: () => {\n              this.$emit(\"add\", group);\n            }\n          });\n        });\n\n        menus.push({});\n        menus.push({\n          title: this.$t(\"collection.addGroup\"),\n          fn: () => {\n            this.createGroup();\n          }\n        });\n\n        PPF.showContextMenu(menus, event);\n      });\n    },\n\n    createGroup() {\n      let name = window.prompt(this.$t(\"collection.inputGroupName\").toString());\n      if (name) {\n        extension\n          .sendRequest(EAction.addTorrentCollectionGroup, null, {\n            name,\n            color: BASE_COLORS[Math.floor(Math.random() * BASE_COLORS.length)]\n          })\n          .then(result => {\n            if (result) {\n              this.$emit(\"add\", result[result.length - 1]);\n            }\n          });\n      }\n    },\n\n    clearStatus() {\n      this.haveSuccess = false;\n      this.haveError = false;\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/views/search/KeepUpload.vue",
    "content": "<template>\n  <v-dialog\n    v-model=\"dialog\"\n    persistent\n    scrollable\n    max-width=\"1024\"\n    :fullscreen=\"$vuetify.breakpoint.smAndDown\"\n  >\n    <template v-slot:activator=\"{ on }\">\n      <v-btn\n        dark\n        v-on=\"on\"\n        small\n        :class=\"$vuetify.breakpoint.smAndUp ? '' : 'mini'\"\n        :title=\"$t('keepUploadTask.keepUpload')\"\n        :color=\"color\"\n      >\n        <v-icon small>merge_type</v-icon>\n        <span class=\"ml-2\">{{ label || $t(\"keepUploadTask.keepUpload\") }}</span>\n      </v-btn>\n    </template>\n    <v-card>\n      <v-toolbar dark color=\"blue-grey darken-2\">\n        <v-toolbar-title>{{\n          $t(\"keepUploadTask.verification\")\n        }}</v-toolbar-title>\n        <v-spacer></v-spacer>\n        <v-btn\n          icon\n          flat\n          color=\"success\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/keep-upload-task\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n          :title=\"$t('common.help')\"\n        >\n          <v-icon>help</v-icon>\n        </v-btn>\n      </v-toolbar>\n      <v-card-text style=\"max-height: 80vh\">\n        <v-list two-line subheader dense>\n          <template v-for=\"(item, index) in verifiedItems\">\n            <v-subheader v-if=\"index == 0\" :key=\"index\">{{\n              $t(\"keepUploadTask.baseTorrent\")\n            }}</v-subheader>\n            <v-subheader v-if=\"index == 1\" :key=\"index\">{{\n              $t(\"keepUploadTask.otherTorrent\")\n            }}</v-subheader>\n            <v-list-tile :key=\"item.title\">\n              <v-list-tile-avatar>\n                <v-avatar size=\"18\">\n                  <img :src=\"item.data.site.icon\" />\n                </v-avatar>\n              </v-list-tile-avatar>\n\n              <v-list-tile-content>\n                <v-list-tile-title class=\"list-item\">\n                  <a\n                    :href=\"item.data.link\"\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer nofollow\"\n                    >{{ item.data.title }}</a\n                  >\n                </v-list-tile-title>\n                <v-list-tile-sub-title\n                  >{{ $t(\"keepUploadTask.size\")\n                  }}{{ item.data.size | formatSize }},\n                  {{ $t(\"keepUploadTask.fileCount\")\n                  }}{{ item.torrent ? item.torrent.files.length : \"N/A\" }},\n                  {{ $t(\"keepUploadTask.status.label\")\n                  }}{{ item.status }}</v-list-tile-sub-title\n                >\n              </v-list-tile-content>\n\n              <v-list-tile-action>\n                <div>\n                  <v-btn\n                    icon\n                    v-if=\"\n                      verifiedItems[0].verified &&\n                      !item.loading &&\n                      !item.verified &&\n                      index > 0\n                    \"\n                    :title=\"$t('keepUploadTask.addToKeepUpload')\"\n                    @click.stop=\"addToVerified(item)\"\n                    class=\"mr-1\"\n                  >\n                    <v-icon color=\"info\">add</v-icon>\n                  </v-btn>\n\n                  <v-btn\n                    icon\n                    v-if=\"\n                      verifiedItems[0].verified &&\n                      !item.loading &&\n                      !item.torrent &&\n                      index > 0\n                    \"\n                    :title=\"$t('keepUploadTask.redownload')\"\n                    @click.stop=\"reDownload(index)\"\n                    class=\"mr-1\"\n                  >\n                    <v-icon color=\"green\">sync</v-icon>\n                  </v-btn>\n\n                  <v-btn icon :loading=\"item.loading\" :title=\"item.status\">\n                    <v-icon color=\"success\" v-if=\"item.verified\"\n                      >done_all</v-icon\n                    >\n                    <v-icon color=\"error\" :title=\"$t('keepUploadTask.removeFromKeepUpload')\" @click.stop=\"deleteVerifiedItem(index)\" v-else>clear</v-icon>\n                  </v-btn>\n                </div>\n              </v-list-tile-action>\n            </v-list-tile>\n            <v-divider v-if=\"index > 0\" :key=\"'d' + index\" inset></v-divider>\n          </template>\n        </v-list>\n      </v-card-text>\n      <v-divider v-if=\"$vuetify.breakpoint.smAndDown\"></v-divider>\n      <div\n        v-if=\"$vuetify.breakpoint.smAndDown && downloadOptions\"\n        class=\"caption ml-1 py-2\"\n      >\n        {{ $t(\"keepUploadTask.savePath\")\n        }}{{\n          downloadOptions\n            ? `${downloadOptions.clientName} -> ${downloadOptions.savePath}`\n            : \"\"\n        }}\n      </div>\n      <v-divider></v-divider>\n      <v-card-actions>\n        <template v-if=\"verifiedCount > 1\">\n          <DownloadTo\n            flat\n            get-options-only\n            small\n            :label=\"\n              $vuetify.breakpoint.smAndDown\n                ? $t('keepUploadTask.setSavePath')\n                : downloadOptions\n                ? `${downloadOptions.clientName} -> ${downloadOptions.savePath}`\n                : $t('keepUploadTask.setSavePath')\n            \"\n            @itemClick=\"setDownloadOptions\"\n            :downloadOptions=\"items[0]\"\n          />\n          <v-btn\n            flat\n            small\n            @click=\"create\"\n            v-if=\"downloadOptions && verifiedItems.length > 0\"\n            :loading=\"creating\"\n            color=\"info\"\n          >\n            <v-icon small>date_range</v-icon>\n            <span class=\"ml-2\">{{ $t(\"keepUploadTask.create\") }}</span>\n          </v-btn>\n        </template>\n\n        <v-spacer></v-spacer>\n        <v-btn color=\"error\" flat @click=\"dialog = false\">{{\n          $t(\"common.close\")\n        }}</v-btn>\n      </v-card-actions>\n    </v-card>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      errorMsg\n    }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{\n      successMsg\n    }}</v-snackbar>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { SearchResultItem, EAction, IKeepUploadTask } from \"@/interface/common\";\nimport DownloadTo from \"@/options/components/DownloadTo.vue\";\nimport Extension from \"@/service/extension\";\nimport { PPF } from \"@/service/public\";\nimport { ParsedFile } from \"parse-torrent-file\";\nconst extension = new Extension();\n\ninterface IVerifiedItem {\n  data: any;\n  torrent: any;\n  loading: boolean;\n  verified: boolean;\n  status: string;\n  error: boolean;\n}\n\nexport default Vue.extend({\n  components: {\n    DownloadTo\n  },\n  data() {\n    return {\n      dialog: false,\n      verified: false,\n      baseTorrent: null as any,\n      loading: false,\n      verifiedItems: [] as any[],\n      downloadOptions: null as any,\n      creating: false,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      verifiedCount: 0\n    };\n  },\n  props: {\n    label: String,\n    color: String,\n    items: {\n      type: Array as () => SearchResultItem[],\n      default: () => {\n        return [] as SearchResultItem[];\n      }\n    }\n  },\n  mounted() { },\n  watch: {\n    dialog() {\n      if (this.dialog) {\n        this.start();\n      }\n    },\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  },\n  methods: {\n    deleteVerifiedItem(index: number){\n      this.$delete(this.verifiedItems, index);\n    },\n    setDownloadOptions(options: any) {\n      console.log(options);\n      this.downloadOptions = options.downloadOptions;\n    },\n    /**\n     * 生成辅种任务\n     */\n    create() {\n      if (this.verifiedItems.length == 0 || !this.downloadOptions) {\n        return;\n      }\n      this.creating = true;\n      let task = {\n        title: this.verifiedItems[0].data.title,\n        size: this.verifiedItems[0].data.size,\n        downloadOptions: this.downloadOptions,\n        items: [] as any[]\n      };\n\n      let items: any[] = [];\n\n      this.verifiedItems.forEach((item: IVerifiedItem) => {\n        if (item.verified) {\n          let _item = PPF.clone(item);\n          if (_item.data.site) {\n            _item.data.host = _item.data.site.host;\n            delete _item.data.site;\n          }\n\n          // 移除一些用不到的内容\n          [\n            \"author\",\n            \"category\",\n            \"comments\",\n            \"completed\",\n            \"entryName\",\n            \"status\",\n            \"tags\",\n            \"titleHTML\",\n            \"progress\",\n            \"seeders\",\n            \"leechers\"\n          ].forEach((key: string) => {\n            if (_item.data.hasOwnProperty(key)) {\n              delete _item.data[key];\n            }\n          });\n\n          items.push(_item.data);\n        }\n      });\n\n      if (items.length == 0) {\n        this.errorMsg = this.$t(\"keepUploadTask.noItem\").toString();\n        this.creating = false;\n        return;\n      }\n\n      task.items = items;\n\n      console.log(task);\n\n      extension\n        .sendRequest(EAction.createKeepUploadTask, null, task)\n        .then(result => {\n          this.successMsg = this.$t(\"keepUploadTask.createSuccess\").toString();\n          setTimeout(() => {\n            this.creating = false;\n            this.dialog = false;\n          }, 3000);\n          console.log(\"createKeepUploadTask\", result);\n\n           // 生成辅种任务后清除选择\n           this.$root.$emit('KeepUploadTaskCreateSuccess');\n        })\n        .catch(() => {\n          this.creating = false;\n          this.errorMsg = this.$t(\"keepUploadTask.createError\").toString();\n        });\n    },\n    start() {\n      this.baseTorrent = null;\n      this.verifiedItems = [];\n      this.verifiedCount = 0;\n      this.downloadOptions = null;\n      this.clearMessage();\n\n      this.items.forEach((item: SearchResultItem, index: number) => {\n      if (item.url) {\n        this.verifiedItems.push({\n          data: item,\n          torrent: null,\n          loading: true,\n          verified: false,\n          status: this.$t(\"keepUploadTask.status.downloading\").toString()\n        });\n        // requests.push(this.getTorrent(item.url, index));\n        this.getTorrent(item.url, index)\n          .then((result: any) => {\n            this.verification(result, index);\n          })\n          .catch(() => {\n            this.verification(null, index);\n          });\n        }\n      });\n    },\n    reDownload(index: number)\n    {\n      this.verifiedItems[index].loading = true;\n      this.verifiedItems[index].status = this.$t(\"keepUploadTask.status.downloading\").toString();\n\n      this.getTorrent(this.verifiedItems[index].data.url, index)\n        .then((result: any) => {\n          this.verification(result, index);\n        })\n        .catch(() => {\n          this.verification(null, index);\n        });\n    },\n    /**\n     * 验证\n     */\n    verification(item: IVerifiedItem | null, index: number) {\n      if (index == 0) {\n        if (!this.baseTorrent) {\n          this.baseTorrent = item;\n\n          this.verifiedItems[0].loading = false;\n          if (item) {\n            this.verifiedItems[0].torrent = this.baseTorrent.torrent;\n            this.verifiedItems[0].verified = true;\n            this.verifiedItems[0].status = this.$t(\n              \"keepUploadTask.status.downloaded\"\n            ).toString();\n            this.verifiedCount++;\n          } else {\n            this.verifiedItems[0].verified = false;\n          }\n        }\n      } else {\n        // 如果基准种子未下载完成，则等待\n        if (this.verifiedItems[0].loading) {\n          setTimeout(() => {\n            this.verification(item, index);\n          }, 200);\n          return;\n        }\n        let result: any = {\n          verified: false,\n          torrent: null,\n          loading: false\n        };\n\n        if (!this.verifiedItems[0].verified) {\n          result.status = this.$t(\"keepUploadTask.status.failed\").toString();\n        }\n\n        if (!item || !this.verifiedItems[0].verified) {\n          this.verifiedItems[index] = Object.assign(\n            this.verifiedItems[index],\n            result\n          );\n          return;\n        }\n\n        const torrent = item.torrent;\n        const baseTorrent = this.baseTorrent.torrent;\n\n        // 验证名称、长度、及文件列表内容是否相同\n        if (\n          torrent.name == baseTorrent.name &&\n          torrent.length == baseTorrent.length &&\n          torrent.files.length == baseTorrent.files.length\n        ) {\n          result.verified = baseTorrent.files.every(\n            (sourceFile: any, index: number) => {\n              const file = torrent.files[index];\n              return (\n                file.path == sourceFile.path && file.length == sourceFile.length\n              );\n            }\n          );\n        }\n\n        result.torrent = torrent;\n        if (result.verified) {\n          this.verifiedCount++;\n        }\n\n        result.status = result.verified\n          ? this.$t(\"keepUploadTask.status.success\").toString()\n          : this.$t(\"keepUploadTask.status.failed\").toString();\n\n        // 验证是否因为文件顺序错误或缺少文件而失败\n        if (!result.verified && torrent.name == baseTorrent.name &&\n          torrent.length <= baseTorrent.length &&\n          torrent.files.length <= baseTorrent.files.length)\n        {\n          if (torrent.files.every((file: any) =>\n          {\n            return baseTorrent.files.find((sourceFile: any) => \n              file.path == sourceFile.path && file.length == sourceFile.length\n            );\n          }))\n          {\n            if (torrent.files.length == baseTorrent.files.length)\n              result.status = this.$t(\"keepUploadTask.status.incorrectOrder\").toString();\n            else\n              result.status = this.$t(\"keepUploadTask.status.missingFiles\").toString();\n          }\n        }\n\n        this.verifiedItems[index] = Object.assign(\n          this.verifiedItems[index],\n          result\n        );\n      }\n    },\n    /**\n     * 获取种子文件内容\n     */\n    getTorrent(url: string, index: number): Promise<any> {\n      return new Promise<any>((resolve?: any, reject?: any) => {\n        extension\n          .sendRequest(EAction.getTorrentDataFromURL, null, {\n            url,\n            parseTorrent: true\n          })\n          .then(result => {\n            console.log(result);\n            this.verifiedItems[index].status = this.$t(\n              \"keepUploadTask.status.waiting\"\n            ).toString();\n            resolve(result);\n          })\n          .catch(result => {\n            this.verifiedItems[index].status = this.$t(\n              \"keepUploadTask.status.downloadFailed\"\n            ).toString();\n            this.verifiedItems[index].error = true;\n            reject(result);\n          });\n      });\n    },\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n    addToVerified(item: IVerifiedItem) {\n      if (\n        window.confirm(\n          this.$t(\"keepUploadTask.addToKeepUploadConfirm\").toString()\n        )\n      ) {\n        item.verified = true;\n        this.verifiedCount++;\n      }\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.list-item {\n  a {\n    color: #000;\n    text-decoration: none;\n  }\n\n  a:hover {\n    color: #008c00;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/search/SearchResultSnapshot.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{\n      $t(\"searchResultSnapshot.title\")\n    }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n\n        <v-btn color=\"error\" @click=\"clear\" :disabled=\"items.length == 0\">\n          <v-icon class=\"mr-2\">clear</v-icon>\n          {{ $t(\"common.clear\") }}\n        </v-btn>\n\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/search-result-snapshot\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.searchSolution.index.help\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"id\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width: 20px\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n            ></v-checkbox>\n          </td>\n          <!-- 关键字 -->\n          <td>{{ props.item.key }} {{ getInfos(props.item) }}</td>\n          <td>{{ props.item.time | formatDate }}</td>\n          <td>\n            <v-btn\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              :title=\"$t('searchResultSnapshot.show')\"\n              :to=\"`/search-torrent/show-snapshot-${props.item.id}`\"\n            >\n              <v-icon small>image_search</v-icon>\n            </v-btn>\n\n            <v-btn\n              flat\n              icon\n              small\n              class=\"mx-0\"\n              color=\"error\"\n              :title=\"$t('common.remove')\"\n              @click=\"removeConfirm(props.item)\"\n            >\n              <v-icon small>delete</v-icon>\n            </v-btn>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"searchResultSnapshot.removeConfirmTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{\n          $t(\"searchResultSnapshot.removeConfirm\")\n        }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove()\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      errorMsg\n    }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{\n      successMsg\n    }}</v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  EAction,\n  DownloadOptions,\n  Site,\n  Dictionary,\n  ISearchResultSnapshot\n} from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\n\nconst extension = new Extension();\nexport default Vue.extend({\n  data() {\n    return {\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: 10,\n        sortBy: \"time\",\n        descending: true\n      },\n      items: [] as any[],\n      dialogRemoveConfirm: false,\n      options: this.$store.state.options,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      siteCache: {} as Dictionary<any>\n    };\n  },\n\n  methods: {\n    clear() {\n      if (confirm(this.$t(\"searchResultSnapshot.clearConfirm\").toString())) {\n        extension\n          .sendRequest(EAction.clearSearchResultSnapshot)\n          .then((result: any) => {\n            console.log(\"clearSearchResultSnapshot\", result);\n            this.items = [];\n          });\n      }\n    },\n\n    removeSelected() {\n      if (this.selected && this.selected.length > 0) {\n        if (\n          confirm(\n            this.$t(\"common.removeSelectedConfirm\", {\n              count: this.selected.length\n            }).toString()\n          )\n        ) {\n          this.remove(this.selected);\n        }\n      }\n    },\n\n    remove(items: any) {\n      if (!items) {\n        items = [this.selectedItem];\n      }\n\n      extension\n        .sendRequest(EAction.removeSearchResultSnapshot, null, items)\n        .then((result: any) => {\n          console.log(\"removeSearchResultSnapshot\", result);\n          this.items = result;\n        });\n      this.dialogRemoveConfirm = false;\n    },\n\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    loadSearchResultSnapshot() {\n      extension\n        .sendRequest(EAction.loadSearchResultSnapshot)\n        .then((result: any) => {\n          console.log(\"loadSearchResultSnapshot\", result);\n          this.items = result;\n        });\n    },\n    getInfos(item: ISearchResultSnapshot) {\n      let result = \"\";\n      if (item.searchPayload) {\n        if (item.searchPayload.cn) {\n          result = item.searchPayload.cn;\n        } else if (item.searchPayload.en) {\n          result = item.searchPayload.en;\n        }\n      }\n\n      return result;\n    }\n  },\n\n  created() {\n    this.loadSearchResultSnapshot();\n  },\n\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"searchResultSnapshot.headers.key\"),\n          align: \"left\",\n          value: \"data.key\"\n        },\n        {\n          text: this.$t(\"searchResultSnapshot.headers.time\"),\n          align: \"left\",\n          value: \"time\"\n        },\n        {\n          text: this.$t(\"history.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/search/SearchTorrent.scss",
    "content": ".search-torrent {\n  .torrent {\n\n    table.v-table thead tr:not(.v-datatable__progress) th,\n    table.v-table tbody tr td {\n      padding: 10px 8px !important;\n      font-size: 12px;\n    }\n\n    table.v-table thead th:first-child:not([role]) {\n      width: 35px;\n    }\n\n    table.v-table tbody tr:nth-child(even) {\n      background-color: #f1f1f1;\n    }\n\n    table.v-table tbody tr[active='true'] {\n      background-color: #B3E5FC !important;\n    }\n\n    table.v-table.theme--dark tbody tr:nth-child(even) {\n      background-color: #1f1f1f;\n    }\n\n    table.v-table.theme--dark tbody tr[active='true'] {\n      background-color: #4c1a03 !important;\n    }\n  }\n\n  .titleCell {\n    max-width: 35vw;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    padding: 6px 0 !important;\n  }\n\n  .titleCell-mobile {\n    max-width: 90vw;\n    overflow: hidden;\n    padding: 6px 0 !important;\n  }\n\n  a {\n    color: #000;\n    text-decoration: none;\n  }\n\n  .theme--dark a {\n    color: #fff;\n    text-decoration: none;    \n  }\n\n  a:hover {\n    color: #008c00;\n  }\n\n  .theme--dark a:hover {\n    color: #ff73ff;\n  }\n\n  .sub-title {\n    word-break: break-all;\n    margin-top: 2px;\n    color: #8c8c8c !important;\n  }\n\n  .theme--dark .sub-title {\n    word-break: break-all;\n    margin-top: 2px;\n    color: #737373 !important;\n  }\n\n  .progress {\n    position: absolute;\n    bottom: 0;\n    right: 8px;\n    width: 80%;\n    opacity: .8;\n  }\n\n  .center {\n    text-align: center;\n  }\n\n  .size {\n    position: relative;\n    text-align: right;\n  }\n\n  .tag {\n    font-size: 9px;\n    margin: 0 1px;\n    border-radius: 2px;\n    color: #fff;\n    padding: 1px 3px;\n  }\n\n  .theme--dark .tag {\n    font-size: 9px;\n    margin: 0 1px;\n    border-radius: 2px;\n    color: #000;\n    padding: 1px 3px;\n  }\n\n  .theme--dark .captionText {\n    font-size: 12px;\n    color: #aaaaaa;\n  }\n\n  .captionText {\n    font-size: 12px;\n    color: #555555;\n  }\n\n  .category {\n    max-width: 120px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n\n\n  .theme--dark.v-table thead th {\n    background-color: #424242;\n  }\n\n  .theme--light.v-table thead th {\n    background-color: #ffffff;\n  }\n\n  .fixed-table {\n    max-height: 68vh;\n    backface-visibility: hidden;\n  }\n\n  .fixed-header {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n\n    table {\n      table-layout: fixed;\n    }\n\n    th {\n      position: sticky;\n      top: 0;\n      z-index: 5;\n\n      &:after {\n        content: '';\n        position: absolute;\n        left: 0;\n        bottom: 0;\n        width: 100%;\n      }\n    }\n\n    tr.v-datatable__progress th {\n      height: 1px;\n    }\n\n    .v-table__overflow {\n      flex-grow: 1;\n      flex-shrink: 1;\n      overflow-x: auto;\n      overflow-y: auto;\n    }\n\n    .v-datatable.v-table {\n      flex-grow: 0;\n      flex-shrink: 1;\n\n      .v-datatable__actions {\n        flex-wrap: nowrap;\n\n        .v-datatable__actions__pagination {\n          white-space: nowrap;\n        }\n      }\n    }\n  }\n\n  .mini {\n    min-width: unset;\n    margin: 6px 3px;\n  }\n\n  .isFixedToolbar {\n    position: fixed;\n    background-color: #fff;\n    top: 64px;\n    z-index: 3;\n    width: 100%;\n    padding: 5px;\n    box-shadow: 0px 5px 5px -1px rgba(0, 0, 0, 0.2);\n  }\n\n  .chip-compact {\n    .v-chip__content {\n      padding-right: 0px;\n    }\n  }\n}"
  },
  {
    "path": "src/options/views/search/SearchTorrent.ts",
    "content": "import Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport { Route } from \"vue-router\";\nimport {\n  EAction,\n  Site,\n  SiteSchema,\n  Dictionary,\n  DataResult,\n  EPaginationKey,\n  EModule,\n  LogItem,\n  SearchResultItem,\n  SearchResultItemTag,\n  SearchSolution,\n  Options,\n  SearchSolutionRange,\n  SearchEntry,\n  DownloadClient,\n  DownloadOptions,\n  ECommonKey,\n  ERequestMethod,\n  ISearchPayload,\n  EResourceOrderMode,\n  ICollectionGroup,\n  EViewKey,\n  EDataResultType\n} from \"@/interface/common\";\nimport { filters } from \"@/service/filters\";\nimport dayjs from \"dayjs\";\nimport { Downloader, downloadFile, FileDownloader } from \"@/service/downloader\";\nimport * as basicContext from \"basiccontext\";\nimport { PathHandler } from \"@/service/pathHandler\";\nimport MovieInfoCard from \"@/options/components/MovieInfoCard.vue\";\nimport TorrentProgress from \"@/options/components/TorrentProgress.vue\";\nimport AddToCollectionGroup from \"./AddToCollectionGroup.vue\";\nimport Actions from \"./Actions.vue\";\nimport { PPF } from \"@/service/public\";\nimport KeepUpload from \"./KeepUpload.vue\";\n\ntype searchResult = {\n  sites: Dictionary<any>;\n  tags: Dictionary<any>;\n  categories: Dictionary<any>;\n  failedSites: any[];\n  noResultsSites: any[];\n};\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  components: {\n    MovieInfoCard,\n    TorrentProgress,\n    Actions,\n    AddToCollectionGroup,\n    KeepUpload\n  },\n  data() {\n    return {\n      allSitesKey: \"allSites\",\n      key: \"\",\n      // 指定站点搜索\n      host: \"\",\n      options: this.$store.state.options as Options,\n      getters: this.$store.getters,\n      searchMsg: \"\",\n      datas: [] as any,\n      getLinks: [] as any,\n      selected: [] as any,\n      pagination: {\n        page: 1,\n        rowsPerPage: 100,\n        descending: false,\n        sortBy: \"\"\n      },\n      loading: false,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      currentSite: {} as Site,\n      skipSites: \"\",\n      beginTime: null as any,\n      reloadCount: 0,\n      searchQueue: [] as any[],\n      searchTimer: 0,\n      // 搜索结果\n      searchResult: {\n        sites: {},\n        tags: {},\n        categories: {},\n        failedSites: [],\n        noResultsSites: []\n      } as searchResult,\n      checkBox: false,\n      // 正在下载的种子文件进度信息\n      downloading: {\n        count: 0,\n        completed: 0,\n        speed: 0,\n        progress: 0\n      },\n      latestTorrentsKey: \"__LatestTorrents__\",\n      latestTorrentsOnly: false,\n      searchSiteCount: 0,\n      sending: {\n        count: 0,\n        completed: 0,\n        speed: 0,\n        progress: 0\n      },\n      showCategory: false,\n      fixedTable: false,\n      siteContentMenus: {} as any,\n      clientContentMenus: [] as any,\n      filterKey: \"\",\n      // 已过滤的数据\n      filteredDatas: [] as any,\n      showFailedSites: false,\n      showNoResultsSites: false,\n      pathHandler: new PathHandler(),\n      IMDbId: \"\",\n      // 下载失败的种子列表\n      downloadFailedTorrents: [] as FileDownloader[],\n      // 最后操作的checkbox索引\n      lastCheckedIndex: -1,\n      shiftKey: false,\n      searchPayload: {} as ISearchPayload,\n      torrentCollectionLinks: [] as string[],\n      headerOrderClickCount: 0,\n\n      currentOrderMode: EResourceOrderMode.asc,\n      rawDatas: [] as any[],\n      toolbarClass: \"mt-3\",\n      toolbarIsFixed: false\n    };\n  },\n  created() {\n    if (!this.options.system) {\n      this.writeLog({\n        event: `SearchTorrent.init`,\n        msg: this.$t(\"searchTorrent.optionsIsMissing\").toString()\n      });\n    }\n    this.pagination = this.$store.getters.pagination(\n      EPaginationKey.searchTorrent,\n      {\n        rowsPerPage: 100\n      }\n    );\n\n    let viewOptions = this.$store.getters.viewsOptions(EViewKey.searchTorrent, {\n      checkBox: false,\n      showCategory: false\n    });\n    Object.assign(this, viewOptions);\n\n    this.loadTorrentCollections();\n  },\n  mounted() {\n    // 初始化鼠标点击事件，用于按shift键多选操作\n    const downEvent = \"mousedown.torrentSearch\";\n    const upEvent = \"mouseUp.torrentSearch\";\n    $(\".search-torrent\").off(downEvent);\n    $(\".search-torrent\").off(upEvent);\n    $(\".search-torrent\").on(downEvent, (e) => {\n      this.shiftKey = e.shiftKey || false;\n    });\n\n    $(\".search-torrent\").on(upEvent, (e) => {\n      this.shiftKey = false;\n    });\n    window.addEventListener(\"scroll\", this.handleScroll);\n\n    // 生成辅种任务后清除选择\n    this.$root.$on(\"KeepUploadTaskCreateSuccess\",() => {\n      this.toggleAll();\n    });\n  },\n  destroyed() {\n    window.removeEventListener(\"scroll\", this.handleScroll);\n  },\n  beforeRouteUpdate(to: Route, from: Route, next: any) {\n    if (!to.params.key) {\n      return;\n    }\n    this.key = to.params.key;\n    this.host = to.params.host;\n    // this.$route.params\n    next();\n  },\n  /**\n   * 当前组件激活时触发\n   * 因为启用了搜索结果缓存，所以需要在这里处理关键字\n   */\n  activated() {\n    if (this.$route.params[\"key\"]) {\n      this.key = this.$route.params[\"key\"];\n    }\n\n    this.host = this.$route.params[\"host\"];\n    this.loadTorrentCollections();\n    this.handleScroll();\n  },\n  watch: {\n    key(newValue, oldValue) {\n      if (newValue && newValue != oldValue) {\n        this.doSearch();\n      }\n    },\n    host(newValue, oldValue) {\n      if (newValue && newValue != oldValue) {\n        this.doSearch();\n      }\n    },\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    },\n    \"$store.state.options.defaultSearchSolutionId\"(newValue, oldValue) {\n      // 设置为<默认>时，newValue 为空，故与 key, host 处理方式不同\n      if (newValue != oldValue) {\n        this.doSearch();\n      }\n    },\n    loading() {\n      this.$store.commit(\"updateSearchStatus\", this.loading);\n    },\n    pagination: {\n      handler() {\n        if (this.pagination.descending) {\n          this.currentOrderMode = EResourceOrderMode.desc;\n        } else {\n          this.currentOrderMode = EResourceOrderMode.asc;\n        }\n        this.updatePagination(this.pagination);\n      },\n      deep: true\n    },\n    currentOrderMode() {\n      this.pagination.descending =\n        this.currentOrderMode === EResourceOrderMode.desc;\n    },\n    checkBox() {\n      if (this.checkBox === false) {\n        this.selected = [];\n      }\n    }\n  },\n  methods: {\n    /**\n     * 记录日志\n     * @param options\n     */\n    writeLog(options: LogItem) {\n      extension.sendRequest(EAction.writeLog, null, {\n        module: EModule.options,\n        event: options.event,\n        msg: options.msg,\n        data: options.data\n      });\n    },\n    /**\n     * 延迟执行搜索\n     */\n    doSearch(searchPayload?: ISearchPayload) {\n      clearTimeout(this.searchTimer);\n      let _searchPayload: ISearchPayload;\n      if (searchPayload) {\n        _searchPayload = this.clone(searchPayload);\n      }\n      this.searchTimer = window.setTimeout(() => {\n        this.search(_searchPayload);\n      }, 220);\n    },\n    reset() {\n      this.selected = [];\n      this.clearMessage();\n      this.datas = [];\n      this.rawDatas = [];\n      this.getLinks = [];\n      this.searchResult = {\n        sites: {},\n        tags: {},\n        categories: {},\n        failedSites: [],\n        noResultsSites: []\n      } as searchResult;\n      this.filterKey = \"\";\n      this.searchPayload = {};\n    },\n    /**\n     * 开始搜索\n     */\n    search(searchPayload?: ISearchPayload) {\n      if (this.loading || !this.key) return;\n\n      this.reset();\n      if (window.location.protocol === \"http:\") {\n        $.getJSON(\n          `http://${window.location.hostname}:8001/test/searchData.json`\n        ).done((result: any) => {\n          if (result) {\n            this.addSearchResult(result);\n            // this.datas = result;\n          }\n          // console.log(result);\n        });\n        return;\n      }\n\n      if (!this.options.system) {\n        if (this.reloadCount >= 10) {\n          this.errorMsg = this.$t(\n            \"searchTorrent.optionsIsMissingErrorMsg\"\n          ).toString();\n          this.writeLog({\n            event: `SearchTorrent.init`,\n            msg: this.errorMsg\n          });\n          return;\n        }\n        setTimeout(() => {\n          this.search();\n        }, 200);\n        this.reloadCount++;\n        return;\n      }\n\n      if (!this.options.sites) {\n        this.errorMsg = this.$t(\"searchTorrent.sitesIsMissing\").toString();\n        return;\n      }\n\n      // 显示搜索快照\n      if (/(show-snapshot)-([a-z0-9]{32})/.test(this.key)) {\n        let match = this.key.match(/(show-snapshot)-([a-z0-9]{32})/);\n        if (match) {\n          this.loadSearchResultSnapshot(match[2]);\n          return;\n        }\n      }\n\n      if (searchPayload) {\n        this.searchPayload = searchPayload;\n      }\n\n      let searchKeys = {\n        id: \"\",\n        cn: \"\",\n        en: \"\",\n        key: this.key\n      };\n\n      // 当搜索关键字包含|时表示指定了多个内容，格式如下\n      // doubanid|中文名|英文名|原始搜索关键字\n      // imdbid|中文名|英文名|原始搜索关键字\n      if (this.key.indexOf(\"|\") !== -1) {\n        let tmp = (this.key + \"||\").split(\"|\");\n        searchKeys.id = tmp[0];\n        searchKeys.cn = tmp[1];\n        searchKeys.en = tmp[2];\n        searchKeys.key = tmp[3];\n\n        if (/(douban\\d+)/.test(searchKeys.id)) {\n          this.searchPayload.doubanId = (searchKeys.id as any).match(\n            /douban(\\d+)/\n          )[1];\n        } else {\n          this.searchPayload.imdbId = searchKeys.id;\n        }\n\n        this.searchPayload.cn = searchKeys.cn;\n        this.searchPayload.en = searchKeys.en;\n        this.searchPayload.key = searchKeys.key;\n      }\n\n      // 豆瓣ID\n      if (/(douban\\d+)/.test(this.key)) {\n        this.searchPayload.doubanId = (this.key as any).match(\n          /douban(\\d+)/\n        )[1];\n        this.getIMDbIdFromDouban(this.key)\n          .then((result) => {\n            if (typeof result == \"string\") {\n              this.searchPayload.imdbId = result;\n              this.key = result;\n              this.search(this.searchPayload);\n            } else {\n              if (searchKeys.cn) {\n                this.key = searchKeys.cn;\n                this.search(this.searchPayload);\n              } else {\n                this.errorMsg = this.$t(\n                  \"searchTorrent.doubanIdConversionFailed\"\n                ).toString();\n                this.searchMsg = this.errorMsg;\n                this.loading = false;\n              }\n            }\n          })\n          .catch((error) => {\n            if (searchKeys.cn) {\n              this.key = searchKeys.cn;\n              this.search(this.searchPayload);\n            } else {\n              this.errorMsg =\n                error ||\n                this.$t(\"searchTorrent.doubanIdConversionFailed\").toString();\n              this.searchMsg = this.errorMsg;\n              this.loading = false;\n            }\n          });\n        return;\n      }\n\n      let sites: Site[] = [];\n      let skipSites: string[] = [];\n      this.skipSites = \"\";\n\n      if (this.key === this.latestTorrentsKey) {\n        this.latestTorrentsOnly = true;\n      } else {\n        this.latestTorrentsOnly = false;\n      }\n\n      this.options = this.$store.state.options;\n      let searchSolutionId = this.options.defaultSearchSolutionId;\n\n      // 指定搜索方案id\n      if (/^[a-z0-9]{32}$/.test(this.host)) {\n        searchSolutionId = this.host;\n        this.host = \"\";\n      } else if (this.host === \"all\") {\n        searchSolutionId = \"all\";\n        this.host = \"\";\n      }\n\n      // 是否指定了站点\n      if (this.host) {\n        let site = this.options.sites.find((item: Site) => {\n          return item.host === this.host && !item.offline;\n        });\n        if (site) {\n          sites.push(this.clone(site));\n        }\n      } else if (\n        // 指定了搜索方案\n        searchSolutionId &&\n        this.options.searchSolutions &&\n        searchSolutionId != \"all\"\n      ) {\n        let _sites: Site[] = [];\n        this.options.sites.forEach((item: Site) => {\n          if (item.offline) return false;\n          _sites.push(this.clone(item));\n        });\n\n        let searchSolution:\n          | SearchSolution\n          | undefined = this.options.searchSolutions.find(\n            (solution: SearchSolution) => {\n              return solution.id === searchSolutionId;\n            }\n          );\n\n        if (searchSolution) {\n          searchSolution.range.forEach((range: SearchSolutionRange) => {\n            let index = _sites.findIndex((item: any) => {\n              return item.host === range.host;\n            });\n\n            if (index > -1) {\n              let site: any = _sites[index];\n              let siteEntry: SearchEntry[] = site.searchEntry;\n              if (siteEntry) {\n                siteEntry.forEach((v, index) => {\n                  siteEntry[index].enabled = false;\n                });\n                range.entry &&\n                  range.entry.forEach((key: string) => {\n                    let index: number = siteEntry.findIndex(\n                      (entry: SearchEntry) => {\n                        return entry.id == key || entry.name == key;\n                      }\n                    );\n                    if (siteEntry[index] && siteEntry[index].name) {\n                      siteEntry[index].enabled = true;\n                    }\n                  });\n              }\n\n              sites.push(site);\n            }\n          });\n        }\n      } else {\n        this.options.sites.forEach((item: Site) => {\n          if (item.offline) return false;\n\n          if (item.allowSearch || searchSolutionId == \"all\") {\n            let siteSchema: SiteSchema = this.getSiteSchema(item);\n            if (\n              siteSchema &&\n              siteSchema.searchEntry &&\n              siteSchema.searchEntry.length > 0\n            ) {\n              sites.push(this.clone(item));\n            } else if (item.searchEntry && item.searchEntry.length > 0) {\n              sites.push(this.clone(item));\n            } else {\n              skipSites.push(item.name);\n            }\n          }\n        });\n      }\n\n      if (skipSites.length > 0) {\n        this.skipSites =\n          this.$t(\"searchTorrent.skipSites\").toString() + skipSites.join(\",\");\n      }\n\n      if (sites.length === 0) {\n        this.errorMsg = this.$t(\"searchTorrent.noAllowSearchSites\").toString();\n        return;\n      }\n\n      this.searchSiteCount = sites.length;\n      this.beginTime = dayjs();\n      this.writeLog({\n        event: `SearchTorrent.Search.Start`,\n        msg: this.$t(\"searchTorrent.searchStartMsg\", {\n          count: sites.length\n        }).toString(),\n        data: {\n          key: this.key\n        }\n      });\n\n      // 保存搜索关键字\n      this.$store.dispatch(\"saveConfig\", {\n        lastSearchKey: this.searchPayload.key || this.key\n      });\n\n      this.pagination.page = 1;\n      if (/(tt\\d+)/.test(this.key)) {\n        // 提取 IMDb 编号，如果带整个网址，则只取编号部分\n        let imdb = this.key.match(/(tt\\d+)/);\n        if (imdb && imdb.length >= 2) {\n          this.key = imdb[1];\n        }\n        this.IMDbId = this.key;\n      } else {\n        this.IMDbId = \"\";\n      }\n      this.doSearchTorrentWithQueue(sites);\n    },\n\n    /**\n     * 执行搜索队列\n     */\n    doSearchTorrentWithQueue(sites: Site[]) {\n      this.loading = true;\n      this.searchMsg = this.$t(\"searchTorrent.searching\").toString();\n      sites.forEach((site: Site, index: number) => {\n        // 站点是否跳过IMDbId搜索\n        if (\n          this.IMDbId &&\n          site.searchEntryConfig &&\n          site.searchEntryConfig.skipIMDbId\n        ) {\n          return;\n        }\n        this.searchQueue.push({\n          site,\n          key: this.key\n        });\n\n        this.writeLog({\n          event: `SearchTorrent.Search.Processing`,\n          msg: this.$t(\"searchTorrent.siteIsSearching\", {\n            siteName: site.name\n          }).toString(),\n          data: {\n            host: site.host,\n            name: site.name,\n            key: this.key\n          }\n        });\n\n        this.sendSearchRequest(PPF.clone(site));\n      });\n    },\n\n    /**\n     * 发送搜索请求\n     * @param site\n     */\n    sendSearchRequest(site: Site) {\n      extension\n        .sendRequest(EAction.getSearchResult, null, {\n          key: this.latestTorrentsOnly ? \"\" : this.key,\n          site: site,\n          payload: this.searchPayload\n        })\n        .then((result: any) => {\n          if (result && result.length) {\n            this.writeLog({\n              event: `SearchTorrent.Search.Done[${site.name}]`,\n              msg: this.$t(\"searchTorrent.siteIsSearchDone\", {\n                siteName: site.name,\n                count: result.length\n              }).toString(),\n              data: {\n                host: site.host,\n                name: site.name,\n                key: this.key\n              }\n            });\n            this.addSearchResult(result);\n            return;\n          } else if (result && result.msg) {\n            this.writeLog({\n              event: `SearchTorrent.Search.Error1`,\n              msg: result.msg,\n              data: {\n                host: site.host,\n                name: site.name,\n                key: this.key\n              }\n            });\n            this.errorMsg = result.msg;\n          } else {\n            if (result && result.statusText == \"abort\") {\n              this.errorMsg = this.$t(\"searchTorrent.siteSearchAbort\", {\n                host: site.host\n              }).toString();\n            } else {\n              if (result && result.statusText == \"timeout\") {\n                this.errorMsg = this.$t(\"searchTorrent.siteSearchTimeout\", {\n                  host: site.host\n                }).toString();\n              } else {\n                this.errorMsg = this.$t(\"searchTorrent.siteSearchError\", {\n                  host: site.host\n                }).toString();\n              }\n\n              this.writeLog({\n                event: `SearchTorrent.Search.Error2`,\n                msg: this.errorMsg,\n                data: {\n                  host: site.host,\n                  name: site.name,\n                  key: this.key,\n                  result\n                }\n              });\n            }\n          }\n          this.searchResult.failedSites.push({\n            site: site,\n            msg: this.errorMsg,\n            color: \"orange darken-1\"\n          });\n        })\n        .catch((result: DataResult) => {\n          console.log(result);\n          if (result.msg) {\n            this.errorMsg = result.msg;\n          }\n          this.writeLog({\n            event: `SearchTorrent.Search.Error3`,\n            msg: result.msg,\n            data: result\n          });\n\n          if (result.data && result.data.isLogged == false) {\n            this.searchResult.failedSites.push({\n              site: site,\n              url: site.url,\n              msg: this.$t(\"searchTorrent.notLogged\").toString(),\n              color: \"grey\"\n            });\n          } else {\n            if (result.type === EDataResultType.error) {\n              this.searchResult.failedSites.push({\n                site: site,\n                url: site.url,\n                msg: result.msg || result.data || result,\n                color: \"grey\"\n              });\n            } else {\n              this.searchResult.noResultsSites.push({\n                site: site,\n                msg: result.msg || result.data || result,\n                color: \"light-blue darken-2\"\n              });\n            }\n          }\n        })\n        .finally(() => {\n          this.removeQueue(site);\n        });\n    },\n\n    /**\n     * 取消一个搜索队列\n     */\n    abortSearch(site: Site) {\n      extension\n        .sendRequest(EAction.abortSearch, null, {\n          key: this.key,\n          site: site\n        })\n        .then(() => {\n          this.writeLog({\n            event: `SearchTorrent.Search.Abort`,\n            msg: this.$t(\"searchTorrent.siteSearchAbort\", {\n              host: site.name\n            }).toString()\n          });\n        })\n        .catch(() => {\n          this.writeLog({\n            event: `SearchTorrent.Search.Abort.Error`,\n            msg: this.$t(\"searchTorrent.siteSearchAbortError\", {\n              host: site.name\n            }).toString()\n          });\n          this.removeQueue(site);\n        });\n    },\n\n    /**\n     * 移除搜索队列\n     */\n    removeQueue(site: Site) {\n      let index = this.searchQueue.findIndex((item: any) => {\n        return item.site.host === site.host;\n      });\n      if (index !== -1) {\n        this.searchQueue.splice(index, 1);\n        if (this.searchQueue.length == 0) {\n          this.searchMsg = this.$t(\"searchTorrent.searchFinished\", {\n            count: this.datas.length,\n            second: dayjs().diff(this.beginTime, \"second\", true)\n          }).toString();\n          this.loading = false;\n          this.writeLog({\n            event: `SearchTorrent.Search.Finished`,\n            msg: this.searchMsg,\n            data: {\n              key: this.key\n            }\n          });\n        }\n      }\n    },\n    /**\n     * 创建搜索结果快照\n     */\n    createSearchResultSnapshot() {\n      extension\n        .sendRequest(EAction.createSearchResultSnapshot, null, {\n          key: this.key,\n          searchPayload: this.searchPayload,\n          result: this.rawDatas\n        })\n        .then((result) => {\n          this.successMsg = this.$t(\n            \"searchResultSnapshot.createSuccess\"\n          ).toString();\n          console.log(\"createSearchResultSnapshot\", result);\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\n            \"searchResultSnapshot.createError\"\n          ).toString();\n        });\n    },\n    /**\n     * 加载搜索结果快照\n     * @param id 快照ID\n     */\n    loadSearchResultSnapshot(id: string) {\n      this.loading = true;\n      extension\n        .sendRequest(EAction.getSearchResultSnapshot, null, id)\n        .then((data) => {\n          console.log(\"loadSearchResultSnapshot\", data);\n          this.key = data.key;\n          this.searchPayload = data.searchPayload;\n          if (this.searchPayload && this.searchPayload.imdbId) {\n            this.IMDbId = this.searchPayload.imdbId;\n          } else if (/^(tt\\d+)$/.test(this.key)) {\n            this.IMDbId = this.key;\n          } else {\n            this.IMDbId = \"\";\n          }\n          this.addSearchResult(PPF.clone(data.result));\n          this.searchMsg = this.$t(\"searchResultSnapshot.snapshotTime\", {\n            time: dayjs(data.time).format(\"YYYY-MM-DD hh:mm:ss\")\n          }).toString();\n          setTimeout(() => {\n            this.loading = false;\n          }, 300);\n        });\n    },\n    /**\n     * 添加搜索结果，并组织字段格式\n     */\n    addSearchResult(result: any[]) {\n      let allSites = this.allSitesKey;\n\n      if (!this.searchResult.sites[allSites]) {\n        this.searchResult.sites[allSites] = [];\n      }\n\n      result.forEach((item: SearchResultItem) => {\n        let _item = PPF.clone(item);\n        if (_item.site) {\n          _item.host = _item.site.host;\n          delete _item.site;\n        }\n        this.rawDatas.push(_item);\n\n        // 将 // 替换为 /\n        item.link = (item.link as string)\n          .replace(\"://\", \"****\")\n          .replace(/\\/\\//g, \"/\")\n          .replace(\"****\", \"://\");\n\n        // 忽略重复的搜索结果\n        if (this.getLinks.includes(item.link)) {\n          // 跳过本次循环进行下一个元素\n          return;\n        }\n\n        if (!item.site) {\n          let host = item.host || \"\";\n          item.site = PPF.getSiteFromHost(host, this.options);\n          if (!item.site) {\n            return;\n          }\n        }\n\n        if (!item.progress && !item.status) {\n          // 对比用户信息的seedingList修改做种状态信息\n          if (item.site && item.site.user && item.site.user.seedingList) {\n            let seedingList = item.site.user.seedingList;\n            let seeding = seedingList.some(id => item.id && item.id == id);\n            if (seeding) {\n              item.progress = 100;\n              item.status = 2;\n            }\n          }\n        }\n\n        if (dayjs(item.time).isValid()) {\n          let val: number | string = item.time + \"\";\n          // 标准时间戳需要 * 1000\n          if (/^(\\d){10}$/.test(val + \"\")) {\n            item.time = parseInt(val) * 1000;\n          } else {\n            // 转成整数是为了排序\n            item.time = dayjs(val).valueOf();\n          }\n\n          // 尝试转换本地时间\n          item.time = PPF.transformTime(item.time, item.site.timezoneOffset);\n        } else if (typeof item.time == \"string\") {\n          let time = filters.timeAgoToNumber(item.time);\n          if (time > 0) {\n            item.time = time;\n          }\n        }\n\n        if (!item.titleHTML) {\n          item.titleHTML = item.title;\n        }\n        item.title = $(\"<span/>\")\n          .html(item.titleHTML)\n          .text()\n          .trim();\n        if (item.size) {\n          item.size = this.fileSizetoLength(item.size as string);\n        }\n\n        if (item.seeders && typeof item.seeders == \"string\") {\n          item.seeders = parseInt((item.seeders as string).replace(\",\", \"\"));\n          if (isNaN(item.seeders)) {\n            item.seeders = 0;\n          }\n        }\n\n        if (item.leechers && typeof item.leechers == \"string\") {\n          item.leechers = parseInt((item.leechers as string).replace(\",\", \"\"));\n          if (isNaN(item.leechers)) {\n            item.leechers = 0;\n          }\n        }\n\n        if (item.completed && typeof item.completed == \"string\") {\n          item.completed = parseInt(\n            (item.completed as string).replace(\",\", \"\")\n          );\n          if (isNaN(item.completed)) {\n            item.completed = 0;\n          }\n        }\n\n        if (item.url) {\n          item.url = item.url\n            .replace(\"://\", \"****\")\n            .replace(/\\/\\//g, \"/\")\n            .replace(\"****\", \"://\");\n        }\n\n        this.datas.push(item);\n        this.getLinks.push(item.link);\n\n        this.searchMsg = this.$t(\"searchTorrent.searchProgress\", {\n          count: this.datas.length\n        }).toString();\n\n        let siteName = item.site.name;\n        if (!this.searchResult.sites[siteName]) {\n          this.searchResult.sites[siteName] = [];\n        }\n        this.searchResult.sites[siteName].push(item);\n        this.addTagResult(item);\n        this.addCategoryResult(item);\n      });\n\n      this.searchResult.sites[allSites] = this.datas;\n    },\n\n    /**\n     * 添加搜索结果标签\n     * @param item\n     */\n    addTagResult(item: SearchResultItem) {\n      let noTag = this.$t(\"searchTorrent.noTag\").toString();\n\n      if (!this.searchResult.tags[noTag]) {\n        this.searchResult.tags[noTag] = {\n          tag: {\n            name: noTag,\n            color: \"blue-grey darken-2\"\n          },\n          items: []\n        };\n      }\n\n      if (item.tags == undefined || item.tags == null || !item.tags.length) {\n        this.searchResult.tags[noTag].items.push(item);\n        return;\n      }\n\n      item.tags.forEach((tag: SearchResultItemTag) => {\n        let name = tag.name as string;\n        if (!name) return;\n        if (!this.searchResult.tags[name]) {\n          this.searchResult.tags[name] = {\n            tag,\n            items: []\n          };\n        }\n        this.searchResult.tags[name].items.push(item);\n      });\n    },\n\n    /**\n     * 添加搜索结果分类\n     * @param item\n     */\n    addCategoryResult(item: SearchResultItem) {\n      if (!item.category) {\n        return;\n      }\n\n      let name = \"\";\n      if (typeof item.category == \"string\") {\n        name = item.category;\n        item.category = {\n          name: name\n        };\n      } else {\n        name = item.category.name as string;\n      }\n\n      if (!name) return;\n\n      if (!this.searchResult.categories[name]) {\n        this.searchResult.categories[name] = {\n          name,\n          items: []\n        };\n      }\n      this.searchResult.categories[name].items.push(item);\n    },\n    /**\n     * @return {number}\n     */\n    fileSizetoLength(size: string | number): number {\n      if (typeof size == \"number\") {\n        return size;\n      }\n      let _size_raw_match = size\n        .replace(/,/g, \"\")\n        .trim()\n        .match(/^(\\d*\\.?\\d+)(.*[^ZEPTGMK])?([ZEPTGMK](B|iB))$/i);\n      if (_size_raw_match) {\n        let _size_num = parseFloat(_size_raw_match[1]);\n        let _size_type = _size_raw_match[3];\n        switch (true) {\n          case /Zi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 70);\n          case /Ei?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 60);\n          case /Pi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 50);\n          case /Ti?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 40);\n          case /Gi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 30);\n          case /Mi?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 20);\n          case /Ki?B/i.test(_size_type):\n            return _size_num * Math.pow(2, 10);\n          default:\n            return _size_num;\n        }\n      }\n      return 0;\n    },\n\n    /**\n     * 根据指定的站点获取站点的架构信息\n     * @param site 站点信息\n     */\n    getSiteSchema(site: Site): SiteSchema {\n      let schema: SiteSchema = {};\n      if (typeof site.schema === \"string\") {\n        schema =\n          this.options.system &&\n          this.options.system.schemas &&\n          this.options.system.schemas.find((item: SiteSchema) => {\n            return item.name == site.schema;\n          });\n      }\n\n      return schema;\n    },\n    /**\n     * 发送下载链接到服务器\n     * @param url\n     * @param title\n     */\n    sendToClient(\n      url: string,\n      title?: string,\n      options?: any,\n      callback?: any,\n      link: string = \"\",\n      imdbId?: string\n    ) {\n      console.log(url);\n      this.clearMessage();\n      let host = filters.parseURL(url).host;\n      let site = this.options.sites.find((site: Site) => {\n        // 当定义了CDN列表时，匹配其中之一即可\n        if (site.cdn) {\n          let index = site.cdn.findIndex((cdn) => {\n            return cdn.indexOf(host) > -1;\n          });\n          if (index > -1) {\n            return true;\n          }\n        }\n        return site.host === host;\n      });\n      let defaultClientOptions: any = {};\n      let defaultPath: string = \"\";\n\n      if (options) {\n        defaultClientOptions = options.client;\n        defaultPath = options.path;\n      } else {\n        defaultClientOptions = this.getters.clientOptions(site);\n        defaultPath = this.getters.siteDefaultPath(site);\n      }\n\n      let savePath = this.pathHandler.getSavePath(defaultPath, site);\n      // 取消\n      if (savePath === false) {\n        this.errorMsg = this.$t(\"searchTorrent.userCanceled\").toString();\n        return;\n      }\n\n      this.haveSuccess = true;\n      this.successMsg = this.$t(\"searchTorrent.seedingTorrent\").toString();\n\n      let data: DownloadOptions = {\n        url,\n        title,\n        savePath: savePath,\n        autoStart: defaultClientOptions.autoStart,\n        tagIMDb: defaultClientOptions.tagIMDb,\n        clientId: defaultClientOptions.id,\n        link,\n        imdbId\n      };\n      this.writeLog({\n        event: \"SearchTorrent.sendTorrentToClient\",\n        msg: this.$t(\"searchTorrent.sendTorrentToClient\").toString(),\n        data\n      });\n      extension\n        .sendRequest(EAction.sendTorrentToClient, null, data)\n        .then((result: any) => {\n          console.log(\"命令执行完成\", result);\n\n          if (result.type == \"success\") {\n            this.successMsg = result.msg;\n            this.writeLog({\n              event: \"SearchTorrent.sendTorrentToClient.Success\",\n              msg: this.$t(\n                \"searchTorrent.sendTorrentToClientSuccess\"\n              ).toString(),\n              data: result\n            });\n          } else {\n            this.errorMsg = result.msg;\n            this.writeLog({\n              event: \"SearchTorrent.sendTorrentToClient.Error\",\n              msg: this.$t(\"searchTorrent.sendTorrentToClientError\").toString(),\n              data: result\n            });\n          }\n          callback && callback();\n        })\n        .catch((result: any) => {\n          this.writeLog({\n            event: \"SearchTorrent.sendTorrentToClient.Error\",\n            msg: this.$t(\"searchTorrent.sendTorrentToClientError\").toString(),\n            data: result\n          });\n          this.errorMsg = result.msg;\n          callback && callback();\n        });\n    },\n    /**\n     * 更新分页信息\n     * @param value\n     */\n    updatePagination(value: any) {\n      console.log(value);\n      this.$store.dispatch(\"updatePagination\", {\n        key: EPaginationKey.searchTorrent,\n        options: value\n      });\n    },\n    /**\n     * 获取随机字符串\n     * @param  {number} length    [长度，默认为16]\n     * @param  {boolean} noSimilar [是否包含容易混淆的字符，默认为包含]\n     * @return {string}           [返回的内容]\n     */\n    getRandomString(length: number = 16, noSimilar: boolean = true): string {\n      // 是否包含容易混淆的字符[oO,Ll,9gq,Vv,Uu,I1]，默认为包含\n      let chars = noSimilar\n        ? \"abcdefhijkmnprstwxyz2345678ABCDEFGHJKMNPQRSTWXYZ\"\n        : \"abcdefghijkmnopqrstuvwxyz0123456789ABCDEFGHIJKMNOPQRSTUVWXYZ\";\n      let maxLength = chars.length;\n      let result = [];\n      for (let i = 0; i < length; i++) {\n        result.push(chars.charAt(Math.floor(Math.random() * maxLength)));\n      }\n\n      return result.join(\"\");\n    },\n    /**\n     * 重设当前列表数据\n     * @param datas\n     */\n    resetDatas(datas: any) {\n      if (this.loading) return;\n      if (datas.length) {\n        this.pagination.page = 1;\n        this.datas = datas;\n        this.selected = [];\n      }\n    },\n    /**\n     * 下载已选中的种子文件\n     */\n    downloadSelected() {\n      let files: downloadFile[] = [];\n      this.selected.forEach((item: SearchResultItem) => {\n        item.url &&\n          files.push({\n            url: item.url,\n            fileName: `[${item.site.name}][${item.title}].torrent`,\n            method: item.site.downloadMethod,\n            timeout: this.options.connectClientTimeout\n          });\n      });\n      console.log(files);\n      if (files.length) {\n        if (files.length > 1) {\n          if (\n            !confirm(this.$t(\"searchTorrent.multiDownloadConfirm\").toString())\n          ) {\n            return;\n          }\n        }\n\n        this.downloadTorrentFiles(files);\n      }\n    },\n    /**\n     * 批量下载指定的种子文件\n     * @param files 需要下载的文件列表\n     */\n    downloadTorrentFiles(files: downloadFile[]) {\n      this.downloading.count = files.length;\n      this.downloading.completed = 0;\n      this.downloading.speed = 0;\n      this.downloading.progress = 0;\n      new Downloader({\n        files: files,\n        autoStart: true,\n        tagIMDb: true,\n        onCompleted: (file: FileDownloader) => {\n          this.downloadTorrentFilesCompleted(file);\n        },\n        onError: (file: FileDownloader, e: any) => {\n          this.downloadTorrentFilesCompleted();\n          this.writeLog({\n            event: \"SearchTorrent.downloadSelected.Error\",\n            msg: this.$t(\"searchTorrent.downloadSelectedError\", {\n              name: file.fileName\n            }).toString(),\n            data: e\n          });\n          let index = this.downloadFailedTorrents.findIndex(\n            (item: FileDownloader) => {\n              return item.url == file.url;\n            }\n          );\n          if (index == -1) {\n            this.downloadFailedTorrents.push(file);\n          }\n        }\n      });\n    },\n\n    /**\n     * 批量下载指定的种子文件完成\n     * @param file\n     */\n    downloadTorrentFilesCompleted(file?: FileDownloader) {\n      this.downloading.completed++;\n      this.downloading.progress =\n        (this.downloading.completed / this.downloading.count) * 100;\n\n      // 是否已完成\n      if (this.downloading.completed >= this.downloading.count) {\n        this.downloading.count = 0;\n        this.selected = [];\n      }\n\n      if (file) {\n        // 从失败列表中删除已完成的种子\n        for (\n          let index = 0;\n          index < this.downloadFailedTorrents.length;\n          index++\n        ) {\n          const element = this.downloadFailedTorrents[index];\n          if (element.url == file.url) {\n            this.downloadFailedTorrents.splice(index, 1);\n            break;\n          }\n        }\n      }\n    },\n\n    /**\n     * 保存当前行的种子文件\n     * @param item\n     */\n    saveTorrentFile(item: SearchResultItem) {\n      let requestMethod = ERequestMethod.GET;\n      if (item.site) {\n        requestMethod = item.site.downloadMethod || ERequestMethod.GET;\n      }\n      let url = item.url + \"\";\n      let file = new FileDownloader({\n        url,\n        timeout: this.options.connectClientTimeout,\n        fileName: `[${item.site.name}][${item.title}].torrent`\n      });\n\n      file.requestMethod = requestMethod;\n      file.onError = (error: any) => { };\n      file.start();\n    },\n    /**\n     * 发送已选择的种子到下载服务器\n     * @param datas\n     * @param count\n     */\n    sendSelectedToClient(\n      datas?: SearchResultItem[],\n      count: number = 0,\n      downloadOptions?: any\n    ) {\n      if (datas === undefined) {\n        datas = [...this.selected];\n        count = datas.length;\n        this.sending.count = count;\n        this.sending.completed = 0;\n        this.sending.speed = 0;\n        this.sending.progress = 0;\n      }\n      if (datas.length === 0) {\n        this.sending.count = 0;\n        return;\n      }\n      let data: SearchResultItem = datas.shift() as SearchResultItem;\n      console.log(data.imdbId)\n      this.sendToClient(\n        data.url as string,\n        data.title,\n        downloadOptions,\n        () => {\n          this.sending.completed++;\n          this.sending.progress =\n            (this.sending.completed / this.sending.count) * 100;\n\n          // 是否已完成\n          if (this.sending.completed >= this.sending.count) {\n            this.sending.count = 0;\n            this.selected = [];\n            return;\n          }\n          this.sendSelectedToClient(datas, count, downloadOptions);\n        },\n        data.link,\n        data.imdbId\n      );\n    },\n    /**\n     * 复制当前链接到剪切板\n     * @param url\n     */\n    copyLinkToClipboard(url: string) {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n      extension\n        .sendRequest(EAction.copyTextToClipboard, null, url)\n        .then((result) => {\n          this.successMsg = this.$t(\n            \"searchTorrent.copyLinkToClipboardSuccess\"\n          ).toString();\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\n            \"searchTorrent.copyLinkToClipboardError\"\n          ).toString();\n        });\n    },\n    getSelectedURLs() {\n      let urls: string[] = [];\n      this.selected.forEach((item: SearchResultItem) => {\n        item.url && urls.push(item.url);\n      });\n      return urls;\n    },\n    /**\n     * 复制下载链接到剪切板\n     */\n    copySelectedToClipboard() {\n      let urls: string[] = this.getSelectedURLs();\n      this.clearMessage();\n      extension\n        .sendRequest(EAction.copyTextToClipboard, null, urls.join(\"\\n\"))\n        .then((result) => {\n          this.successMsg = this.$t(\n            \"searchTorrent.copySelectedToClipboardSuccess\",\n            {\n              count: urls.length\n            }\n          ).toString();\n          this.selected = [];\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\n            \"searchTorrent.copyLinkToClipboardError\"\n          ).toString();\n        });\n    },\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n      this.haveSuccess = false;\n      this.haveError = false;\n    },\n\n    /**\n     * 根据指定的站点获取可用的下载目录及客户端信息\n     * @param site\n     */\n    getSiteContentMenus(site: Site): any[] {\n      let results: any[] = [];\n      let clients: any[] = [];\n      let host = site.host;\n      if (!host) {\n        return [];\n      }\n\n      if (this.siteContentMenus[host]) {\n        return this.siteContentMenus[host];\n      }\n\n      /**\n       * 增加下载目录\n       * @param paths\n       * @param client\n       */\n      function pushPath(paths: string[], client: any) {\n        paths.forEach((path: string) => {\n          results.push({\n            client: client,\n            path: path,\n            host: site.host\n          });\n        });\n      }\n\n      this.options.clients.forEach((client: DownloadClient) => {\n        clients.push({\n          client: client,\n          path: \"\",\n          host: site.host\n        });\n\n        if (client.paths) {\n          // 根据已定义的路径创建菜单\n          for (const host in client.paths) {\n            let paths = client.paths[host];\n\n            if (host !== site.host) {\n              continue;\n            }\n\n            pushPath(paths, client);\n          }\n\n          // 最后添加当前客户端适用于所有站点的目录\n          let publicPaths = client.paths[ECommonKey.allSite];\n          if (publicPaths) {\n            if (results.length > 0) {\n              results.push({});\n            }\n\n            pushPath(publicPaths, client);\n          }\n        }\n      });\n\n      if (results.length > 0) {\n        clients.splice(0, 0, {});\n      }\n\n      results = results.concat(clients);\n\n      this.siteContentMenus[host] = results;\n\n      return results;\n    },\n\n    /**\n     * 显示指定链接的下载服务器及目录菜单\n     * @param options\n     * @param event\n     */\n    showSiteContentMenus(options: SearchResultItem, event?: any) {\n      let items = this.getSiteContentMenus(options.site);\n      let menus: any[] = [];\n\n      items.forEach((item: any) => {\n        if (item.client && item.client.name) {\n          menus.push({\n            title: this.$t(\"searchTorrent.downloadTo\", {\n              path:\n                `${item.client.name} -> ${item.client.address}` +\n                (item.path\n                  ? ` -> ${this.pathHandler.replacePathKey(\n                    item.path,\n                    options.site\n                  )}`\n                  : \"\"),\n            }).toString(),\n            fn: () => {\n              if (options.url) {\n                // console.log(options, item);\n                this.sendToClient(\n                  options.url,\n                  options.title,\n                  item,\n                  null,\n                  options.link,\n                  options.imdbId\n                );\n              }\n            }\n          });\n        } else {\n          menus.push({});\n        }\n      });\n\n      console.log(items, menus);\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    },\n\n    /**\n     * 显示批量下载时可用下载服务器菜单\n     * @param event\n     */\n    showAllContentMenus(event: any) {\n      let clients: any[] = [];\n      let menus: any[] = [];\n      let _this = this;\n\n      function addMenu(item: any) {\n        let title = _this.$vuetify.breakpoint.xs\n          ? item.client.name\n          : _this\n            .$t(\"searchTorrent.downloadTo\", {\n              path: `${item.client.name} -> ${item.client.address}`\n            })\n            .toString();\n\n        if (item.path) {\n          title += ` -> ${item.path}`;\n        }\n        menus.push({\n          title: title,\n          fn: () => {\n            _this.sendSelectedToClient(undefined, 0, item);\n          }\n        });\n      }\n\n      if (this.clientContentMenus.length == 0) {\n        this.options.clients.forEach((client: DownloadClient) => {\n          clients.push({\n            client: client,\n            path: \"\"\n          });\n        });\n        clients.forEach((item: any) => {\n          if (item.client && item.client.name) {\n            addMenu(item);\n\n            if (item.client.paths) {\n              // 添加适用于所有站点的目录\n              let publicPaths = item.client.paths[ECommonKey.allSite];\n              if (publicPaths) {\n                publicPaths.forEach((path: string) => {\n                  // 去除带关键字的目录\n                  if (\n                    path.indexOf(\"$site.name$\") == -1 &&\n                    path.indexOf(\"$site.host$\") == -1 &&\n                    path.indexOf(\"<...>\") == -1\n                  ) {\n                    let _item = this.clone(item);\n                    _item.path = path;\n                    addMenu(_item);\n                  }\n                });\n              }\n            }\n          } else {\n            menus.push({});\n          }\n        });\n        this.clientContentMenus = menus;\n      } else {\n        menus = this.clientContentMenus;\n      }\n\n      basicContext.show(menus, event);\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    },\n\n    /**\n     * 重新搜索指定的站点\n     * @param host\n     */\n    reSearchWithSite(host: string) {\n      // 重新获取站点信息\n      const site = this.options.sites.find((item: Site) => {\n        return item.host === host;\n      });\n\n      if (!site) {\n        return;\n      }\n\n      let index = this.searchResult.failedSites.findIndex((item: any) => {\n        return item.site.host === host;\n      });\n\n      if (index !== -1) {\n        this.searchResult.failedSites.splice(index, 1);\n      }\n\n      index = this.searchResult.noResultsSites.findIndex((item: any) => {\n        return item.site.host === host;\n      });\n\n      if (index !== -1) {\n        this.searchResult.noResultsSites.splice(index, 1);\n      }\n\n      this.doSearchTorrentWithQueue([site]);\n    },\n\n    /**\n     * 重新搜索失败的站点\n     */\n    reSearchFailedSites() {\n      if (this.searchResult.failedSites.length == 0) {\n        return false;\n      }\n\n      let sites: Site[] = [];\n      this.searchResult.failedSites.forEach((item: any) => {\n        sites.push(item.site);\n      });\n\n      if (sites.length === 0) {\n        this.errorMsg = this.$t(\"searchTorrent.noReSearchSites\").toString();\n        return;\n      }\n\n      this.searchResult.failedSites = [];\n\n      this.beginTime = dayjs();\n      this.writeLog({\n        event: `SearchTorrent.Search.Start`,\n        msg: this.$t(\"searchTorrent.searchStartMsg\", {\n          count: sites.length\n        }).toString(),\n        data: {\n          key: this.key\n        }\n      });\n\n      this.doSearchTorrentWithQueue(sites);\n    },\n\n    /**\n     * 用JSON对象模拟对象克隆\n     * @param source\n     */\n    clone(source: any) {\n      return JSON.parse(JSON.stringify(source));\n    },\n\n    /**\n     * 搜索结果过滤器，用于用户二次过滤\n     * @param items\n     * @param search\n     */\n    searchResultFilter(items: any[], search: string) {\n      search = search.toString().toLowerCase();\n      this.filteredDatas = [];\n      if (search.trim() === \"\") return items;\n\n      // 以空格分隔要过滤的关键字\n      let searchs = search.split(\" \");\n\n      this.filteredDatas = items.filter((item: SearchResultItem) => {\n        // 过滤标题和副标题\n        let source = (item.title + (item.subTitle || \"\")).toLowerCase();\n        let result = true;\n        searchs.forEach((key) => {\n          if (key.trim() != \"\") {\n            result = result && source.indexOf(key) > -1;\n          }\n        });\n        return result;\n      });\n      return this.filteredDatas;\n    },\n\n    getIMDbIdFromDouban(doubanId: string) {\n      let match = doubanId.match(/douban(\\d+)/);\n      if (match && match.length >= 2) {\n        this.searchMsg = this.$t(\"searchTorrent.doubanIdConverting\").toString();\n        return extension.sendRequest(\n          EAction.getIMDbIdFromDouban,\n          null,\n          match[1]\n        );\n      } else {\n        return new Promise<any>((resolve?: any, reject?: any) => {\n          reject(this.$t(\"searchTorrent.invalidDoubanId\").toString());\n        });\n      }\n    },\n\n    /**\n     * 重新下载失败的种子文件\n     */\n    reDownloadFailedTorrents() {\n      this.downloadTorrentFiles(this.downloadFailedTorrents);\n    },\n\n    /**\n     * shift键多选操作\n     * @param selected 是否被选中\n     * @param index 当前索引\n     */\n    shiftCheck(selected: boolean, index: number) {\n      if (this.lastCheckedIndex === -1) {\n        this.lastCheckedIndex = index;\n        return;\n      }\n      if (this.shiftKey) {\n        let start = index;\n        let end = this.lastCheckedIndex;\n        let startIndex = Math.min(start, end);\n        let endIndex = Math.max(start, end) + 1;\n        let datas = this.clone(this.filteredDatas.length > 0 ? this.filteredDatas : this.datas);\n\n        datas = datas.sort(\n          this.arrayObjectSort(\n            this.pagination.sortBy,\n            this.pagination.descending\n              ? EResourceOrderMode.desc\n              : EResourceOrderMode.asc\n          )\n        );\n\n        for (let i = startIndex; i < endIndex; i++) {\n          let data = datas[i];\n          let _index = this.selected.findIndex((_item: any) => {\n            return _item.link === data.link;\n          });\n\n          if (selected) {\n            if (_index === -1) {\n              this.selected.push(data);\n            }\n          } else {\n            if (_index !== -1) {\n              this.selected.splice(_index, 1);\n            }\n          }\n        }\n      }\n      this.lastCheckedIndex = index;\n    },\n    /**\n     * 对指定的对象进行排序\n     * @param field 字段\n     * @param sortOrder 排序方式\n     */\n    arrayObjectSort(\n      field: string,\n      sortOrder: EResourceOrderMode = EResourceOrderMode.asc\n    ) {\n      // 深层获取对象指定的属性值\n      function getObjectValue(obj: any, path: string) {\n        return new Function(\"o\", \"return o.\" + path)(obj);\n      }\n      return function (object1: any, object2: any) {\n        var value1 = getObjectValue(object1, field);\n        var value2 = getObjectValue(object2, field);\n        if (value1 < value2) {\n          if (sortOrder == EResourceOrderMode.desc) {\n            return 1;\n          } else return -1;\n        } else if (value1 > value2) {\n          if (sortOrder == EResourceOrderMode.desc) {\n            return -1;\n          } else return 1;\n        } else {\n          return 0;\n        }\n      };\n    },\n    addSelectedToCollection(group: ICollectionGroup) {\n      this.selected.forEach((item: SearchResultItem) => {\n        if (item.url) {\n          this.addToCollection(item, group);\n        }\n      });\n    },\n    /**\n     * 添加到收藏\n     * @param item 当前种子相关信息\n     * @param group 收藏分组信息\n     */\n    addToCollection(item: any, group?: ICollectionGroup) {\n      let options: any = {\n        title: item.title,\n        url: item.url,\n        link: item.link,\n        host: item.site.host,\n        size: item.size,\n        subTitle: item.subTitle,\n        movieInfo: {\n          imdbId: this.IMDbId || this.searchPayload.imdbId,\n          doubanId: this.searchPayload.doubanId\n        }\n      };\n\n      if (group && group.id) {\n        options.groups = [group.id];\n      }\n\n      extension\n        .sendRequest(EAction.addTorrentToCollection, null, options)\n        .then((result) => {\n          this.loadTorrentCollections();\n          console.log(result);\n        });\n    },\n    deleteCollection(item: any) {\n      extension\n        .sendRequest(EAction.deleteTorrentFromCollention, null, {\n          link: PPF.getCleaningURL(item.link)\n        })\n        .then((result) => {\n          this.loadTorrentCollections();\n        });\n    },\n    loadTorrentCollections() {\n      extension\n        .sendRequest(EAction.getAllTorrentCollectionLinks)\n        .then((result) => {\n          this.torrentCollectionLinks = result;\n        });\n    },\n    isCollectioned(link: string): boolean {\n      return this.torrentCollectionLinks.includes(PPF.getCleaningURL(link));\n    },\n    /**\n     * 全选/反选\n     */\n    toggleAll() {\n      // 当有内容被选中时，取消选择\n      if (this.selected.length > 0) {\n        this.selected = [];\n\n        // 当有过滤数据时，返回已过滤的数据\n      } else if (this.filteredDatas.length > 0) {\n        this.selected = this.filteredDatas.slice();\n      } else {\n        this.selected = this.datas.slice();\n      }\n    },\n    changeSort(column: string) {\n      if (this.pagination.sortBy === column) {\n        this.pagination.descending = !this.pagination.descending;\n        this.headerOrderClickCount++;\n        if (this.headerOrderClickCount == 2) {\n          this.pagination.sortBy = \"\";\n        }\n      } else {\n        this.headerOrderClickCount = 0;\n        this.pagination.sortBy = column;\n        this.pagination.descending = false;\n      }\n    },\n    getHeaderClass(header: any) {\n      let result: string[] = [];\n      result.push(\"column\");\n\n      if (header.sortable !== false) {\n        result.push(\"sortable\");\n\n        result.push(this.pagination.descending ? \"desc\" : \"asc\");\n        if (header.value === this.pagination.sortBy) {\n          result.push(\"active\");\n        }\n      }\n\n      if (header.align) {\n        result.push(`text-xs-${header.align}`);\n      }\n\n      return result;\n    },\n\n    downloadSuccess(msg: string) {\n      this.successMsg = msg;\n    },\n\n    downloadError(msg: string) {\n      this.errorMsg = msg;\n    },\n\n    updateViewOptions() {\n      this.$store.dispatch(\"updateViewOptions\", {\n        key: EViewKey.searchTorrent,\n        options: {\n          checkBox: this.checkBox,\n          showCategory: this.showCategory\n        }\n      });\n    },\n    handleScroll() {\n      const divToolbar: any = $(\"#divToolbar\");\n      if (!divToolbar || !divToolbar.offset()) {\n        return;\n      }\n      const sysTopBar: any = $(\"#system-topbar\");\n      const top = sysTopBar.height();\n      const scrollTop =\n        window.pageYOffset ||\n        document.documentElement.scrollTop ||\n        document.body.scrollTop;\n      const offsetTop = divToolbar.offset().top;\n      if (scrollTop + top > offsetTop) {\n        this.toolbarClass = \"isFixedToolbar\";\n        this.toolbarIsFixed = true;\n        const height = $(\"#divToobarInner\").height() || 0;\n        $(\"#divToobarHeight\").height(height);\n        $(\"#divToobarInner\").css({\n          top: top\n        });\n      } else {\n        this.toolbarIsFixed = false;\n        this.toolbarClass = \"mt-3\";\n      }\n    }\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"searchTorrent.headers.site\"),\n          align: \"center\",\n          value: this.$store.state.options.searchResultOrderBySitePriority\n            ? \"site.priority\"\n            : \"site.host\",\n          visible: this.$vuetify.breakpoint.mdAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.title\"),\n          align: \"left\",\n          value: \"title\",\n          visible: true,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.category\"),\n          align: \"center\",\n          value: \"category.name\",\n          width: \"150px\",\n          visible: this.$vuetify.breakpoint.width > 1200,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.size\"),\n          align: \"right\",\n          value: \"size\",\n          width: \"100px\",\n          visible: this.$vuetify.breakpoint.smAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.seeders\"),\n          align: \"right\",\n          value: \"seeders\",\n          width: \"60px\",\n          visible: this.$vuetify.breakpoint.smAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.leechers\"),\n          align: \"right\",\n          value: \"leechers\",\n          width: \"60px\",\n          visible: this.$vuetify.breakpoint.mdAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.completed\"),\n          align: \"right\",\n          value: \"completed\",\n          width: \"60px\",\n          visible: this.$vuetify.breakpoint.mdAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.comments\"),\n          align: \"right\",\n          value: \"comments\",\n          width: \"60px\",\n          visible: this.$vuetify.breakpoint.smAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.time\"),\n          align: \"left\",\n          value: \"time\",\n          width: \"130px\",\n          visible: this.$vuetify.breakpoint.mdAndUp,\n        },\n        {\n          text: this.$t(\"searchTorrent.headers.action\"),\n          sortable: false,\n          width: this.$vuetify.breakpoint.mdAndUp ? \"130px\" : \"80px\",\n          align: \"center\",\n          visible: this.$vuetify.breakpoint.smAndUp,\n        },\n      ];\n    },\n    orderHeaders(): Array<any> {\n      return this.headers.filter((item) => {\n        return item.sortable !== false;\n      });\n    },\n    orderMode(): Array<any> {\n      return [\n        {\n          text: this.$t(\"common.orderMode.asc\"),\n          value: EResourceOrderMode.asc\n        },\n        {\n          text: this.$t(\"common.orderMode.desc\"),\n          value: EResourceOrderMode.desc\n        }\n      ];\n    },\n    indeterminate(): boolean {\n      if (\n        this.selected.length > 0 &&\n        this.selected.length < this.datas.length\n      ) {\n        return true;\n      }\n      return false;\n    },\n    // 已选中的种子大小\n    selectedSize(): number {\n      if (this.selected.length > 0) {\n        let totalSize = 0;\n        this.selected.forEach((item: SearchResultItem) => {\n          const size: any = item.size;\n          if (size > 0) {\n            totalSize += size;\n          }\n        });\n\n        return totalSize;\n      }\n      return 0;\n    }\n  }\n});\n"
  },
  {
    "path": "src/options/views/search/SearchTorrent.vue",
    "content": "<template>\n  <div class=\"search-torrent\">\n    <MovieInfoCard\n      :IMDbId=\"IMDbId\"\n      :doubanId=\"searchPayload.doubanId\"\n      v-if=\"!!options.showMoiveInfoCardOnSearch\"\n    />\n    <v-alert :value=\"true\" type=\"info\" style=\"padding:8px 16px;\">\n      {{ $t(\"searchTorrent.title\") }} [{{ key }}], {{ searchMsg }}\n      {{ skipSites }}\n      <v-btn\n        flat\n        icon\n        small\n        color=\"white\"\n        @click.stop=\"doSearch(searchPayload)\"\n        :title=\"$t('searchTorrent.reSearch')\"\n        v-if=\"!loading && key != ''\"\n      >\n        <v-icon>cached</v-icon>\n      </v-btn>\n\n      <!-- 无结果的站点 -->\n      <v-btn\n        v-if=\"searchResult.noResultsSites.length > 0\"\n        class=\"mt-1\"\n        flat\n        small\n        color=\"white\"\n        @click.stop=\"showNoResultsSites = !showNoResultsSites\"\n      >\n        <v-icon small class=\"mr-1\" color=\"grey darken-2\">face</v-icon>\n        {{ $t(\"searchTorrent.noResultsSites\") }}\n        {{ searchResult.noResultsSites.length }}\n      </v-btn>\n\n      <!-- 失败的站点 -->\n      <v-btn\n        v-if=\"searchResult.failedSites.length > 0\"\n        class=\"mt-1\"\n        flat\n        small\n        color=\"white\"\n        @click.stop=\"showFailedSites = !showFailedSites\"\n      >\n        <v-icon small class=\"mr-1\" color=\"orange\">warning</v-icon>\n        {{ $t(\"searchTorrent.failedSites\") }}\n        {{ searchResult.failedSites.length }}\n      </v-btn>\n\n      <v-btn\n        v-if=\"searchResult.failedSites.length > 0 && showFailedSites\"\n        class=\"mt-1\"\n        flat\n        small\n        color=\"white\"\n        @click.stop=\"reSearchFailedSites\"\n      >\n        <v-icon small class=\"mr-1\">autorenew</v-icon>\n        {{ $t(\"searchTorrent.reSearchFailedSites\") }}\n      </v-btn>\n    </v-alert>\n    <!-- 搜索队列-->\n    <v-list small v-if=\"searchQueue && searchQueue.length\">\n      <template v-for=\"(item, index) in searchQueue\">\n        <v-list-tile :key=\"item.site.host\">\n          <v-list-tile-action>\n            <v-progress-circular :size=\"18\" :width=\"2\" indeterminate color=\"primary\"></v-progress-circular>\n          </v-list-tile-action>\n\n          <v-list-tile-content>\n            <v-list-tile-title>\n              <v-avatar size=\"18\" class=\"mr-2\">\n                <img :src=\"item.site.icon\" />\n              </v-avatar>\n              {{ item.site.name }} {{ $t(\"searchTorrent.searching\") }}\n            </v-list-tile-title>\n          </v-list-tile-content>\n\n          <v-list-tile-action class=\"mr-5\">\n            <v-icon\n              @click=\"abortSearch(item.site)\"\n              color=\"red\"\n              :title=\"$t('searchTorrent.cancelSearch')\"\n            >cancel</v-icon>\n          </v-list-tile-action>\n        </v-list-tile>\n        <v-divider v-if=\"index + 1 < searchQueue.length\" :key=\"'line' + item.site.host + index\"></v-divider>\n      </template>\n    </v-list>\n\n    <!-- 搜索结果列表 -->\n    <v-card>\n      <v-card-title style=\"padding: 0 5px 0 3px;\">\n        <v-flex xs12>\n          <!-- 站点返回的搜索结果 -->\n          <div v-if=\"searchSiteCount > 1\">\n            <template v-for=\"(item, key) in searchResult.sites\">\n              <v-chip\n                :key=\"key\"\n                label\n                :color=\"item.length ? 'blue-grey darken-2' : 'grey'\"\n                text-color=\"white\"\n                small\n                class=\"mr-1 py-3 pl-1\"\n                @click.stop=\"resetDatas(item)\"\n                :disabled=\"!item.length\"\n              >\n                <v-icon class=\"mr-1\" left v-if=\"key === allSitesKey\">public</v-icon>\n                <template v-else>\n                  <v-avatar class=\"mr-1\" v-if=\"item.length > 0\">\n                    <img :src=\"item[0].site.icon\" style=\"width:60%;height:60%;\" />\n                  </v-avatar>\n                  <v-avatar class=\"mr-1\" v-else>\n                    <img :src=\"item.site.icon\" style=\"width:60%;height:60%;\" />\n                  </v-avatar>\n                </template>\n                <span>\n                  {{\n                  key === allSitesKey ? $t(\"searchTorrent.allSites\") : key\n                  }}\n                </span>\n                <v-chip\n                  label\n                  :color=\"item.length ? 'blue-grey' : 'grey'\"\n                  small\n                  text-color=\"white\"\n                  style=\"margin-right:-13px;\"\n                  class=\"ml-2 py-3\"\n                  disabled\n                >\n                  <span>{{ item.length || item.msg }}</span>\n                </v-chip>\n              </v-chip>\n            </template>\n          </div>\n\n          <!-- 无结果的站点 -->\n          <div v-if=\"searchResult.noResultsSites.length > 0 && showNoResultsSites\">\n            <template v-for=\"(item, index) in searchResult.noResultsSites\">\n              <v-chip\n                :key=\"index\"\n                label\n                color=\"grey darken-1\"\n                text-color=\"white\"\n                small\n                class=\"mr-1 py-3 pl-1\"\n                disabled\n              >\n                <template>\n                  <v-avatar class=\"mr-1\">\n                    <img :src=\"item.site.icon\" style=\"width:60%;height:60%;\" />\n                  </v-avatar>\n                </template>\n                <a\n                  v-if=\"item.site.activeURL || item.site.url\"\n                  :href=\"item.site.activeURL || item.site.url\"\n                  rel=\"noopener noreferrer nofollow\"\n                  target=\"_blank\"\n                >{{ item.site.name }}</a>\n                <span v-else>{{ item.site.name }}</span>\n                <v-chip\n                  label\n                  color=\"grey\"\n                  small\n                  text-color=\"white\"\n                  style=\"margin-right:-13px;\"\n                  class=\"ml-2 py-3 chip-compact\"\n                  disabled\n                >\n                  <span>{{ item.msg }}</span>\n\n                  <v-btn\n                    flat\n                    icon\n                    small\n                    color=\"grey lighten-2\"\n                    @click.stop=\"reSearchWithSite(item.site.host)\"\n                    :title=\"$t('searchTorrent.reSearch')\"\n                  >\n                    <v-icon small>refresh</v-icon>\n                  </v-btn>\n                </v-chip>\n              </v-chip>\n            </template>\n          </div>\n\n          <!-- 站点返回的失败的站点 -->\n          <div v-if=\"searchResult.failedSites.length > 0 && showFailedSites\">\n            <template v-for=\"(item, index) in searchResult.failedSites\">\n              <v-chip\n                :key=\"index\"\n                label\n                color=\"orange darken-3\"\n                text-color=\"white\"\n                small\n                class=\"mr-1 py-3 pl-1\"\n                disabled\n              >\n                <template>\n                  <v-avatar class=\"mr-1\">\n                    <img :src=\"item.site.icon\" style=\"width:60%;height:60%;\" />\n                  </v-avatar>\n                </template>\n                <span>{{ item.site.name }}</span>\n                <v-chip\n                  label\n                  :color=\"item.color\"\n                  small\n                  text-color=\"white\"\n                  style=\"margin-right:-13px;\"\n                  class=\"ml-2 py-3\"\n                  disabled\n                >\n                  <a\n                    v-if=\"item.url\"\n                    :href=\"item.url\"\n                    rel=\"noopener noreferrer nofollow\"\n                    target=\"_blank\"\n                  >{{ item.msg }}</a>\n                  <span v-if=\"!item.url\">{{ item.msg }}</span>\n\n                  <v-btn\n                    flat\n                    icon\n                    small\n                    color=\"grey lighten-2\"\n                    @click.stop=\"reSearchWithSite(item.site.host)\"\n                    :title=\"$t('searchTorrent.reSearch')\"\n                  >\n                    <v-icon small>refresh</v-icon>\n                  </v-btn>\n                </v-chip>\n              </v-chip>\n            </template>\n          </div>\n        </v-flex>\n        <v-flex xs6>\n          <!-- 标签列表 -->\n          <div class=\"mt-1\">\n            <template v-for=\"(item, key) in searchResult.tags\">\n              <v-chip\n                :key=\"key\"\n                label\n                :color=\"item.tag.color\"\n                text-color=\"white\"\n                small\n                class=\"mr-1 pl-0\"\n                @click.stop=\"resetDatas(item.items)\"\n              >\n                <span>{{ key }}</span>\n                <v-chip\n                  label\n                  color=\"blue-grey\"\n                  small\n                  text-color=\"white\"\n                  style=\"margin-right:-13px;\"\n                  class=\"ml-2\"\n                  disabled\n                >\n                  <span>{{ item.items.length }}</span>\n                </v-chip>\n              </v-chip>\n            </template>\n          </div>\n          <!-- 分类列表 -->\n          <div class=\"mt-1\" v-if=\"showCategory\">\n            <template v-for=\"(item, key) in searchResult.categories\">\n              <v-chip\n                :key=\"key\"\n                label\n                color=\"grey darken-1\"\n                text-color=\"white\"\n                small\n                class=\"mr-1 pl-0\"\n                @click.stop=\"resetDatas(item.items)\"\n              >\n                <span>{{ key }}</span>\n                <v-chip\n                  label\n                  color=\"grey\"\n                  small\n                  text-color=\"white\"\n                  style=\"margin-right:-13px;\"\n                  class=\"ml-2\"\n                  disabled\n                >\n                  <span>{{ item.items.length }}</span>\n                </v-chip>\n              </v-chip>\n            </template>\n          </div>\n        </v-flex>\n\n        <v-flex xs6>\n          <div>\n            <v-text-field\n              v-model=\"filterKey\"\n              append-icon=\"search\"\n              :label=\"$t('searchTorrent.filterSearchResults')\"\n              single-line\n              hide-details\n              enterkeyhint=\"search\"\n            ></v-text-field>\n          </div>\n        </v-flex>\n      </v-card-title>\n\n      <!-- 操作按钮列表 -->\n      <div ref=\"divToolbar\" id=\"divToolbar\">\n        <div v-show=\"toolbarIsFixed\" id=\"divToobarHeight\"></div>\n        <div id=\"divToobarInner\" :class=\"toolbarClass\">\n          <!-- 排序，小屏幕显示 -->\n          <div v-if=\"$vuetify.breakpoint.smAndDown\" style=\"display: inline-flex;\">\n            <v-flex xs6 class=\"px-2\" style=\"height: 50px;\">\n              <v-select\n                :items=\"orderHeaders\"\n                :label=\"$t('common.orderBy')\"\n                v-model=\"pagination.sortBy\"\n              ></v-select>\n            </v-flex>\n            <v-flex xs6 class=\"px-0\" style=\"height: 50px;\">\n              <v-radio-group v-model=\"currentOrderMode\" row>\n                <v-radio\n                  class=\"mr-2\"\n                  v-for=\"(item, index) in orderMode\"\n                  :key=\"index\"\n                  :label=\"item.text\"\n                  :value=\"item.value\"\n                ></v-radio>\n              </v-radio-group>\n            </v-flex>\n          </div>\n\n          <div style=\"display: inline-flex;overflow-x:auto;width: 100%;overflow-y:hidden;\">\n            <!-- 行选择框，当工具栏被固定时显示 -->\n            <v-checkbox\n              v-show=\"checkBox && toolbarIsFixed\"\n              :indeterminate=\"indeterminate\"\n              style=\"margin: 8px 0 0 3px;padding: 0;height: 32px;flex: unset;\"\n              @click.stop=\"toggleAll\"\n              :value=\"selected.length > 0 && selected.length == datas.length\"\n            ></v-checkbox>\n            <template v-if=\"selected.length > 0\">\n              <!-- 发送到下载服务器 -->\n              <v-btn\n                :disabled=\"selected.length == 0\"\n                color=\"success\"\n                small\n                :title=\"$t('searchTorrent.sendToClientTip')\"\n                @click.stop=\"showAllContentMenus($event)\"\n                :class=\"$vuetify.breakpoint.smAndUp ? '' : 'mini'\"\n              >\n                <v-icon small>cloud_download</v-icon>\n                <span class=\"ml-2\" v-if=\"$vuetify.breakpoint.smAndUp\">\n                  {{ $t(\"searchTorrent.sendToClient\") }} ({{\n                  selected.length\n                  }})\n                </span>\n                <span class=\"ml-2\" v-else>{{ selected.length }}</span>\n                <span class=\"ml-1\">{{ selectedSize | formatSize }}</span>\n              </v-btn>\n\n              <!-- 文件发送进度 -->\n              <v-progress-circular\n                v-if=\"sending.count > 0\"\n                :rotate=\"-90\"\n                :size=\"60\"\n                :width=\"10\"\n                :value=\"sending.progress\"\n                color=\"primary\"\n              >{{ sending.progress.toFixed(0) }}%</v-progress-circular>\n\n              <!-- 复制下载链接 -->\n              <v-btn\n                :disabled=\"selected.length == 0\"\n                color=\"success\"\n                small\n                :title=\"$t('searchTorrent.copyToClipboardTip')\"\n                @click=\"copySelectedToClipboard()\"\n                :class=\"$vuetify.breakpoint.smAndUp ? '' : 'mini'\"\n              >\n                <v-icon small>file_copy</v-icon>\n                <span class=\"ml-2\" v-if=\"$vuetify.breakpoint.smAndUp\">\n                  {{ $t(\"searchTorrent.copyToClipboard\") }} ({{\n                  selected.length\n                  }})\n                </span>\n                <span class=\"ml-2\" v-else>{{ selected.length }}</span>\n              </v-btn>\n\n              <!-- 保存种子文件 -->\n              <v-btn\n                :disabled=\"selected.length == 0\"\n                @click=\"downloadSelected\"\n                color=\"success\"\n                small\n                :title=\"$t('searchTorrent.saveTip')\"\n                v-if=\"$vuetify.breakpoint.mdAndUp\"\n              >\n                <v-icon class=\"mr-2\" small>save</v-icon>\n                {{ $t(\"searchTorrent.save\") }} ({{ selected.length }})\n              </v-btn>\n              <!-- 文件下载进度 -->\n              <v-progress-circular\n                v-if=\"downloading.count > 0\"\n                :rotate=\"-90\"\n                :size=\"60\"\n                :width=\"10\"\n                :value=\"downloading.progress\"\n                color=\"primary\"\n              >{{ downloading.progress.toFixed(0) }}%</v-progress-circular>\n\n              <!-- 下载失败的种子 -->\n              <v-btn\n                v-if=\"downloadFailedTorrents.length > 0\"\n                class=\"error\"\n                @click=\"reDownloadFailedTorrents\"\n                small\n                :title=\"$t('searchTorrent.downloadFailed')\"\n                :loading=\"downloading.count > 0\"\n              >\n                <v-icon class=\"mr-2\" small>get_app</v-icon>\n                {{ $t(\"searchTorrent.downloadFailed\") }} ({{\n                downloadFailedTorrents.length\n                }})\n              </v-btn>\n\n              <!-- 添加到收藏 -->\n              <AddToCollectionGroup\n                :disabled=\"selected.length == 0\"\n                :label=\"\n                  $vuetify.breakpoint.smAndUp\n                    ? $t('searchTorrent.collection') + ` (${selected.length})`\n                    : selected.length\n                \"\n                @add=\"addSelectedToCollection\"\n                small\n              />\n\n              <!-- 辅种 -->\n              <KeepUpload\n                :items=\"selected\"\n                :label=\"\n                  $vuetify.breakpoint.smAndUp\n                    ? `${$t('keepUploadTask.keepUpload')} (${selected.length})`\n                    : selected.length\n                \"\n                color=\"success\"\n              />\n            </template>\n\n            <!-- 保存搜索结果快照 -->\n            <v-btn\n              v-if=\"$store.state.options.allowSaveSnapshot\"\n              :loading=\"loading\"\n              color=\"cyan\"\n              small\n              dark\n              :title=\"$t('searchResultSnapshot.create')\"\n              @click.stop=\"createSearchResultSnapshot()\"\n              :class=\"$vuetify.breakpoint.smAndUp ? '' : 'mini'\"\n            >\n              <v-icon small>add_a_photo</v-icon>\n              <span class=\"ml-2\" v-if=\"$vuetify.breakpoint.smAndUp\">\n                {{\n                $t(\"searchResultSnapshot.create\")\n                }}\n              </span>\n            </v-btn>\n\n            <!-- 设置 -->\n            <v-menu :close-on-content-click=\"false\" offset-y class=\"ml-2\">\n              <template v-slot:activator=\"{ on }\">\n                <v-btn\n                  color=\"blue\"\n                  dark\n                  v-on=\"on\"\n                  :title=\"$t('home.settings')\"\n                  small\n                  :class=\"$vuetify.breakpoint.smAndUp ? '' : 'mini'\"\n                >\n                  <v-icon small>settings</v-icon>\n                </v-btn>\n              </template>\n\n              <v-card>\n                <v-container grid-list-xs>\n                  <!-- 显示多选框 -->\n                  <v-switch\n                    color=\"success\"\n                    v-model=\"checkBox\"\n                    :label=\"$t('searchTorrent.showCheckbox')\"\n                    @change=\"updateViewOptions\"\n                  ></v-switch>\n                  <!-- 显示资源分类标签 -->\n                  <v-switch\n                    color=\"success\"\n                    v-model=\"showCategory\"\n                    :label=\"$t('searchTorrent.showCategory')\"\n                    @change=\"updateViewOptions\"\n                  ></v-switch>\n                </v-container>\n              </v-card>\n            </v-menu>\n          </div>\n        </div>\n      </div>\n\n      <!-- 数据表格 -->\n      <v-data-table\n        v-model=\"selected\"\n        :search=\"filterKey\"\n        :custom-filter=\"searchResultFilter\"\n        :headers=\"headers\"\n        :items=\"datas\"\n        :pagination.sync=\"pagination\"\n        :loading=\"loading\"\n        item-key=\"link\"\n        :class=\"\n          'torrent' +\n            (fixedTable ? ' fixed-table fixed-header v-table__overflow' : '')\n        \"\n        :select-all=\"checkBox\"\n        :rows-per-page-items=\"options.rowsPerPageItems\"\n      >\n        <!-- 表头内容 -->\n        <template v-slot:headers=\"props\">\n          <tr>\n            <th v-if=\"checkBox\">\n              <v-checkbox\n                :input-value=\"props.all\"\n                :indeterminate=\"props.indeterminate\"\n                primary\n                hide-details\n                @click.stop=\"toggleAll\"\n              ></v-checkbox>\n            </th>\n            <template v-for=\"header in props.headers\">\n              <th\n                v-if=\"header.visible\"\n                :key=\"header.text\"\n                :class=\"getHeaderClass(header)\"\n                @click=\"header.sortable !== false && changeSort(header.value)\"\n                :style=\"header.width ? `width:${header.width};` : ''\"\n              >\n                <v-icon small v-if=\"header.sortable !== false\">arrow_upward</v-icon>\n                {{ header.text }}\n              </th>\n            </template>\n          </tr>\n        </template>\n\n        <!-- 表格内容 -->\n        <template slot=\"items\" slot-scope=\"props\">\n          <td v-if=\"checkBox\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n              @change=\"shiftCheck($event, props.index)\"\n            ></v-checkbox>\n          </td>\n          <!-- 站点 -->\n          <td class=\"center\" v-if=\"$vuetify.breakpoint.mdAndUp\">\n            <v-avatar size=\"18\">\n              <img :src=\"props.item.site.icon\" />\n            </v-avatar>\n            <template v-if=\"$vuetify.breakpoint.width > 1200\">\n              <br />\n              <a\n                :href=\"props.item.site.activeURL || props.item.site.url\"\n                target=\"_blank\"\n                v-html=\"props.item.site.name\"\n                rel=\"noopener noreferrer nofollow\"\n                class=\"captionText\"\n              ></a>\n            </template>\n          </td>\n          <!-- 标题 -->\n          <td :class=\"$vuetify.breakpoint.xs ? 'titleCell-mobile' : 'titleCell'\">\n            <v-avatar\n              size=\"14\"\n              class=\"mr-1\"\n              style=\"vertical-align: unset;\"\n              v-if=\"$vuetify.breakpoint.smAndDown\"\n            >\n              <img :src=\"props.item.site.icon\" />\n            </v-avatar>\n            <a\n              :href=\"props.item.link\"\n              target=\"_blank\"\n              v-html=\"props.item.titleHTML\"\n              :title=\"props.item.title\"\n              rel=\"noopener noreferrer nofollow\"\n              :class=\"[\n                $vuetify.breakpoint.xs ? 'body-2' : 'subheading',\n                'font-weight-medium',\n              ]\"\n            ></a>\n            <div\n              class=\"sub-title captionText\"\n              v-if=\"\n                (props.item.tags && props.item.tags.length) ||\n                  props.item.subTitle\n              \"\n            >\n              <span class=\"mr-1\" v-if=\"props.item.tags && props.item.tags.length\">\n                <span\n                  :class=\"['tag', `${tag.color}`]\"\n                  :style=\"{'background-color':`${tag.color}`,'border-color':`${tag.color}`}\"\n                  v-for=\"(tag, index) in props.item.tags\"\n                  :key=\"index\"\n                  :title=\"tag.title\"\n                >{{ tag.name }}</span>\n              </span>\n\n              <span v-if=\"props.item.subTitle\" :title=\"props.item.subTitle\">{{ props.item.subTitle }}</span>\n            </div>\n\n            <v-layout v-if=\"$vuetify.breakpoint.xs\">\n              <v-flex xs3 class=\"pt-2 captionText\">\n                {{\n                props.item.size | formatSize\n                }}\n              </v-flex>\n              <v-flex xs3 class=\"pt-2 captionText\">\n                <v-icon style=\"font-size:12px;margin-bottom: 2px;\">arrow_upward</v-icon>\n                {{ props.item.seeders }}\n                <v-icon style=\"font-size:12px;margin-bottom: 2px;\">arrow_downward</v-icon>\n                {{ props.item.leechers }}\n              </v-flex>\n              <v-flex xs3>\n                <!-- 进度条 -->\n                <TorrentProgress\n                  class=\"progress\"\n                  style=\"position: unset; padding-top:2px;\"\n                  v-if=\"props.item.progress != null\"\n                  :progress=\"parseInt(props.item.progress)\"\n                  :status=\"props.item.status\"\n                ></TorrentProgress>\n              </v-flex>\n\n              <v-flex xs3>\n                <!-- 工具栏 -->\n                <Actions\n                  v-if=\"$vuetify.breakpoint.xs\"\n                  :url=\"props.item.url\"\n                  :downloadMethod=\"props.item.site.downloadMethod\"\n                  :isCollectioned=\"isCollectioned(props.item.link)\"\n                  :item=\"props.item\"\n                  @copyLinkToClipboard=\"copyLinkToClipboard(props.item.url)\"\n                  @saveTorrentFile=\"saveTorrentFile(props.item)\"\n                  @addToCollection=\"addToCollection(props.item)\"\n                  @deleteCollection=\"deleteCollection(props.item)\"\n                  @downloadSuccess=\"downloadSuccess\"\n                  @downloadError=\"downloadError\"\n                />\n              </v-flex>\n            </v-layout>\n          </td>\n          <!-- 分类 -->\n          <td class=\"category center\" v-if=\"$vuetify.breakpoint.width > 1200\">\n            <span\n              v-if=\"props.item.category && !!props.item.category.name\"\n              :title=\"props.item.category.name\"\n              class=\"captionText\"\n            >{{ props.item.category.name }}</span>\n            <br />\n            <span class=\"captionText\">&lt;{{ props.item.entryName }}&gt;</span>\n          </td>\n          <td class=\"size\" v-if=\"$vuetify.breakpoint.smAndUp\">\n            {{ props.item.size | formatSize }}\n            <TorrentProgress\n              class=\"progress\"\n              v-if=\"props.item.progress != null\"\n              :progress=\"parseInt(props.item.progress)\"\n              :status=\"props.item.status\"\n            ></TorrentProgress>\n          </td>\n          <td class=\"size\" v-if=\"$vuetify.breakpoint.smAndUp\">{{ props.item.seeders }}</td>\n          <td class=\"size\" v-if=\"$vuetify.breakpoint.mdAndUp\">{{ props.item.leechers }}</td>\n          <td class=\"size\" v-if=\"$vuetify.breakpoint.mdAndUp\">{{ props.item.completed }}</td>\n          <td class=\"size\" v-if=\"$vuetify.breakpoint.smAndUp\">{{ props.item.comments }}</td>\n          <!-- <td>{{ props.item.author }}</td> -->\n          <td v-if=\"$vuetify.breakpoint.mdAndUp\">{{ props.item.time | formatDate }}</td>\n          <td class=\"text-xs-center\" v-if=\"$vuetify.breakpoint.smAndUp\">\n            <template v-if=\"!!props.item.url\">\n              <Actions\n                :url=\"props.item.url\"\n                :downloadMethod=\"props.item.site.downloadMethod\"\n                :isCollectioned=\"isCollectioned(props.item.link)\"\n                :item=\"props.item\"\n                @copyLinkToClipboard=\"copyLinkToClipboard(props.item.url)\"\n                @saveTorrentFile=\"saveTorrentFile(props.item)\"\n                @addToCollection=\"addToCollection(props.item)\"\n                @deleteCollection=\"deleteCollection(props.item)\"\n                @downloadSuccess=\"downloadSuccess\"\n                @downloadError=\"downloadError\"\n              />\n            </template>\n            <span v-else>{{ $t(\"searchTorrent.failUrl\") }}</span>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" multi-line color=\"error\">\n      <div v-html=\"errorMsg\"></div>\n    </v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" multi-line color=\"success\">\n      <div v-html=\"successMsg\"></div>\n    </v-snackbar>\n  </div>\n</template>\n<script lang=\"ts\" src=\"./SearchTorrent.ts\"></script>\n<style lang=\"scss\" src=\"./SearchTorrent.scss\"></style>\n"
  },
  {
    "path": "src/options/views/settings/Backup/Index.vue",
    "content": "<template>\n  <div>\n    <v-alert :value=\"true\" type=\"info\">{{ $t('settings.backup.title') }}</v-alert>\n    <v-card>\n      <v-card-actions class=\"pa-3\">\n        <input type=\"file\" ref=\"fileRestore\" style=\"display:none;\" />\n        <v-btn color=\"success\" @click=\"createBackupFile\">\n          <v-icon>save</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.backup.backup') }}</span>\n        </v-btn>\n        <v-btn color=\"info\" @click=\"restore\">\n          <v-icon>restore</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.backup.restore') }}</span>\n        </v-btn>\n\n        <v-divider class=\"mx-3 mt-0\" vertical></v-divider>\n\n        <v-menu offset-y>\n          <template v-slot:activator=\"{ on }\">\n            <v-btn color=\"blue-grey\" dark v-on=\"on\">\n              <v-icon>add</v-icon>\n              <span class=\"ml-1\">{{ $t('settings.backup.server.add.title') }}</span>\n            </v-btn>\n          </template>\n          <v-list>\n            <v-list-tile\n              v-for=\"(item, index) in backupServerTypes\"\n              :key=\"index\"\n              @click=\"showAddServer(item.type)\"\n            >\n              <v-list-tile-title>{{ item.type }}</v-list-tile-title>\n            </v-list-tile>\n          </v-list>\n        </v-menu>\n\n        <v-spacer></v-spacer>\n        <div v-if=\"!isDevelopmentMode\">\n          <v-btn\n            color=\"success\"\n            @click=\"backupToGoogle\"\n            :loading=\"status.backupToGoogle\"\n            :disabled=\"status.backupToGoogle\"\n            :title=\"$t('settings.backup.backupToGoogle')\"\n          >\n            <v-icon>backup</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.backup.backupToGoogle') }}</span>\n          </v-btn>\n          <v-btn\n            color=\"info\"\n            @click=\"restoreFromGoogle\"\n            :loading=\"status.restoreFromGoogle\"\n            :disabled=\"status.restoreFromGoogle\"\n            :title=\"$t('settings.backup.restoreFromGoogle')\"\n          >\n            <v-icon>cloud_download</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.backup.restoreFromGoogle') }}</span>\n          </v-btn>\n\n          <v-btn\n            color=\"error\"\n            @click=\"clearFromGoogle\"\n            :loading=\"status.clearFromGoogle\"\n            :disabled=\"status.clearFromGoogle\"\n            :title=\"$t('settings.backup.clearFromGoogleTip')\"\n          >\n            <v-icon>delete_sweep</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.backup.clearFromGoogle') }}</span>\n          </v-btn>\n        </div>\n      </v-card-actions>\n\n      <WorkingStatus ref=\"workingStatus\" />\n\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"servers\"\n        :pagination.sync=\"pagination\"\n        item-key=\"id\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width:20px;\">\n            <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"editBackupServer(props.item)\">{{ props.item.name }}</a>\n          </td>\n          <td>\n            <v-btn\n              flat\n              icon\n              small\n              @click=\"backupToServer(props.item)\"\n              :loading=\"props.item.backingup\"\n              color=\"success\"\n              class=\"mx-0\"\n              :title=\"$t('settings.backup.server.list.backupToServer')\"\n            >\n              <v-icon small>backup</v-icon>\n            </v-btn>\n            <v-btn\n              flat\n              icon\n              small\n              @click=\"getBackupServerFileList(props)\"\n              :loading=\"props.item.loading\"\n              color=\"info\"\n              class=\"mx-0\"\n              :title=\"$t('settings.backup.server.list.loadBackupList')\"\n            >\n              <v-icon small>restore</v-icon>\n            </v-btn>\n            <v-btn\n              flat\n              icon\n              small\n              @click=\"editBackupServer(props.item)\"\n              color=\"grey\"\n              class=\"mx-0\"\n              :title=\"$t('common.edit')\"\n            >\n              <v-icon small>edit</v-icon>\n            </v-btn>\n            <v-btn\n              flat\n              icon\n              small\n              @click=\"removeBackupServer(props.item)\"\n              :loading=\"props.item.deleting\"\n              color=\"error\"\n              class=\"mx-0\"\n              :title=\"$t('common.remove')\"\n            >\n              <v-icon small>delete</v-icon>\n            </v-btn>\n          </td>\n          <td>{{ props.item.type }}</td>\n          <td>{{ props.item.lastBackupTime | formatDate }}</td>\n        </template>\n        <template slot=\"expand\" slot-scope=\"props\">\n          <div class=\"px-5\" style=\"padding-left: 80px !important;\">\n            <ServerList\n              :items=\"props.item.dataList\"\n              :server=\"props.item\"\n              :loading=\"props.item.loading\"\n              :downloading=\"props.item.restoring\"\n              @download=\"restoreFromServer\"\n              @delete=\"deleteFileFromBackupServer\"\n            />\n          </div>\n        </template>\n      </v-data-table>\n    </v-card>\n    <v-alert :value=\"true\" color=\"grey\">{{ $t('settings.backup.subTitle') }}</v-alert>\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{ successMsg }}</v-snackbar>\n\n    <!-- 新增备份服务器 -->\n    <ServerAdd v-model=\"showServerAdd\" :type=\"currentServerType\" @save=\"addBackupServer\" />\n    <!-- 编辑备份服务器 -->\n    <ServerEdit\n      v-model=\"showServerEdit\"\n      :type=\"currentServerType\"\n      :initData=\"selectedItem\"\n      @save=\"updateBackupServer\"\n    />\n  </div>\n</template>\n<script lang=\"ts\">\nimport FileSaver from \"file-saver\";\nimport Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport {\n  EAction,\n  EModule,\n  Options,\n  IBackupServer,\n  EBackupServerType,\n  ERestoreContent,\n  EBrowserType,\n  IWorkingStatusItem,\n  EWorkingStatus,\n  Site,\n  EInstallType\n} from \"@/interface/common\";\nimport { PPF } from \"@/service/public\";\nimport { FileDownloader } from \"@/service/downloader\";\n\nimport ServerAdd from \"./Server/Add.vue\";\nimport ServerEdit from \"./Server/Edit.vue\";\nimport ServerList from \"./Server/List.vue\";\n\nimport { BackupFileParser } from \"@/service/backupFileParser\";\nimport WorkingStatus from \"@/options/components/WorkingStatus.vue\";\nimport { APP } from \"@/service/api\";\n\ninterface IBackupServerPro extends IBackupServer {\n  loading?: boolean;\n  dataList?: any[];\n  backingup?: boolean;\n  restoring?: boolean;\n  deleting?: boolean;\n}\n\nconst extension = new Extension();\nconst backupFileParser = new BackupFileParser();\n\nexport default Vue.extend({\n  components: {\n    WorkingStatus,\n    ServerAdd,\n    ServerEdit,\n    ServerList\n  },\n  data() {\n    return {\n      fileName: \"PT-plugin-plus-config.json\",\n      fileInput: null as any,\n      zipFileName: \"PT-Plugin-Plus-Config.zip\",\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      status: {\n        backupToGoogle: false,\n        restoreFromGoogle: false,\n        clearFromGoogle: false\n      },\n      isDevelopmentMode: true,\n      pagination: {\n        rowsPerPage: -1\n      },\n      selected: [] as any,\n      showServerAdd: false,\n      showServerEdit: false,\n      currentServerType: EBackupServerType.OWSS,\n      servers: [] as any,\n      selectedItem: {} as IBackupServer,\n      backupServerTypes: [\n        {\n          type: EBackupServerType.OWSS\n        },\n        {\n          type: EBackupServerType.WebDAV\n        }\n      ],\n      workingStatus: null as any\n    };\n  },\n  mounted() {\n    this.fileInput = this.$refs.fileRestore;\n    this.fileInput.addEventListener(\"change\", this.restoreFile);\n    this.workingStatus = this.$refs.workingStatus;\n  },\n  beforeDestroy() {\n    this.fileInput.removeEventListener(\"change\", this.restoreFile);\n  },\n  created() {\n    APP.getInstallType()\n      .then(result => {\n        this.isDevelopmentMode = [\n          EInstallType.development,\n          EInstallType.crx\n        ].includes(result);\n      })\n      .catch(() => {\n        console.log(\"获取安装方式失败\");\n      });\n\n    this.initBackupServers();\n  },\n  methods: {\n    t(key: string): string {\n      return this.$t(key).toString();\n    },\n    /**\n     * 初始化备份服务器列表\n     */\n    initBackupServers() {\n      if (\n        this.$store.state.options.backupServers &&\n        this.$store.state.options.backupServers.length > 0\n      ) {\n        this.servers = [];\n        this.$store.state.options.backupServers.forEach(\n          (item: IBackupServer) => {\n            this.servers.push(\n              Object.assign(\n                {\n                  loading: false,\n                  backingup: false,\n                  deleting: false,\n                  restoring: false,\n                  dataList: []\n                },\n                JSON.parse(JSON.stringify(item))\n              )\n            );\n          }\n        );\n      }\n    },\n    backup() {\n      this.clearMessage();\n      extension\n        .sendRequest(EAction.getClearedOptions)\n        .then((options: any) => {\n          const Blob = window.Blob;\n          delete options.system;\n          const data = new Blob([JSON.stringify(options)], {\n            type: \"text/plain\"\n          });\n          FileSaver.saveAs(data, this.fileName);\n          this.successMsg = this.$t(\"settings.backup.backupDone\").toString();\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\"settings.backup.backupError\").toString();\n        });\n    },\n    restore() {\n      this.fileInput.click();\n    },\n    restoreFile(event: Event) {\n      this.clearMessage();\n      let restoreFile: any = event.srcElement;\n      if (\n        restoreFile.files.length > 0 &&\n        restoreFile.files[0].name.length > 0\n      ) {\n        let file = restoreFile.files[0];\n        if (file.name.substr(-4) === \".zip\") {\n          this.restoreFromZipFile(file);\n          restoreFile.value = \"\";\n          return;\n        }\n        var r = new FileReader();\n        r.onload = (e: any) => {\n          let result = JSON.parse(e.target.result);\n          this.restoreConfirm({\n            options: result\n          });\n        };\n        r.onerror = () => {\n          this.errorMsg = this.$t(\"settings.backup.loadError\").toString();\n        };\n        r.readAsText(file);\n        restoreFile.value = \"\";\n      }\n    },\n    backupToGoogle() {\n      this.clearMessage();\n      this.status.backupToGoogle = true;\n      extension\n        .sendRequest(EAction.backupToGoogle)\n        .then(() => {\n          this.successMsg = this.$t(\"settings.backup.backupDone\").toString();\n        })\n        .catch((error: any) => {\n          console.log(error.msg);\n          if (error.msg && error.msg.message) {\n            switch (true) {\n              case error.msg.message.indexOf(\"QUOTA_BYTES_PER_ITEM\") != -1:\n                this.errorMsg = this.$t(\n                  \"settings.backup.errorMessage.QUOTA_BYTES_PER_ITEM\"\n                ).toString();\n                break;\n\n              default:\n                this.errorMsg = this.$t(\n                  \"settings.backup.backupError\"\n                ).toString();\n                break;\n            }\n          } else {\n            this.errorMsg = this.$t(\"settings.backup.backupError\").toString();\n          }\n          extension.sendRequest(EAction.writeLog, null, {\n            module: EModule.options,\n            event: \"backupToGoogle\",\n            msg: this.$t(\"settings.backup.backupError\").toString(),\n            data: error\n          });\n        })\n        .finally(() => {\n          this.status.backupToGoogle = false;\n        });\n    },\n    restoreFromGoogle() {\n      if (!confirm(this.$t(\"settings.backup.restoreConfirm\").toString())) {\n        return;\n      }\n      this.clearMessage();\n      this.status.restoreFromGoogle = true;\n      extension\n        .sendRequest(EAction.restoreFromGoogle)\n        .then((options: Options) => {\n          this.successMsg = this.$t(\n            \"settings.backup.restoreSuccess\"\n          ).toString();\n          this.$store.commit(\"updateOptions\", options);\n        })\n        .catch((error: any) => {\n          this.errorMsg = this.$t(\"settings.backup.restoreError\").toString();\n          extension.sendRequest(EAction.writeLog, null, {\n            module: EModule.options,\n            event: \"restoreFromGoogle\",\n            msg: this.$t(\"settings.backup.restoreError\").toString(),\n            data: error\n          });\n        })\n        .finally(() => {\n          this.status.restoreFromGoogle = false;\n        });\n    },\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n    clearFromGoogle() {\n      if (\n        !confirm(this.$t(\"settings.backup.clearFromGoogleConfirm\").toString())\n      ) {\n        return;\n      }\n      this.clearMessage();\n      this.status.clearFromGoogle = true;\n      extension\n        .sendRequest(EAction.clearFromGoogle)\n        .then((options: Options) => {\n          this.successMsg = this.$t(\n            \"settings.backup.clearFromGoogleSuccess\"\n          ).toString();\n        })\n        .catch((error: any) => {\n          this.errorMsg = this.$t(\n            \"settings.backup.clearFromGoogleError\"\n          ).toString();\n          extension.sendRequest(EAction.writeLog, null, {\n            module: EModule.options,\n            event: \"clearFromGoogle\",\n            msg: this.$t(\"settings.backup.clearFromGoogleError\").toString(),\n            data: error\n          });\n        })\n        .finally(() => {\n          this.status.clearFromGoogle = false;\n        });\n    },\n    /**\n     * 创建备份文件\n     */\n    createBackupFile() {\n      switch (PPF.browserName) {\n        case EBrowserType.Chrome:\n          extension\n            .sendRequest(EAction.createBackupFile)\n            .then(result => {\n              console.log(result);\n            })\n            .catch(error => {\n              console.log(error);\n              this.errorMsg = this.$t(\"settings.backup.backupError\").toString();\n            });\n          break;\n\n        default:\n          extension\n            .sendRequest(EAction.getBackupRawData)\n            .then(result => {\n              backupFileParser\n                .createBackupFileBlob(result)\n                .then((blob: any) => {\n                  FileSaver.saveAs(blob, PPF.getNewBackupFileName());\n                });\n              console.log(result);\n            })\n            .catch(error => {\n              console.log(error);\n              this.errorMsg = this.$t(\"settings.backup.backupError\").toString();\n            });\n          break;\n      }\n    },\n    /**\n     * 从 zip 文件中恢复配置信息\n     */\n    restoreFromZipFile(file: any) {\n      backupFileParser\n        .loadZipData(\n          file,\n          this.$t(\"settings.backup.enterSecretKey\").toString(),\n          this.$store.state.options.encryptSecretKey\n        )\n        .then(result => {\n          console.log(result);\n          this.restoreConfirm(result);\n        })\n        .catch(error => {\n          console.log(error);\n          if (typeof error === \"string\") {\n            if (this.$te(`settings.backup.restoreErrorType.${error}`)) {\n              this.errorMsg = this.$t(\n                `settings.backup.restoreErrorType.${error}`\n              ).toString();\n              return;\n            }\n          }\n          this.errorMsg = this.$t(\"settings.backup.restoreError\").toString();\n        });\n    },\n    /**\n     * 恢复配置确认\n     */\n    restoreConfirm(\n      infos: any,\n      restoreContent: ERestoreContent = ERestoreContent.all\n    ) {\n      // 如果指定了恢复内容，检测要恢复的内容是否存在\n      switch (restoreContent) {\n        case ERestoreContent.collection:\n          if (!infos.collection) {\n            this.errorMsg = this.$t(\n              \"settings.backup.contentNotExist.collection\"\n            ).toString();\n            return;\n          }\n          break;\n\n        case ERestoreContent.cookies:\n          if (!infos.cookies) {\n            this.errorMsg = this.$t(\n              \"settings.backup.contentNotExist.cookies\"\n            ).toString();\n            return;\n          }\n          break;\n\n        case ERestoreContent.keepUploadTask:\n          if (!infos.keepUploadTask) {\n            this.errorMsg = this.$t(\n              \"settings.backup.contentNotExist.keepUploadTask\"\n            ).toString();\n            return;\n          }\n          break;\n\n        case ERestoreContent.searchResultSnapshot:\n          if (!infos.searchResultSnapshot) {\n            this.errorMsg = this.$t(\n              \"settings.backup.contentNotExist.searchResultSnapshot\"\n            ).toString();\n            return;\n          }\n          break;\n\n        case ERestoreContent.downloadHistory:\n          if (!infos.downloadHistory) {\n            this.errorMsg = this.$t(\n              \"settings.backup.contentNotExist.downloadHistory\"\n            ).toString();\n            return;\n          }\n          break;\n      }\n\n      if (confirm(this.$t(\"settings.backup.restoreConfirm\").toString())) {\n        try {\n          this.workingStatus.clear();\n          // 恢复运行时配置\n          if (\n            infos.options &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.options)\n          ) {\n            this.workingStatus.add({\n              key: \"options\",\n              title: this.t(\"settings.backup.backupItem.base\")\n            });\n\n            let sites: Site[] = [];\n\n            // 去除没有 host 字段的站点\n            // 可能因自定义的站点之前出错导致 host 缺失\n            infos.options.sites.forEach((site: Site) => {\n              if (site.host) {\n                sites.push(site);\n              }\n            });\n\n            infos.options.sites = sites;\n            // 不覆盖当前的密钥值\n            infos.options.encryptSecretKey = this.$store.state.options.encryptSecretKey;\n\n            this.$store.dispatch(\"resetRunTimeOptions\", infos.options);\n            this.workingStatus.update(\"options\", EWorkingStatus.success);\n          }\n\n          // 恢复用户数据\n          if (\n            infos.datas &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.userDatas)\n          ) {\n            this.workingStatus.add({\n              key: \"userDatas\",\n              title: this.t(\"settings.backup.backupItem.userDatas\")\n            });\n            extension\n              .sendRequest(EAction.resetUserDatas, null, infos.datas)\n              .then(() => {\n                this.workingStatus.update(\"userDatas\", EWorkingStatus.success);\n              })\n              .catch(() => {\n                this.workingStatus.update(\"userDatas\", EWorkingStatus.error);\n              });\n          }\n\n          // 恢复收藏\n          if (\n            infos.collection &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.collection)\n          ) {\n            this.workingStatus.add({\n              key: \"collection\",\n              title: this.t(\"settings.backup.backupItem.collection\")\n            });\n            extension\n              .sendRequest(\n                EAction.resetTorrentCollections,\n                null,\n                infos.collection\n              )\n              .then(() => {\n                // 取消默认收藏分组信息\n                this.$store.dispatch(\"saveConfig\", {\n                  defaultCollectionGroupId: \"\"\n                });\n                this.workingStatus.update(\"collection\", EWorkingStatus.success);\n              })\n              .catch(() => {\n                this.workingStatus.update(\"collection\", EWorkingStatus.error);\n              });\n          }\n\n          // 恢复搜索快照\n          if (\n            infos.searchResultSnapshot &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.searchResultSnapshot)\n          ) {\n            this.workingStatus.add({\n              key: \"searchResultSnapshot\",\n              title: this.t(\"settings.backup.backupItem.searchResultSnapshot\")\n            });\n            extension\n              .sendRequest(\n                EAction.resetSearchResultSnapshot,\n                null,\n                infos.searchResultSnapshot\n              )\n              .then(() => {\n                this.workingStatus.update(\n                  \"searchResultSnapshot\",\n                  EWorkingStatus.success\n                );\n              })\n              .catch(() => {\n                this.workingStatus.update(\n                  \"searchResultSnapshot\",\n                  EWorkingStatus.error\n                );\n              });\n          }\n\n          // 恢复辅种任务\n          if (\n            infos.keepUploadTask &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.keepUploadTask)\n          ) {\n            this.workingStatus.add({\n              key: \"keepUploadTask\",\n              title: this.t(\"settings.backup.backupItem.keepUploadTask\")\n            });\n            extension\n              .sendRequest(\n                EAction.resetKeepUploadTask,\n                null,\n                infos.keepUploadTask\n              )\n              .then(() => {\n                this.workingStatus.update(\n                  \"keepUploadTask\",\n                  EWorkingStatus.success\n                );\n              })\n              .catch(() => {\n                this.workingStatus.update(\n                  \"keepUploadTask\",\n                  EWorkingStatus.error\n                );\n              });\n          }\n\n          // 恢复下载历史\n          if (\n            infos.downloadHistory &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.downloadHistory)\n          ) {\n            this.workingStatus.add({\n              key: \"downloadHistory\",\n              title: this.t(\"settings.backup.backupItem.downloadHistory\")\n            });\n            extension\n              .sendRequest(\n                EAction.resetDownloadHistory,\n                null,\n                infos.downloadHistory\n              )\n              .then(() => {\n                this.workingStatus.update(\n                  \"downloadHistory\",\n                  EWorkingStatus.success\n                );\n              })\n              .catch(() => {\n                this.workingStatus.update(\n                  \"downloadHistory\",\n                  EWorkingStatus.error\n                );\n              });\n          }\n\n          // 恢复Cookies，需要放到最后一项\n          if (\n            infos.cookies &&\n            (restoreContent == ERestoreContent.all ||\n              restoreContent == ERestoreContent.cookies) &&\n            PPF.checkOptionalPermission(\"cookies\")\n          ) {\n            // 当恢复所有内容，并且包含cookies时，需要确认是否恢复\n            if (\n              restoreContent == ERestoreContent.all &&\n              !confirm(\n                this.$t(\"settings.backup.restoreCookiesConfirm\").toString()\n              )\n            ) {\n              this.successMsg = this.$t(\n                \"settings.backup.restoreSuccess\"\n              ).toString();\n              return;\n            }\n            this.workingStatus.add({\n              key: \"cookies\",\n              title: this.t(\"settings.backup.backupItem.cookies\")\n            });\n            PPF.usePermissions(\n              [\"cookies\"],\n              true,\n              this.$t(\"permissions.request.cookies\").toString()\n            )\n              .then(() => {\n                return extension.sendRequest(\n                  EAction.restoreCookies,\n                  null,\n                  infos.cookies\n                );\n              })\n              .then(() => {\n                this.successMsg = this.$t(\n                  \"settings.backup.restoreSuccess\"\n                ).toString();\n                this.workingStatus.update(\"cookies\", EWorkingStatus.success);\n              })\n              .catch(() => {\n                this.errorMsg = this.$t(\n                  \"settings.backup.restoreError\"\n                ).toString();\n                this.workingStatus.update(\"cookies\", EWorkingStatus.error);\n              });\n            return;\n          }\n\n          this.successMsg = this.$t(\n            \"settings.backup.restoreSuccess\"\n          ).toString();\n        } catch (error) {\n          this.errorMsg = this.$t(\"settings.backup.restoreError\").toString();\n        }\n      }\n    },\n    /**\n     * 添加备份服务器\n     */\n    addBackupServer(server: IBackupServer) {\n      this.$store.dispatch(\"addBackupServer\", server).then(() => {\n        this.initBackupServers();\n      });\n    },\n    /**\n     * 修改备份服务器\n     */\n    editBackupServer(server: IBackupServer) {\n      this.selectedItem = server;\n      this.currentServerType = server.type;\n      this.showServerEdit = true;\n    },\n    /**\n     * 更新备份服务器\n     */\n    updateBackupServer(server: IBackupServer) {\n      this.$store.dispatch(\"updateBackupServer\", server).then(() => {\n        this.initBackupServers();\n      });\n    },\n    /**\n     * 删除备份服务器\n     */\n    removeBackupServer(server: IBackupServer) {\n      if (confirm(this.$t(\"common.removeConfirm\").toString())) {\n        let index = this.servers.findIndex((item: IBackupServer) => {\n          return item.id === server.id;\n        });\n        this.$store.dispatch(\"removeBackupServer\", server);\n        this.servers.splice(index, 1);\n      }\n    },\n    /**\n     * 备份到服务器\n     */\n    backupToServer(server: IBackupServerPro) {\n      server.backingup = true;\n      extension\n        .sendRequest(EAction.backupToServer, null, server)\n        .then((result: any) => {\n          server.lastBackupTime = result.time;\n          console.log(result);\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\"settings.backup.backupError\").toString();\n        })\n        .finally(() => {\n          server.backingup = false;\n        });\n    },\n    /**\n     * 从服务器恢复指定的内容\n     */\n    restoreFromServer(\n      server: IBackupServerPro,\n      options: any,\n      restoreContent: ERestoreContent = ERestoreContent.all\n    ) {\n      server.restoring = true;\n      extension\n        .sendRequest(EAction.restoreFromServer, null, {\n          server,\n          path: options.name\n        })\n        .then((result: any) => {\n          this.restoreConfirm(result, restoreContent);\n          // console.log(result);\n        })\n        .catch(error => {\n          console.log(error);\n          this.errorMsg = this.$t(\"settings.backup.restoreError\").toString();\n        })\n        .finally(() => {\n          server.restoring = false;\n        });\n    },\n    /**\n     * 获取已备份的文件列表\n     */\n    getBackupServerFileList(prop: any) {\n      let server: IBackupServerPro = prop.item;\n      prop.expanded = true;\n      server.loading = true;\n      extension\n        .sendRequest(EAction.getBackupListFromServer, null, {\n          server,\n          pageSize: 5,\n          search: \"PT-Plugin-Plus\"\n        })\n        .then((result: any) => {\n          prop.item.dataList = result;\n          console.log(result);\n        })\n        .catch(() => {\n          this.errorMsg = this.$t(\n            \"settings.backup.server.getFileListError\"\n          ).toString();\n        })\n        .finally(() => {\n          server.loading = false;\n        });\n    },\n    showAddServer(type: EBackupServerType) {\n      this.currentServerType = type;\n      this.showServerAdd = true;\n    },\n    deleteFileFromBackupServer(\n      server: IBackupServerPro,\n      options: any,\n      index: number\n    ) {\n      if (!confirm(this.$t(\"common.removeConfirm\").toString())) {\n        return;\n      }\n      extension\n        .sendRequest(EAction.deleteFileFromBackupServer, null, {\n          server,\n          path: options.name\n        })\n        .then((result: any) => {\n          if (server.dataList && server.dataList[index]) {\n            server.dataList.splice(index, 1);\n          }\n          console.log(result);\n        })\n        .catch(error => {\n          console.log(error);\n          // this.errorMsg = this.$t(\"settings.backup.restoreError\").toString();\n        })\n        .finally(() => {\n          server.loading = false;\n        });\n    }\n  },\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.backup.index.headers.name\"),\n          align: \"left\",\n          width: 280,\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.backup.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        },\n        {\n          text: this.$t(\"settings.backup.index.headers.type\"),\n          align: \"left\",\n          value: \"type\"\n        },\n        {\n          text: this.$t(\"settings.backup.index.headers.lastBackupTime\"),\n          align: \"left\",\n          value: \"lastBackupTime\"\n        }\n      ];\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Backup/Server/Add.vue",
    "content": "<template>\n  <div>\n    <v-dialog v-model=\"show\" max-width=\"800\">\n      <v-card>\n        <v-toolbar dark color=\"blue-grey darken-2\">\n          <v-toolbar-title>{{\n            $t(\"settings.backup.server.add.title\")\n          }}</v-toolbar-title>\n          <v-spacer></v-spacer>\n          <v-btn\n            icon\n            flat\n            color=\"success\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-backup-server\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer nofollow\"\n            :title=\"$t('common.help')\"\n          >\n            <v-icon>help</v-icon>\n          </v-btn>\n        </v-toolbar>\n\n        <v-card-text class=\"body\">\n          <Editor\n            :type=\"type\"\n            :initData=\"selected\"\n            @change=\"change\"\n            :show=\"show\"\n          />\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3 toolbar\">\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nimport { IBackupServer, EBackupServerType } from \"@/interface/common\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      selected: {} as any,\n      valid: false,\n      newData: {} as any\n    };\n  },\n  props: {\n    value: Boolean,\n    type: {\n      type: String,\n      default: EBackupServerType.OWSS\n    }\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.selected = {};\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\n        \"save\",\n        Object.assign({ type: EBackupServerType.WebDAV }, this.newData)\n      );\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n// .body {\n//   position: absolute;\n//   bottom: 65px;\n//   top: 65px;\n//   overflow-y: auto;\n// }\n// .toolbar {\n//   position: absolute;\n//   bottom: 0;\n//   right: 0;\n// }\n</style>\n"
  },
  {
    "path": "src/options/views/settings/Backup/Server/Edit.vue",
    "content": "<template>\n  <div>\n    <v-dialog v-model=\"show\" max-width=\"800\">\n      <v-card>\n        <v-toolbar dark color=\"blue-grey darken-2\">\n          <v-toolbar-title>{{\n            $t(\"settings.backup.server.edit.title\")\n          }}</v-toolbar-title>\n          <v-spacer></v-spacer>\n          <v-btn\n            icon\n            flat\n            color=\"success\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-backup-server\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer nofollow\"\n            :title=\"$t('common.help')\"\n          >\n            <v-icon>help</v-icon>\n          </v-btn>\n        </v-toolbar>\n\n        <v-card-text class=\"body\">\n          <Editor\n            :type=\"type\"\n            :initData=\"defaultItem\"\n            @change=\"change\"\n            :show=\"show\"\n          />\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3 toolbar\">\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nimport { IBackupServer, EBackupServerType } from \"@/interface/common\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      valid: false,\n      newData: {} as any,\n      defaultItem: {} as IBackupServer\n    };\n  },\n  props: {\n    value: Boolean,\n    initData: Object,\n    type: {\n      type: String,\n      default: EBackupServerType.OWSS\n    }\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultItem = Object.assign({}, this.initData);\n      }\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", this.newData);\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Backup/Server/Editor.vue",
    "content": "<template>\n  <div>\n    <v-card class=\"mb-5\" :color=\"$vuetify.dark ? '' : 'grey lighten-4'\">\n      <v-card-text>\n        <v-form v-model=\"valid\">\n          <!-- 类型 -->\n          <v-text-field\n            :label=\"$t('settings.backup.server.editor.type')\"\n            :placeholder=\"$t('settings.backup.server.editor.type')\"\n            disabled\n            :value=\"type\"\n          ></v-text-field>\n\n          <!-- 名称 -->\n          <v-text-field\n            v-model=\"option.name\"\n            :label=\"$t('settings.backup.server.editor.name')\"\n            :placeholder=\"$t('settings.backup.server.editor.name')\"\n            required\n            :rules=\"rules.require\"\n            ref=\"name\"\n          ></v-text-field>\n\n          <!-- 地址 -->\n          <v-text-field\n            v-model=\"option.address\"\n            :label=\"$t('settings.backup.server.editor.address')\"\n            :placeholder=\"$t('settings.backup.server.editor.address')\"\n            required\n            :rules=\"[rules.url]\"\n          ></v-text-field>\n\n          <!-- 授权码 -->\n          <template v-if=\"type==='OWSS'\">\n            <v-text-field\n              v-model=\"option.authCode\"\n              :label=\"$t('settings.backup.server.editor.authCode')\"\n              :placeholder=\"$t('settings.backup.server.editor.authCode')\"\n              required\n              :rules=\"rules.require\"\n              :type=\"showAuthCode ? 'text' : 'password'\"\n            >\n              <template v-slot:append>\n                <v-icon\n                  @click=\"showAuthCode = !showAuthCode\"\n                >{{showAuthCode ? 'visibility_off' : 'visibility'}}</v-icon>\n                <v-btn\n                  flat\n                  small\n                  color=\"primary\"\n                  @click=\"applyAuthCode\"\n                >{{ $t('settings.backup.server.editor.applyAuthCode') }}</v-btn>\n              </template>\n            </v-text-field>\n          </template>\n\n          <template v-else>\n            <!-- 登录名 -->\n            <v-text-field\n              v-model=\"option.loginName\"\n              :label=\"$t('settings.backup.server.editor.loginName')\"\n              :placeholder=\"$t('settings.backup.server.editor.loginName')\"\n              required\n              :rules=\"rules.require\"\n            ></v-text-field>\n\n            <!-- 密码 -->\n            <v-text-field\n              v-model=\"option.loginPwd\"\n              :label=\"$t('settings.backup.server.editor.loginPwd')\"\n              :placeholder=\"$t('settings.backup.server.editor.loginPwd')\"\n              required\n              :rules=\"rules.require\"\n              :type=\"showLoginPwd ? 'text' : 'password'\"\n            >\n              <template v-slot:append>\n                <v-icon\n                  @click=\"showLoginPwd = !showLoginPwd\"\n                >{{showLoginPwd ? 'visibility_off' : 'visibility'}}</v-icon>\n              </template>\n            </v-text-field>\n\n            <v-switch :label=\"$t('settings.backup.server.editor.digest')\" v-model=\"option.digest\"></v-switch>\n          </template>\n        </v-form>\n\n        <v-btn\n          flat\n          block\n          :color=\"testButtonColor\"\n          :loading=\"testing\"\n          :disabled=\"testing || !valid\"\n          @click=\"testServerConnectivity\"\n        >\n          <v-icon class=\"mr-2\">{{ testButtonIcon }}</v-icon>\n          {{ successMsg || errorMsg || $t('settings.downloadClients.editor.test') }}\n        </v-btn>\n      </v-card-text>\n    </v-card>\n    <v-snackbar v-model=\"haveError\" absolute top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar\n      v-model=\"haveSuccess\"\n      absolute\n      bottom\n      :timeout=\"3000\"\n      color=\"success\"\n    >{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport md5 from \"blueimp-md5\";\nimport Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport {\n  EAction,\n  DataResult,\n  Dictionary,\n  EBackupServerType\n} from \"@/interface/common\";\nconst extension = new Extension();\nexport default Vue.extend({\n  data() {\n    return {\n      showLoginPwd: false,\n      showAuthCode: false,\n      rules: {\n        require: [(v: any) => !!v || \"!\"],\n        url: (v: any) => {\n          return (\n            /^(https?):\\/\\/[-A-Za-z0-9+&@#/%?=~_|!:,.;\\[\\]]+[-A-Za-z0-9+&@#/%=~_|]$/.test(\n              v\n            ) || this.$t(\"settings.backup.server.owss.addressTip\")\n          );\n        }\n      },\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      errorMsg: \"\",\n      valid: false,\n      option: {\n        authCode: \"\",\n        address: \"\",\n        name: \"\",\n        loginName: \"\",\n        loginPwd: \"\",\n        type: EBackupServerType.OWSS,\n        digest: false\n      } as any,\n      testing: false,\n      testButtonIcon: \"compass_calibration\",\n      testButtonColor: \"info\",\n      testButtonStatus: {\n        success: \"success\",\n        error: \"error\"\n      },\n      buttonColor: {\n        default: \"info\",\n        success: \"success\",\n        error: \"error\"\n      } as Dictionary<any>,\n      buttonIcon: {\n        default: \"compass_calibration\",\n        success: \"done\",\n        error: \"close\"\n      } as Dictionary<any>\n    };\n  },\n  props: {\n    initData: Object,\n    isNew: Boolean,\n    type: {\n      type: String,\n      default: EBackupServerType.OWSS\n    },\n    show: Boolean\n  },\n  watch: {\n    show() {\n      if (this.show && this.$refs.name) {\n        (this.$refs.name as any).focus();\n      }\n    },\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    },\n    option: {\n      handler() {\n        setTimeout(() => {\n          this.$emit(\"change\", {\n            data: this.option,\n            valid: this.valid\n          });\n        }, 100);\n      },\n      deep: true\n    },\n    type() {\n      this.option = Object.assign({}, this.initData);\n      this.option.type = this.type;\n    },\n    initData() {\n      if (this.initData) {\n        if (this.initData.digest === undefined) {\n          this.initData.digest = false;\n        }\n        this.option = Object.assign({\n          authCode: \"\",\n          address: \"\",\n          name: \"\",\n          loginName: \"\",\n          loginPwd: \"\",\n          type: EBackupServerType.OWSS,\n          digest: false\n        }, this.initData);\n        this.option.type = this.type;\n      }\n    }\n  },\n  methods: {\n    applyAuthCode() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n      if (this.option.address) {\n        $.ajax({\n          url: `${this.option.address}/create`\n        })\n          .done(result => {\n            console.log(result);\n            if (result && result.data) {\n              this.option.authCode = result.data;\n              this.successMsg = result.data;\n            } else if (result.code && result.msg) {\n              this.errorMsg = result.msg + \" (\" + result.code + \")\";\n            }\n          })\n          .fail((jqXHR, status, text) => {\n            if (\n              jqXHR.responseJSON &&\n              jqXHR.responseJSON.code &&\n              jqXHR.responseJSON.msg\n            ) {\n              this.errorMsg =\n                jqXHR.responseJSON.msg + \" (\" + jqXHR.responseJSON.code + \")\";\n            } else {\n              this.errorMsg = status + \", \" + text;\n            }\n          });\n      }\n    },\n\n    /**\n     * 测试服务器是否可用\n     */\n    testServerConnectivity() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n      let options = Object.assign({}, this.option);\n      if (!options.address) {\n        this.errorMsg = this.$t(\n          \"settings.downloadClients.editor.testAddressError\"\n        ).toString();\n        return;\n      }\n      this.testing = true;\n\n      extension\n        .sendRequest(EAction.testBackupServerConnectivity, null, options)\n        .then((result: DataResult) => {\n          console.log(result);\n          if (result) {\n            this.successMsg = this.$t(\n              \"settings.downloadClients.editor.testSuccess\"\n            ).toString();\n            this.setTestButtonStatus(this.testButtonStatus.success);\n          } else {\n            this.errorMsg = this.$t(\n              \"settings.downloadClients.editor.testError\"\n            ).toString();\n          }\n          this.errorMsg &&\n            this.setTestButtonStatus(this.testButtonStatus.error);\n          this.testing = false;\n        })\n        .catch((result: DataResult) => {\n          console.log(result);\n          if (result && result.data && result.data.msg) {\n            this.errorMsg = result.data.msg;\n          } else {\n            this.errorMsg = this.$t(\n              \"settings.downloadClients.editor.testError\"\n            ).toString();\n          }\n\n          this.setTestButtonStatus(this.testButtonStatus.error);\n          this.testing = false;\n        });\n    },\n\n    setTestButtonStatus(status: string) {\n      this.testButtonIcon = this.buttonIcon[status];\n      this.testButtonColor = this.buttonColor[status];\n      window.setTimeout(() => {\n        this.testButtonIcon = this.buttonIcon.default;\n        this.testButtonColor = this.buttonColor.default;\n        this.successMsg = \"\";\n        this.errorMsg = \"\";\n      }, 3000);\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Backup/Server/List.vue",
    "content": "<template>\n  <div>\n    <v-progress-circular v-if=\"loading\" indeterminate color=\"primary\" class=\"ma-2\" :width=\"3\"></v-progress-circular>\n    <v-list two-line dense v-if=\"items && items.length>0\">\n      <template v-for=\"(item, index) in items\">\n        <v-list-tile :key=\"item.name\" v-if=\"item.type=='file'\">\n          <v-list-tile-avatar>\n            <v-icon class=\"grey lighten-1 white--text\">bookmark</v-icon>\n          </v-list-tile-avatar>\n\n          <v-list-tile-content>\n            <v-list-tile-title>{{ item.name }}</v-list-tile-title>\n            <v-list-tile-sub-title>\n              <div>\n                <span class=\"caption mr-2\">\n                  <span>{{ item.time | formatDate }}</span>\n                  <span class=\"mx-2\">{{ item.size | formatSize }}</span>\n                </span>\n                <!-- 恢复 -->\n                <v-btn\n                  icon\n                  small\n                  class=\"mx-0\"\n                  :loading=\"downloading && downloadingIndex==index\"\n                  @click=\"selectRestoreType(item, index, $event)\"\n                  :title=\"$t('settings.backup.restore')\"\n                >\n                  <v-icon color=\"info\" small>cloud_download</v-icon>\n                </v-btn>\n\n                <!-- 删除备份 -->\n                <v-btn\n                  icon\n                  ripple\n                  class=\"mx-0\"\n                  small\n                  @click=\"onDelete(item, index)\"\n                  :title=\"$t('common.remove')\"\n                >\n                  <v-icon color=\"red\" small>delete</v-icon>\n                </v-btn>\n              </div>\n            </v-list-tile-sub-title>\n          </v-list-tile-content>\n\n          <v-list-tile-action></v-list-tile-action>\n        </v-list-tile>\n\n        <v-divider :key=\"index\"></v-divider>\n      </template>\n    </v-list>\n    <v-list two-line dense v-else-if=\"!loading\">\n      <v-list-tile>\n        <v-list-tile-content>{{ $t('settings.backup.server.list.noData') }}</v-list-tile-content>\n      </v-list-tile>\n    </v-list>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { PPF } from \"@/service/public\";\nimport { ERestoreContent } from \"@/interface/enum\";\nexport default Vue.extend({\n  data() {\n    return {\n      downloadingIndex: 0\n    };\n  },\n  props: {\n    items: Array,\n    loading: Boolean,\n    downloading: Boolean,\n    server: Object\n  },\n  methods: {\n    onDelete(item: any, index: number) {\n      this.$emit(\"delete\", this.server, item, index);\n    },\n\n    selectRestoreType(item: any, index: number, event: any) {\n      this.downloadingIndex = index;\n\n      let menus: any[] = [];\n\n      menus.push({\n        title: this.$t(\"settings.backup.restoreAll\"),\n        fn: () => {\n          console.log(this.server);\n          this.$emit(\"download\", this.server, item, ERestoreContent.all);\n        }\n      });\n\n      menus.push({});\n\n      menus.push({\n        title: this.$t(\"settings.backup.restoreCollection\"),\n        fn: () => {\n          this.$emit(\"download\", this.server, item, ERestoreContent.collection);\n        }\n      });\n\n      if (PPF.checkOptionalPermission(\"cookies\")) {\n        menus.push({});\n\n        menus.push({\n          title: this.$t(\"settings.backup.restoreCookies\"),\n          fn: () => {\n            this.$emit(\"download\", this.server, item, ERestoreContent.cookies);\n          }\n        });\n      }\n\n      menus.push({});\n\n      menus.push({\n        title: this.$t(\"settings.backup.restoreSearchResultSnapshot\"),\n        fn: () => {\n          this.$emit(\n            \"download\",\n            this.server,\n            item,\n            ERestoreContent.searchResultSnapshot\n          );\n        }\n      });\n\n      menus.push({});\n\n      menus.push({\n        title: this.$t(\"settings.backup.restoreKeepUploadTask\"),\n        fn: () => {\n          this.$emit(\n            \"download\",\n            this.server,\n            item,\n            ERestoreContent.keepUploadTask\n          );\n        }\n      });\n\n      menus.push({});\n\n      menus.push({\n        title: this.$t(\"settings.backup.restoreDownloadHistory\"),\n        fn: () => {\n          this.$emit(\n            \"download\",\n            this.server,\n            item,\n            ERestoreContent.downloadHistory\n          );\n        }\n      });\n\n      PPF.showContextMenu(menus, event);\n    }\n  }\n});\n</script>"
  },
  {
    "path": "src/options/views/settings/Base/Index.vue",
    "content": "<template>\n  <div>\n    <!-- <v-alert :value=\"true\" type=\"info\">{{ $t('settings.base.title') }}</v-alert> -->\n    <v-card>\n      <v-card-text>\n        <v-form v-model=\"valid\" ref=\"form\">\n          <!--  -->\n          <v-tabs centered dark icons-and-text v-model=\"activeTab\">\n            <v-tabs-slider color=\"yellow\"></v-tabs-slider>\n\n            <v-tab key=\"base\">\n              {{ $t('settings.base.tabs.base') }}\n              <v-icon>settings</v-icon>\n            </v-tab>\n\n            <v-tab key=\"search\">\n              {{ $t('settings.base.tabs.search') }}\n              <v-icon>search</v-icon>\n            </v-tab>\n\n            <v-tab key=\"download\">\n              {{ $t('settings.base.tabs.download') }}\n              <v-icon>cloud_download</v-icon>\n            </v-tab>\n\n            <v-tab key=\"advanced\">\n              {{ $t('settings.base.tabs.advanced') }}\n              <v-icon>memory</v-icon>\n            </v-tab>\n\n            <!-- 常规选项 -->\n            <v-tab-item key=\"base\">\n              <v-container fluid grid-list-xs>\n                <v-layout row wrap>\n                  <!-- 默认下载服务器 -->\n                  <v-flex xs10>\n                    <v-autocomplete\n                      v-model=\"options.defaultClientId\"\n                      :items=\"this.$store.state.options.clients\"\n                      :label=\"$t('settings.base.defaultClient')\"\n                      :menu-props=\"{maxHeight:'auto'}\"\n                      :hint=\"getClientAddress\"\n                      persistent-hint\n                      item-text=\"name\"\n                      item-value=\"id\"\n                      required\n                      :rules=\"rules.require\"\n                      autofocus\n                      ref=\"defaultClient\"\n                    >\n                      <template slot=\"selection\" slot-scope=\"{ item }\">\n                        <span v-text=\"item.name\"></span>\n                      </template>\n                      <template slot=\"item\" slot-scope=\"data\" style>\n                        <v-list-tile-content>\n                          <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n                          <v-list-tile-sub-title v-html=\"data.item.address\"></v-list-tile-sub-title>\n                        </v-list-tile-content>\n                        <v-list-tile-action>\n                          <v-list-tile-action-text>{{ data.item.type }}</v-list-tile-action-text>\n                        </v-list-tile-action>\n                      </template>\n                      <template slot=\"no-data\">\n                        <span class=\"ma-2\">{{ $t('settings.base.noClient') }}</span>\n                      </template>\n                    </v-autocomplete>\n                  </v-flex>\n                  <v-flex xs2></v-flex>\n\n                  <!-- 连接超时设置 -->\n                  <v-flex xs10>\n                    <v-text-field\n                      v-model=\"options.connectClientTimeout\"\n                      :label=\"$t('settings.base.connectClientTimeout')\"\n                      :placeholder=\"$t('settings.base.connectClientTimeout')\"\n                      type=\"number\"\n                    ></v-text-field>\n                  </v-flex>\n                  <v-flex xs2>\n                    <v-slider\n                      style=\"display:none;\"\n                      v-model=\"options.connectClientTimeout\"\n                      :max=\"60000\"\n                      :min=\"500\"\n                      :step=\"1\"\n                    ></v-slider>\n                  </v-flex>\n\n                  <v-flex xs12>\n                    <!-- 自动刷新用户数据 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.autoRefreshUserData\"\n                      :label=\"$t('settings.base.autoRefreshUserData')+autoRefreshUserDataLastUpdate\"\n                    ></v-switch>\n\n                    <!-- 自动刷新用户数据时间 -->\n                    <v-flex xs12 v-if=\"options.autoRefreshUserData\">\n                      <div style=\"margin: -40px 0 10px 45px;\">\n                        <span>{{ $t('settings.base.autoRefreshUserDataTip1') }}</span>\n                        <v-select\n                          v-model=\"options.autoRefreshUserDataHours\"\n                          :items=\"hours\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 30px;\"\n                        ></v-select>\n                        <span>:</span>\n                        <v-select\n                          v-model=\"options.autoRefreshUserDataMinutes\"\n                          :items=\"minutes\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 30px;\"\n                        ></v-select>\n                        <span>{{ $t('settings.base.autoRefreshUserDataTip2') }}</span>\n                      </div>\n                    </v-flex>\n\n                    <!-- 失败重试 -->\n                    <v-flex xs12 v-if=\"options.autoRefreshUserData\">\n                      <div style=\"margin: -20px 0 10px 45px;\">\n                        <span>{{ $t('settings.base.autoRefreshUserDataTip3') }}</span>\n                        <v-select\n                          v-model=\"options.autoRefreshUserDataFailedRetryCount\"\n                          :items=\"[1,2,3,4,5]\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 30px;\"\n                        ></v-select>\n                        <span>{{ $t('settings.base.autoRefreshUserDataTip4') }}</span>\n                        <v-select\n                          v-model=\"options.autoRefreshUserDataFailedRetryInterval\"\n                          :items=\"[1,2,3,4,5]\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 30px;\"\n                        ></v-select>\n                        <span>{{ $t('settings.base.autoRefreshUserDataTip5') }}</span>\n                      </div>\n                    </v-flex>\n\n                    <!-- 显示插件图标 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.showToolbarOnContentPage\"\n                      :label=\"$t('settings.base.showToolbarOnContentPage')\"\n                    ></v-switch>\n\n                    <!-- 插件显示位置 -->\n                    <v-radio-group\n                      v-model=\"options.position\"\n                      row\n                      v-if=\"options.showToolbarOnContentPage\"\n                      class=\"ml-5\"\n                    >\n                      <span class=\"mr-1\">{{ $t('settings.base.position.label') }}</span>\n                      <v-radio\n                        :label=\"$t('settings.base.position.left')\"\n                        color=\"success\"\n                        value=\"left\"\n                      ></v-radio>\n                      <v-radio\n                        :label=\"$t('settings.base.position.right')\"\n                        color=\"success\"\n                        value=\"right\"\n                      ></v-radio>\n                    </v-radio-group>\n\n                    <!-- 拖放下载 -->\n                    <v-switch\n                      color=\"success\"\n                      v-if=\"options.showToolbarOnContentPage\"\n                      v-model=\"options.allowDropToSend\"\n                      :label=\"$t('settings.base.allowDropToSend')\"\n                      class=\"ml-5\"\n                    ></v-switch>\n\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.searchResultOrderBySitePriority\"\n                      :label=\"$t('settings.base.searchResultOrderBySitePriority')\"\n                    ></v-switch>\n\n                    <!-- 允许备份Cookies -->\n                    <v-switch\n                      v-if=\"checkOptionalPermission('cookies')\"\n                      color=\"success\"\n                      v-model=\"options.allowBackupCookies\"\n                      :label=\"$t('settings.base.allowBackupCookies')\"\n                    ></v-switch>\n\n                    <!-- 加密备份数据 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.encryptBackupData\"\n                      :label=\"$t('settings.base.encryptBackupData')\"\n                    ></v-switch>\n\n                    <!-- 加密备份数据 -->\n                    <v-flex xs12 v-if=\"options.encryptBackupData\">\n                      <div style=\"margin: -20px 0 10px 45px;\">\n                        <v-text-field\n                          v-model=\"options.encryptSecretKey\"\n                          :label=\"$t('settings.base.encryptSecretKey')\"\n                          :placeholder=\"$t('settings.base.encryptTip')\"\n                          :type=\"showEncryptSecretKey ? 'text' : 'password'\"\n                          class=\"d-inline-flex\"\n                          style=\"min-width: 800px;\"\n                        >\n                          <template v-slot:append>\n                            <v-icon\n                              @click=\"showEncryptSecretKey = !showEncryptSecretKey\"\n                            >{{showEncryptSecretKey ? 'visibility_off' : 'visibility'}}</v-icon>\n                            <v-btn\n                              flat\n                              small\n                              color=\"primary\"\n                              @click=\"createSecretKey\"\n                              style=\"min-width:unset;\"\n                            >{{ $t('settings.base.createSecretKey') }}</v-btn>\n\n                            <v-btn\n                              flat\n                              small\n                              color=\"success\"\n                              @click=\"copySecretKeyToClipboard\"\n                              style=\"min-width:unset;\"\n                            >{{ $t('common.copy') }}</v-btn>\n                          </template>\n                        </v-text-field>\n\n                        <v-alert :value=\"true\" type=\"info\">{{ $t('settings.base.encryptTip') }}</v-alert>\n                      </div>\n                    </v-flex>\n                  </v-flex>\n                </v-layout>\n              </v-container>\n            </v-tab-item>\n\n            <!-- 搜索选项 -->\n            <v-tab-item key=\"search\">\n              <v-container fluid grid-list-xs>\n                <v-layout row wrap>\n                  <!-- 搜索返回的结果 -->\n                  <v-flex xs6>\n                    <v-text-field\n                      v-model=\"options.search.rows\"\n                      type=\"number\"\n                      :label=\"$t('settings.base.searchResultRows')\"\n                      :placeholder=\"$t('settings.base.searchResultRows')\"\n                    ></v-text-field>\n                  </v-flex>\n                  <v-flex xs6>\n                    <v-slider\n                      style=\"display:none;\"\n                      v-model=\"options.search.rows\"\n                      :max=\"200\"\n                      :min=\"1\"\n                    ></v-slider>\n                  </v-flex>\n\n                  <v-flex xs12>\n                    <!-- 启用内容选择搜索 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.allowSelectionTextSearch\"\n                      :label=\"$t('settings.base.allowSelectionTextSearch')\"\n                    ></v-switch>\n\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.search.saveKey\"\n                      :label=\"$t('settings.base.saveSearchKey')\"\n                    ></v-switch>\n\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.allowSaveSnapshot\"\n                      :label=\"$t('settings.base.allowSaveSnapshot')\"\n                    ></v-switch>\n\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.showMoiveInfoCardOnSearch\"\n                      :label=\"$t('settings.base.showMoiveInfoCardOnSearch')\"\n                    ></v-switch>\n\n                    <!-- 在搜索之前一些选项配置 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.beforeSearchingOptions.getMovieInformation\"\n                      :label=\"$t('settings.base.getMovieInformationBeforeSearching')\"\n                    ></v-switch>\n                    <v-flex xs12 v-if=\"options.beforeSearchingOptions.getMovieInformation\">\n                      <div style=\"margin: -40px 0 10px 45px;\">\n                        <span>{{ $t('settings.base.maxMovieInformationCount') }}</span>\n                        <v-text-field\n                          v-model=\"options.beforeSearchingOptions.maxMovieInformationCount\"\n                          class=\"ml-2 d-inline-flex\"\n                          style=\"max-width: 100px;max-height: 30px;\"\n                          type=\"number\"\n                        ></v-text-field>\n                        <v-slider\n                          style=\"display:none;\"\n                          v-model=\"options.beforeSearchingOptions.maxMovieInformationCount\"\n                          :max=\"20\"\n                          :min=\"1\"\n                        ></v-slider>\n                      </div>\n                    </v-flex>\n\n                    <!-- 当点击预选条目时，搜索模式 -->\n                    <v-flex xs12 v-if=\"options.beforeSearchingOptions.getMovieInformation\">\n                      <div style=\"margin: -20px 0 10px 45px;\">\n                        <span>{{ $t('settings.base.searchModeForItem') }}</span>\n                        <v-select\n                          v-model=\"options.beforeSearchingOptions.searchModeForItem\"\n                          :items=\"searchModes\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-height: 30px;\"\n                        ></v-select>\n                      </div>\n                    </v-flex>\n                  </v-flex>\n                </v-layout>\n              </v-container>\n            </v-tab-item>\n\n            <!-- 下载选项 -->\n            <v-tab-item key=\"download\">\n              <v-container fluid grid-list-xs>\n                <v-layout row wrap>\n                  <v-flex xs12>\n                    <!-- 批量下载时间间隔 -->\n                    <v-flex xs12>\n                      <v-text-field\n                        v-model=\"options.batchDownloadInterval\"\n                        :label=\"$t('settings.base.batchDownloadInterval')\"\n                        :placeholder=\"$t('settings.base.batchDownloadInterval')\"\n                        type=\"number\"\n                      ></v-text-field>\n                    </v-flex>\n\n                    <!-- 保存下载历史 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.saveDownloadHistory\"\n                      :label=\"$t('settings.base.saveDownloadHistory')\"\n                    ></v-switch>\n\n                    <!-- 下载失败重试 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.downloadFailedRetry\"\n                      :label=\"$t('settings.base.downloadFailedRetry')\"\n                    ></v-switch>\n\n                    <!-- 下载失败重试选项 -->\n                    <v-flex xs12 v-if=\"options.downloadFailedRetry\">\n                      <div style=\"margin: -35px 0 10px 45px;\">\n                        <span>{{ $t('settings.base.downloadFailedRetryTip1') }}</span>\n                        <v-select\n                          v-model=\"options.downloadFailedFailedRetryCount\"\n                          :items=\"[1,2,3,4,5]\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 20px;\"\n                        ></v-select>\n                        <span>{{ $t('settings.base.downloadFailedRetryTip2') }}</span>\n                        <v-text-field\n                          v-model=\"options.downloadFailedFailedRetryInterval\"\n                          class=\"ml-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 20px;\"\n                          type=\"number\"\n                        ></v-text-field>\n                        <span>{{ $t('settings.base.downloadFailedRetryTip3') }}</span>\n                      </div>\n                    </v-flex>\n\n                    <!-- 启用后台下载任务 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.enableBackgroundDownload\"\n                      :label=\"$t('settings.base.enableBackgroundDownload')\"\n                    ></v-switch>\n\n                    <!-- 批量下载确认 -->\n                    <v-switch\n                      color=\"success\"\n                      v-model=\"options.needConfirmWhenExceedSize\"\n                      :label=\"$t('settings.base.needConfirmWhenExceedSize')\"\n                    ></v-switch>\n\n                    <v-flex xs12 v-if=\"options.needConfirmWhenExceedSize\">\n                      <div style=\"margin: -40px 0 10px 40px;\">\n                        <v-text-field\n                          v-model=\"options.exceedSize\"\n                          :placeholder=\"$t('settings.base.exceedSize')\"\n                          class=\"ml-2 d-inline-flex\"\n                          style=\"max-width: 100px;max-height: 30px;\"\n                        ></v-text-field>\n                        <v-select\n                          v-model=\"options.exceedSizeUnit\"\n                          :items=\"units\"\n                          class=\"mx-2 d-inline-flex\"\n                          style=\"max-width: 50px;max-height: 30px;\"\n                        ></v-select>\n                      </div>\n                    </v-flex>\n                  </v-flex>\n                </v-layout>\n              </v-container>\n            </v-tab-item>\n\n            <!-- 高级 -->\n            <v-tab-item key=\"advanced\">\n              <v-container fluid grid-list-xs>\n                <v-layout row wrap>\n                  <v-flex xs12>\n                    <!-- omdb api key -->\n                    <v-textarea\n                      v-model=\"apiKey.omdb\"\n                      :label=\"$t('settings.base.apiKey.omdb')\"\n                      auto-grow\n                      box\n                    ></v-textarea>\n\n                    <!-- douban api key -->\n                    <v-textarea\n                      v-model=\"apiKey.douban\"\n                      :label=\"$t('settings.base.apiKey.douban')\"\n                      auto-grow\n                      box\n                    ></v-textarea>\n\n                    <div class=\"mb-4 text-xs-right\">\n                      <v-btn\n                        @click=\"verifyApiKey\"\n                        :loading=\"apiKeyVerifying\"\n                      >{{ $t('settings.base.verifyApiKey') }}</v-btn>\n                    </div>\n\n                    <v-alert :value=\"showVerifyingStatus\" color=\"info\" icon=\"info\" outline>\n                      <div>OMDb:</div>\n                      <div v-html=\"apiKeyVerifyResults.omdb.join('<br>')\"></div>\n                      <v-divider></v-divider>\n                      <div>Douban:</div>\n                      <div v-html=\"apiKeyVerifyResults.douban.join('<br>')\"></div>\n                    </v-alert>\n\n                    <v-alert :value=\"true\" color=\"info\" icon=\"info\" outline>\n                      <div v-html=\"$t('settings.base.apiKeyTip').toString().replace(/\\n/g, '<br>')\"></div>\n                    </v-alert>\n                  </v-flex>\n                </v-layout>\n              </v-container>\n            </v-tab-item>\n          </v-tabs>\n        </v-form>\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-btn color=\"success\" @click=\"save\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.base.save') }}</span>\n        </v-btn>\n        <v-spacer></v-spacer>\n      </v-card-actions>\n    </v-card>\n    <v-snackbar v-model=\"haveError\" absolute top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar\n      v-model=\"haveSuccess\"\n      absolute\n      bottom\n      :timeout=\"3000\"\n      color=\"success\"\n    >{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { APP } from \"@/service/api\";\nimport {\n  ESizeUnit,\n  EAction,\n  Options,\n  EBeforeSearchingItemSearchMode,\n  EEncryptMode\n} from \"@/interface/common\";\nimport Extension from \"@/service/extension\";\nimport { MovieInfoService } from \"@/service/movieInfoService\";\nimport { PPF } from \"@/service/public\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  data() {\n    return {\n      valid: false,\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      options: {\n        defaultClientId: \"\",\n        search: {\n          saveKey: true\n        },\n        needConfirmWhenExceedSize: false,\n        autoRefreshUserData: false,\n        autoRefreshUserDataHours: \"00\",\n        autoRefreshUserDataMinutes: \"00\",\n        autoRefreshUserDataFailedRetryCount: 3,\n        autoRefreshUserDataFailedRetryInterval: 5,\n        connectClientTimeout: 10000,\n        beforeSearchingOptions: {\n          getMovieInformation: true,\n          maxMovieInformationCount: 5,\n          searchModeForItem: EBeforeSearchingItemSearchMode.id\n        },\n        showToolbarOnContentPage: true,\n        downloadFailedRetry: false,\n        downloadFailedFailedRetryCount: 3,\n        downloadFailedFailedRetryInterval: 5,\n        apiKey: {},\n        batchDownloadInterval: 0,\n        enableBackgroundDownload: false,\n        position: \"right\",\n        allowBackupCookies: false,\n        encryptBackupData: false,\n        encryptMode: EEncryptMode.AES,\n        encryptSecretKey: \"\",\n        allowSaveSnapshot: true\n      } as Options,\n      units: [] as any,\n      hours: [] as any,\n      minutes: [] as any,\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      errorMsg: \"\",\n      lastUpdate: \"\",\n      autoRefreshUserDataLastUpdate: \"\",\n      activeTab: \"base\",\n      apiKey: {\n        omdb: \"\",\n        douban: \"\"\n      },\n      apiKeyVerifyResults: {\n        omdb: [] as any,\n        douban: [] as any\n      },\n      apiKeyVerifying: false,\n      showVerifyingStatus: false,\n      showEncryptSecretKey: false\n    };\n  },\n  methods: {\n    save() {\n      console.log(this.options);\n      this.successMsg = \"\";\n      if (!(this.$refs.form as any).validate()) {\n        this.activeTab = \"base\";\n        (this.$refs.defaultClient as any).focus();\n      }\n\n      if (!this.options.apiKey) {\n        this.options.apiKey = {};\n      }\n\n      let omdbApiKeys: string[] = [];\n      let doubanApiKeys: string[] = [];\n\n      // 添加 omdb api key\n      if (this.apiKey.omdb) {\n        let items = this.apiKey.omdb.split(\"\\n\");\n        items.forEach((item: string) => {\n          if (/^[a-z0-9]{7,8}$/.test(item)) {\n            omdbApiKeys.push(item);\n          }\n        });\n      }\n\n      // 添加 douban api key\n      if (this.apiKey.douban) {\n        let items = this.apiKey.douban.split(\"\\n\");\n        items.forEach((item: string) => {\n          if (/^[a-z0-9]{32}$/.test(item)) {\n            doubanApiKeys.push(item);\n          }\n        });\n      }\n\n      this.options.apiKey.omdb = omdbApiKeys;\n      this.options.apiKey.douban = doubanApiKeys;\n\n      // 如果启用备份 Cookies，检测是否有权限\n      if (this.options.allowBackupCookies) {\n        PPF.usePermissions(\n          [\"cookies\"],\n          true,\n          this.$t(\"permissions.request.cookies\").toString()\n        )\n          .then(() => {\n            this.$store.dispatch(\"saveConfig\", this.options);\n            this.successMsg = this.$t(\"settings.base.saved\").toString();\n          })\n          .catch(() => {\n            this.options.allowBackupCookies = false;\n            this.$store.dispatch(\"saveConfig\", this.options);\n            this.successMsg = this.$t(\"settings.base.saved\").toString();\n          });\n        return;\n      }\n\n      this.$store.dispatch(\"saveConfig\", this.options);\n      this.successMsg = this.$t(\"settings.base.saved\").toString();\n    },\n    clearCache() {\n      if (confirm(this.$t(\"settings.base.clearCacheConfirm\").toString())) {\n        APP.cache.clear();\n\n        setTimeout(() => {\n          extension\n            .sendRequest(EAction.reloadConfig)\n            .then(() => {\n              this.successMsg = this.$t(\n                \"settings.base.cacheIsCleared\"\n              ).toString();\n            })\n            .catch();\n        }, 200);\n      }\n    },\n    /**\n     * 测试 Api Key\n     */\n    verifyApiKey() {\n      let omdbKeys = this.apiKey.omdb.split(\"\\n\");\n      let doubanKeys = this.apiKey.douban.split(\"\\n\");\n      let count = omdbKeys.length + doubanKeys.length;\n      if (count == 0) {\n        return;\n      }\n      let movieSerice = new MovieInfoService();\n\n      this.apiKeyVerifyResults = {\n        omdb: [],\n        douban: []\n      };\n      this.apiKeyVerifying = true;\n      this.showVerifyingStatus = true;\n\n      let doneCount = 0;\n      omdbKeys.forEach((item: string) => {\n        if (/^[a-z0-9]{7,8}$/.test(item)) {\n          movieSerice\n            .verifyOmdbApiKey(item)\n            .then(() => {\n              this.apiKeyVerifyResults.omdb.push(`「${item}」 ok.`);\n            })\n            .catch(error => {\n              this.apiKeyVerifyResults.omdb.push(\n                `<span style='color:red'>「${item}」 error. (${error})</span>`\n              );\n            })\n            .finally(() => {\n              doneCount++;\n              if (doneCount === count) {\n                this.apiKeyVerifying = false;\n                window.setTimeout(() => {\n                  this.showVerifyingStatus = false;\n                }, 60000);\n              }\n            });\n        } else {\n          doneCount++;\n        }\n      });\n\n      doubanKeys.forEach((item: string) => {\n        if (/^[a-z0-9]{32}$/.test(item)) {\n          movieSerice\n            .verifyDoubanApiKey(item)\n            .then(() => {\n              this.apiKeyVerifyResults.douban.push(`「${item}」 ok.`);\n            })\n            .catch(error => {\n              this.apiKeyVerifyResults.douban.push(\n                `<span style='color:red'>「${item}」 error.</span>`\n              );\n            })\n            .finally(() => {\n              doneCount++;\n              if (doneCount === count) {\n                this.apiKeyVerifying = false;\n                window.setTimeout(() => {\n                  this.showVerifyingStatus = false;\n                }, 60000);\n              }\n            });\n        } else {\n          doneCount++;\n        }\n      });\n\n      if (doneCount === count) {\n        this.apiKeyVerifying = false;\n        this.showVerifyingStatus = false;\n      }\n    },\n\n    /**\n     * 随机生成一个密钥\n     */\n    createSecretKey() {\n      this.options.encryptSecretKey = PPF.getRandomString();\n    },\n\n    /**\n     * 复制密钥到剪切板\n     */\n    copySecretKeyToClipboard() {\n      this.successMsg = \"\";\n      extension\n        .sendRequest(\n          EAction.copyTextToClipboard,\n          null,\n          this.options.encryptSecretKey\n        )\n        .then(result => {\n          this.successMsg = this.$t(\"common.copyed\").toString();\n        })\n        .catch(() => {});\n    },\n\n    checkOptionalPermission(key: string): boolean {\n      return PPF.checkOptionalPermission(key);\n    }\n  },\n  created() {\n    this.options = Object.assign(this.options, this.$store.state.options);\n    this.units.push(ESizeUnit.MiB);\n    this.units.push(ESizeUnit.GiB);\n    this.units.push(ESizeUnit.TiB);\n    this.units.push(ESizeUnit.PiB);\n\n    for (let index = 0; index < 24; index++) {\n      this.hours.push(`0${index}`.substr(-2));\n    }\n\n    for (let index = 0; index < 60; index += 5) {\n      this.minutes.push(`0${index}`.substr(-2));\n    }\n\n    if (this.options.apiKey) {\n      if (this.options.apiKey.omdb && this.options.apiKey.omdb.length > 0) {\n        this.apiKey.omdb = this.options.apiKey.omdb.join(\"\\n\");\n      }\n\n      if (this.options.apiKey.douban && this.options.apiKey.douban.length > 0) {\n        this.apiKey.douban = this.options.apiKey.douban.join(\"\\n\");\n      }\n    }\n\n    APP.cache\n      .getLastUpdateTime()\n      .then((time: number) => {\n        if (time > 0) {\n          this.lastUpdate = this.$t(\"settings.base.lastUpdate\", {\n            time: new Date(time).toLocaleString()\n          }).toString();\n        } else {\n          this.lastUpdate = this.$t(\n            \"settings.base.lastUpdateUnknown\"\n          ).toString();\n        }\n      })\n      .catch(() => {\n        this.lastUpdate = this.$t(\"settings.base.lastUpdateFailed\").toString();\n      });\n\n    if (this.options.autoRefreshUserDataLastTime) {\n      this.autoRefreshUserDataLastUpdate = this.$t(\n        \"settings.base.autoRefreshUserDataLastUpdate\",\n        {\n          time: new Date(\n            this.options.autoRefreshUserDataLastTime\n          ).toLocaleString()\n        }\n      ).toString();\n    }\n  },\n  watch: { \n    successMsg: {\n      handler() {\n        this.haveSuccess = this.successMsg != \"\";\n      },\n      deep: true,\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  },\n  computed: {\n    getClientAddress(): any {\n      if (!this.options.defaultClientId) {\n        return \"\";\n      }\n      let client = this.$store.state.options.clients.find((data: any) => {\n        return this.options.defaultClientId === data.id;\n      });\n\n      if (client) {\n        return client.address;\n      }\n      return \"\";\n    },\n    searchModes(): Array<any> {\n      return [\n        {\n          value: EBeforeSearchingItemSearchMode.id,\n          text: this.$t(\n            \"settings.base.beforeSearchingItemSearchMode.id\"\n          ).toString()\n        },\n        {\n          value: EBeforeSearchingItemSearchMode.name,\n          text: this.$t(\n            \"settings.base.beforeSearchingItemSearchMode.name\"\n          ).toString()\n        }\n      ];\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.v-input--selection-controls {\n  margin: 0;\n  padding: 0;\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/DownloadClients/Add.vue",
    "content": "<template>\n  <div>\n    <v-snackbar v-model=\"valid\" top :timeout=\"3000\" color=\"error\">{{\n      $t(\"settings.downloadClients.add.validMsg\")\n    }}</v-snackbar>\n    <v-dialog v-model=\"show\" max-width=\"800\">\n      <v-card>\n        <v-toolbar dark color=\"blue-grey darken-2\">\n          <v-toolbar-title>{{\n            $t(\"settings.downloadClients.add.title\")\n          }}</v-toolbar-title>\n          <v-spacer></v-spacer>\n          <v-btn\n            icon\n            flat\n            color=\"success\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-download-client\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer nofollow\"\n            :title=\"$t('common.help')\"\n          >\n            <v-icon>help</v-icon>\n          </v-btn>\n        </v-toolbar>\n\n        <v-card-text>\n          <v-stepper v-model=\"step\">\n            <v-stepper-header>\n              <v-stepper-step :complete=\"step > 1\" step=\"1\">{{\n                $t(\"settings.downloadClients.add.titleStep1\")\n              }}</v-stepper-step>\n\n              <v-divider></v-divider>\n\n              <v-stepper-step step=\"2\">{{\n                $t(\"settings.downloadClients.add.titleStep2\")\n              }}</v-stepper-step>\n            </v-stepper-header>\n\n            <v-stepper-items>\n              <!-- 选择一个下载服务器 -->\n              <v-stepper-content step=\"1\">\n                <v-autocomplete\n                  v-model=\"selectedItem\"\n                  :items=\"items\"\n                  :label=\"$t('settings.downloadClients.add.validMsg')\"\n                  :menu-props=\"{ maxHeight: 'auto' }\"\n                  :hint=\"selectedItem.description\"\n                  persistent-hint\n                  return-object\n                  single-line\n                  item-text=\"name\"\n                  item-value=\"name\"\n                >\n                  <template slot=\"selection\" slot-scope=\"{ item }\">\n                    <v-list-tile-avatar>\n                      <img :src=\"item.icon\" />\n                    </v-list-tile-avatar>\n                    <span v-text=\"item.name\"></span>\n                  </template>\n                  <template slot=\"item\" slot-scope=\"data\" style>\n                    <v-list-tile-avatar>\n                      <img :src=\"data.item.icon\" />\n                    </v-list-tile-avatar>\n                    <v-list-tile-content>\n                      <v-list-tile-title\n                        v-html=\"data.item.name\"\n                      ></v-list-tile-title>\n                    </v-list-tile-content>\n                    <v-list-tile-action>\n                      <v-list-tile-action-text>{{\n                        data.item.ver\n                      }}</v-list-tile-action-text>\n                    </v-list-tile-action>\n                  </template>\n                </v-autocomplete>\n              </v-stepper-content>\n\n              <!-- 站点配置 -->\n              <v-stepper-content step=\"2\">\n                <Editor :option=\"selectedData\" />\n              </v-stepper-content>\n            </v-stepper-items>\n          </v-stepper>\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3\">\n          <v-btn\n            flat\n            color=\"grey darken-1\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/clients\"\n            target=\"_blank\"\n            v-show=\"step == 1\"\n            rel=\"noopener noreferrer nofollow\"\n          >\n            <v-icon>help</v-icon>\n            <span class=\"ml-1\">{{\n              $t(\"settings.downloadClients.add.helpMsg\")\n            }}</span>\n          </v-btn>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{\n              $t(\"settings.downloadClients.add.cancel\")\n            }}</span>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"grey darken-1\"\n            @click=\"step--\"\n            :disabled=\"step === 1\"\n          >\n            <v-icon>navigate_before</v-icon>\n            <span>{{ $t(\"settings.downloadClients.add.prevStep\") }}</span>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"blue\"\n            @click=\"next(step)\"\n            v-show=\"step < stepCount\"\n          >\n            <span>{{ $t(\"settings.downloadClients.add.nextStep\") }}</span>\n            <v-icon>navigate_next</v-icon>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"success\"\n            @click=\"save\"\n            v-show=\"step === stepCount\"\n            :disabled=\"!selectedData.valid\"\n          >\n            <v-icon>check_circle_outline</v-icon>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { Site, Options } from \"@/interface/common\";\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      step: 1,\n      show: false,\n      stepCount: 2,\n      selectedData: {} as any,\n      selectedItem: {} as any,\n      valid: false,\n      items: this.$store.state.options.system.clients,\n      options: this.$store.state.options\n    };\n  },\n  props: {\n    value: Boolean\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.step = 1;\n        this.selectedItem = { name: \"\" };\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", this.selectedData);\n      this.show = false;\n    },\n    next(step: number) {\n      if (this.selectedItem && this.selectedItem.name) {\n        this.selectedData = Object.assign({}, this.selectedItem);\n        this.valid = false;\n        this.step++;\n      } else {\n        this.valid = true;\n      }\n    },\n    cancel() {\n      this.show = false;\n    }\n  },\n  created() { }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/DownloadClients/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" max-width=\"800\">\n    <v-card>\n      <v-card-title\n        class=\"headline blue-grey darken-2\"\n        style=\"color:white\"\n      >{{ $t('settings.downloadClients.edit.title') }}</v-card-title>\n\n      <v-card-text>\n        <Editor :option=\"defaultItem\"/>\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.downloadClients.edit.cancel') }}</span>\n        </v-btn>\n        <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!defaultItem.valid\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.downloadClients.edit.ok') }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      defaultItem: {\n        name: \"\"\n      }\n    };\n  },\n  props: {\n    value: Boolean,\n    option: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultItem = Object.assign({}, this.option);\n      }\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", this.defaultItem);\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/DownloadClients/Editor.vue",
    "content": "<template>\n  <div>\n    <v-card class=\"mb-5\" :color=\"$vuetify.dark ? '' : 'grey lighten-4'\">\n      <v-card-text>\n        <v-form v-model=\"option.valid\">\n          <v-text-field\n            v-model=\"option.name\"\n            :label=\"$t('settings.downloadClients.editor.name')\"\n            :placeholder=\"$t('settings.downloadClients.editor.name')\"\n            required\n            :rules=\"rules.require\"\n          ></v-text-field>\n          <v-text-field\n            v-model=\"option.address\"\n            :label=\"$t('settings.downloadClients.editor.address')\"\n            :placeholder=\"$t('settings.downloadClients.editor.addressTip')\"\n            required\n            :rules=\"[rules.url]\"\n          ></v-text-field>\n\n          <v-text-field\n            v-model=\"option.loginName\"\n            :label=\"$t('settings.downloadClients.editor.loginName')\"\n            :placeholder=\"$t('settings.downloadClients.editor.loginName')\"\n            v-if=\"!option.passwordOnly\"\n          ></v-text-field>\n\n          <v-text-field\n            v-model=\"option.loginPwd\"\n            :label=\"$t('settings.downloadClients.editor.loginPwd')\"\n            :placeholder=\"$t('settings.downloadClients.editor.loginPwd')\"\n            :type=\"showPassword ? 'text' : 'password'\"\n            :append-icon=\"showPassword ? 'visibility_off' : 'visibility'\"\n            @click:append=\"showPassword = !showPassword\"\n          ></v-text-field>\n\n          <v-switch\n            :label=\"$t('settings.downloadClients.editor.autoStart')\"\n            v-model=\"option.autoStart\"\n            v-if=\"['transmission', 'qbittorrent'].includes(option.type)\"\n          ></v-switch>\n\n          <v-switch\n            :label=\"$t('settings.downloadClients.editor.tagIMDb')\"\n            v-model=\"option.tagIMDb\"\n            v-if=\"['qbittorrent'].includes(option.type)\"\n          ></v-switch>\n\n          <v-text-field\n            :value=\"option.type\"\n            :label=\"$t('settings.downloadClients.editor.type')\"\n            disabled\n          ></v-text-field>\n          <v-text-field\n            :label=\"$t('settings.downloadClients.editor.id')\"\n            disabled\n            :value=\"option.id\"\n            :placeholder=\"$t('settings.downloadClients.editor.autoCreate')\"\n          ></v-text-field>\n        </v-form>\n\n        <v-btn\n          flat\n          block\n          :color=\"testButtonColor\"\n          :loading=\"testing\"\n          :disabled=\"testing || !option.valid\"\n          @click=\"testClientConnectivity\"\n        >\n          <v-icon class=\"mr-2\">{{ testButtonIcon }}</v-icon>\n          {{ successMsg || errorMsg || $t('settings.downloadClients.editor.test') }}\n        </v-btn>\n        <v-alert :value=\"true\" color=\"info\" v-if=\"option.description\">{{ option.description }}</v-alert>\n        <v-alert :value=\"true\" color=\"warning\" v-if=\"option.warning\">{{ option.warning }}</v-alert>\n      </v-card-text>\n    </v-card>\n    <v-snackbar v-model=\"haveError\" absolute top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar\n      v-model=\"haveSuccess\"\n      absolute\n      bottom\n      :timeout=\"3000\"\n      color=\"success\"\n    >{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport md5 from \"blueimp-md5\";\nimport Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport { EAction, DataResult, Dictionary } from \"@/interface/common\";\nconst extension = new Extension();\nexport default Vue.extend({\n  data() {\n    return {\n      showPassword: false,\n      rules: {\n        require: [(v: any) => !!v || \"!\"],\n        url: (v: any) => {\n          return (\n            /^(https?):\\/\\/[-A-Za-z0-9+&@#/%?=~_|!:,.;\\[\\]]+[-A-Za-z0-9+&@#/%=~_|]$/.test(\n              v\n            ) || this.$t(\"settings.downloadClients.editor.addressTip\")\n          );\n        }\n      },\n      testing: false,\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      errorMsg: \"\",\n      testButtonIcon: \"compass_calibration\",\n      testButtonColor: \"info\",\n      testButtonStatus: {\n        success: \"success\",\n        error: \"error\"\n      },\n      buttonColor: {\n        default: \"info\",\n        success: \"success\",\n        error: \"error\"\n      } as Dictionary<any>,\n      buttonIcon: {\n        default: \"compass_calibration\",\n        success: \"done\",\n        error: \"close\"\n      } as Dictionary<any>\n    };\n  },\n  props: {\n    option: Object\n  },\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  },\n  methods: {\n    testClientConnectivity() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n      let options = Object.assign({}, this.option);\n      if (!options.address) {\n        this.errorMsg = this.$t(\n          \"settings.downloadClients.editor.testAddressError\"\n        ).toString();\n        return;\n      }\n      this.testing = true;\n\n      extension\n        .sendRequest(EAction.testClientConnectivity, null, options)\n        .then((result: DataResult) => {\n          console.log(result);\n          if (result.success) {\n            this.successMsg = this.$t(\n              \"settings.downloadClients.editor.testSuccess\"\n            ).toString();\n            this.setTestButtonStatus(this.testButtonStatus.success);\n          } else if (result && result.data) {\n            if (result.data.msg) {\n              this.errorMsg = result.data.msg;\n            } else if (result.data.code === 0) {\n              this.errorMsg = this.$t(\n                \"settings.downloadClients.editor.testConnectionError\"\n              ).toString();\n            } else {\n              this.errorMsg = this.$t(\n                \"settings.downloadClients.editor.testOtherError\",\n                {\n                  code: result.data.code\n                }\n              ).toString();\n            }\n          } else {\n            this.errorMsg = this.$t(\n              \"settings.downloadClients.editor.testUnknownError\"\n            ).toString();\n          }\n          this.errorMsg &&\n            this.setTestButtonStatus(this.testButtonStatus.error);\n          this.testing = false;\n        })\n        .catch((result: DataResult) => {\n          console.log(result);\n          if (result && result.data && result.data.msg) {\n            this.errorMsg = result.data.msg;\n          } else {\n            this.errorMsg = this.$t(\n              \"settings.downloadClients.editor.testError\"\n            ).toString();\n          }\n\n          this.setTestButtonStatus(this.testButtonStatus.error);\n          this.testing = false;\n        });\n    },\n\n    setTestButtonStatus(status: string) {\n      this.testButtonIcon = this.buttonIcon[status];\n      this.testButtonColor = this.buttonColor[status];\n      window.setTimeout(() => {\n        this.testButtonIcon = this.buttonIcon.default;\n        this.testButtonColor = this.buttonColor.default;\n        this.successMsg = \"\";\n        this.errorMsg = \"\";\n      }, 3000);\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/DownloadClients/Index.vue",
    "content": "<template>\n  <div class=\"set-download-clients\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t('settings.downloadClients.index.title') }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"add\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t('settings.downloadClients.index.add') }}\n        </v-btn>\n        <v-btn color=\"error\" :disabled=\"selected.length==0\" @click=\"removeSelected\">\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t('settings.downloadClients.index.remove') }}\n        </v-btn>\n        <v-spacer></v-spacer>\n        <v-text-field class=\"search\" append-icon=\"search\" label=\"Search\" single-line hide-details></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"this.$store.state.options.clients\"\n        :pagination.sync=\"pagination\"\n        item-key=\"id\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width:20px;\">\n            <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"edit(props.item)\">{{ props.item.name }}</a>\n          </td>\n          <td>{{ props.item.type }}</td>\n          <td>\n            <a\n              :href=\"props.item.address\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer nofollow\"\n            >{{ props.item.address }}</a>\n          </td>\n          <td>\n            <v-icon small class=\"mr-2\" @click=\"edit(props.item)\">edit</v-icon>\n            <v-icon small color=\"error\" @click=\"removeConfirm(props.item)\">delete</v-icon>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增 -->\n    <AddItem v-model=\"showAddDialog\" @save=\"addItem\"/>\n    <!-- 编辑 -->\n    <EditItem v-model=\"showEditDialog\" :option=\"selectedItem\" @save=\"updateItem\"/>\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title\n          class=\"headline red lighten-2\"\n        >{{ $t('settings.downloadClients.index.removeConfirmTitle') }}</v-card-title>\n\n        <v-card-text>{{ $t('settings.downloadClients.index.removeConfirm') }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm=false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadClients.index.cancel') }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadClients.index.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-alert :value=\"true\" color=\"grey\">\n      <div v-html=\"$t('settings.downloadClients.index.subTitle')\"></div>\n    </v-alert>\n\n    <v-snackbar\n      v-model=\"itemDuplicate\"\n      top\n      :timeout=\"3000\"\n      color=\"error\"\n    >{{ $t('settings.downloadClients.index.itemDuplicate') }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport AddItem from \"./Add.vue\";\nimport EditItem from \"./Edit.vue\";\nexport default Vue.extend({\n  components: {\n    AddItem,\n    EditItem\n  },\n  data() {\n    return {\n      showAddDialog: false,\n      showEditDialog: false,\n      itemDuplicate: false,\n      selected: [],\n      selectedItem: {},\n      pagination: {\n        rowsPerPage: -1\n      },\n      items: [],\n      dialogRemoveConfirm: false\n    };\n  },\n  created() {\n    this.items = this.$store.state.options.system.clients;\n  },\n  methods: {\n    add() {\n      this.showAddDialog = true;\n    },\n    addItem(item: any) {\n      let index = this.$store.state.options.clients.findIndex((data: any) => {\n        return data.name === item.name;\n      });\n      if (index === -1) {\n        this.$store.commit(\"addClient\", item);\n      } else {\n        this.itemDuplicate = true;\n      }\n    },\n\n    edit(item: any) {\n      let index = this.$store.state.options.clients.findIndex((data: any) => {\n        return item.id === data.id;\n      });\n\n      if (index !== -1) {\n        this.selectedItem = this.$store.state.options.clients[index];\n        this.showEditDialog = true;\n      }\n    },\n    updateItem(item: any) {\n      this.$store.commit(\"updateClient\", item);\n    },\n\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.commit(\"removeClient\", this.selectedItem);\n      this.selectedItem = {};\n    },\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    clear() {\n      if (\n        confirm(\n          this.$t(\"settings.downloadClients.index.clearConfirm\").toString()\n        )\n      ) {\n        this.$store.commit(\"clearClients\");\n      }\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\n            \"settings.downloadClients.index.removeSelectedConfirm\"\n          ).toString()\n        )\n      ) {\n        console.log(this.selected);\n        this.selected.forEach((item: any) => {\n          this.$store.commit(\"removeClient\", item);\n        });\n        this.selected = [];\n      }\n    }\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.downloadClients.index.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.downloadClients.index.headers.type\"),\n          align: \"left\",\n          value: \"type\"\n        },\n        {\n          text: this.$t(\"settings.downloadClients.index.headers.address\"),\n          align: \"left\",\n          value: \"address\"\n        },\n        {\n          text: this.$t(\"settings.downloadClients.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.set-download-clients {\n  .search {\n    max-width: 400px;\n  }\n}\n</style>"
  },
  {
    "path": "src/options/views/settings/DownloadPaths/Add.vue",
    "content": "<template>\n  <div>\n    <v-dialog v-model=\"show\" max-width=\"800\">\n      <v-card>\n        <v-card-title\n          class=\"headline blue-grey darken-2\"\n          style=\"color:white\"\n        >{{ $t('settings.downloadPaths.add.title') }}</v-card-title>\n\n        <v-card-text>\n          <v-form v-model=\"valid\">\n            <v-autocomplete\n              v-model=\"selectedSite\"\n              :items=\"getSites()\"\n              :label=\"$t('settings.downloadPaths.add.selectSite')\"\n              persistent-hint\n              single-line\n              item-text=\"name\"\n              item-value=\"host\"\n              return-object\n              :rules=\"rules.require\"\n            >\n              <template slot=\"selection\" slot-scope=\"{ item }\">\n                <v-list-tile-avatar>\n                  <img :src=\"item.icon\">\n                </v-list-tile-avatar>\n                <span v-text=\"item.name\"></span>\n              </template>\n              <template slot=\"item\" slot-scope=\"data\" style>\n                <v-list-tile-avatar>\n                  <img :src=\"data.item.icon\">\n                </v-list-tile-avatar>\n                <v-list-tile-content>\n                  <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n                  <v-list-tile-sub-title v-html=\"data.item.url\"></v-list-tile-sub-title>\n                </v-list-tile-content>\n                <v-list-tile-action>\n                  <v-list-tile-action-text>{{ joinTags(data.item.tags) }}</v-list-tile-action-text>\n                </v-list-tile-action>\n              </template>\n            </v-autocomplete>\n            <v-textarea\n              v-model=\"paths\"\n              :label=\"$t('settings.downloadPaths.add.path')\"\n              value\n              :hint=\"$t('settings.downloadPaths.add.pathTip')\"\n              :rules=\"rules.require\"\n            ></v-textarea>\n            <v-alert :value=\"true\" color=\"info\" icon=\"info\" outline v-if=\"client.pathDescription\">\n              <div v-html=\"client.pathDescription\"></div>\n              <KeyDescription/>\n            </v-alert>\n          </v-form>\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3\">\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadPaths.add.cancel') }}</span>\n          </v-btn>\n          <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadPaths.add.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { Site, DownloadClient } from \"@/interface/common\";\nimport Vue from \"vue\";\nimport KeyDescription from \"./KeyDescription.vue\";\n\nexport default Vue.extend({\n  components: {\n    KeyDescription\n  },\n  data() {\n    return {\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      show: false,\n      valid: false,\n      paths: \"\",\n      selectedSite: {}\n    };\n  },\n  props: {\n    value: Boolean,\n    client: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.paths = \"\";\n        this.selectedSite = {};\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", {\n        site: this.selectedSite,\n        paths: this.paths.split(\"\\n\")\n      });\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    joinTags(tags: any): string {\n      if (tags && tags.join) {\n        return tags.join(\", \");\n      }\n      return \"\";\n    },\n    getSites(): Site[] {\n      let clients = this.$store.state.options.clients;\n      let sites = this.$store.state.options.sites;\n      let result: Site[] = [];\n      if (clients && clients.length) {\n        let client: DownloadClient = clients.find((item: DownloadClient) => {\n          return item.id === this.client.id;\n        });\n        if (client && client.paths) {\n          sites.forEach((site: Site) => {\n            if (!client.paths.hasOwnProperty(site.host)) {\n              result.push(site);\n            }\n          });\n        } else {\n          result = sites;\n        }\n        return result;\n      }\n      return sites;\n    }\n  },\n  computed: {},\n  created() {}\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/DownloadPaths/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" max-width=\"800\">\n    <v-card>\n      <v-card-title\n        class=\"headline blue-grey darken-2\"\n        style=\"color:white\"\n      >{{ $t('settings.downloadPaths.edit.title') }}</v-card-title>\n\n      <v-card-text>\n        <v-form v-model=\"valid\">\n          <v-text-field\n            :label=\"$t('settings.downloadPaths.edit.site')\"\n            disabled\n            :value=\"defaultItem.name\"\n          ></v-text-field>\n          <v-textarea\n            v-model=\"defaultItem.paths\"\n            :label=\"$t('settings.downloadPaths.add.path')\"\n            value\n            :hint=\"$t('settings.downloadPaths.add.pathTip')\"\n            :rules=\"rules.require\"\n          ></v-textarea>\n          <v-alert :value=\"true\" color=\"info\" icon=\"info\" outline v-if=\"client.pathDescription\">\n            <div v-html=\"client.pathDescription\"></div>\n            <KeyDescription/>\n          </v-alert>\n        </v-form>\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.downloadPaths.add.cancel') }}</span>\n        </v-btn>\n        <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('settings.downloadPaths.add.ok') }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport KeyDescription from \"./KeyDescription.vue\";\n\nexport default Vue.extend({\n  components: {\n    KeyDescription\n  },\n  data() {\n    return {\n      show: false,\n      valid: false,\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      defaultItem: {\n        name: \"\",\n        site: {},\n        paths: \"\"\n      }\n    };\n  },\n  props: {\n    value: Boolean,\n    option: Object,\n    client: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultItem = Object.assign({}, this.option);\n        this.defaultItem.paths = this.option.paths.join(\"\\n\");\n      }\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", {\n        site: this.defaultItem.site,\n        paths: this.defaultItem.paths.split(\"\\n\")\n      });\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/DownloadPaths/Index.vue",
    "content": "<template>\n  <div class=\"set-download-clients\">\n    <v-alert :value=\"true\" type=\"info\">{{ $t('settings.downloadPaths.index.title') }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-autocomplete\n          v-model=\"selectedClient\"\n          :items=\"items\"\n          :label=\"$t('settings.downloadPaths.index.selectedClient')\"\n          :menu-props=\"{maxHeight:'auto'}\"\n          style=\"max-width: 500px;\"\n          :hint=\"selectedClient.address\"\n          return-object\n          persistent-hint\n          item-text=\"name\"\n          item-value=\"id\"\n        >\n          <template slot=\"selection\" slot-scope=\"{ item }\">\n            <span>{{ item.name }}</span>\n          </template>\n          <template slot=\"item\" slot-scope=\"data\">\n            <v-list-tile-content>\n              <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n              <v-list-tile-sub-title v-html=\"data.item.address\"></v-list-tile-sub-title>\n            </v-list-tile-content>\n            <v-list-tile-action>\n              <v-list-tile-action-text>{{ data.item.allowCustomPath?data.item.type:$t('settings.downloadPaths.index.notSupport') }}</v-list-tile-action-text>\n            </v-list-tile-action>\n          </template>\n        </v-autocomplete>\n\n        <v-spacer></v-spacer>\n\n        <v-btn\n          color=\"success\"\n          @click=\"add\"\n          :disabled=\"!selectedClient.id||!selectedClient.allowCustomPath\"\n        >\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t('settings.downloadPaths.index.add') }}\n        </v-btn>\n        <v-btn color=\"error\" :disabled=\"selected.length==0\" @click.stop=\"removeSelected\">\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t('settings.downloadPaths.index.remove') }}\n        </v-btn>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"getClientPaths\"\n        :pagination.sync=\"pagination\"\n        item-key=\"name\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width:20px;\">\n            <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"edit(props.item)\">{{ props.item.name }}</a>\n          </td>\n          <td>\n            <div v-for=\"(path, index) in props.item.paths\" :key=\"index\">{{path}}</div>\n          </td>\n          <td>\n            <v-icon small class=\"mr-2\" @click=\"edit(props.item)\">edit</v-icon>\n            <v-icon small color=\"error\" @click=\"removeConfirm(props.item)\">delete</v-icon>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增 -->\n    <AddItem v-model=\"showAddDialog\" @save=\"addItem\" :client=\"selectedClient\" />\n    <!-- 编辑 -->\n    <EditItem\n      v-model=\"showEditDialog\"\n      :option=\"selectedItem\"\n      @save=\"updateItem\"\n      :client=\"selectedClient\"\n    />\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title\n          class=\"headline red lighten-2\"\n        >{{ $t('settings.downloadPaths.index.removeConfirmTitle') }}</v-card-title>\n\n        <v-card-text>{{ $t('settings.downloadPaths.index.removeConfirm') }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm=false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadPaths.index.cancel') }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('settings.downloadPaths.index.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar\n      v-model=\"itemDuplicate\"\n      top\n      :timeout=\"3000\"\n      color=\"error\"\n    >{{ $t('settings.downloadPaths.index.itemDuplicate') }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport AddItem from \"./Add.vue\";\nimport EditItem from \"./Edit.vue\";\nimport { ECommonKey, EViewKey } from \"@/interface/enum\";\nexport default Vue.extend({\n  components: {\n    AddItem,\n    EditItem\n  },\n  data() {\n    return {\n      showAddDialog: false,\n      showEditDialog: false,\n      itemDuplicate: false,\n      selected: [],\n      selectedItem: {} as any,\n      pagination: {\n        rowsPerPage: -1\n      },\n      items: [],\n      dialogRemoveConfirm: false,\n      selectedClient: {\n        address: \"\"\n      } as any,\n      lastClientId: \"\"\n    };\n  },\n  created() {\n    if (\n      this.$store.state.options.clients &&\n      this.$store.state.options.clients.length > 0\n    ) {\n      this.items = this.$store.state.options.clients;\n      this.initView();\n    }\n  },\n  watch: {\n    selectedClient() {\n      if (this.selectedClient && this.selectedClient.id) {\n        this.lastClientId = this.selectedClient.id;\n        this.updateViewOptions();\n      }\n    }\n  },\n  methods: {\n    getPaths(paths: any) {\n      return paths.join(\"<br>\");\n    },\n    add() {\n      this.showAddDialog = true;\n    },\n    addItem(item: any) {\n      this.$store.commit(\"updatePathsOfClient\", {\n        clientId: this.selectedClient.id,\n        site: item.site,\n        paths: item.paths\n      });\n      this.reload();\n    },\n\n    edit(item: any) {\n      this.selectedItem = item;\n      this.showEditDialog = true;\n    },\n    updateItem(item: any) {\n      console.log(item);\n      this.$store.commit(\"updatePathsOfClient\", {\n        clientId: this.selectedClient.id,\n        site: item.site,\n        paths: item.paths\n      });\n      this.reload();\n    },\n\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.commit(\"removePathsOfClient\", {\n        clientId: this.selectedClient.id,\n        site: this.selectedItem.site\n      });\n      this.reload();\n    },\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    reload() {\n      let item = Object.assign({}, this.selectedClient);\n      this.selectedClient = null;\n      this.selectedClient = item;\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\n            \"settings.downloadPaths.index.removeSelectedConfirm\"\n          ).toString()\n        )\n      ) {\n        console.log(this.selected);\n        this.selected.forEach((item: any) => {\n          this.$store.commit(\"removePathsOfClient\", {\n            clientId: this.selectedClient.id,\n            site: item.site\n          });\n        });\n        this.selected = [];\n        this.reload();\n      }\n    },\n    /**\n     * 初始当前界面\n     */\n    initView() {\n      let options = this.$store.getters.viewsOptions(EViewKey.downloadPaths, {\n        lastClientId: \"\"\n      });\n\n      this.lastClientId = options.lastClientId;\n\n      if (this.lastClientId && this.items.length > 0) {\n        let selectedClient = this.items.find((item: any) => {\n          return item.id == this.lastClientId;\n        });\n\n        if (selectedClient) {\n          this.selectedClient = selectedClient;\n        }\n      }\n    },\n    /**\n     * 更新当前界面配置\n     */\n    updateViewOptions() {\n      this.$store.dispatch(\"updateViewOptions\", {\n        key: EViewKey.downloadPaths,\n        options: {\n          lastClientId: this.lastClientId\n        }\n      });\n    }\n  },\n  computed: {\n    getClientPaths(): any {\n      if (!this.selectedClient) {\n        return [];\n      }\n      if (!this.selectedClient.paths) {\n        return [];\n      }\n      let result = [];\n\n      let allSite = this.selectedClient.paths[ECommonKey.allSite];\n      if (allSite) {\n        result.push({\n          name: this.$t(\"settings.downloadPaths.index.allSite\").toString(),\n          site: null,\n          paths: allSite\n        });\n      }\n\n      for (const host in this.selectedClient.paths) {\n        let site = this.$store.state.options.sites.find((item: any) => {\n          return item.host == host;\n        });\n        if (site) {\n          result.push({\n            name: site.name,\n            site: site,\n            paths: this.selectedClient.paths[host]\n          });\n        }\n      }\n      return result;\n    },\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.downloadPaths.index.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.downloadPaths.index.headers.path\"),\n          align: \"left\",\n          sortable: false\n        },\n        {\n          text: this.$t(\"settings.downloadPaths.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.set-download-clients {\n  .search {\n    max-width: 400px;\n  }\n}\n</style>"
  },
  {
    "path": "src/options/views/settings/DownloadPaths/KeyDescription.vue",
    "content": "<template>\n  <div class=\"keyDescription\">\n    <span>{{ $t('settings.downloadPaths.keyDescription.allowKeys') }}</span>\n    <table>\n      <tr>\n        <td>$site.name$</td>\n        <td>\n          {{ $t('settings.downloadPaths.keyDescription.siteName') }}\n          <br />\n          {{ $t('settings.downloadPaths.keyDescription.example') }}/volume1/$site.name$/music -> /volume1/OpenCD/music\n        </td>\n      </tr>\n      <tr>\n        <td>$site.host$</td>\n        <td>\n          {{ $t('settings.downloadPaths.keyDescription.siteHost') }}\n          <br />\n          {{ $t('settings.downloadPaths.keyDescription.example') }}/volume1/$site.host$/music -> /volume1/open.cd/music\n        </td>\n      </tr>\n      <tr>\n        <td>$YYYY$</td>\n        <td>{{ $t('settings.downloadPaths.keyDescription.example') }}/volume1/$YYYY$/music -> /volume1/2019/music</td>\n      </tr>\n      <tr>\n        <td>$MM$</td>\n        <td>{{ $t('settings.downloadPaths.keyDescription.example') }}/volume1/$MM$/music -> /volume1/10/music</td>\n      </tr>\n      <tr>\n        <td>$DD$</td>\n        <td>{{ $t('settings.downloadPaths.keyDescription.example') }}/volume1/$DD$/music -> /volume1/01/music</td>\n      </tr>\n      <tr>\n        <td>&lt;...&gt;</td>\n        <td>\n          {{ $t('settings.downloadPaths.keyDescription.dynamic', {\n          key: \"&lt;...&gt;\"\n          }) }}\n          <br />\n          {{ $t('settings.downloadPaths.keyDescription.dynamicExample') }}\n        </td>\n      </tr>\n    </table>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({});\n</script>\n\n<style lang=\"scss\" scoped>\n.keyDescription {\n  padding-top: 10px;\n\n  table tr td {\n    padding: 3px;\n  }\n  table tr td:first-child {\n    text-align: right;\n    padding-right: 10px;\n  }\n}\n</style>"
  },
  {
    "path": "src/options/views/settings/Language/Index.vue",
    "content": "<template>\n  <div>未完成</div>\n</template>"
  },
  {
    "path": "src/options/views/settings/SearchSolution/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" fullscreen>\n    <v-card>\n      <v-card-title\n        class=\"headline blue-grey darken-2\"\n        style=\"color:white\"\n      >{{ $t(\"settings.searchSolution.edit.title\") }}</v-card-title>\n\n      <v-card-text class=\"body\">\n        <Editor :option=\"defaultItem\" :initSites=\"sites\" @change=\"change\" />\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3 toolber\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n        </v-btn>\n        <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nimport {\n  SearchSolution,\n  Site,\n  SearchSolutionRange,\n  SearchEntry\n} from \"@/interface/common\";\nimport { PPF } from \"@/service/public\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      valid: false,\n      sites: [] as Site[],\n      defaultItem: {\n        id: \"\",\n        name: \"\",\n        range: []\n      } as SearchSolution,\n      newValue: {} as SearchSolution\n    };\n  },\n  props: {\n    value: Boolean,\n    option: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show && this.option) {\n        this.defaultItem = Object.assign({}, this.option);\n      }\n    },\n    option() {\n      console.log(\"option change\", this.option);\n      this.resetSites();\n    }\n  },\n  methods: {\n    save() {\n      if (this.newValue) {\n        this.$emit(\"save\", this.newValue);\n        this.resetSites();\n      }\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    change(value: SearchSolution) {\n      console.log(value);\n      this.newValue = value;\n      this.valid = !!value.name;\n    },\n    resetSites() {\n      let sites: Site[] = PPF.clone(this.$store.state.options.sites);\n      // this.sites = JSON.parse(JSON.stringify(options.sites)) as Site[];\n      this.sites = [];\n      sites.forEach((item: Site) => {\n        this.sites.push(Object.assign({}, item));\n      });\n\n      console.log(\"resetSites\", this.option, this.sites);\n      if (this.option && this.option.id) {\n        // this.valid = true;\n        this.option.range.forEach((range: SearchSolutionRange) => {\n          let index = this.sites.findIndex((item: any) => {\n            return item.host === range.host;\n          });\n\n          if (index > -1) {\n            let site: any = this.sites[index];\n            site.enabled = true;\n            let results: string[] = [];\n            let siteEntry: SearchEntry[] = site.searchEntry;\n\n            if (siteEntry) {\n              siteEntry.forEach((v, index) => {\n                siteEntry[index].enabled = false;\n              });\n              range.entry &&\n                range.entry.forEach((key: string) => {\n                  let index: number = siteEntry.findIndex(\n                    (entry: SearchEntry) => {\n                      return entry.id == key || entry.name == key;\n                    }\n                  );\n                  if (siteEntry[index] && siteEntry[index].name) {\n                    siteEntry[index].enabled = true;\n                  }\n                });\n            }\n          }\n        });\n      }\n    }\n  },\n  created() {\n    this.resetSites();\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.body {\n  position: absolute;\n  bottom: 65px;\n  top: 65px;\n  overflow-y: auto;\n}\n.toolber {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SearchSolution/Editor.vue",
    "content": "<template>\n  <div class=\"search-solution-editor\">\n    <v-card :color=\"$vuetify.dark ? '' : 'grey lighten-4'\" class=\"body\">\n      <v-card-text>\n        <v-form v-model=\"isValid\" class=\"content\">\n          <v-layout row>\n            <v-flex xs3>\n              <v-subheader>{{ $t('settings.searchSolution.editor.name') }}</v-subheader>\n            </v-flex>\n            <v-flex xs9>\n              <v-text-field\n                v-model=\"option.name\"\n                :label=\"$t('settings.searchSolution.editor.name')\"\n                :placeholder=\"$t('settings.searchSolution.editor.name')\"\n                required\n                :rules=\"rules.require\"\n                @change=\"change(true)\"\n              ></v-text-field>\n            </v-flex>\n          </v-layout>\n          <v-layout row>\n            <v-flex xs3>\n              <v-subheader>{{$t('settings.searchSolution.editor.range')}}</v-subheader>\n            </v-flex>\n            <v-flex xs9>\n              <div class=\"list\">\n                <v-list dense>\n                  <!-- 站点列表 -->\n                  <v-data-table\n                    v-model=\"selected\"\n                    :headers=\"headers\"\n                    :items=\"sites\"\n                    :pagination.sync=\"pagination\"\n                    item-key=\"host\"\n                    select-all\n                    class=\"elevation-1\"\n                    hide-actions\n                  >\n                    <template slot=\"items\" slot-scope=\"props\">\n                      <tr>\n                        <td style=\"width:20px;\">\n                          <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n                        </td>\n                        <td>\n                          <div>{{ props.item.name }}</div>\n                          <v-container v-if=\"props.item.enabled\" fluid class=\"ma-0 pa-0 ml-4\">\n                            <v-layout row wrap class=\"ma-0 pa-0\">\n                              <v-flex\n                                class=\"ma-0 pa-0\"\n                                xs3\n                                v-for=\"(item, key, index) in props.item.searchEntry\"\n                                :key=\"index\"\n                              >\n                                <v-checkbox\n                                  class=\"ma-0 pa-0 caption\"\n                                  :label=\"item.name\"\n                                  v-model=\"item.enabled\"\n                                  @change=\"change(true)\"\n                                ></v-checkbox>\n                              </v-flex>\n                            </v-layout>\n                          </v-container>\n                        </td>\n                      </tr>\n                    </template>\n                  </v-data-table>\n                </v-list>\n              </div>\n            </v-flex>\n          </v-layout>\n        </v-form>\n        <v-divider class=\"mb-2\"></v-divider>\n        <div class=\"bottom\">\n          <template v-for=\"(item, index) in checked\">\n            <v-chip\n              :key=\"index\"\n              label\n              color=\"blue-grey\"\n              text-color=\"white\"\n              small\n              class=\"mr-2 pl-0\"\n              disabled\n            >{{ item.siteName }}{{ getSiteEntry(item.host, item.entry) }}</v-chip>\n          </template>\n        </div>\n      </v-card-text>\n    </v-card>\n    <v-snackbar v-model=\"haveError\" absolute top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar\n      v-model=\"haveSuccess\"\n      absolute\n      bottom\n      :timeout=\"3000\"\n      color=\"success\"\n    >{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Extension from \"@/service/extension\";\nimport {\n  EAction,\n  DataResult,\n  Site,\n  SearchSolutionRange,\n  SearchEntry\n} from \"@/interface/common\";\nconst extension = new Extension();\nexport default Vue.extend({\n  data() {\n    return {\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      pagination: {\n        rowsPerPage: -1\n      },\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      errorMsg: \"\",\n      isValid: false,\n      checked: [] as SearchSolutionRange[],\n      sites: [] as Site[],\n      selected: [] as any,\n      loading: false\n    };\n  },\n  props: {\n    option: Object,\n    initSites: {\n      type: Array as () => Array<any>\n    }\n  },\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    },\n    initSites() {\n      this.loading = true;\n      this.selected = [];\n      this.sites = this.initSites;\n      this.sites.forEach((item: any) => {\n        if (item.enabled) {\n          this.selected.push(item);\n        }\n      });\n      this.loading = false;\n      this.change(false);\n    },\n    selected() {\n      this.sites.forEach((item: any) => {\n        item.enabled = false;\n      });\n\n      this.selected.forEach((item: any) => {\n        item.enabled = true;\n      });\n\n      this.change();\n    }\n  },\n  methods: {\n    change(update: boolean = true) {\n      if (this.loading) return;\n      let checked: SearchSolutionRange[] = [];\n\n      this.sites.forEach((item: any) => {\n        if (item.enabled) {\n          let entry: any[] = [];\n          if (item.searchEntry) {\n            item.searchEntry.forEach((e: SearchEntry) => {\n              if (e.enabled) {\n                entry.push(e.id || e.name);\n              }\n            });\n          }\n          checked.push({\n            host: item.host,\n            siteName: item.name,\n            entry\n          });\n        }\n      });\n\n      if (update) {\n        this.$emit(\"change\", {\n          id: this.option.id,\n          name: this.option.name,\n          range: checked\n        });\n      }\n\n      this.checked = checked;\n    },\n    selectAll(selected: boolean) {\n      this.sites.forEach((item: any) => {\n        item.enabled = selected;\n        if (item.searchEntry) {\n          item.searchEntry.forEach((e: SearchEntry) => {\n            e.enabled = selected;\n          });\n        }\n      });\n      this.change();\n    },\n    getSiteEntry(host: string, entry: string[]): string {\n      let site: Site | undefined = this.sites.find((item: Site) => {\n        return item.host === host;\n      });\n\n      if (site && site.searchEntry) {\n        let results: string[] = [];\n        let siteEntry: SearchEntry[] = site.searchEntry;\n\n        entry.forEach((key: string) => {\n          let index: number = siteEntry.findIndex((entry: SearchEntry) => {\n            return entry.id == key || entry.name == key;\n          });\n          if (siteEntry[index] && siteEntry[index].name) {\n            results.push(siteEntry[index].name as string);\n          }\n        });\n\n        if (results.length > 0) {\n          return \" -> \" + results.join(\";\");\n        }\n      }\n      return \"\";\n    }\n  },\n  created() {},\n  computed: {\n    selectedAll(): boolean {\n      return this.checked.length === this.sites.length;\n    },\n    selectedSome(): boolean {\n      return this.checked.length > 0 && !this.selectedAll;\n    },\n    selectAllIcon(): string {\n      if (this.selectedAll) return \"check_box\";\n      if (this.selectedSome) return \"indeterminate_check_box\";\n      return \"check_box_outline_blank\";\n    },\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.searchSolution.editor.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        }\n      ];\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" >\n.search-solution-editor {\n  height: 100%;\n  .body {\n    height: 100%;\n\n    .content {\n      position: absolute;\n      bottom: 80px;\n      top: 20px;\n      width: 99%;\n      overflow-y: auto;\n\n      .list {\n        .caption {\n          .v-input__control {\n            .v-input__slot {\n              margin: 0;\n            }\n\n            .v-input--selection-controls__input {\n              margin-right: 0;\n            }\n          }\n\n          .v-icon {\n            font-size: 18px;\n          }\n\n          .v-label {\n            font-size: 12px;\n          }\n        }\n\n        .grey {\n          filter: grayscale(100%);\n        }\n      }\n    }\n    .bottom {\n      position: absolute;\n      bottom: 0;\n      width: 99%;\n      max-height: 80px;\n      overflow-y: auto;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SearchSolution/Index.vue",
    "content": "<template>\n  <div class=\"set-download-clients\">\n    <v-alert :value=\"true\" type=\"info\">{{\n      $t(\"settings.searchSolution.index.title\")\n    }}</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"add\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t(\"common.add\") }}\n        </v-btn>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/search-solution-definition\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.searchSolution.index.help\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n        ></v-text-field>\n      </v-card-title>\n\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"items\"\n        :pagination.sync=\"pagination\"\n        item-key=\"name\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width: 20px\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n            ></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"edit(props.item)\">{{ props.item.name }}</a>\n          </td>\n          <td>\n            <template v-for=\"(item, index) in props.item.range\">\n              <v-chip\n                :key=\"index\"\n                label\n                color=\"blue-grey\"\n                text-color=\"white\"\n                small\n                class=\"mr-2 pl-0\"\n                @click=\"editSearchEntry(item.host)\"\n                >{{ item.siteName\n                }}{{ getSiteEntry(item.host, item.entry) }}</v-chip\n              >\n            </template>\n          </td>\n          <td>\n            <v-icon small class=\"mr-2\" @click=\"edit(props.item)\">edit</v-icon>\n            <v-icon small color=\"error\" @click=\"removeConfirm(props.item)\"\n              >delete</v-icon\n            >\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增 -->\n    <EditItem v-model=\"showAddDialog\" @save=\"updateItem\" />\n    <!-- 编辑 -->\n    <EditItem\n      v-model=\"showEditDialog\"\n      :option=\"selectedItem\"\n      @save=\"updateItem\"\n    />\n\n    <!-- 删除确认 -->\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"settings.searchSolution.index.removeConfirmTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{\n          $t(\"settings.searchSolution.index.removeConfirm\")\n        }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"itemDuplicate\" top :timeout=\"3000\" color=\"error\">{{\n      $t(\"settings.searchSolution.index.itemDuplicate\")\n    }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport EditItem from \"./Edit.vue\";\nimport { SearchSolution, Site, SearchEntry, Options } from \"@/interface/common\";\nimport { PPF } from \"@/service/public\";\nexport default Vue.extend({\n  components: {\n    EditItem\n  },\n  data() {\n    return {\n      showAddDialog: false,\n      showEditDialog: false,\n      itemDuplicate: false,\n      selected: [],\n      selectedItem: {},\n      pagination: {\n        rowsPerPage: -1\n      },\n      items: [] as SearchSolution[],\n      dialogRemoveConfirm: false,\n      options: {} as Options\n    };\n  },\n  methods: {\n    add() {\n      this.selectedItem = {};\n      this.showEditDialog = true;\n    },\n    updateItem(item: SearchSolution) {\n      this.$store.dispatch(\"updateSearchSolution\", item);\n      this.pagination.rowsPerPage = 0;\n      this.pagination.rowsPerPage = -1;\n    },\n\n    edit(item: any) {\n      if (!this.options.searchSolutions) {\n        return;\n      }\n      let index = this.options.searchSolutions.findIndex((data: any) => {\n        return item.id === data.id;\n      });\n\n      if (index !== -1) {\n        this.selectedItem = PPF.clone(this.options.searchSolutions[index]);\n        this.showEditDialog = true;\n      }\n    },\n\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.dispatch(\"removeSearchSolution\", this.selectedItem);\n      this.selectedItem = {};\n    },\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\n            \"settings.searchSolution.index.removeSelectedConfirm\"\n          ).toString()\n        )\n      ) {\n        console.log(this.selected);\n        this.selected.forEach((item: any) => {\n          this.$store.dispatch(\"removeSearchSolution\", item);\n        });\n        this.selected = [];\n      }\n    },\n    getSiteEntry(host: string, entry: string[]): string {\n      let site: Site = this.options.sites.find((item: Site) => {\n        return item.host === host;\n      });\n\n      if (site && site.searchEntry) {\n        let results: string[] = [];\n        let siteEntry: SearchEntry[] = site.searchEntry;\n\n        entry.forEach((key: string) => {\n          let index: number = siteEntry.findIndex((entry: SearchEntry) => {\n            return entry.id == key || entry.name == key;\n          });\n          if (siteEntry[index] && siteEntry[index].name) {\n            results.push(siteEntry[index].name as string);\n          }\n        });\n\n        if (results.length > 0) {\n          return \" -> \" + results.join(\";\");\n        }\n      }\n      return \"\";\n    },\n    editSearchEntry(host: string) {\n      this.$router.push({\n        name: \"set-site-search-entry\",\n        params: {\n          host: host\n        }\n      });\n    }\n  },\n  created() {\n    this.options = this.$store.state.options;\n    this.items = this.$store.state.options.searchSolutions;\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.searchSolution.index.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.searchSolution.index.headers.range\"),\n          align: \"left\",\n          value: \"range\"\n        },\n        {\n          text: this.$t(\"settings.searchSolution.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.set-download-clients {\n  .search {\n    max-width: 400px;\n  }\n}\n</style>"
  },
  {
    "path": "src/options/views/settings/SitePlugins/Add.vue",
    "content": "<template>\n  <div>\n    <v-dialog v-model=\"show\" fullscreen>\n      <v-card>\n        <v-toolbar dark color=\"blue-grey darken-2\">\n          <v-toolbar-title>{{\n            $t(\"settings.sitePlugins.add.title\")\n          }}</v-toolbar-title>\n          <v-spacer></v-spacer>\n          <v-btn\n            icon\n            flat\n            color=\"success\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-custom-plugin\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer nofollow\"\n            :title=\"$t('common.help')\"\n          >\n            <v-icon>help</v-icon>\n          </v-btn>\n        </v-toolbar>\n\n        <v-card-text class=\"body\">\n          <Editor :initData=\"selected\" @change=\"change\" />\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3 toolbar\">\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      selected: {\n        script: `(function() {\n  console.log(\"I'm a plugin.\");\n})();`\n      } as any,\n      valid: false,\n      newData: {}\n    };\n  },\n  props: {\n    value: Boolean\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.selected = {};\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", Object.assign({ isCustom: true }, this.newData));\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.body {\n  position: absolute;\n  bottom: 65px;\n  top: 65px;\n  overflow-y: auto;\n}\n.toolbar {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SitePlugins/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" fullscreen>\n    <v-card>\n      <v-toolbar dark color=\"blue-grey darken-2\">\n        <v-toolbar-title>{{\n          $t(\"settings.sitePlugins.edit.title\")\n        }}</v-toolbar-title>\n        <v-spacer></v-spacer>\n        <v-btn\n          icon\n          flat\n          color=\"success\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-custom-plugin\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n          :title=\"$t('common.help')\"\n        >\n          <v-icon>help</v-icon>\n        </v-btn>\n      </v-toolbar>\n\n      <v-card-text class=\"body\">\n        <Editor :initData=\"defaultItem\" @change=\"change\" />\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3 toolbar\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n        </v-btn>\n        <v-btn\n          flat\n          color=\"success\"\n          @click=\"save\"\n          :disabled=\"!valid || defaultItem.readonly\"\n        >\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      defaultItem: {},\n      newData: {} as any,\n      valid: false\n    };\n  },\n  props: {\n    value: Boolean,\n    initData: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultItem = Object.assign({}, this.initData);\n      }\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", this.newData);\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    },\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    }\n  }\n});\n</script>\n<style lang=\"scss\" scoped>\n.body {\n  position: absolute;\n  bottom: 65px;\n  top: 65px;\n  overflow-y: auto;\n}\n.toolbar {\n  position: absolute;\n  bottom: 0;\n  right: 0;\n}\n</style>"
  },
  {
    "path": "src/options/views/settings/SitePlugins/Editor.vue",
    "content": "<template>\n  <v-card class=\"mb-5\" :color=\"$vuetify.dark ? '' : 'grey lighten-4'\">\n    <v-card-text>\n      <v-form v-model=\"valid\">\n        <!-- 站点名称 -->\n        <v-text-field\n          ref=\"name\"\n          v-model=\"option.name\"\n          :label=\"$t('settings.sitePlugins.editor.name')\"\n          :placeholder=\"$t('settings.sitePlugins.editor.name')\"\n          required\n          :rules=\"rules.require\"\n          :disabled=\"option.readonly\"\n        ></v-text-field>\n\n        <!-- 页面 -->\n        <v-combobox\n          v-model=\"option.pages\"\n          hide-selected\n          :hint=\"$t('settings.sitePlugins.editor.pagesTip')\"\n          :label=\"$t('settings.sitePlugins.editor.pages')\"\n          multiple\n          persistent-hint\n          small-chips\n          :disabled=\"option.readonly\"\n        ></v-combobox>\n\n        <!-- 附加脚本文件 -->\n        <v-combobox\n          v-model=\"option.scripts\"\n          hide-selected\n          :hint=\"$t('settings.sitePlugins.editor.scriptsTip')\"\n          :label=\"$t('settings.sitePlugins.editor.scripts')\"\n          multiple\n          persistent-hint\n          small-chips\n          :disabled=\"option.readonly\"\n        ></v-combobox>\n\n        <!-- 附加样式文件 -->\n        <v-combobox\n          v-model=\"option.styles\"\n          hide-selected\n          :hint=\"$t('settings.sitePlugins.editor.stylesTip')\"\n          :label=\"$t('settings.sitePlugins.editor.styles')\"\n          multiple\n          persistent-hint\n          small-chips\n          :disabled=\"option.readonly\"\n        ></v-combobox>\n\n        <!-- 脚本 -->\n        <v-textarea\n          v-model=\"option.script\"\n          :label=\"$t('settings.sitePlugins.editor.script')\"\n          height=\"200\"\n          :disabled=\"option.readonly\"\n        ></v-textarea>\n\n        <!-- 样式 -->\n        <v-textarea\n          v-model=\"option.style\"\n          :label=\"$t('settings.sitePlugins.editor.style')\"\n          height=\"200\"\n          :disabled=\"option.readonly\"\n        ></v-textarea>\n      </v-form>\n    </v-card-text>\n  </v-card>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Site } from \"@/interface/common\";\nexport default Vue.extend({\n  data() {\n    return {\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      valid: false,\n      option: {\n        name: \"\",\n        id: null,\n        pages: [],\n        scripts: [],\n        styles: [],\n        script: \"\",\n        style: \"\",\n        readonly: false\n      }\n    };\n  },\n  props: {\n    initData: {\n      type: Object,\n      default: () => ({\n        valid: false,\n        readonly: false\n      })\n    }\n  },\n  watch: {\n    option: {\n      handler() {\n        this.$emit(\"change\", {\n          data: this.option,\n          valid: this.valid\n        });\n      },\n      deep: true\n    },\n    initData() {\n      if (this.initData) {\n        this.option = Object.assign({}, this.initData);\n      }\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\">\n.v-textarea {\n  .v-text-field__slot {\n    height: 100%;\n    textarea {\n      height: 100%;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SitePlugins/Index.vue",
    "content": "<template>\n  <div class=\"site-plugins\">\n    <v-alert :value=\"true\" type=\"info\"\n      >{{ $t(\"settings.sitePlugins.index.title\") }} [{{ site.name }}]</v-alert\n    >\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"add\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t(\"common.add\") }}\n        </v-btn>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n        <v-divider class=\"mx-3 mt-0\" inset vertical></v-divider>\n\n        <input\n          type=\"file\"\n          ref=\"fileImport\"\n          style=\"display: none\"\n          multiple\n          accept=\"application/json\"\n        />\n        <!-- 导入配置文件 -->\n        <v-btn color=\"info\" @click=\"importConfig\">\n          <v-icon class=\"mr-2\">folder_open</v-icon>\n          {{ $t(\"settings.sites.index.importConfig\") }}\n        </v-btn>\n\n        <v-divider class=\"mx-3 mt-0\" inset vertical></v-divider>\n\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-custom-plugin\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.siteSearchEntry.index.help\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n        ></v-text-field>\n      </v-card-title>\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"plugins\"\n        :pagination.sync=\"pagination\"\n        item-key=\"name\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width: 20px\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n              v-if=\"props.item.isCustom\"\n            ></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"edit(props.item)\">\n              <span class=\"ml-2\">{{ props.item.name }}</span>\n            </a>\n          </td>\n          <td>\n            <v-chip\n              label\n              color=\"light-blue\"\n              text-color=\"white\"\n              v-for=\"(page, index) in props.item.pages\"\n              :key=\"index\"\n              small\n            >\n              <!-- <v-icon left small>label</v-icon> -->\n              {{ page }}\n            </v-chip>\n          </td>\n          <td>{{ props.item.url }}</td>\n          <td>\n            <v-icon\n              small\n              class=\"mr-2\"\n              @click=\"edit(props.item)\"\n              :title=\"$t('common.edit')\"\n              v-if=\"props.item.isCustom\"\n              >edit</v-icon\n            >\n            <v-icon\n              small\n              color=\"error\"\n              @click=\"removeConfirm(props.item)\"\n              :title=\"$t('common.remove')\"\n              v-if=\"props.item.isCustom\"\n              >delete</v-icon\n            >\n\n            <v-icon\n              small\n              color=\"info\"\n              class=\"ml-2\"\n              @click=\"share(props.item)\"\n              :title=\"$t('common.share')\"\n              v-if=\"props.item.isCustom\"\n              >share</v-icon\n            >\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增插件 -->\n    <AddItem v-model=\"showAddDialog\" @save=\"addItem\" />\n    <!-- 编辑插件 -->\n    <EditItem\n      v-model=\"showEditDialog\"\n      :initData=\"selectedItem\"\n      @save=\"updateItem\"\n    />\n\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"settings.sitePlugins.index.removeTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{\n          $t(\"settings.sitePlugins.index.removeConfirm\")\n        }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      errorMsg\n    }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{\n      successMsg\n    }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Site, Plugin } from \"@/interface/common\";\nimport AddItem from \"./Add.vue\";\nimport EditItem from \"./Edit.vue\";\n\nimport { filters } from \"@/service/filters\";\nimport { PPF } from \"@/service/public\";\nimport FileSaver from \"file-saver\";\n\nexport default Vue.extend({\n  components: {\n    AddItem,\n    EditItem\n  },\n  data() {\n    return {\n      selected: [],\n      pagination: {\n        rowsPerPage: -1\n      },\n      showAddDialog: false,\n      showEditDialog: false,\n      site: {} as Site,\n      selectedItem: {\n        name: \"\",\n        id: null,\n        pages: [],\n        scripts: [],\n        styles: [],\n        script: \"\",\n        style: \"\",\n        readonly: false\n      } as any,\n      dialogRemoveConfirm: false,\n      plugins: [] as any,\n      fileImport: null as any,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\"\n    };\n  },\n  methods: {\n    add() {\n      this.showAddDialog = true;\n    },\n    edit(source: any) {\n      this.selectedItem = PPF.clone(source);\n      if (!source.id) {\n        this.selectedItem.readonly = true;\n      }\n\n      this.showEditDialog = true;\n    },\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.commit(\"removePlugin\", {\n        host: this.site.host,\n        plugin: this.selectedItem\n      });\n      this.selectedItem = {};\n      this.reloadPlugins(this.site.host);\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\"settings.sitePlugins.index.removeSelectedConfirm\").toString()\n        )\n      ) {\n        this.selected.forEach((item: Plugin) => {\n          if (item.isCustom) {\n            this.$store.commit(\"removePlugin\", {\n              host: this.site.host,\n              plugin: item\n            });\n          }\n        });\n        this.selected = [];\n        this.reloadPlugins(this.site.host);\n      }\n    },\n    updateItem(item: any) {\n      console.log(item);\n      this.selectedItem = item;\n      this.$store.commit(\"updatePlugin\", {\n        host: this.site.host,\n        plugin: item\n      });\n      this.reloadPlugins(this.site.host);\n    },\n    addItem(item: any) {\n      console.log(item);\n      this.$store.commit(\"addPlugin\", {\n        host: this.site.host,\n        plugin: item\n      });\n      this.reloadPlugins(this.site.host);\n    },\n    reloadPlugins(host: any) {\n      this.site = this.$store.state.options.sites.find((item: Site) => {\n        return item.host == host;\n      });\n\n      if (this.site) {\n        let plugins: any[] = [];\n\n        let schema = this.site.schema;\n        if (typeof schema === \"string\") {\n          let _schema = this.$store.state.options.system.schemas.find(\n            (item: Site) => {\n              return item.name == schema;\n            }\n          );\n          if (_schema) {\n            plugins.push(..._schema.plugins);\n          }\n        } else if (schema && schema.plugins) {\n          let site = this.$store.state.options.system.sites.find(\n            (item: Site) => {\n              return item.host == host;\n            }\n          );\n          if (site && site.schema && site.schema.plugins) {\n            plugins.push(...site.schema.plugins);\n          }\n        }\n\n        if (this.site.plugins) {\n          plugins.push(...this.site.plugins);\n        }\n\n        this.plugins = plugins;\n      }\n    },\n\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n\n    /**\n     * 导出插件\n     */\n    share(item: Plugin) {\n      let fileName =\n        (this.site.host || this.site.name) + \"-plugin-\" + item.name + \".json\";\n\n      const blob = new Blob([JSON.stringify(item)], {\n        type: \"text/plain\"\n      });\n      FileSaver.saveAs(blob, fileName);\n    },\n    /**\n     * 导入配置文件\n     */\n    importConfig() {\n      this.fileImport.click();\n    },\n\n    importConfigFile(event: Event) {\n      this.clearMessage();\n      let inputDOM: any = event.srcElement;\n      if (inputDOM.files.length > 0 && inputDOM.files[0].name.length > 0) {\n        for (let index = 0; index < inputDOM.files.length; index++) {\n          const file = inputDOM.files[index];\n          const r = new FileReader();\n          r.onload = (e: any) => {\n            try {\n              const result = JSON.parse(e.target.result);\n              this.importPlugin(result);\n            } catch (error) {\n              console.log(error);\n              this.errorMsg = this.$t(\"common.importFailed\").toString();\n            }\n          };\n          r.onerror = () => {\n            this.errorMsg = this.$t(\"settings.backup.loadError\").toString();\n          };\n          r.readAsText(file);\n        }\n\n        inputDOM.value = \"\";\n      }\n    },\n\n    /**\n     * 导入插件信息\n     */\n    importPlugin(source: Plugin) {\n      if (\n        !(\n          source.name &&\n          source.id &&\n          source.isCustom &&\n          source.pages &&\n          source.pages.length > 0\n        )\n      ) {\n        this.errorMsg = this.$t(\n          \"settings.sitePlugins.index.invalidPlugin\"\n        ).toString();\n        return;\n      }\n      const plugin = this.getPlugin(source);\n      if (plugin) {\n        const newName = window.prompt(\n          this.$t(\"settings.sitePlugins.index.importNameDuplicate\", {\n            name: plugin.name\n          }).toString()\n        );\n\n        if (newName) {\n          source.name = newName;\n          this.importPlugin(source);\n          return;\n        } else {\n          return;\n        }\n      }\n      this.addItem(source);\n\n      this.successMsg = this.$t(\"common.importSuccess\").toString();\n    },\n\n    getPlugin(source: Plugin) {\n      const plugins = this.site.plugins;\n      if (plugins && plugins.length > 0) {\n        return plugins.find((item: Plugin) => {\n          return item.name === source.name;\n        });\n      }\n      return null;\n    }\n  },\n  created() {\n    let host = this.$route.params[\"host\"];\n    console.log(\"create\", this.$route.params);\n    if (host) {\n      this.reloadPlugins(host);\n    }\n  },\n  mounted() {\n    this.fileImport = this.$refs.fileImport;\n    this.fileImport.addEventListener(\"change\", this.importConfigFile);\n  },\n  beforeDestroy() {\n    this.fileImport.removeEventListener(\"change\", this.importConfigFile);\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.sitePlugins.index.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.sitePlugins.index.headers.pages\"),\n          align: \"left\",\n          value: \"pages\"\n        },\n        {\n          text: this.$t(\"settings.sitePlugins.index.headers.enable\"),\n          align: \"left\",\n          value: \"enable\"\n        },\n        {\n          text: this.$t(\"settings.sitePlugins.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  },\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.site-plugins {\n  .search {\n    max-width: 400px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SiteSearchEntry/Add.vue",
    "content": "<template>\n  <div>\n    <v-dialog v-model=\"show\" max-width=\"900\">\n      <v-card>\n        <v-card-title\n          class=\"headline blue-grey darken-2\"\n          style=\"color:white\"\n        >{{ $t('settings.siteSearchEntry.add.title') }}</v-card-title>\n\n        <v-card-text>\n          <Editor :data=\"selected\" :site=\"site\"/>\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3\">\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('common.cancel') }}</span>\n          </v-btn>\n          <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!selected.valid\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nimport { ERequestResultType } from \"@/interface/common\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      selected: { isCustom: true } as any,\n      valid: false\n    };\n  },\n  props: {\n    value: Boolean,\n    site: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.selected = { isCustom: true };\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\n        \"save\",\n        Object.assign(\n          { isCustom: true, resultType: ERequestResultType.HTML },\n          this.selected\n        )\n      );\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/SiteSearchEntry/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" max-width=\"900\">\n    <v-card>\n      <v-card-title\n        class=\"headline blue-grey darken-2\"\n        style=\"color:white\"\n      >{{ $t('settings.siteSearchEntry.edit.title') }}</v-card-title>\n\n      <v-card-text>\n        <Editor :data=\"defaultItem\" :site=\"site\"/>\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t('common.cancel') }}</span>\n        </v-btn>\n        <v-btn\n          flat\n          color=\"success\"\n          @click=\"save\"\n          :disabled=\"!defaultItem.valid && !defaultItem.isCustom\"\n        >\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport Editor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    Editor\n  },\n  data() {\n    return {\n      show: false,\n      defaultItem: {}\n    };\n  },\n  props: {\n    value: Boolean,\n    data: Object,\n    site: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultItem = Object.assign({}, this.data);\n      }\n    }\n  },\n  methods: {\n    save() {\n      this.$emit(\"save\", this.defaultItem);\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/SiteSearchEntry/Editor.vue",
    "content": "<template>\n  <v-card :color=\"$vuetify.dark ? '' : 'grey lighten-4'\">\n    <v-card-text>\n      <v-form v-model=\"data.valid\">\n        <!-- 名称 -->\n        <v-text-field\n          ref=\"name\"\n          v-model=\"data.name\"\n          :label=\"$t('settings.siteSearchEntry.editor.name')\"\n          :placeholder=\"$t('settings.siteSearchEntry.editor.name')\"\n          required\n          :rules=\"rules.require\"\n          :disabled=\"!data.isCustom\"\n        ></v-text-field>\n\n        <!-- 入口页面 -->\n        <v-text-field\n          v-model=\"data.entry\"\n          :label=\"$t('settings.siteSearchEntry.editor.entry')\"\n          :placeholder=\"$t('settings.siteSearchEntry.editor.entry')\"\n          :disabled=\"!data.isCustom\"\n        ></v-text-field>\n\n        <!-- 资源分类 -->\n        <v-autocomplete\n          v-if=\"category && category.length\"\n          v-model=\"data.categories\"\n          :items=\"category\"\n          :label=\"$t('settings.siteSearchEntry.editor.category')\"\n          item-text=\"name\"\n          item-value=\"id\"\n          chips\n          clearable\n          multiple\n          :disabled=\"!data.isCustom\"\n        >\n          <template slot=\"selection\" slot-scope=\"data\">\n            <v-chip\n              small\n              :selected=\"data.selected\"\n              close\n              @input=\"remove(data.item)\"\n            >{{ data.item.name }}</v-chip>\n          </template>\n        </v-autocomplete>\n\n        <!-- 追加的查询字符串 -->\n        <v-text-field\n          v-model=\"data.queryString\"\n          :label=\"$t('settings.siteSearchEntry.editor.queryString')\"\n          :placeholder=\"$t('settings.siteSearchEntry.editor.queryString')\"\n          :disabled=\"!data.isCustom\"\n        ></v-text-field>\n\n        <v-text-field\n          v-model=\"data.parseScriptFile\"\n          :label=\"$t('settings.siteSearchEntry.editor.parseScriptFile')\"\n          :placeholder=\"$t('settings.siteSearchEntry.editor.parseScriptFile')\"\n          :disabled=\"!data.isCustom\"\n        ></v-text-field>\n\n        <!-- 脚本 -->\n        <v-textarea\n          v-model=\"data.parseScript\"\n          :label=\"$t('settings.siteSearchEntry.editor.parseScript')\"\n          height=\"200\"\n          :disabled=\"!data.isCustom\"\n        ></v-textarea>\n\n        <!-- 种子列表定位选择器 -->\n        <v-textarea\n          v-model=\"data.resultSelector\"\n          :label=\"$t('settings.siteSearchEntry.editor.resultSelector')\"\n          height=\"80\"\n          :disabled=\"!data.isCustom\"\n        ></v-textarea>\n      </v-form>\n    </v-card-text>\n  </v-card>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Site, SiteCategories, SiteCategory } from \"@/interface/common\";\nexport default Vue.extend({\n  data() {\n    return {\n      rules: {\n        require: [(v: any) => !!v || \"!\"]\n      },\n      checked: [],\n      categoryConfig: {} as SiteCategories\n    };\n  },\n  props: {\n    data: {\n      type: Object,\n      default: () => ({\n        valid: false\n      })\n    },\n    site: Object\n  },\n  watch: {\n    \"data.categories\"() {\n      let result: string[] = [];\n      if (\n        this.data &&\n        this.data.categories &&\n        this.data.categories.length > 0\n      ) {\n        this.data.categories.forEach((id: number | string) => {\n          let cat: any = this.category.find((c: any) => {\n            return c.id == id;\n          });\n          if (cat) {\n            result.push(cat.key);\n          }\n        });\n      }\n\n      if (this.categoryConfig.appendToSearchKey) {\n        this.data.appendToSearchKeyString = result.join(\"\");\n      } else {\n        this.data.queryString = result.join(\"\");\n      }\n    }\n  },\n  methods: {\n    remove(category: SiteCategory) {\n      let index: number = this.data.categories.findIndex((item: any) => {\n        return category.id === item.id;\n      });\n\n      if (index != -1) {\n        this.data.categories.splice(index, 1);\n      }\n    }\n  },\n  computed: {\n    /**\n     * 获取当前可用类别\n     */\n    category(): SiteCategory[] {\n      let site: Site = this.site;\n      let result: SiteCategory[] = [];\n      if (site.categories) {\n        site.categories.find((item: SiteCategories) => {\n          if (\n            item.category &&\n            (item.entry == \"*\" ||\n              (this.data.entry && this.data.entry.indexOf(item.entry) > -1))\n          ) {\n            this.categoryConfig = item;\n            let key = item.result + \"\";\n            item.category.forEach((category: SiteCategory) => {\n              result.push(\n                Object.assign(\n                  {\n                    key: key.replace(/\\$id\\$/gi, category.id + \"\")\n                  },\n                  category\n                )\n              );\n            });\n            return true;\n          }\n          return false;\n        });\n      }\n      return result;\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\">\n.v-textarea {\n  .v-text-field__slot {\n    height: 100%;\n    textarea {\n      height: 100%;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SiteSearchEntry/Index.vue",
    "content": "<template>\n  <div class=\"site-search-entry\">\n    <v-alert :value=\"true\" type=\"info\"\n      >{{ $t(\"settings.siteSearchEntry.index.title\") }} [{{\n        site.name\n      }}]</v-alert\n    >\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"add\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{ $t(\"common.add\") }}\n        </v-btn>\n        <v-btn\n          color=\"error\"\n          :disabled=\"selected.length == 0\"\n          @click=\"removeSelected\"\n        >\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{ $t(\"common.remove\") }}\n        </v-btn>\n        <v-btn\n          color=\"info\"\n          href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/search-entry-definition\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer nofollow\"\n        >\n          <v-icon class=\"mr-2\">help</v-icon>\n          {{ $t(\"settings.siteSearchEntry.index.help\") }}\n        </v-btn>\n        <v-spacer></v-spacer>\n        <v-text-field\n          class=\"search\"\n          append-icon=\"search\"\n          label=\"Search\"\n          single-line\n          hide-details\n        ></v-text-field>\n      </v-card-title>\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"searchEntry\"\n        :pagination.sync=\"pagination\"\n        item-key=\"name\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width: 20px\">\n            <v-checkbox\n              v-model=\"props.selected\"\n              primary\n              hide-details\n              v-if=\"props.item.isCustom\"\n            ></v-checkbox>\n          </td>\n          <td>\n            <a @click=\"edit(props.item)\">\n              <span class=\"ml-2\">{{ props.item.name }}</span>\n            </a>\n          </td>\n          <td class=\"cat\">\n            <template v-for=\"(item, index) in getCategory(props.item)\">\n              <v-chip\n                :key=\"index\"\n                label\n                color=\"blue-grey\"\n                text-color=\"white\"\n                small\n                class=\"mr-2 pl-0\"\n                disabled\n                >{{ item }}</v-chip\n              >\n            </template>\n          </td>\n          <td>\n            <v-switch\n              true-value=\"true\"\n              false-value=\"false\"\n              :input-value=\"props.item.enabled ? 'true' : 'false'\"\n              hide-details\n            ></v-switch>\n          </td>\n          <td>\n            <v-icon\n              small\n              class=\"mr-2\"\n              @click=\"copy(props.item)\"\n              :title=\"$t('common.copy')\"\n              >file_copy</v-icon\n            >\n            <v-icon\n              small\n              class=\"mr-2\"\n              @click=\"edit(props.item)\"\n              v-if=\"props.item.isCustom\"\n              :title=\"$t('common.edit')\"\n              >edit</v-icon\n            >\n            <v-icon\n              small\n              color=\"error\"\n              @click=\"removeConfirm(props.item)\"\n              v-if=\"props.item.isCustom\"\n              :title=\"$t('common.remove')\"\n              >delete</v-icon\n            >\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增 -->\n    <AddItem v-model=\"showAddDialog\" @save=\"addItem\" :site=\"site\" />\n    <!-- 编辑 -->\n    <EditItem\n      v-model=\"showEditDialog\"\n      :site=\"site\"\n      :data=\"selectedItem\"\n      @save=\"updateItem\"\n    />\n\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{\n          $t(\"settings.siteSearchEntry.index.removeTitle\")\n        }}</v-card-title>\n\n        <v-card-text>{{\n          $t(\"settings.siteSearchEntry.index.removeConfirm\")\n        }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm = false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.ok\") }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  Site,\n  SearchEntry,\n  SiteCategory,\n  SiteCategories\n} from \"@/interface/common\";\nimport AddItem from \"./Add.vue\";\nimport EditItem from \"./Edit.vue\";\n\nimport { filters } from \"@/service/filters\";\nimport { PPF } from \"@/service/public\";\nexport default Vue.extend({\n  components: {\n    AddItem,\n    EditItem\n  },\n  data() {\n    return {\n      selected: [],\n      pagination: {\n        rowsPerPage: -1\n      },\n      showAddDialog: false,\n      showEditDialog: false,\n      site: {} as Site,\n      selectedItem: {},\n      dialogRemoveConfirm: false,\n      searchEntry: [] as any,\n      options: this.$store.state.options\n    };\n  },\n  methods: {\n    add() {\n      this.showAddDialog = true;\n    },\n    copy(item: SearchEntry) {\n      let newItem = Object.assign({}, item);\n      newItem.name += \" Copy\";\n      newItem.isCustom = true;\n      this.addItem(newItem);\n    },\n    edit(item: any) {\n      if (item) {\n        this.selectedItem = item;\n        this.showEditDialog = true;\n      }\n    },\n    removeConfirm(item: any) {\n      this.selectedItem = item;\n      this.dialogRemoveConfirm = true;\n    },\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.dispatch(\"removeSiteSearchEntry\", {\n        host: this.site.host,\n        item: this.selectedItem\n      });\n      this.selectedItem = {};\n      this.reloadEntry(this.site.host);\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\n            \"settings.siteSearchEntry.index.removeSelectedConfirm\"\n          ).toString()\n        )\n      ) {\n        this.selected.forEach((item: any) => {\n          this.$store.dispatch(\"removeSiteSearchEntry\", {\n            host: this.site.host,\n            item\n          });\n        });\n        this.selected = [];\n        this.reloadEntry(this.site.host);\n      }\n    },\n    updateItem(item: any) {\n      console.log(item);\n      this.selectedItem = item;\n      this.$store.dispatch(\"updateSiteSearchEntry\", {\n        host: this.site.host,\n        item\n      });\n      this.reloadEntry(this.site.host);\n    },\n    addItem(item: any) {\n      console.log(item);\n      this.$store.dispatch(\"addSiteSearchEntry\", {\n        host: this.site.host,\n        item: item\n      });\n      this.reloadEntry(this.site.host);\n    },\n    reloadEntry(host: string | undefined) {\n      let site = this.$store.state.options.sites.find((item: Site) => {\n        return item.host == host;\n      });\n\n      if (site) {\n        this.site = PPF.clone(site);\n        let systemSite = this.options.system.sites.find((item: Site) => {\n          return item.host == host;\n        });\n        if (systemSite) {\n          this.site.categories = PPF.clone(systemSite.categories);\n        }\n        let searchEntry: any[] = [];\n\n        if (this.site.searchEntry && this.site.searchEntry.length > 0) {\n          searchEntry.push(...this.site.searchEntry);\n        } else {\n          let schema = this.site.schema;\n          if (typeof schema === \"string\") {\n            let _schema = this.$store.state.options.system.schemas.find(\n              (item: Site) => {\n                return item.name == schema;\n              }\n            );\n            if (_schema) {\n              searchEntry.push(..._schema.searchEntry);\n            }\n          }\n        }\n\n        this.searchEntry = PPF.clone(searchEntry);\n      }\n    },\n    getCategory(entry: SearchEntry): string[] {\n      let site: Site = this.site;\n      let result: string[] = [];\n      if (site.categories && entry.categories) {\n        site.categories.forEach((item: SiteCategories) => {\n          if (\n            item.category &&\n            (item.entry == \"*\" ||\n              (entry.entry as string).indexOf(item.entry as string))\n          ) {\n            item.category.forEach((c: SiteCategory) => {\n              if (\n                entry.categories &&\n                entry.categories.includes(c.id as string)\n              ) {\n                result.push(c.name as string);\n              }\n            });\n          }\n        });\n      }\n      return result;\n    }\n  },\n  created() {\n    let host = this.$route.params[\"host\"];\n    console.log(\"create\", this.$route.params);\n    if (host) {\n      this.reloadEntry(host);\n    }\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.siteSearchEntry.index.headers.name\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"settings.siteSearchEntry.index.headers.categories\"),\n          align: \"left\",\n          value: \"categories\"\n        },\n        {\n          text: this.$t(\"settings.siteSearchEntry.index.headers.enable\"),\n          align: \"left\",\n          value: \"enable\"\n        },\n        {\n          text: this.$t(\"settings.siteSearchEntry.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.site-search-entry {\n  .search {\n    max-width: 400px;\n  }\n\n  .cat {\n    max-width: 40vw;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/Sites/Add.vue",
    "content": "<template>\n  <div>\n    <v-snackbar :value=\"haveError\" top :timeout=\"3000\" color=\"error\">{{\n      $t(\"settings.sites.add.validMsg\")\n    }}</v-snackbar>\n    <v-dialog v-model=\"show\" max-width=\"800\">\n      <v-card>\n        <v-toolbar dark color=\"blue-grey darken-2\">\n          <v-toolbar-title>{{\n            $t(\"settings.sites.add.title\")\n          }}</v-toolbar-title>\n          <v-spacer></v-spacer>\n          <v-btn\n            icon\n            flat\n            color=\"success\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/wiki/config-site\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer nofollow\"\n            :title=\"$t('common.help')\"\n          >\n            <v-icon>help</v-icon>\n          </v-btn>\n        </v-toolbar>\n\n        <v-card-text>\n          <v-stepper v-model=\"step\">\n            <v-stepper-header>\n              <v-stepper-step :complete=\"step > 1\" step=\"1\">{{\n                $t(\"settings.sites.add.step1\")\n              }}</v-stepper-step>\n\n              <v-divider></v-divider>\n\n              <v-stepper-step step=\"2\">{{\n                $t(\"settings.sites.add.step2\")\n              }}</v-stepper-step>\n            </v-stepper-header>\n\n            <v-stepper-items>\n              <!-- 选择一个站点 -->\n              <v-stepper-content step=\"1\">\n                <v-autocomplete\n                  v-model=\"selectedSite\"\n                  :items=\"$store.getters.sites\"\n                  :label=\"$t('settings.sites.add.validMsg')\"\n                  :hint=\"selectedSiteDescription\"\n                  :filter=\"filterSite\"\n                  persistent-hint\n                  return-object\n                  single-line\n                  item-text=\"name\"\n                  item-value=\"name\"\n                >\n                  <template slot=\"selection\" slot-scope=\"{ item }\">\n                    <v-list-tile-avatar>\n                      <img :src=\"item.icon\" />\n                    </v-list-tile-avatar>\n                    <span v-text=\"item.name\"></span>\n                  </template>\n                  <template slot=\"item\" slot-scope=\"data\" style>\n                    <v-list-tile-avatar>\n                      <img :src=\"data.item.icon\" />\n                    </v-list-tile-avatar>\n                    <v-list-tile-content>\n                      <v-list-tile-title\n                        v-html=\"data.item.name\"\n                      ></v-list-tile-title>\n                      <v-list-tile-sub-title\n                        v-html=\"data.item.url\"\n                      ></v-list-tile-sub-title>\n                    </v-list-tile-content>\n                    <v-list-tile-action>\n                      <v-list-tile-action-text>{{\n                        joinTags(data.item.tags)\n                      }}</v-list-tile-action-text>\n                    </v-list-tile-action>\n                  </template>\n                </v-autocomplete>\n              </v-stepper-content>\n\n              <!-- 站点配置 -->\n              <v-stepper-content step=\"2\">\n                <SiteEditor\n                  :initData=\"selectedSite\"\n                  :custom=\"isCustom\"\n                  @change=\"change\"\n                />\n              </v-stepper-content>\n            </v-stepper-items>\n          </v-stepper>\n        </v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions class=\"pa-3\">\n          <v-btn\n            flat\n            color=\"grey darken-1\"\n            href=\"https://github.com/pt-plugins/PT-Plugin-Plus/tree/master/resource/sites\"\n            target=\"_blank\"\n            v-show=\"step == 1\"\n            rel=\"noopener noreferrer nofollow\"\n          >\n            <v-icon>help</v-icon>\n            <span class=\"ml-1\">{{ $t(\"settings.sites.add.help\") }}</span>\n          </v-btn>\n          <v-btn flat @click=\"custom\" v-show=\"step < stepCount\">\n            <v-icon>add_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t(\"settings.sites.add.custom\") }}</span>\n          </v-btn>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"error\" @click=\"cancel\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t(\"common.cancel\") }}</span>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"grey darken-1\"\n            @click=\"step--\"\n            :disabled=\"step === 1\"\n          >\n            <v-icon>navigate_before</v-icon>\n            <span>{{ $t(\"settings.sites.add.prev\") }}</span>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"blue\"\n            @click=\"next(step)\"\n            v-show=\"step < stepCount\"\n          >\n            <span>{{ $t(\"settings.sites.add.next\") }}</span>\n            <v-icon>navigate_next</v-icon>\n          </v-btn>\n          <v-btn\n            flat\n            color=\"success\"\n            @click=\"save\"\n            v-show=\"step === stepCount\"\n            :disabled=\"!valid\"\n          >\n            <v-icon>check_circle_outline</v-icon>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { Site } from \"@/interface/common\";\nimport Vue from \"vue\";\nimport SiteEditor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    SiteEditor\n  },\n  data() {\n    return {\n      step: 1,\n      show: false,\n      stepCount: 2,\n      selectedSite: {} as Site,\n      valid: false,\n      isCustom: false,\n      newData: {} as Site,\n      haveError: false\n    };\n  },\n  props: {\n    value: Boolean\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n      if (!this.show) {\n        this.step = 1;\n        this.selectedSite = { name: \"\" };\n      }\n    },\n    value() {\n      this.show = this.value;\n    }\n  },\n  methods: {\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    },\n    save() {\n      this.$emit(\n        \"save\",\n        Object.assign(\n          {\n            isCustom: this.isCustom\n          },\n          this.newData\n        )\n      );\n      this.show = false;\n    },\n    next(step: number) {\n      if (this.selectedSite && this.selectedSite.name) {\n        this.valid = true;\n        this.haveError = false;\n        this.step++;\n      } else {\n        this.haveError = true;\n        this.valid = false;\n      }\n      this.isCustom = false;\n    },\n    custom() {\n      this.selectedSite = {\n        name: \"\",\n        isCustom: true\n      };\n      this.isCustom = true;\n      this.valid = false;\n      this.step = 2;\n    },\n    joinTags(tags: any): string {\n      if (tags && tags.join) {\n        return tags.join(\", \");\n      }\n      return \"\";\n    },\n    filterSite(item: Site, queryText: string, itemText: string): boolean {\n      function hasValue(val: any): string {\n        return val != null ? val : \"\";\n      }\n\n      // 支持在Site中host,name,url属性中搜索\n      const text =\n        hasValue(item.host) + hasValue(item.name) + hasValue(item.url);\n      const query = hasValue(queryText);\n\n      return (\n        text\n          .toString()\n          .toLowerCase()\n          .indexOf(query.toString().toLowerCase()) > -1\n      );\n    },\n    cancel() {\n      this.show = false;\n    }\n  },\n  computed: {\n    selectedSiteDescription(): string {\n      if (!this.selectedSite) {\n        return \"\";\n      }\n      let site = this.selectedSite;\n      let description = \"\";\n      if (site.description !== undefined) {\n        description = \"; \" + site.description;\n      }\n      return (site.url ? site.url : \"\") + description;\n    }\n  },\n  created() { }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Sites/Edit.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" max-width=\"800\">\n    <v-card>\n      <v-card-title\n        class=\"headline blue-grey darken-2\"\n        style=\"color:white\"\n      >{{ $t('settings.sites.edit.title') }}</v-card-title>\n\n      <v-card-text>\n        <SiteEditor :initData=\"defaultSite\" @change=\"change\" />\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"error\" @click=\"cancel\">\n          <v-icon>cancel</v-icon>\n          <span class=\"ml-1\">{{ $t('common.cancel') }}</span>\n        </v-btn>\n        <v-btn flat color=\"success\" @click=\"save\" :disabled=\"!valid\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport SiteEditor from \"./Editor.vue\";\nexport default Vue.extend({\n  components: {\n    SiteEditor\n  },\n  data() {\n    return {\n      show: false,\n      defaultSite: {},\n      valid: true,\n      newData: {}\n    };\n  },\n  props: {\n    value: Boolean,\n    site: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      this.show = this.value;\n      if (this.show) {\n        this.defaultSite = Object.assign({}, this.site);\n      }\n    }\n  },\n  methods: {\n    change(options: any) {\n      console.log(options);\n      this.newData = options.data;\n      this.valid = options.valid;\n    },\n    save() {\n      this.$emit(\"save\", this.newData);\n      this.show = false;\n    },\n    cancel() {\n      this.show = false;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Sites/Editor.vue",
    "content": "<template>\n  <v-card class=\"mb-5\" :color=\"$vuetify.dark ? '' : 'grey lighten-4'\">\n    <v-card-text>\n      <v-form v-model=\"valid\">\n        <!-- 站点名称 -->\n        <v-text-field\n          ref=\"name\"\n          v-model=\"site.name\"\n          :label=\"$t('settings.sites.editor.name')\"\n          :placeholder=\"$t('settings.sites.editor.name')\"\n          required\n          :rules=\"rules.require\"\n        ></v-text-field>\n\n        <!-- 标签 -->\n        <v-combobox\n          v-model=\"site.tags\"\n          hide-selected\n          :hint=\"$t('settings.sites.editor.inputTags')\"\n          :label=\"$t('settings.sites.editor.tags')\"\n          multiple\n          persistent-hint\n          small-chips\n        ></v-combobox>\n\n        <!-- 当前架构 -->\n        <v-text-field\n          :value=\"getSchema\"\n          :label=\"$t('settings.sites.editor.schema')\"\n          disabled\n          v-if=\"!site.isCustom\"\n        ></v-text-field>\n\n        <!-- 当前架构(自定义时) -->\n        <v-autocomplete\n          v-model=\"site.schema\"\n          :items=\"$store.state.options.system.schemas\"\n          :label=\"$t('settings.sites.editor.schema')\"\n          :menu-props=\"{maxHeight:'auto'}\"\n          persistent-hint\n          single-line\n          item-text=\"name\"\n          item-value=\"name\"\n          v-if=\"site.isCustom\"\n        >\n          <template slot=\"selection\" slot-scope=\"{ item }\">\n            <span v-text=\"item.name\"></span>\n          </template>\n          <template slot=\"item\" slot-scope=\"data\" style>\n            <v-list-tile-content>\n              <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n            </v-list-tile-content>\n            <v-list-tile-action>\n              <v-list-tile-action-text>{{ data.item.ver }}</v-list-tile-action-text>\n            </v-list-tile-action>\n          </template>\n        </v-autocomplete>\n\n        <v-text-field\n          v-model=\"site.passkey\"\n          :label=\"$t('settings.sites.editor.passkey')\"\n          :placeholder=\"$t('settings.sites.editor.passkeyTip')\"\n          :type=\"showPasskey ? 'text' : 'password'\"\n          :append-icon=\"showPasskey ? 'visibility_off' : 'visibility'\"\n          @click:append=\"showPasskey = !showPasskey\"\n          v-if=\"!site.securityKeys\"\n        ></v-text-field>\n\n        <template v-else>\n          <v-text-field\n            v-for=\"(value, key, index) in site.securityKeys\"\n            :key=\"index\"\n            :label=\"key\"\n            :type=\"showPasskey ? 'text' : 'password'\"\n            v-model=\"site.securityKeys[key]\"\n            :append-icon=\"showPasskey ? 'visibility_off' : 'visibility'\"\n            @click:append=\"showPasskey = !showPasskey\"\n          ></v-text-field>\n        </template>\n\n        <v-text-field\n          v-model=\"site.url\"\n          :label=\"$t('settings.sites.editor.url')\"\n          :placeholder=\"$t('settings.sites.editor.urlTip')\"\n          required\n          :rules=\"[rules.url]\"\n          :disabled=\"!custom\"\n        ></v-text-field>\n\n        <v-text-field\n          v-model=\"site.priority\"\n          :label=\"$t('settings.sites.editor.priority')\"\n          :placeholder=\"$t('settings.sites.editor.priorityTip')\"\n          type=\"number\"\n        ></v-text-field>\n\n        <v-textarea\n          v-model=\"cdn\"\n          :label=\"$t('settings.sites.editor.cdn')\"\n          value\n          :hint=\"$t('settings.sites.editor.cdnTip')\"\n        ></v-textarea>\n\n        <!-- 时区 -->\n        <v-autocomplete\n          v-model=\"site.timezoneOffset\"\n          :items=\"timezone\"\n          :label=\"$t('settings.sites.editor.timezone')\"\n          persistent-hint\n          item-text=\"text\"\n          item-value=\"value\"\n        ></v-autocomplete>\n\n        <v-text-field v-model=\"site.description\" :label=\"$t('settings.sites.editor.description')\"></v-text-field>\n\n        <v-autocomplete\n          v-model=\"site.defaultClientId\"\n          :items=\"this.$store.state.options.clients\"\n          :label=\"$t('settings.sites.editor.defaultClient')\"\n          :menu-props=\"{maxHeight:'auto'}\"\n          persistent-hint\n          item-text=\"name\"\n          item-value=\"id\"\n        >\n          <template slot=\"selection\" slot-scope=\"{ item }\">\n            <span v-text=\"item.name\"></span>\n          </template>\n          <template slot=\"item\" slot-scope=\"data\" style>\n            <v-list-tile-content>\n              <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n              <v-list-tile-sub-title v-html=\"data.item.address\"></v-list-tile-sub-title>\n            </v-list-tile-content>\n            <v-list-tile-action>\n              <v-list-tile-action-text>{{ data.item.type }}</v-list-tile-action-text>\n            </v-list-tile-action>\n          </template>\n        </v-autocomplete>\n\n        <v-text-field\n                v-model=\"site.upLoadLimit\"\n                :label=\"$t('settings.sites.editor.upLoadLimit')\"\n                :placeholder=\"$t('settings.sites.editor.upLoadLimitTip')\"\n        ></v-text-field>\n        <!-- 允许获取用户信息 -->\n        <v-switch\n          :label=\"$t('settings.sites.editor.allowGetUserInfo')\"\n          v-model=\"site.allowGetUserInfo\"\n          :disabled=\"site.offline\"\n        ></v-switch>\n\n        <!-- 允许搜索 -->\n        <v-switch v-model=\"site.allowSearch\" :disabled=\"site.offline\"\n                  :label=\"$t('settings.sites.editor.allowSearch')\"></v-switch>\n\n        <!-- 搜索入口设置 v-if=\"site.allowSearch\"  -->\n        <template v-if=\"site.allowSearch\">\n          <v-container fluid class=\"ma-0 pa-0 ml-4\">\n            <v-layout row wrap class=\"ma-0 pa-0\">\n              <v-flex\n                class=\"ma-0 pa-0\"\n                xs3\n                v-for=\"(item, key, index) in site.searchEntry\"\n                :key=\"index\"\n              >\n                <v-checkbox\n                  :disabled=\"!site.allowSearch || site.offline\"\n                  class=\"ma-0 pa-0\"\n                  :label=\"item.name\"\n                  v-model=\"item.enabled\"\n                ></v-checkbox>\n              </v-flex>\n            </v-layout>\n          </v-container>\n        </template>\n\n        <!-- 站点已离线（停机/关闭） -->\n        <v-switch :label=\"$t('settings.sites.editor.offline')\" v-model=\"site.offline\"></v-switch>\n\n        <!-- 消息提醒开关 -->\n        <v-switch :label=\"$t('settings.sites.editor.disableMessageCount')\" v-model=\"site.disableMessageCount\"></v-switch>\n      </v-form>\n    </v-card-text>\n  </v-card>\n</template>\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Site } from \"@/interface/common\";\nexport default Vue.extend({\n  data() {\n    return {\n      showPasskey: false,\n      rules: {\n        require: [(v: any) => !!v || \"!\"],\n        url: (v: any) => {\n          return (\n            /^(https?):\\/\\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]$/.test(\n              v\n            ) || this.$t(\"settings.sites.editor.urlTip\")\n          );\n        }\n      },\n      cdn: \"\",\n      valid: false,\n      site: {} as Site,\n      timezone: [\n        {\n          value: \"-1200\",\n          text: \"(UTC -12:00) Enitwetok, Kwajalien\"\n        },\n        {\n          value: \"-1100\",\n          text: \"(UTC -11:00) Midway Island, Samoa\"\n        },\n        {\n          value: \"-1000\",\n          text: \"(UTC -10:00) Hawaii\"\n        },\n        {\n          value: \"-0900\",\n          text: \"(UTC -09:00) Alaska\"\n        },\n        {\n          value: \"-0800\",\n          text: \"(UTC -08:00) Pacific Time (US & Canada)\"\n        },\n        {\n          value: \"-0700\",\n          text: \"(UTC -07:00) Mountain Time (US & Canada)\"\n        },\n        {\n          value: \"-0600\",\n          text: \"(UTC -06:00) Central Time (US & Canada), Mexico City\"\n        },\n        {\n          value: \"-0500\",\n          text: \"(UTC -05:00) Eastern Time (US & Canada), Bogota, Lima\"\n        },\n        {\n          value: \"-0400\",\n          text: \"(UTC -04:00) Atlantic Time (Canada), Caracas, La Paz\"\n        },\n        {\n          value: \"-0330\",\n          text: \"(UTC -03:30) Newfoundland\"\n        },\n        {\n          value: \"-0300\",\n          text: \"(UTC -03:00) Brazil, Buenos Aires, Falkland Is.\"\n        },\n        {\n          value: \"-0200\",\n          text: \"(UTC -02:00) Mid-Atlantic, Ascention Is., St Helena\"\n        },\n        {\n          value: \"-0100\",\n          text: \"(UTC -01:00) Azores, Cape Verde Islands\"\n        },\n        {\n          value: \"+0000\",\n          text: \"(UTC ±00:00) Casablanca, Dublin, London, Lisbon, Monrovia\"\n        },\n        {\n          value: \"+0100\",\n          text: \"(UTC +01:00) Brussels, Copenhagen, Madrid, Paris\"\n        },\n        {\n          value: \"+0200\",\n          text: \"(UTC +02:00) Sofia, Izrael, South Africa,\"\n        },\n        {\n          value: \"+0300\",\n          text: \"(UTC +03:00) Baghdad, Riyadh, Moscow, Nairobi\"\n        },\n        {\n          value: \"+0330\",\n          text: \"(UTC +03:30) Tehran\"\n        },\n        {\n          value: \"+0400\",\n          text: \"(UTC +04:00) Abu Dhabi, Baku, Muscat, Tbilisi\"\n        },\n        {\n          value: \"+0430\",\n          text: \"(UTC +04:30) Kabul\"\n        },\n        {\n          value: \"+0500\",\n          text: \"(UTC +05:00) Ekaterinburg, Karachi, Tashkent\"\n        },\n        {\n          value: \"+0530\",\n          text: \"(UTC +05:30) Bombay, Calcutta, Madras, New Delhi\"\n        },\n        {\n          value: \"+0600\",\n          text: \"(UTC +06:00) Almaty, Colomba, Dhakra\"\n        },\n        {\n          value: \"+0700\",\n          text: \"(UTC +07:00) Bangkok, Hanoi, Jakarta\"\n        },\n        {\n          value: \"+0800\",\n          text: \"(UTC +08:00) ShangHai, HongKong, Perth, Singapore, Taipei\"\n        },\n        {\n          value: \"+0900\",\n          text: \"(UTC +09:00) Osaka, Sapporo, Seoul, Tokyo, Yakutsk\"\n        },\n        {\n          value: \"+0930\",\n          text: \"(UTC +09:30) Adelaide, Darwin\"\n        },\n        {\n          value: \"+1000\",\n          text: \"(UTC +10:00) Melbourne, Papua New Guinea, Sydney\"\n        },\n        {\n          value: \"+1100\",\n          text: \"(UTC +11:00) Magadan, New Caledonia, Solomon Is.\"\n        },\n        {\n          value: \"+1200\",\n          text: \"(UTC +12:00) Auckland, Fiji, Marshall Island\"\n        }\n      ]\n    };\n  },\n  props: {\n    custom: Boolean,\n    initData: {\n      type: Object,\n      default: () => ({\n        valid: false\n      })\n    }\n  },\n  watch: {\n    site: {\n      handler() {\n        if (this.site.cdn) {\n          this.cdn = this.site.cdn.join(\"\\n\");\n        } else {\n          this.cdn = \"\";\n        }\n        this.$emit(\"change\", {\n          data: this.site,\n          valid: this.valid\n        });\n      },\n      deep: true\n    },\n    cdn() {\n      let items = this.cdn.split(\"\\n\");\n      let result: string[] = [];\n      items.forEach(cdn => {\n        if (\n          /(https?):\\/\\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/.test(\n            cdn\n          )\n        ) {\n          result.push(cdn);\n        }\n      });\n\n      if (result.length > 0) {\n        this.site.activeURL = result[0];\n      } else {\n        this.site.activeURL = this.site.url;\n      }\n\n      this.site.cdn = result;\n    },\n    initData() {\n      if (this.initData) {\n        this.site = Object.assign({}, this.initData);\n        this.valid = this.site.name && this.site.host ? true : false;\n      }\n    }\n  },\n  computed: {\n    getSchema(): string {\n      let result: string = \"\";\n      if (typeof this.site.schema === \"string\") {\n        result = this.site.schema;\n      } else if (this.site.schema && this.site.schema.name) {\n        result = this.site.schema.name;\n      }\n      return result;\n    }\n  }\n});\n</script>\n"
  },
  {
    "path": "src/options/views/settings/Sites/Index.vue",
    "content": "<template>\n  <div class=\"set-sites\">\n    <v-alert :value=\"true\" type=\"info\">\n      <div>{{ $t('settings.sites.index.title') }}</div>\n    </v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"add\">\n          <v-icon class=\"mr-2\">add</v-icon>\n          {{$t('common.add')}}\n        </v-btn>\n        <v-btn color=\"error\" :disabled=\"selected.length==0\" @click=\"removeSelected\">\n          <v-icon class=\"mr-2\">remove</v-icon>\n          {{$t('common.remove')}}\n        </v-btn>\n\n        <v-divider class=\"mx-3 mt-0\" inset vertical></v-divider>\n\n        <input\n          type=\"file\"\n          ref=\"fileImport\"\n          style=\"display:none;\"\n          multiple\n          accept=\"application/json\"\n        />\n        <!-- 导入配置文件 -->\n        <v-btn color=\"info\" @click=\"importConfig\">\n          <v-icon class=\"mr-2\">folder_open</v-icon>\n          {{$t('settings.sites.index.importConfig')}}\n        </v-btn>\n\n        <v-divider class=\"mx-3 mt-0\" inset vertical></v-divider>\n\n        <!-- 一键导入已登录站点 -->\n        <v-btn color=\"info\" @click=\"importAll\" :loading=\"importing\">\n          <v-icon class=\"mr-2\">gps_fixed</v-icon>\n          {{$t('settings.sites.index.importAll')}}\n        </v-btn>\n        <span v-if=\"importing\">{{ $t('settings.sites.index.importedText') }} {{importedCount}}</span>\n\n        <v-divider class=\"mx-3 mt-0\" inset vertical></v-divider>\n\n        <!-- 重置站点图标缓存 -->\n        <v-btn color=\"purple\" dark @click=\"resetFavicons\" :loading=\"faviconReseting\">\n          <v-icon class=\"mr-2\">cached</v-icon>\n          {{$t('settings.sites.index.resetFavicons')}}\n        </v-btn>\n\n        <v-spacer></v-spacer>\n        <v-text-field class=\"search\" append-icon=\"search\" label=\"Search\" single-line hide-details></v-text-field>\n      </v-card-title>\n      <v-data-table\n        v-model=\"selected\"\n        :headers=\"headers\"\n        :items=\"this.$store.state.options.sites\"\n        :pagination.sync=\"pagination\"\n        item-key=\"host\"\n        select-all\n        class=\"elevation-1\"\n      >\n        <template slot=\"items\" slot-scope=\"props\">\n          <td style=\"width:20px;\">\n            <v-checkbox v-model=\"props.selected\" primary hide-details></v-checkbox>\n          </td>\n          <td class=\"text-xs-center pb-1\">\n            <v-btn\n              flat\n              icon\n              :title=\"$t('settings.sites.index.resetFavicons')\"\n              @click.stop=\"resetFavicon(props.item)\"\n              :loading=\"loadingIconSites.includes(props.item.host)\"\n              class=\"siteIcon\"\n            >\n              <v-avatar :size=\"18\" v-if=\"!loadingIconSites.includes(props.item.host)\">\n                <img :src=\"props.item.icon\" />\n              </v-avatar>\n            </v-btn>\n            <br />\n            <a @click=\"edit(props.item)\">\n              <span>{{ props.item.name }}</span>\n            </a>\n          </td>\n          <td>{{ props.item.tags && props.item.tags.join(\", \") }}</td>\n          <td>\n            <v-switch\n              true-value=\"true\"\n              false-value=\"false\"\n              :input-value=\"props.item.allowSearch?'true':'false'\"\n              hide-details\n              :disabled=\"props.item.offline\"\n              @click.stop=\"updateSearchStatus(props.item)\"\n            ></v-switch>\n          </td>\n          <td>\n            <v-switch\n              true-value=\"true\"\n              false-value=\"false\"\n              :input-value=\"props.item.allowGetUserInfo?'true':'false'\"\n              hide-details\n              :disabled=\"props.item.offline\"\n              @click.stop=\"updateAllowGetUserInfo(props.item)\"\n            ></v-switch>\n          </td>\n          <td>\n            <v-switch\n              true-value=\"true\"\n              false-value=\"false\"\n              :input-value=\"props.item.offline?'true':'false'\"\n              hide-details\n              @click.stop=\"updateOfflineStatus(props.item)\"\n            ></v-switch>\n          </td>\n          <td>\n            <a\n              :href=\"props.item.activeURL\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer nofollow\"\n            >{{ props.item.activeURL }}</a>\n          </td>\n          <td>\n            <v-icon small @click=\"edit(props.item)\" :title=\"$t('common.edit')\">edit</v-icon>\n            <v-icon\n              small\n              class=\"ml-2\"\n              @click=\"editPlugins(props.item)\"\n              :title=\"$t('settings.sites.index.plugins')\"\n            >assistant</v-icon>\n            <v-icon\n                v-if=\"props.item.allowGetUserInfo\"\n                small\n                class=\"ml-2\"\n                @click=\"editUserInfo(props.item)\"\n                :title=\"$t('settings.sites.index.showUserInfo')\"\n            >view_list</v-icon>\n            <v-icon\n              small\n              class=\"ml-2\"\n              @click=\"editSearchEntry(props.item)\"\n              :title=\"$t('settings.sites.index.searchEntry')\"\n            >search</v-icon>\n            <v-icon\n              small\n              color=\"error\"\n              class=\"ml-2\"\n              @click=\"removeConfirm(props.item)\"\n              :title=\"$t('common.remove')\"\n            >delete</v-icon>\n            <v-icon\n              small\n              color=\"info\"\n              class=\"ml-2\"\n              @click=\"shareSiteConfig(props.item)\"\n              :title=\"$t('common.share')\"\n            >share</v-icon>\n          </td>\n        </template>\n      </v-data-table>\n    </v-card>\n\n    <!-- 新增站点 -->\n    <AddSite v-model=\"showAddDialog\" @save=\"addSite\" />\n    <!-- 编辑站点 -->\n    <EditSite v-model=\"showEditDialog\" :site=\"selectedSite\" @save=\"updateSite\" />\n    <UserInfo v-model=\"showUserInfo\" :site=\"selectedSite\"></UserInfo>\n\n    <v-dialog v-model=\"dialogRemoveConfirm\" width=\"300\">\n      <v-card>\n        <v-card-title class=\"headline red lighten-2\">{{ $t('settings.sites.index.removeTitle') }}</v-card-title>\n\n        <v-card-text>{{ $t('settings.sites.index.removeConfirm') }}</v-card-text>\n\n        <v-divider></v-divider>\n\n        <v-card-actions>\n          <v-spacer></v-spacer>\n          <v-btn flat color=\"info\" @click=\"dialogRemoveConfirm=false\">\n            <v-icon>cancel</v-icon>\n            <span class=\"ml-1\">{{ $t('common.cancel') }}</span>\n          </v-btn>\n          <v-btn color=\"error\" flat @click=\"remove\">\n            <v-icon>check_circle_outline</v-icon>\n            <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n          </v-btn>\n        </v-card-actions>\n      </v-card>\n    </v-dialog>\n\n    <v-alert :value=\"true\" color=\"grey\">\n      <div v-html=\"$t('settings.sites.index.subTitle')\"></div>\n    </v-alert>\n\n    <v-snackbar\n      v-model=\"siteDuplicate\"\n      top\n      :timeout=\"3000\"\n      color=\"error\"\n    >{{ $t('settings.sites.index.siteDuplicateText') }}</v-snackbar>\n\n    <v-snackbar v-model=\"haveError\" top :timeout=\"3000\" color=\"error\">{{ errorMsg }}</v-snackbar>\n    <v-snackbar v-model=\"haveSuccess\" bottom :timeout=\"3000\" color=\"success\">{{ successMsg }}</v-snackbar>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport {\n  Site,\n  LogItem,\n  EAction,\n  EModule,\n  Plugin,\n  SearchEntry,\n  Options\n} from \"@/interface/common\";\nimport AddSite from \"./Add.vue\";\nimport EditSite from \"./Edit.vue\";\nimport UserInfo from \"./UserInfo.vue\";\n\nimport { filters } from \"@/service/filters\";\nimport Extension from \"@/service/extension\";\nimport FileSaver from \"file-saver\";\nimport { PPF } from \"@/service/public\";\n\nconst extension = new Extension();\n\nexport default Vue.extend({\n  components: {\n    AddSite,\n    EditSite,\n    UserInfo\n  },\n  data() {\n    return {\n      selected: [],\n      pagination: {\n        rowsPerPage: -1\n      },\n      showAddDialog: false,\n      showEditDialog: false,\n      showUserInfo: false,\n      siteDuplicate: false,\n      sites: [] as Site[],\n      selectedSite: {},\n      dialogRemoveConfirm: false,\n      options: this.$store.state.options,\n      importing: false,\n      importingCount: 0,\n      importedCount: 0,\n      fileImport: null as any,\n      errorMsg: \"\",\n      haveError: false,\n      haveSuccess: false,\n      successMsg: \"\",\n      faviconReseting: false,\n      loadingIconSites: [] as string[]\n    };\n  },\n  methods: {\n    add() {\n      this.showAddDialog = true;\n    },\n    edit(item: any) {\n      let index = this.$store.state.options.sites.findIndex((site: any) => {\n        return item.name === site.name;\n      });\n\n      if (index !== -1) {\n        this.selectedSite = this.$store.state.options.sites[index];\n        this.showEditDialog = true;\n      }\n    },\n    editUserInfo(item: any) {\n      let index = this.$store.state.options.sites.findIndex((site: any) => {\n        return item.name === site.name;\n      });\n\n      if (index !== -1) {\n        this.selectedSite = this.$store.state.options.sites[index];\n        this.showUserInfo = true;\n      }\n    },\n    removeConfirm(item: any) {\n      this.selectedSite = item;\n      this.dialogRemoveConfirm = true;\n    },\n    remove() {\n      this.dialogRemoveConfirm = false;\n      this.$store.commit(\"removeSite\", this.selectedSite);\n      this.selectedSite = {};\n    },\n    removeSelected() {\n      if (\n        confirm(\n          this.$t(\"settings.sites.index.removeSelectedConfirm\").toString()\n        )\n      ) {\n        this.selected.forEach((item: any) => {\n          this.$store.commit(\"removeSite\", item);\n        });\n        this.selected = [];\n      }\n    },\n    updateSearchStatus(item: any) {\n      item.allowSearch = !(<boolean>item.allowSearch);\n      this.$store.commit(\"updateSite\", item);\n      this.pagination.rowsPerPage = 0;\n      this.pagination.rowsPerPage = -1;\n    },\n    updateAllowGetUserInfo(item: any) {\n      item.allowGetUserInfo = !(<boolean>item.allowGetUserInfo);\n      this.$store.commit(\"updateSite\", item);\n      this.pagination.rowsPerPage = 0;\n      this.pagination.rowsPerPage = -1;\n    },\n    updateOfflineStatus(item: any) {\n      item.offline = !(<boolean>item.offline);\n      this.$store.commit(\"updateSite\", item);\n      this.pagination.rowsPerPage = 0;\n      this.pagination.rowsPerPage = -1;\n    },\n    updateSite(item: any) {\n      // this.selectedSite = item;\n      this.$store.commit(\"updateSite\", item);\n      this.pagination.rowsPerPage = 0;\n      this.pagination.rowsPerPage = -1;\n    },\n    addSite(item: any) {\n      if (!item.host) {\n        let url = filters.parseURL(item.url);\n        item.host = url.host;\n      }\n\n      if (!item.icon) {\n        let url = filters.parseURL(item.url);\n        item.icon = `${url.protocol}://${item.host}/favicon.ico`;\n      }\n\n      let index = this.$store.state.options.sites.findIndex((site: any) => {\n        return site.host === item.host;\n      });\n      if (index === -1) {\n        if (!item.activeURL) {\n          item.activeURL = item.url;\n        }\n        this.$store.commit(\"addSite\", item);\n      } else {\n        this.siteDuplicate = true;\n      }\n    },\n    importAll() {\n      if (\n        !confirm(this.$t(\"settings.sites.index.importAllConfirm\").toString())\n      ) {\n        return;\n      }\n      if (this.importing) {\n        return;\n      }\n      this.importing = true;\n      this.importedCount = 0;\n      this.$store.state.options.system.sites.forEach((site: any) => {\n        let index = this.$store.state.options.sites\n          ? this.$store.state.options.sites.findIndex((item: any) => {\n              return item.host === site.host;\n            })\n          : -1;\n        if (index === -1) {\n          this.checkAndAddSite(site);\n        }\n      });\n    },\n    editPlugins(item: any) {\n      this.$router.push({\n        name: \"set-site-plugins\",\n        params: {\n          host: item.host\n        }\n      });\n    },\n    writeLog(options: LogItem) {\n      extension.sendRequest(EAction.writeLog, null, {\n        module: EModule.options,\n        event: options.event,\n        msg: options.msg,\n        data: options.data\n      });\n    },\n    editSearchEntry(item: Site) {\n      this.$router.push({\n        name: \"set-site-search-entry\",\n        params: {\n          host: item.host as string\n        }\n      });\n    },\n    /**\n     * 验证并添加站点\n     */\n    checkAndAddSite(site: Site) {\n      this.importingCount++;\n      extension\n        .sendRequest(EAction.getUserInfo, null, site)\n        .then((result: any) => {\n          console.log(result);\n          if (result && result.name) {\n            this.$store.commit(\n              \"addSite\",\n              Object.assign(\n                {\n                  valid: true,\n                  activeURL: site.url,\n                  allowSearch: true,\n                  allowGetUserInfo: true\n                },\n                site\n              )\n            );\n            this.importedCount++;\n          }\n        })\n        .catch(result => {\n          console.log(\"error\", result);\n        })\n        .finally(() => {\n          this.importingCount--;\n          if (this.importingCount == 0) {\n            this.importing = false;\n          }\n        });\n    },\n\n    clearMessage() {\n      this.successMsg = \"\";\n      this.errorMsg = \"\";\n    },\n\n    /**\n     * 导出站点配置\n     */\n    shareSiteConfig(site: Site) {\n      let fileName = (site.host || site.name) + \".json\";\n\n      let data: Site = JSON.parse(JSON.stringify(site));\n\n      // 清除个人相关信息\n      [\"id\", \"user\", \"passkey\", \"defaultClientId\"].forEach((field: string) => {\n        if ((data as any)[field]) {\n          delete (data as any)[field];\n        }\n      });\n\n      // 非自定义站点时，删除系统自带的一些参数\n      if (!data.isCustom) {\n        [\n          \"categories\",\n          \"selectors\",\n          \"searchEntryConfig\",\n          \"description\",\n          \"icon\",\n          \"url\",\n          \"schema\",\n          \"tags\",\n          \"formerHosts\"\n        ].forEach((field: string) => {\n          if ((data as any)[field]) {\n            delete (data as any)[field];\n          }\n        });\n      }\n\n      // 处理需要保留的插件\n      if (data.plugins && data.plugins.length > 0) {\n        const keepItems: any[] = [];\n        data.plugins.forEach((item: Plugin) => {\n          if (item.isCustom) {\n            keepItems.push(item);\n          }\n        });\n        data.plugins = keepItems;\n      }\n\n      // 处理需要保留的入口\n      if (data.searchEntry && data.searchEntry.length > 0) {\n        const keepItems: any[] = [];\n        data.searchEntry.forEach((item: SearchEntry) => {\n          if (item.isCustom) {\n            keepItems.push(item);\n          }\n        });\n        data.searchEntry = keepItems;\n      }\n\n      const blob = new Blob([JSON.stringify(data)], {\n        type: \"text/plain\"\n      });\n      FileSaver.saveAs(blob, fileName);\n    },\n\n    /**\n     * 导入配置文件\n     */\n    importConfig() {\n      this.fileImport.click();\n    },\n\n    importConfigFile(event: Event) {\n      this.clearMessage();\n      let restoreFile: any = event.srcElement;\n      if (\n        restoreFile.files.length > 0 &&\n        restoreFile.files[0].name.length > 0\n      ) {\n        console.log(restoreFile.files);\n\n        for (let index = 0; index < restoreFile.files.length; index++) {\n          const file = restoreFile.files[index];\n          const r = new FileReader();\n          r.onload = (e: any) => {\n            try {\n              const result = JSON.parse(e.target.result);\n              this.importSite(result);\n            } catch (error) {\n              console.log(error);\n              this.errorMsg = this.$t(\"common.importFailed\").toString();\n            }\n          };\n          r.onerror = () => {\n            this.errorMsg = this.$t(\"settings.backup.loadError\").toString();\n          };\n          r.readAsText(file);\n        }\n\n        restoreFile.value = \"\";\n      }\n    },\n\n    /**\n     * 导入站点信息\n     */\n    importSite(sourceSite: Site) {\n      const options: Options = JSON.parse(JSON.stringify(this.options));\n      let site: Site | null = null;\n      let systemSite: Site | null = null;\n      if (options.sites && options.sites.length > 0) {\n        site = options.sites.find((item: Site) => {\n          return item.host === sourceSite.host;\n        });\n      }\n\n      if (\n        options.system &&\n        options.system.sites &&\n        options.system.sites.length > 0\n      ) {\n        systemSite = options.system.sites.find((item: Site) => {\n          return item.host === sourceSite.host;\n        });\n      }\n\n      // 如果当前用户已定义该站点\n      if (site) {\n        if (\n          !confirm(\n            this.$t(\"settings.sites.index.importDuplicateConfirm\", {\n              name: site.name || site.host\n            }).toString()\n          )\n        ) {\n          return;\n        }\n\n        // 导入插件\n        if (sourceSite.plugins && sourceSite.plugins.length > 0) {\n          if (!site.plugins) {\n            site.plugins = [];\n          }\n\n          let items = site.plugins;\n          let keepItems: any[] = [];\n\n          items.forEach((item: Plugin) => {\n            if (item.isCustom) {\n              keepItems.push(item);\n            }\n          });\n\n          sourceSite.plugins.forEach((item: Plugin) => {\n            let index = items.findIndex((_item: Plugin) => {\n              return _item.name === item.name;\n            });\n\n            if (index === -1) {\n              item.id = PPF.getNewId();\n              keepItems.push(item);\n            }\n          });\n\n          site.plugins = keepItems;\n        }\n\n        // 导入搜索入口\n        if (sourceSite.searchEntry && sourceSite.searchEntry.length > 0) {\n          if (!site.searchEntry) {\n            site.searchEntry = [];\n          }\n\n          let items = site.searchEntry;\n\n          sourceSite.searchEntry.forEach((item: SearchEntry) => {\n            let index = items.findIndex((_item: SearchEntry) => {\n              return _item.name === item.name;\n            });\n\n            if (index === -1) {\n              item.id = PPF.getNewId();\n              items.push(item);\n            }\n          });\n        }\n\n        this.updateSite(site);\n      } else {\n        if (\n          !confirm(\n            this.$t(\"settings.sites.index.importConfirm\", {\n              name: sourceSite.name || sourceSite.host\n            }).toString()\n          )\n        ) {\n          return;\n        }\n\n        // 如果当前用户未定义该站点，但系统已定义时\n        if (systemSite) {\n          this.addSite(Object.assign(systemSite, sourceSite));\n        } else {\n          this.addSite(sourceSite);\n        }\n      }\n\n      this.successMsg = this.$t(\"settings.sites.index.importedText\").toString();\n    },\n\n    resetFavicons() {\n      this.faviconReseting = true;\n      extension\n        .sendRequest(EAction.resetFavicons)\n        .then(options => {\n          this.$store.commit(\"updateOptions\", options);\n        })\n        .finally(() => {\n          this.faviconReseting = false;\n        });\n    },\n\n    resetFavicon(site: Site) {\n      if (!site.host) {\n        return;\n      }\n      const host = site.host;\n      if (!this.loadingIconSites.includes(host)) {\n        this.loadingIconSites.push(host);\n      }\n\n      extension\n        .sendRequest(EAction.resetFavicon, null, site.activeURL || site.url)\n        .then(options => {\n          // 重新加载配置信息\n          this.$store.commit(\"readConfig\");\n        })\n        .catch(error => {\n          console.log(error);\n        })\n        .finally(() => {\n          let index = this.loadingIconSites.indexOf(host);\n          console.log(\"host: %s, index: %s\", host, index);\n          if (index != -1) {\n            this.loadingIconSites.splice(index, 1);\n          }\n        });\n    }\n  },\n  created() {\n    if (!this.options.system) {\n      this.writeLog({\n        event: \"Sites.init.error\",\n        msg: \"系统配置信息丢失\"\n      });\n    }\n\n    if (this.options.system && !this.options.system.sites) {\n      this.writeLog({\n        event: \"Sites.init.error\",\n        msg: \"系统配置网站信息丢失\"\n      });\n    }\n  },\n  mounted() {\n    this.fileImport = this.$refs.fileImport;\n    this.fileImport.addEventListener(\"change\", this.importConfigFile);\n  },\n  beforeDestroy() {\n    this.fileImport.removeEventListener(\"change\", this.importConfigFile);\n  },\n  computed: {\n    headers(): Array<any> {\n      return [\n        {\n          text: this.$t(\"settings.sites.index.headers.name\"),\n          align: \"center\",\n          value: \"name\",\n          width: \"110px\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.tags\"),\n          align: \"left\",\n          value: \"tags\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.allowSearch\"),\n          align: \"left\",\n          value: \"allowSearch\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.allowGetUserInfo\"),\n          align: \"left\",\n          value: \"allowGetUserInfo\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.offline\"),\n          align: \"left\",\n          value: \"offline\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.activeURL\"),\n          align: \"left\",\n          value: \"activeURL\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.action\"),\n          value: \"name\",\n          sortable: false\n        }\n      ];\n    }\n  },\n  watch: {\n    successMsg() {\n      this.haveSuccess = this.successMsg != \"\";\n    },\n    errorMsg() {\n      this.haveError = this.errorMsg != \"\";\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.set-sites {\n  .search {\n    max-width: 400px;\n  }\n\n  .siteIcon {\n    margin: 0;\n    margin-top: 5px;\n    height: 30px;\n    width: 30px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/Sites/UserInfo.vue",
    "content": "<template>\n  <v-dialog v-model=\"show\" max-width=\"1200\">\n    <v-card>\n      <v-card-title\n          class=\"headline blue-grey darken-2\"\n          style=\"color:white\"\n      >{{ $t('settings.sites.userinfo.title') }}@{{ this.site.name }}</v-card-title>\n\n      <v-card-text>\n        <v-data-table\n          :headers=\"headers\"\n          :items=\"userData\"\n          :pagination.sync=\"pagination\"\n        >\n          <template slot=\"items\" slot-scope=\"props\">\n            <td>{{ props.item.date }}</td>\n            <td>{{ props.item.name }}</td>\n            <td>{{ props.item.levelName  }}</td>\n            <td class=\"number\">\n              <div>\n                {{ props.item.uploaded | formatSize }}\n                <v-icon small color=\"green darken-4\">expand_less</v-icon>\n              </div>\n              <div>\n                {{ props.item.downloaded | formatSize }}\n                <v-icon small color=\"red darken-4\">expand_more</v-icon>\n              </div>\n            </td>\n            <td class=\"number\">{{ props.item.ratio | formatRatio }}</td>\n            <td class=\"number\">{{ props.item.seeding }}</td>\n            <td class=\"number\">{{ props.item.seedingSize | formatSize }}</td>\n            <td class=\"number\">{{ props.item.bonus | formatNumber }}</td>\n            <td>\n              <v-icon\n                  small\n                  color=\"error\"\n                  class=\"ml-2\"\n                  @click=\"removeConfirm(props.item)\"\n                  :title=\"$t('common.remove')\"\n                  :disabled=\"props.item.date === 'latest'\"\n              >delete</v-icon>\n            </td>\n          </template>\n\n        </v-data-table>\n      </v-card-text>\n\n      <v-divider></v-divider>\n\n      <v-card-actions class=\"pa-3\">\n        <v-spacer></v-spacer>\n        <v-btn flat color=\"success\" @click=\"save\">\n          <v-icon>check_circle_outline</v-icon>\n          <span class=\"ml-1\">{{ $t('common.ok') }}</span>\n        </v-btn>\n      </v-card-actions>\n    </v-card>\n  </v-dialog>\n</template>\n\n<script lang=\"ts\">\n// @ts-nocheck\nimport Vue from \"vue\";\n\nexport default Vue.extend({\n  name: \"UserInfo\",\n  data() {\n    return {\n      dataKey: 'PT-Plugin-Plus-User-Datas',\n      show: false,\n      rawUserData: [],\n      pagination: {\n        descending: true,\n        sortBy: \"date\",\n        rowsPerPage: 25\n      },\n      headers: [\n        {\n          text: this.$t(\"home.headers.date\"),\n          align: \"left\",\n          value: \"date\",\n          width: \"130px\"\n        },\n        {\n          text: this.$t(\"home.headers.userName\"),\n          align: \"left\",\n          value: \"name\"\n        },\n        {\n          text: this.$t(\"home.headers.levelName\"),\n          align: \"left\",\n          value: \"levelName\"\n        },\n        {\n          text: this.$t(\"home.headers.activitiyData\"),\n          align: \"right\",\n          value: \"uploaded\",\n          width: \"160px\"\n        },\n        {\n          text: this.$t(\"home.headers.ratio\"),\n          align: \"right\",\n          value: \"ratio\"\n        },\n        {\n          text: this.$t(\"home.headers.seeding\"),\n          align: \"right\",\n          value: \"seeding\"\n        },\n        {\n          text: this.$t(\"home.headers.seedingSize\"),\n          align: \"right\",\n          value: \"seedingSize\"\n        },\n        {\n          text: this.$t(\"home.headers.bonus\"),\n          align: \"right\",\n          value: \"bonus\"\n        },\n        {\n          text: this.$t(\"settings.sites.index.headers.action\"),\n          value: \"name\",\n          sortable: false,\n          width: '50px'\n        }\n      ]\n    }\n  },\n  props: {\n    value: Boolean,\n    site: Object\n  },\n  model: {\n    prop: \"value\",\n    event: \"change\"\n  },\n  filters: {\n    formatRatio(v: any) {\n      if (v > 10000 || v == -1) {\n        return \"∞\";\n      }\n      let number = parseFloat(v);\n      if (isNaN(number)) {\n        return \"\";\n      }\n      return number.toFixed(2);\n    }\n  },\n  watch: {\n    show() {\n      this.$emit(\"change\", this.show);\n    },\n    value() {\n      if (this.value) {\n        chrome.storage.local.get(this.dataKey, (result) => {\n          this.rawUserData = result[this.dataKey][this.site.host];\n          this.show = this.value;\n        });\n      }\n    }\n  },\n  computed: {\n    userData() {\n      return Object.entries(this.rawUserData).map(v => {\n        const user = v[1];\n        const {downloaded, uploaded} = user;\n        if (downloaded == 0 && uploaded > 0) {\n          user.ratio = -1;\n        }\n        // 重新以 上传量 / 下载量计算分享率\n        else if (downloaded > 0) {\n          user.ratio = uploaded / downloaded;\n        }\n        user['date'] = v[0];\n        return user;\n      })\n    }\n  },\n  methods: {\n    save() {\n      this.show = false;\n    },\n    removeConfirm(item) {\n      if (confirm(this.$t('settings.sites.userinfo.deleteConfirm'))) {\n        this.$delete(this.rawUserData, item.date);\n        chrome.storage.local.get(this.dataKey, (result) => {\n          delete result[this.dataKey][this.site.host][item.date];\n          chrome.storage.local.set(result, () => {\n\n          });\n        });\n      }\n    }\n  }\n})\n</script>\n\n<style scoped>\n.number {\n  text-align: right;\n}\n</style>\n"
  },
  {
    "path": "src/options/views/settings/SupportSchema.vue",
    "content": "<template>\n  <div class=\"set-support-schema\">\n    <v-alert :value=\"true\" type=\"info\">已支持的网站架构</v-alert>\n    <v-card>\n      <v-card-title>\n        <v-btn color=\"success\" @click=\"update\">\n          <v-icon class=\"mr-2\">autorenew</v-icon>更新\n        </v-btn>\n        <v-spacer></v-spacer>\n        <v-text-field class=\"search\" append-icon=\"search\" label=\"Search\" single-line hide-details></v-text-field>\n      </v-card-title>\n      <v-data-table :headers=\"headers\" :items=\"items\" item-key=\"name\" class=\"elevation-1\">\n        <template slot=\"items\" slot-scope=\"props\">\n          <td>{{ props.item.name }}</td>\n          <td>{{ props.item.ver }}</td>\n          <td>{{ showPlugins(props.item.plugins) }}</td>\n        </template>\n      </v-data-table>\n    </v-card>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nexport default Vue.extend({\n  data() {\n    return {\n      headers: [\n        { text: \"名称\", align: \"left\", value: \"name\" },\n        { text: \"版本\", align: \"left\", value: \"ver\" },\n        { text: \"插件\", align: \"left\", value: \"plugins\" }\n      ],\n      items: []\n    };\n  },\n  created() {\n    this.items = this.$store.state.options.system.schemas;\n  },\n  methods: {\n    showPlugins(plugins: any[]) {\n      let items: string[] = [];\n      plugins.forEach(item => {\n        items.push(item.name);\n      });\n\n      return items.join(\", \");\n    },\n    update() {}\n  },\n  computed: {}\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.set-support-schema {\n  .search {\n    max-width: 400px;\n  }\n}\n</style>"
  },
  {
    "path": "src/options/views/statisticCharts/SiteBase.vue",
    "content": "<template>\n  <div class=\"container\">\n    <v-autocomplete\n      v-model=\"selectedSite\"\n      :items=\"sites\"\n      :label=\"$t('statistic.selectSite')\"\n      persistent-hint\n      single-line\n      item-text=\"name\"\n      item-value=\"host\"\n      return-object\n      @input=\"init(selectedSite.host)\"\n    >\n      <template slot=\"selection\" slot-scope=\"{ item }\">\n        <v-list-tile-avatar v-if=\"item.icon\">\n          <img :src=\"item.icon\" />\n        </v-list-tile-avatar>\n        <span v-text=\"item.name\"></span>\n      </template>\n      <template slot=\"item\" slot-scope=\"data\">\n        <v-list-tile-avatar v-if=\"data.item.icon\">\n          <img :src=\"data.item.icon\" />\n        </v-list-tile-avatar>\n        <v-list-tile-content>\n          <v-list-tile-title v-html=\"data.item.name\"></v-list-tile-title>\n          <v-list-tile-sub-title v-html=\"data.item.url\"></v-list-tile-sub-title>\n        </v-list-tile-content>\n        <v-list-tile-action>\n          <v-list-tile-action-text>{{ joinTags(data.item.tags) }}</v-list-tile-action-text>\n        </v-list-tile-action>\n      </template>\n    </v-autocomplete>\n\n    <v-layout row wrap class=\"mb-2\">\n      <v-btn depressed small to=\"/home\">{{ $t('statistic.goback') }}</v-btn>\n      <v-btn depressed small @click.stop=\"exportRawData\">{{ $t('statistic.exportRawData') }}</v-btn>\n\n      <v-spacer>\n        <v-btn-toggle v-model=\"dateRange\" class=\"ml-5\">\n          <v-btn flat value=\"7day\" v-text=\"$t('statistic.dateRange.7day')\"></v-btn>\n          <v-btn flat value=\"30day\" v-text=\"$t('statistic.dateRange.30day')\"></v-btn>\n          <v-btn flat value=\"60day\" v-text=\"$t('statistic.dateRange.60day')\"></v-btn>\n          <v-btn flat value=\"90day\" v-text=\"$t('statistic.dateRange.90day')\"></v-btn>\n          <v-btn flat value=\"180day\" v-text=\"$t('statistic.dateRange.180day')\"></v-btn>\n          <v-btn flat value=\"all\" v-text=\"$t('statistic.dateRange.all')\"></v-btn>\n        </v-btn-toggle>\n      </v-spacer>\n      <v-btn flat icon small @click=\"share\" :title=\"$t('statistic.share')\" v-if=\"!shareing\">\n        <v-icon small>share</v-icon>\n      </v-btn>\n      <v-progress-circular indeterminate :width=\"3\" size=\"30\" color=\"green\" v-if=\"shareing\" class=\"by_pass_canvas\"></v-progress-circular>\n    </v-layout>\n\n    <div ref=\"charts\" class=\"charts\">\n      <highcharts :options=\"chartBarData\" />\n      <highcharts :options=\"chartBaseData\" class=\"mt-4\" />\n      <highcharts :options=\"chartExtData\" class=\"mt-4\" />\n\n      <v-card-actions>\n        <v-spacer></v-spacer>\n        <span>{{ shareTime | formatDate('YYYY-MM-DD HH:mm:ss') }}</span>\n        <span class=\"ml-1\">Created By {{ $t('app.name') }} {{ version }}</span>\n      </v-card-actions>\n    </div>\n\n    <v-alert :value=\"true\" type=\"info\" color=\"grey\">\n      <div v-html=\"$t('statistic.note')\"></div>\n    </v-alert>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport Vue from \"vue\";\nimport { Chart } from \"highcharts-vue\";\nimport { Route } from \"vue-router\";\nimport Extension from \"@/service/extension\";\nimport { filters } from \"@/service/filters\";\nimport {\n  EAction,\n  EUserDataRange,\n  Site,\n  DownloadClient,\n  EDataResultType,\n  Dictionary,\n  ECommonKey\n} from \"@/interface/common\";\nimport FileSaver from \"file-saver\";\nimport { PPF } from \"@/service/public\";\nimport dayjs from \"dayjs\";\nimport domtoimage from \"dom-to-image\";\n\nconst extension = new Extension();\n\nfunction formatBonus(v: any) {\n  let result: number | string = 0;\n  let unit = \"\";\n  switch (true) {\n    case v >= 1000000000:\n      return \"∞\";\n\n    case v >= 100000000:\n      unit = \"亿\";\n      result = v / 100000000;\n      break;\n\n    case v >= 10000000:\n      unit = \"千万\";\n      result = v / 10000000;\n      break;\n\n    case v >= 1000000:\n      unit = \"百万\";\n      result = v / 1000000;\n      break;\n\n    case v >= 10000:\n      unit = \"万\";\n      result = v / 10000;\n      break;\n\n    case v >= 1000:\n      unit = \"千\";\n      result = v / 1000;\n      break;\n\n    default:\n      return v;\n  }\n\n  return parseFloat(result.toString()).toFixed(2) + \" \" + unit;\n}\n\nexport default Vue.extend({\n  components: {\n    highcharts: Chart\n  },\n  data() {\n    return {\n      chartBaseData: {},\n      chartExtData: {},\n      chartBarData: {},\n      host: \"\",\n      options: this.$store.state.options,\n      selectedSite: {} as Site,\n      shareing: false,\n      shareTime: new Date(),\n      version: \"\",\n      userName: \"\",\n      sites: [] as Site[],\n      rawData: {} as Dictionary<any>,\n      beginDate: \"\",\n      endDate: \"\",\n      dateRange: \"30day\"\n    };\n  },\n\n  mounted() {\n    this.initEvents();\n    this.init(this.$route.params[\"host\"]);\n  },\n\n  created() {\n    this.version = PPF.getVersion();\n\n    // 插入到第一个位置\n    this.sites.push({\n      name: this.$t(\"statistic.allSite\").toString(),\n      host: ECommonKey.allSite,\n      icon: \"\",\n      url: \"\",\n      tags: []\n    });\n\n    this.options.sites.forEach((site: Site) => {\n      if (site.allowGetUserInfo) {\n        this.sites.push(JSON.parse(JSON.stringify(site)));\n      }\n    });\n  },\n\n  methods: {\n    initEvents() {},\n    init(host: string = \"\") {\n      if (host == ECommonKey.allSite) {\n        host = \"\";\n      }\n      this.host = host;\n\n      this.selectedSite = this.options.sites.find((item: Site) => {\n        return item.host == this.host;\n      });\n\n      this.resetDateRange();\n\n      extension\n        .sendRequest(EAction.getUserHistoryData, null, this.host)\n        .then((data: any) => {\n          console.log(data);\n          this.rawData = data;\n          this.resetData(data);\n        });\n    },\n    /**\n     * 获取合计数据\n     */\n    getTotalData(source: any) {\n      let result: Dictionary<any> = {};\n      let nameInfo = { name: \"\", maxCount: 0 };\n      let userNames: Dictionary<any> = {};\n      let days: any[] = [];\n\n      for (const host in source) {\n        if (source.hasOwnProperty(host)) {\n          const siteData = this.fillData(source[host]);\n          let site: Site = this.options.sites.find((item: Site) => {\n            return item.host == host;\n          });\n\n          if (!site) {\n            continue;\n          }\n\n          if (!site.allowGetUserInfo) {\n            continue;\n          }\n\n          for (const date in siteData) {\n            if (siteData.hasOwnProperty(date)) {\n              const data = siteData[date];\n\n              if (\n                !data.uploaded &&\n                !data.downloaded &&\n                !data.seedingSize &&\n                !data.seeding\n              ) {\n                continue;\n              }\n\n              let item = result[date];\n              if (!item) {\n                item = {\n                  uploaded: 0,\n                  downloaded: 0,\n                  seedingSize: 0,\n                  seeding: 0,\n                  bonus: 0,\n                  name: \"\",\n                  lastUpdateStatus: EDataResultType.success,\n                };\n              }\n\n              item.uploaded += this.getNumber(data.uploaded);\n              item.downloaded += this.getNumber(data.downloaded);\n\n              if (data.seeding && data.seeding > 0) {\n                item.seeding += Math.round(data.seeding);\n              }\n\n              item.seedingSize += this.getNumber(data.seedingSize);\n              item.bonus += this.getNumber(data.bonus);\n\n              if (!userNames[data.name]) {\n                userNames[data.name] = 0;\n              }\n              userNames[data.name]++;\n\n              // 获取使用最多的用户名\n              if (userNames[data.name] > nameInfo.maxCount) {\n                nameInfo.name = data.name;\n                nameInfo.maxCount = userNames[data.name];\n              }\n\n              result[date] = item;\n\n              if (!days.includes(date)) {\n                days.push(date);\n              }\n            }\n          }\n        }\n      }\n\n      let datas: Dictionary<any> = {};\n      days.sort().forEach(day => {\n        datas[day] = result[day];\n      });\n\n      this.userName = nameInfo.name;\n\n      return datas;\n    },\n    //-> { site: [ { date, relativeUploaded }] }\n    getRelativeData(source: any) {\n      const result: any = {};\n      for (const [host, siteData] of Object.entries(source)) {\n        const site: Site = this.options.sites.find((item: Site) => item.host == host);\n        if (!site) {\n          continue;\n        }\n        if (!site.allowGetUserInfo) {\n          continue;\n        }\n        const newSiteData = this.fillData(siteData);\n\n        // -> [ { date, uploaded }]\n        const absoluteSiteData = [];\n        for (const [date, item] of (Object.entries(newSiteData) as any[])) {\n          if (date == EUserDataRange.latest) {\n            continue;\n          }\n          absoluteSiteData.push({\n            date: new Date(date),\n            uploaded: item.uploaded,\n          });\n        }\n\n        //-> [ { date, relativeUploaded }]\n        const relativeSiteData = [];\n        for (let i=1; i<absoluteSiteData.length; i++) {\n          const a = absoluteSiteData[i-1];\n          const b = absoluteSiteData[i];\n          relativeSiteData.push({ date: a.date, relativeUploaded: b.uploaded - a.uploaded });\n        }\n\n        result[site.name] = relativeSiteData;\n      }\n      return result;\n    },\n    getNumber(source: any) {\n      if (typeof source === \"string\") {\n        source = source.replace(/,/g, \"\");\n      }\n\n      if (/^(-)?\\d+(.\\d+)?$/.test(source)) {\n        return parseFloat(source.toString());\n      }\n\n      return 0;\n    },\n    /**\n     * 填充数据，将两个日期中间空白的数据由前一天数据填充\n     */\n    fillData(result: any, fill: boolean = true) {\n      let datas: any = {};\n      let lastDate: any = null;\n      let lastData: any = null;\n      for (const key in result) {\n        if (dayjs(key).isValid()) {\n          let data = result[key];\n          let isValidDate = true;\n\n          // 如果当前数据不可用，则使用上一条数据\n          if (\n            !data.uploaded &&\n            !data.downloaded &&\n            !data.seedingSize &&\n            !data.seeding\n          ) {\n            data = lastData;\n          } else if (lastData && !data.id && !data.name) {\n            data = lastData;\n          }\n\n          if (!data) {\n            continue;\n          }\n\n          let date = dayjs(key);\n\n          if (!lastDate) {\n            lastDate = date;\n          }\n\n          if (!lastData) {\n            lastData = PPF.clone(data);\n          }\n\n          if (fill) {\n            let day = date.diff(lastDate, \"day\");\n            if (day > 1) {\n              for (let index = 0; index < day - 1; index++) {\n                lastDate = lastDate.add(1, \"day\");\n                if (this.inDateRange(lastDate)) {\n                  datas[lastDate.format(\"YYYY-MM-DD\")] = lastData;\n                }\n              }\n            }\n          }\n\n          lastData = PPF.clone(data);\n          lastDate = date;\n\n          if (this.inDateRange(date)) {\n            datas[key] = data;\n          }\n        }\n      }\n\n      datas[\"latest\"] = result[\"latest\"];\n\n      return datas;\n    },\n    inDateRange(date: any) {\n      // 小于起始日期时跳过\n      if (\n        dayjs(this.beginDate).isValid() &&\n        date.diff(this.beginDate, \"day\") < 0\n      ) {\n        return false;\n      }\n\n      // 大于截止日期时跳过\n      if (dayjs(this.endDate).isValid() && date.diff(this.endDate, \"day\") > 0) {\n        return false;\n      }\n\n      return true;\n    },\n    resetData(result: any) {\n      if (this.host) {\n        const newResult = this.fillData(result, false);\n        this.resetBaseData(newResult);\n        this.resetExtData(newResult);\n        this.resetBarData(this.getRelativeData({[this.host]: result}));\n      } else {\n        let data = this.getTotalData(result);\n        this.selectedSite = {\n          name: this.$t(\"statistic.allSite\").toString(),\n          host: ECommonKey.allSite\n        };\n        this.resetBaseData(data);\n        this.resetExtData(data);\n        this.resetBarData(this.getRelativeData(result));\n      }\n    },\n    /**\n     * 基础数据\n     */\n    resetBaseData(result: any) {\n      var fillOpacity = 0.3;\n      var datas = [\n        {\n          type: \"spline\",\n          name: this.$t(\"statistic.upload\").toString(),\n          tooltip: {\n            formatter: function(): any {\n              let _this = this as any;\n              return filters.formatSize(_this.x);\n            }\n          },\n          fillOpacity: fillOpacity,\n          data: [] as any\n        },\n        {\n          type: \"spline\",\n          name: this.$t(\"statistic.download\").toString(),\n          tooltip: {\n            valueSuffix: \" \"\n          },\n          fillOpacity: fillOpacity,\n          data: [] as any\n        },\n        {\n          type: \"spline\",\n          name: this.$t(\"statistic.bonus\").toString(),\n          yAxis: 1,\n          tooltip: {\n            valueSuffix: \" \"\n          },\n          fillOpacity: fillOpacity,\n          data: [] as any\n        }\n      ];\n      var types = {};\n      var colors = [\"#1b5e20\", \"#b71c1c\", \"#2f7ed8\", \"#03A9F4\"];\n      var categories = [];\n      let latest = {\n        downloaded: 0,\n        uploaded: 0,\n        bonus: 0,\n        name: \"\"\n      };\n\n      let _self = this;\n\n      // 数据\n      for (const date in result) {\n        if (result.hasOwnProperty(date)) {\n          const data = result[date];\n\n          if (!data.uploaded && !data.downloaded) {\n            continue;\n          }\n          if (date == EUserDataRange.latest) {\n            latest = data;\n            continue;\n          }\n\n          const time = new Date(date).getTime();\n\n          datas[0].data.push([time, this.getNumber(data.uploaded)]);\n          datas[1].data.push([time, this.getNumber(data.downloaded)]);\n          datas[2].data.push([time, this.getNumber(data.bonus)]);\n          categories.push(date);\n        }\n      }\n\n      var chart = {\n        chart: {\n          backgroundColor: null\n        },\n        series: datas,\n        colors: colors,\n        // 版权信息\n        credits: {\n          enabled: false\n        },\n        subtitle: {\n          text: this.$t(\"statistic.baseDataSubTitle\", {\n            uploaded: filters.formatSize(latest.uploaded),\n            downloaded: filters.formatSize(latest.downloaded),\n            bonus: filters.formatNumber(latest.bonus)\n          }).toString()\n        },\n        title: {\n          text: this.$t(\"statistic.baseDataTitle\", {\n            userName: latest.name || this.userName,\n            site: this.selectedSite.name\n          }).toString()\n        },\n        xAxis: {\n          type: \"datetime\",\n          dateTimeLabelFormats: {\n            day: \"%Y-%m-%d\",\n            week: \"%Y-%m-%d\",\n            month: \"%Y-%m-%d\",\n            year: \"%Y-%m-%d\"\n          },\n          // categories: categories,\n          gridLineDashStyle: \"ShortDash\",\n          gridLineWidth: 1,\n          gridLineColor: \"#dddddd\"\n        },\n        yAxis: [\n          {\n            labels: {\n              formatter: function(): any {\n                let _this = this as any;\n                return filters.formatSize(_this.value);\n              },\n              style: {\n                color: colors[3]\n              }\n            },\n            title: {\n              text: this.$t(\"statistic.data\").toString(),\n              style: {\n                color: colors[3]\n              }\n            },\n            lineWidth: 1,\n            gridLineDashStyle: \"ShortDash\"\n          },\n          {\n            opposite: true,\n            labels: {\n              formatter: function(): any {\n                let _this = this as any;\n                return formatBonus(_this.value);\n              },\n              style: {\n                color: colors[2]\n              }\n            },\n            title: {\n              text: this.$t(\"statistic.bonus\").toString(),\n              style: {\n                color: colors[2]\n              }\n            },\n            lineWidth: 1,\n            gridLineDashStyle: \"ShortDash\"\n          }\n        ],\n        tooltip: {\n          shared: true,\n          crosshairs: {\n            width: 1,\n            color: \"red\",\n            dashStyle: \"shortdot\"\n          },\n          useHTML: true,\n          formatter: function(): any {\n            function createTipItem(text: string, color: string = \"#000\") {\n              return `<div style='color:${color};'>${text}</div>`;\n            }\n            let _this = this as any;\n            let tips: string[] = [];\n            // 标题（时间）\n            tips.push(createTipItem(dayjs(_this.x).format(\"YYYY-MM-DD\")));\n            _this.points.forEach((point: any) => {\n              let value = point.y;\n              switch (point.series.name) {\n                case _self.$t(\"statistic.upload\").toString():\n                case _self.$t(\"statistic.download\").toString():\n                  value = filters.formatSize(point.y);\n                  break;\n\n                case _self.$t(\"statistic.bonus\").toString():\n                  value = filters.formatNumber(point.y);\n                  break;\n              }\n\n              tips.push(\n                createTipItem(`${point.series.name}: ${value}`, point.color)\n              );\n            });\n\n            let result = `<div>${tips.join(\"\")}</div>`;\n            return result;\n          }\n        }\n      };\n\n      this.chartBaseData = chart;\n    },\n    /**\n     * 其他数据\n     */\n    resetExtData(result: any) {\n      var fillOpacity = 0.3;\n      var datas = [\n        {\n          type: \"spline\",\n          name: this.$t(\"statistic.seedingSize\").toString(),\n          fillOpacity: fillOpacity,\n          data: [] as any\n        },\n        {\n          type: \"spline\",\n          name: this.$t(\"statistic.seedingCount\").toString(), //\"做种数\",\n          yAxis: 1,\n          fillOpacity: fillOpacity,\n          data: [] as any\n        }\n      ];\n      var types = {};\n      var colors = [\"#FF6F00\", \"#2E7D32\", \"#2f7ed8\", \"#03A9F4\"];\n      var categories = [];\n      let latest = {\n        seeding: 0,\n        seedingSize: 0,\n        name: \"\"\n      };\n\n      // 数据\n      for (const date in result) {\n        if (result.hasOwnProperty(date)) {\n          const data = result[date];\n\n          if (!data.seedingSize && !data.seeding) {\n            continue;\n          }\n          if (date == EUserDataRange.latest) {\n            latest = data;\n            continue;\n          }\n\n          const time = new Date(date).getTime();\n\n          datas[0].data.push([time, parseFloat(data.seedingSize)]);\n          datas[1].data.push([time, parseFloat(data.seeding)]);\n          categories.push(date);\n        }\n      }\n\n      let _self = this;\n      var chart = {\n        chart: {\n          backgroundColor: null\n        },\n        series: datas,\n        colors: colors,\n        // 版权信息\n        credits: {\n          enabled: false\n        },\n        subtitle: {\n          text: this.$t(\"statistic.seedingDataSubTitle\", {\n            seedingSize: filters.formatSize(latest.seedingSize),\n            count: latest.seeding\n          }).toString()\n        },\n        title: {\n          text: this.$t(\"statistic.seedingDataTitle\", {\n            userName: latest.name || this.userName,\n            site: this.selectedSite.name\n          }).toString()\n        },\n        xAxis: {\n          // categories: categories,\n          type: \"datetime\",\n          dateTimeLabelFormats: {\n            day: \"%Y-%m-%d\",\n            week: \"%Y-%m-%d\",\n            month: \"%Y-%m-%d\",\n            year: \"%Y-%m-%d\"\n          },\n          gridLineDashStyle: \"ShortDash\",\n          gridLineWidth: 1,\n          gridLineColor: \"#dddddd\"\n        },\n        yAxis: [\n          {\n            labels: {\n              formatter: function(): any {\n                let _this = this as any;\n                return filters.formatSize(_this.value);\n              },\n              style: {\n                color: colors[0]\n              }\n            },\n            title: {\n              text: this.$t(\"statistic.size\").toString(), //\"体积\",\n              style: {\n                color: colors[0]\n              }\n            },\n            lineWidth: 1,\n            gridLineDashStyle: \"ShortDash\"\n          },\n          {\n            opposite: true,\n            labels: {\n              formatter: function(): any {\n                let _this = this as any;\n                return formatBonus(_this.value);\n              },\n              style: {\n                color: colors[1]\n              }\n            },\n            title: {\n              text: this.$t(\"statistic.count\").toString(), //\"数量\",\n              style: {\n                color: colors[1]\n              }\n            },\n            lineWidth: 1,\n            gridLineDashStyle: \"ShortDash\"\n          }\n        ],\n        tooltip: {\n          shared: true,\n          useHTML: true,\n          crosshairs: {\n            width: 1,\n            color: \"red\",\n            dashStyle: \"shortdot\"\n          },\n          formatter: function(): any {\n            function createTipItem(text: string, color: string = \"#000\") {\n              return `<div style='color:${color};'>${text}</div>`;\n            }\n            let _this = this as any;\n            let tips: string[] = [];\n            // 标题（时间）\n            tips.push(createTipItem(dayjs(_this.x).format(\"YYYY-MM-DD\")));\n            _this.points.forEach((point: any) => {\n              let value = point.y;\n              switch (point.series.name) {\n                // \"做种体积\"\n                case _self.$t(\"statistic.seedingSize\").toString():\n                  value = filters.formatSize(point.y);\n                  break;\n              }\n\n              tips.push(\n                createTipItem(`${point.series.name}: ${value}`, point.color)\n              );\n            });\n\n            let result = `<div>${tips.join(\"\")}</div>`;\n            return result;\n          }\n        }\n      };\n\n      this.chartExtData = chart;\n    },\n    /**\n     * Bar数据\n     */\n    resetBarData(result: any) {\n      const $t = this.$t.bind(this);\n\n      // -> [ { name: siteName, data: [ [ date, relativeUploaded ] ]}]\n      const series = Object.entries(result).map(([siteName, data]: any[]) => ({\n        name: siteName,\n        data: data.map((v: any) => ([\n          v.date.getTime(),\n          v.relativeUploaded,\n        ]))\n      }));\n\n      const chart = {\n        series,\n        chart: {\n          backgroundColor: null,\n          type: 'column'\n        },\n        credits: {\n          enabled: false\n        },\n        title: {\n          text: this.$t(\"statistic.barDataTitle\", {\n            userName: this.userName,\n            site: this.selectedSite.name\n          }).toString()\n        },\n        xAxis: {\n          type: \"datetime\",\n          dateTimeLabelFormats: {\n            day: \"%m-%d\",\n            week: \"%m-%d\",\n            month: \"%m-%d\",\n            year: \"%m-%d\"\n          },\n          gridLineDashStyle: \"ShortDash\",\n          gridLineWidth: 1,\n          gridLineColor: \"#dddddd\"\n        },\n        yAxis: {\n          title: {\n            text: this.$t(\"statistic.data\").toString(),\n          },\n          lineWidth: 1,\n          gridLineDashStyle: \"ShortDash\"\n        },\n        tooltip: {\n          useHTML: true,\n          formatter: function(): any {\n            const { x, y, total, color, series: { name: siteName } }: any = this\n            let sites = []\n            for (const site of series) {\n              const siteY = (site.data.find(([a]: any[]) => a === x) || [0, 0])[1]\n              if (\n                (y < 0 && siteY < 0) ||\n                (y > 0 && siteY > 0)\n               ) {\n                const percentage = Math.ceil(siteY / total * 100)\n                sites.push({\n                  name: site.name,\n                  value: siteY,\n                  valueDisplay: filters.formatSizeWithNegative(siteY),\n                  percentageDisplay: `${percentage}%`,\n                  isActive: site.name === siteName,\n                })\n              }\n            }\n            sites.sort((a,b) => b.value-a.value)\n            const date = dayjs(x).format(\"YYYY-MM-DD\")\n            const totalDisplay = filters.formatSizeWithNegative(total)\n            const totalText = $t('statistic.total').toString()\n\n            const createTr = ({ name, valueDisplay, percentageDisplay, isActive }: any) => {\n              return `\n                <tr style='color: ${isActive ? color : \"inherit\"};'>\n                  <td>${name}</td>\n                  <td style='padding-left: 5px;'>${valueDisplay}</td>\n                  <td style='padding-left: 5px;'>${percentageDisplay}</td>\n                </tr>\n              `\n            }\n\n            return `\n              ${date}<br/>\n              <table>\n                ${createTr({ name: totalText, valueDisplay: totalDisplay, percentageDisplay: '100%' })}\n                ${sites.map(createTr).join('')}\n              </table>\n            `\n          },\n        },\n        plotOptions: {\n          column: {\n            stacking: 'normal',\n          }\n        },\n      };\n      this.chartBarData = chart;\n    },\n    joinTags(tags: any): string {\n      if (tags && tags.join) {\n        return tags.join(\", \");\n      }\n      return \"\";\n    },\n    share() {\n      let div = this.$refs.charts as HTMLDivElement;\n      this.shareing = true;\n      this.shareTime = new Date();\n      domtoimage.toJpeg(div, {\n        filter: (node) => {\n          if (node.nodeType === 1) {\n            return !(node as Element).classList.contains('by_pass_canvas')\n          }\n          return true\n        }\n      }).then((dataUrl: any) => {\n        if (dataUrl) {\n          FileSaver.saveAs(dataUrl, \"PT-Plugin-Plus-UserData.jpg\");\n        }\n        this.shareing = false;\n      });\n    },\n    /**\n     * 导出原始数据\n     */\n    exportRawData() {\n      const data = new Blob([JSON.stringify(this.rawData)], {\n        type: \"text/plain\"\n      });\n      FileSaver.saveAs(\n        data,\n        `PT-Plugin-Plus-Statistic-${this.selectedSite.host}.json`\n      );\n    },\n\n    resetDateRange() {\n      const now = dayjs();\n      this.endDate = now.toString();\n      switch (this.dateRange) {\n        case \"7day\":\n          this.beginDate = now.add(-7, \"day\").toString();\n          break;\n\n        case \"30day\":\n          this.beginDate = now.add(-30, \"day\").toString();\n          break;\n\n        case \"60day\":\n          this.beginDate = now.add(-60, \"day\").toString();\n          break;\n\n        case \"90day\":\n          this.beginDate = now.add(-90, \"day\").toString();\n          break;\n\n        case \"180day\":\n          this.beginDate = now.add(-180, \"day\").toString();\n          break;\n\n        default:\n          this.beginDate = \"\";\n          break;\n      }\n    }\n  },\n\n  watch: {\n    dateRange() {\n      this.resetDateRange();\n      this.resetData(this.rawData);\n    }\n  }\n});\n</script>\n<style lang=\"scss\"  scoped>\n.container {\n  width: 900px;\n  padding: 0;\n\n  .charts {\n    background-color: white;\n  }\n\n  .chart {\n    min-width: 320px;\n    max-width: 800px;\n    height: 240px;\n    margin: 0 auto;\n  }\n}\n\n.theme--dark .container {\n  .charts {\n    background-color: #9e9e9e;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/popup/index.ts",
    "content": "(function($) {\n  $(\"#btnConfig\").on(\"click\", () => {\n    chrome.runtime.sendMessage(\n      {\n        action: \"openOptions\"\n      },\n      () => {}\n    );\n  });\n\n  $(\"#btnSystemLog\").on(\"click\", () => {\n    chrome.runtime.sendMessage(\n      {\n        action: \"openOptions\",\n        data: \"system-logs\"\n      },\n      () => {}\n    );\n  });\n})(jQuery);\n"
  },
  {
    "path": "src/service/api.ts",
    "content": "import localStorage from \"./localStorage\";\nimport md5 from \"blueimp-md5\";\nimport {\n  EConfigKey,\n  DataResult,\n  EDataResultType,\n  EInstallType\n} from \"@/interface/common\";\nimport { PPF } from \"./public\";\nimport \"./favicon\";\n\nlet rootPath = \"\";\nlet isExtensionMode = false;\nconst isDebugMode = process.env.NODE_ENV === \"development\";\n// 检测浏览器当前状态和模式\ntry {\n  let runtime = chrome.runtime as any;\n  isExtensionMode = true;\n  rootPath = runtime.getManifest().options_ui.page.replace(\"index.html\", \"\");\n  if (rootPath && rootPath.substr(-1) == \"/\") {\n    rootPath = rootPath.substr(0, rootPath.length - 1);\n  }\n\n  if (!rootPath) {\n    rootPath = `chrome-extension://${chrome.runtime.id}`;\n  }\n\n  isDebugMode && console.log(\"is extension mode.\");\n} catch (error) {\n  isExtensionMode = false;\n  isDebugMode && console.log(\"is not extension mode.\");\n}\n\nconst RESOURCE_URL = !isExtensionMode\n  ? `http://${window.location.hostname}:8001`\n  : (isExtensionMode ? rootPath : \"\") + \"/resource\";\n// 调试信息\nlet RESOURCE_API = {\n  host: RESOURCE_URL,\n  schemas: `${RESOURCE_URL}/schemas.json`,\n  schemaConfig: `${RESOURCE_URL}/schemas/{$schema}/config.json`,\n  sites: `${RESOURCE_URL}/sites.json`,\n  siteConfig: `${RESOURCE_URL}/sites/{$site}/config.json`,\n  clients: `${RESOURCE_URL}/clients.json`,\n  clientConfig: `${RESOURCE_URL}/clients/{$client}/config.json`,\n  latestReleases: `https://api.github.com/repos/pt-plugins/PT-Plugin-Plus/releases/latest`,\n  systemConfig: `${RESOURCE_URL}/systemConfig.json`\n};\n\nexport const APP = {\n  debugMode: isDebugMode,\n  scriptQueues: [] as any,\n  isExtensionMode: isExtensionMode,\n  cache: {\n    localStorage: new localStorage(),\n    cacheKey: EConfigKey.cache,\n    contents: {} as any,\n    // 1 天\n    expires: 60 * 60 * 24 * 1,\n    init(callback?: any) {\n      APP.debugMode && console.log(\"cache.init\");\n      this.localStorage.get(this.cacheKey, (result: any) => {\n        if (result) {\n          let expires = result[\"expires\"];\n          // 判断是否过期\n          if ((expires && new Date() > new Date(expires)) || APP.debugMode) {\n            this.contents = {};\n          } else {\n            this.contents = result;\n          }\n        }\n        callback && callback();\n      });\n    },\n    /**\n     * 获取缓存\n     * @param key\n     */\n    get(key: string): string | null {\n      if (this.contents) {\n        return this.contents[md5(key)];\n      }\n      return null;\n    },\n    /**\n     * 设置缓存\n     * @param key\n     * @param content\n     */\n    set(key: string, content: string) {\n      this.contents[md5(key)] = content;\n      this.contents[\"update\"] = new Date().getTime();\n      this.contents[\"expires\"] = new Date().getTime() + this.expires;\n      this.localStorage.set(this.cacheKey, this.contents);\n    },\n    /**\n     * 清除缓存\n     */\n    clear() {\n      this.contents = {};\n      this.localStorage.set(this.cacheKey, this.contents);\n    },\n    /**\n     * 获取缓存最后更新时间\n     */\n    getLastUpdateTime(): Promise<any> {\n      return new Promise<any>((resolve?: any, reject?: any) => {\n        this.localStorage.get(this.cacheKey, (result: any) => {\n          if (result) {\n            let update = result[\"update\"];\n            resolve(update || 0);\n          } else {\n            reject();\n          }\n        });\n      });\n    }\n  },\n  addScript(script: any) {\n    APP.debugMode && console.log(\"addScript\", script);\n    this.scriptQueues.push(script);\n  },\n  applyScripts() {\n    let script = this.scriptQueues.shift();\n    if (script) {\n      this.execScript(script).then(() => {\n        this.applyScripts();\n      });\n    }\n  },\n  /**\n   * 执行脚本\n   * @param script\n   */\n  execScript(script: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      switch (script.type) {\n        case \"code\":\n          this.runScript(script.content);\n          resolve();\n          break;\n\n        default:\n          {\n            let url = script.content || script;\n            if (url.substr(0, 4) !== \"http\") {\n              if (url.substr(0, 1) !== \"/\") {\n                url = `/${url}`;\n              }\n              url = `${API.host}${url}`;\n            }\n\n            let content = this.cache.get(url);\n            try {\n              if (content) {\n                this.runScript(content);\n                resolve();\n              } else {\n                console.log(\"execScript: %s\", url);\n                $.ajax({\n                  url,\n                  dataType: \"text\"\n                })\n                  .done(result => {\n                    this.runScript(result);\n                    this.cache.set(url, result);\n                    resolve();\n                  })\n                  .fail((jqXHR, status, text) => {\n                    if (\n                      jqXHR.responseJSON &&\n                      jqXHR.responseJSON.code &&\n                      jqXHR.responseJSON.msg\n                    ) {\n                      reject(\n                        jqXHR.responseJSON.msg +\n                        \" (\" +\n                        jqXHR.responseJSON.code +\n                        \")\"\n                      );\n                    } else {\n                      reject(status + \", \" + text);\n                    }\n                  });\n              }\n            } catch (error) {\n              reject(error);\n            }\n          }\n\n          break;\n      }\n    });\n  },\n  /**\n   * 执行指定的脚本\n   * @param script 脚本内容\n   * @param scope 作用域\n   */\n  runScript(script: string, scope: any = window) {\n    // 默认将脚本作用于 window 对象，这种方式可以正常加载外部的第三方库\n    eval.call(scope, script);\n  },\n  /**\n   * 追加样式信息\n   * @param options\n   */\n  applyStyle(options: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let style = $(\"<style/>\").appendTo(document.body);\n      switch (options.type) {\n        case \"file\": {\n          let url = options.content;\n          if (url.substr(0, 4) !== \"http\") {\n            if (url.substr(0, 1) !== \"/\") {\n              url = `/${url}`;\n            }\n            url = `${API.host}${url}`;\n          }\n          let content = this.cache.get(url);\n\n          if (content) {\n            style.html(content);\n            resolve();\n          } else {\n            $.get(\n              url,\n              result => {\n                style.html(result);\n                this.cache.set(url, result);\n                resolve();\n              },\n              \"text\"\n            );\n          }\n          break;\n        }\n\n        default:\n          style.html(options.content);\n          resolve();\n          break;\n      }\n\n      // var link = $(\"<link/>\")\n      //   .attr({\n      //     rel: \"stylesheet\",\n      //     type: \"text/css\",\n      //     href: `${this.host}/${stylePath}?__t__=` + Math.random()\n      //   })\n      //   .appendTo($(\"head\")[0]);\n\n      // resolve();\n    });\n  },\n  /**\n   * 异步获取脚本内容\n   * @param path 路径\n   */\n  getScriptContent(path: string): JQueryXHR {\n    let url = `${API.host}/${path}`;\n    // 外部链接\n    if (path.substr(0, 4) === \"http\") {\n      url = path;\n    } else {\n      url = url.replace(\"resource//\", \"resource/\");\n    }\n    APP.debugMode && console.log(\"getScriptContent\", url);\n    return $.ajax({\n      url,\n      dataType: \"text\"\n    });\n  },\n  /**\n   * 创建错误信息，用于函数返回\n   * @param msg\n   */\n  createErrorMessage(msg: any): DataResult {\n    return {\n      type: EDataResultType.error,\n      msg,\n      success: false\n    };\n  },\n\n  /**\n   * 显示系统提示信息\n   * @param options\n   */\n  showNotifications(\n    options: chrome.notifications.NotificationOptions,\n    timeout = 3000\n  ) {\n    PPF.showNotifications(options, timeout);\n  },\n  getInstallType(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome && chrome.management) {\n        chrome.management.getSelf(result => {\n          // 判断是否为 crx 方式\n          if (\n            result.updateUrl &&\n            result.updateUrl.indexOf(\"pt-plugins/PT-Plugin-Plus\") > 0\n          ) {\n            resolve(EInstallType.crx);\n          } else {\n            resolve(result.installType);\n          }\n        });\n      } else {\n        reject();\n      }\n    });\n  }\n};\n\nAPP.cache.init();\nexport const API = RESOURCE_API;\n"
  },
  {
    "path": "src/service/backupFileParser.ts",
    "content": "import JSZip from \"jszip\";\nimport md5 from \"blueimp-md5\";\nimport * as CryptoJS from \"crypto-js\";\n\nimport {\n  Dictionary,\n  IManifest,\n  IHashData,\n  EEncryptMode,\n  Options,\n  ERestoreError,\n  IBackupRawData\n} from \"@/interface/common\";\nimport { PPF } from \"./public\";\n\nexport class BackupFileParser {\n  /**\n   * 创建用于验证数据对象\n   */\n  public createHash(data: string): IHashData {\n    const length = data.length;\n\n    const keys: any[] = [];\n\n    let result: IHashData = {\n      hash: \"\",\n      keyMap: [],\n      length\n    };\n\n    for (let n = 0; n < 32; n++) {\n      let index = Math.round(length * Math.random());\n      keys.push(data.substr(index, 1));\n      result.keyMap.push(index);\n    }\n\n    result.hash = md5(keys.join(\"\"));\n\n    return result;\n  }\n\n  /**\n   * 获取备份数据\n   */\n  public createBackupFileBlob(rawData: IBackupRawData): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      try {\n        const zip = new JSZip();\n\n        if (rawData.options.system) {\n          delete rawData.options.system;\n        }\n\n        const _options = rawData.options as Options;\n        const secretKey = _options.encryptBackupData\n          ? _options.encryptSecretKey\n          : \"\";\n        if (_options.encryptSecretKey) {\n          delete rawData.options.encryptSecretKey;\n        }\n        const options = this.encryptData(rawData.options, secretKey);\n        const userData = this.encryptData(rawData.userData, secretKey);\n\n        // 配置\n        zip.file(\"options.json\", options);\n        // 用户数据\n        zip.file(\"userdatas.json\", userData);\n\n        // 创建检证用的文件\n        const manifest = {\n          checkInfo: this.createHash(options + userData),\n          version: PPF.getVersion(),\n          time: new Date().getTime(),\n          encryptMode: secretKey ? EEncryptMode.AES : \"\"\n        };\n        zip.file(\"manifest.json\", JSON.stringify(manifest));\n\n        // 用户收藏\n        if (rawData.collection) {\n          zip.file(\n            \"collection.json\",\n            this.encryptData(rawData.collection, secretKey)\n          );\n        }\n\n        // 站点Cookies\n        if (rawData.cookies) {\n          zip.file(\n            \"cookies.json\",\n            this.encryptData(rawData.cookies, secretKey)\n          );\n        }\n\n        // 搜索结果快照\n        if (rawData.searchResultSnapshot) {\n          zip.file(\n            \"searchResultSnapshot.json\",\n            this.encryptData(rawData.searchResultSnapshot, secretKey)\n          );\n        }\n\n        // 辅种任务\n        if (rawData.keepUploadTask) {\n          zip.file(\n            \"keepUploadTask.json\",\n            this.encryptData(rawData.keepUploadTask, secretKey)\n          );\n        }\n\n        // 下载历史\n        if (rawData.downloadHistory) {\n          zip.file(\n            \"downloadHistory.json\",\n            this.encryptData(rawData.downloadHistory, secretKey)\n          );\n        }\n\n        // 压缩处理\n        zip\n          .generateAsync({\n            type: \"blob\",\n            compression: \"DEFLATE\",\n            // level 范围： 1-9 ，9为最高压缩比\n            compressionOptions: {\n              level: 9\n            }\n          })\n          .then((blob: any) => {\n            resolve(blob);\n          });\n      } catch (error) {\n        reject(error);\n      }\n    });\n  }\n\n  /**\n   * 加载备份数据\n   * @param data\n   * @param secretKeyTitle\n   * @param secretKey\n   */\n  public loadZipData(\n    data: any,\n    secretKeyTitle: string = \"请输入密钥：\",\n    secretKey: string = \"\"\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      JSZip.loadAsync(data)\n        .then(zip => {\n          let requests: any[] = [];\n          requests.push(zip.file(\"manifest.json\")!.async(\"text\"));\n          requests.push(zip.file(\"options.json\")!.async(\"text\"));\n          requests.push(zip.file(\"userdatas.json\")!.async(\"text\"));\n\n          if (zip.file(\"collection.json\")) {\n            requests.push(zip.file(\"collection.json\")!.async(\"text\"));\n          }\n\n          if (zip.file(\"cookies.json\")) {\n            requests.push(zip.file(\"cookies.json\")!.async(\"text\"));\n          }\n\n          if (zip.file(\"searchResultSnapshot.json\")) {\n            requests.push(zip.file(\"searchResultSnapshot.json\")!.async(\"text\"));\n          }\n\n          if (zip.file(\"keepUploadTask.json\")) {\n            requests.push(zip.file(\"keepUploadTask.json\")!.async(\"text\"));\n          }\n\n          if (zip.file(\"downloadHistory.json\")) {\n            requests.push(zip.file(\"downloadHistory.json\")!.async(\"text\"));\n          }\n\n          return Promise.all(requests);\n        })\n        .then(results => {\n          const manifest: IManifest = JSON.parse(results[0]);\n\n          console.log(manifest);\n\n          if (manifest.encryptMode) {\n            // 如果已指定了密钥，则先尝试是否正确\n            if (secretKey) {\n              if (this.decryptData(results[1], secretKey) === null) {\n                secretKey = \"\";\n              }\n            }\n\n            if (!secretKey) {\n              let tmpSecretKey = window.prompt(secretKeyTitle);\n              if (!tmpSecretKey) {\n                reject(ERestoreError.needSecretKey);\n                return;\n              }\n\n              secretKey = tmpSecretKey;\n\n              let test = this.decryptData(results[1], secretKey);\n              if (test === null) {\n                reject(ERestoreError.errorSecretKey);\n                return;\n              }\n            }\n          } else {\n            secretKey = \"\";\n          }\n\n          const result: Dictionary<any> = {\n            manifest,\n            options: this.decryptData(results[1], secretKey),\n            datas: this.decryptData(results[2], secretKey)\n          };\n\n          if (results.length > 3) {\n            result[\"collection\"] = this.decryptData(results[3], secretKey);\n          }\n\n          if (results.length > 4 && PPF.checkOptionalPermission(\"cookies\")) {\n            result[\"cookies\"] = this.decryptData(results[4], secretKey);\n          }\n\n          if (results.length > 5) {\n            result[\"searchResultSnapshot\"] = this.decryptData(\n              results[5],\n              secretKey\n            );\n          }\n\n          if (results.length > 6) {\n            result[\"keepUploadTask\"] = this.decryptData(results[6], secretKey);\n          }\n\n          if (results.length > 7) {\n            result[\"downloadHistory\"] = this.decryptData(results[7], secretKey);\n          }\n\n          if (this.checkData(result.manifest, results[1] + results[2])) {\n            resolve(result);\n          } else {\n            reject(\"error\");\n          }\n        })\n        .catch(error => {\n          console.log(error);\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 简单验证数据，仅防止格式错误的数据\n   */\n  public checkData(manifest: IManifest, data: string): boolean {\n    if (!manifest) {\n      return false;\n    }\n\n    if (!manifest.checkInfo) {\n      return false;\n    }\n\n    const checkInfo = manifest.checkInfo;\n    const length = data.length;\n\n    if (length !== checkInfo.length) {\n      return false;\n    }\n\n    const keys: any[] = [];\n\n    try {\n      if (checkInfo.keyMap.length !== 32) {\n        return false;\n      }\n      for (let n = 0; n < 32; n++) {\n        let index = checkInfo.keyMap[n];\n        keys.push(data.substr(index, 1));\n      }\n\n      if (md5(keys.join(\"\")) === checkInfo.hash) {\n        return true;\n      }\n    } catch (error) {}\n\n    return false;\n  }\n\n  /**\n   * 加密数据\n   * @param data 原始数据，JS对象\n   * @param secretKey 密钥，如果不指定，则不加密\n   */\n  public encryptData(data: any, secretKey: string = \"\") {\n    if (!secretKey) {\n      return JSON.stringify(data);\n    }\n    return this.encrypt(JSON.stringify(data), secretKey);\n  }\n\n  /**\n   * 解密数据\n   * @param data 已加密的数据\n   * @param secretKey 密钥，如果不指定，则直接使用 JSON.parse 返回\n   */\n  public decryptData(data: string, secretKey: string = \"\") {\n    if (!secretKey) {\n      return JSON.parse(data);\n    }\n    try {\n      return JSON.parse(this.decrypt(data, secretKey));\n    } catch (error) {\n      return null;\n    }\n  }\n\n  /**\n   * 以 AES 方式加密数据\n   * @param data 原数据\n   * @param secretKey 密钥\n   */\n  public encrypt(data: string, secretKey: string = \"\") {\n    return CryptoJS.AES.encrypt(data, secretKey).toString();\n  }\n\n  /**\n   * 以 AES 方式解密数据\n   * @param data 已加密的数据\n   * @param secretKey 密钥\n   */\n  public decrypt(data: string, secretKey: string = \"\") {\n    return CryptoJS.AES.decrypt(data, secretKey).toString(CryptoJS.enc.Utf8);\n  }\n}\n"
  },
  {
    "path": "src/service/clientController.ts",
    "content": "import { APP } from \"./api\";\nimport {\n  Options,\n  DownloadClient,\n  EAction,\n  DataResult,\n  EDataResultType\n} from \"@/interface/common\";\n\nexport class ClientController {\n  public options: Options = {\n    sites: [],\n    clients: []\n  };\n\n  public clients: any = {};\n\n  /**\n   * 类初始化\n   */\n  constructor() {}\n\n  public init(options: Options) {\n    this.options = options;\n    this.cleanUpClients();\n  }\n\n  /**\n   * 清理已缓存的客户端\n   */\n  public cleanUpClients() {\n    this.clients = {};\n  }\n\n  /**\n   * 根据指定客户端配置初始化客户端\n   * @param clientOptions 客户端配置\n   */\n  public getClient(clientOptions: any): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (typeof clientOptions === \"string\") {\n        let clientId = clientOptions;\n        clientOptions = this.options.clients.find((item: DownloadClient) => {\n          return item.id === clientId;\n        });\n        let client = this.clients[clientId];\n        if (client) {\n          resolve({ client, options: clientOptions });\n          return;\n        }\n      }\n      if ((<any>window)[clientOptions.type] === undefined) {\n        // 加载初始化脚本\n        APP.execScript({\n          type: \"file\",\n          content: `clients/${clientOptions.type}/init.js`\n        })\n          .then(() => {\n            let client: any;\n            eval(`client = new ${clientOptions.type}()`);\n            client.init({\n              loginName: clientOptions.loginName,\n              loginPwd: clientOptions.loginPwd,\n              address: clientOptions.address,\n              name: clientOptions.name\n            });\n            this.clients[clientOptions.id] = client;\n            resolve({ client, options: clientOptions });\n          })\n          .catch((e: any) => {\n            console.log(e);\n            reject({\n              initFailed: true,\n              msg: e\n            });\n          });\n      } else {\n        let client: any;\n        eval(`client = new ${clientOptions.type}()`);\n        client.init({\n          loginName: clientOptions.loginName,\n          loginPwd: clientOptions.loginPwd,\n          address: clientOptions.address,\n          name: clientOptions.name\n        });\n        this.clients[clientOptions.id] = client;\n        resolve({ client, options: clientOptions });\n      }\n    });\n  }\n\n  /**\n   * 测试客户端是否可连接\n   * @param options 参数\n   */\n  public testClientConnectivity(options: DownloadClient): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let dataResult: DataResult = {\n        type: EDataResultType.unknown,\n        success: false\n      };\n      this.getClient(options)\n        .then((clientOptions: any) => {\n          clientOptions.client\n            .call(EAction.testClientConnectivity, options)\n            .then((result: boolean) => {\n              dataResult.success = result;\n              if (result) {\n                dataResult.type = EDataResultType.success;\n              }\n              resolve(dataResult);\n            })\n            .catch((result: any) => {\n              dataResult.data = result;\n              dataResult.type = EDataResultType.error;\n              reject(dataResult);\n            });\n        })\n        .catch((e: any) => {\n          dataResult.data = e;\n          dataResult.type = EDataResultType.error;\n          reject(dataResult);\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/downloader.ts",
    "content": "import { Dictionary, ERequestMethod } from \"@/interface/common\";\nimport FileSaver from \"file-saver\";\n\nexport type downloadFile = {\n  url: string;\n  fileName?: string;\n  getDataOnly?: boolean;\n  timeout?: number;\n  method?: ERequestMethod;\n};\n\nexport type downloadOptions = {\n  files?: downloadFile[];\n  autoStart?: boolean;\n  tagIMDb?: boolean;\n  onCompleted?: Function;\n  onError?: Function;\n  onProgress?: Function;\n};\n\nexport class Downloader {\n  public count: number = 0;\n  public completedCount: number = 0;\n  public downloadingCount: number = 0;\n\n  private files: Dictionary<any> = {};\n  private queues: any[] = [];\n\n  constructor(public options: downloadOptions) {\n    options.files &&\n      options.files.forEach((item: downloadFile) => {\n        this.push(item);\n      });\n  }\n\n  // 添加下载\n  public push(options: downloadFile) {\n    if (!options.url || (options.url && this.files[options.url])) {\n      return;\n    }\n    var file = new FileDownloader(options);\n\n    file.onCompleted = () => {\n      this.downloadingCount--;\n      this.completedCount++;\n      this.onCompleted(file);\n      delete this.files[file.url];\n    };\n    file.onStart = () => {\n      this.downloadingCount++;\n    };\n    file.onError = (e: any) => {\n      this.downloadingCount--;\n      this.completedCount++;\n      if (this.options.onError) {\n        this.options.onError.call(this, file, e);\n      }\n      delete this.files[file.url];\n    };\n    file.onProgress = (loaded: number, total: number, speed: number) => {\n      if (this.options.onProgress) {\n        this.options.onProgress.call(this, file, loaded, total, speed);\n      }\n    };\n    // file.id = String.getRandomString(16);\n    this.files[file.url] = file;\n    this.queues.push(file);\n    this.count++;\n    if (this.options.autoStart) {\n      file.start();\n    }\n  }\n\n  public start() {}\n\n  public onCompleted(file: FileDownloader) {\n    if (this.options.onCompleted) {\n      this.options.onCompleted.call(this, file);\n    }\n  }\n}\n\nexport class FileDownloader {\n  public lastTime: number = 0;\n  public startTime: number = 0;\n  public status: number = 0;\n  public statusText: string = \"\";\n  public url: string = \"\";\n  public requestMethod: ERequestMethod = ERequestMethod.GET;\n  public postData: any = null;\n  public content: any;\n  public fileName: string = \"\";\n  public loaded: number = 0;\n  public total: number = 0;\n  public percent: number | string = 0;\n  public speed: number = 0;\n  public showSpeed: string = \"\";\n  public onProgress: Function = function() {};\n  public onCompleted: Function = function() {};\n  public onError: Function = function() {};\n  public onStart: Function = function() {};\n  public getDataOnly: boolean = false;\n  public timeout: number = 0;\n\n  private xhr: XMLHttpRequest = new XMLHttpRequest();\n\n  constructor(options: downloadFile) {\n    this.fileName = options.fileName || \"\";\n    this.url = options.url;\n    this.getDataOnly = options.getDataOnly || false;\n    this.timeout = options.timeout || 0;\n    this.requestMethod = options.method || ERequestMethod.GET;\n  }\n\n  public start() {\n    this.lastTime = +new Date();\n    this.startTime = this.lastTime;\n    this.statusText = \"数据准备中……\";\n\n    if (this.timeout > 0) {\n      this.xhr.timeout = this.timeout;\n    }\n    this.xhr.open(this.requestMethod, this.url, true);\n    // 指定返回的实体类型\"blob\"，该类型表示可以为任意文件\n    // https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest\n    this.xhr.responseType = \"blob\";\n    this.xhr.onreadystatechange = () => {\n      switch (this.xhr.readyState) {\n        // 下载完成 DONE\n        case 4:\n          switch (this.xhr.status) {\n            case 200:\n            case 302:\n              this.content = this.xhr.response;\n              this.downloadCompleted();\n              break;\n\n            default:\n              if (this.xhr.status != 0) {\n                this.downloadError(\n                  `[${this.url}] 下载失败，返回的状态码为：${this.xhr.status}`\n                );\n              }\n\n              break;\n          }\n\n          break;\n\n        // 已获取响应头 HEADERS_RECEIVED\n        case 2:\n          var contentDisposition = this.xhr.getResponseHeader(\n            \"Content-Disposition\"\n          );\n          // 从服务端获取文件名\n          if (contentDisposition && !this.fileName && !this.getDataOnly) {\n            this.fileName = this.getFileName(contentDisposition);\n          }\n          break;\n      }\n    };\n\n    // 下载进度事件\n    this.xhr.onprogress = (e: ProgressEvent) => {\n      // 当前传输字节\n      this.loaded = e.loaded;\n      // 总字节\n      this.total = e.total;\n      // 当前进度（百分比）\n      this.percent = (100 * (e.loaded / e.total)).toFixed(2);\n      // 最后读取时间\n      this.lastTime = +new Date();\n      // 当前速度\n      this.speed = this.loaded / (this.startTime - this.lastTime);\n      this.updateProgress();\n    };\n\n    // 错误事件\n    this.xhr.onerror = e => {\n      this.downloadError(e);\n    };\n\n    // 超时\n    this.xhr.ontimeout = () => {\n      this.downloadError(`[${this.url}] 下载超时`);\n    };\n\n    var data = null;\n    if (this.postData) {\n      data = $.param(this.postData);\n    }\n    if (this.requestMethod == ERequestMethod.POST) {\n      this.xhr.setRequestHeader(\n        \"Content-Type\",\n        \"application/x-www-form-urlencoded\"\n      );\n    }\n    // 开始下载\n    this.xhr.send(data);\n    this.onStart && this.onStart.call(this);\n  }\n\n  public getFileName(contentDisposition: string = \"\") {\n    let items = contentDisposition.split(\";\");\n    let fields: Dictionary<any> = {};\n    let result = \"\";\n\n    for (let index = 0; index < items.length; index++) {\n      let item = items[index];\n      let tmp = item.replace(\" \", \"\").split(\"=\");\n      if (tmp.length == 2) {\n        fields[tmp[0]] = tmp[1].trim();\n      }\n    }\n    let fileName;\n    if ((fileName = fields[\"filename*\"])) {\n      let index = fileName.lastIndexOf(\"'\");\n      result = fileName.substr(index + 1);\n    } else {\n      result = fields[\"filename\"];\n    }\n\n    // 替换双引号\n    result = result.replace(/\"/g, \"\");\n    return decodeURI(result);\n  }\n\n  public downloadCompleted() {\n    if (this.loaded > 0 && !this.getDataOnly) {\n      // 保存文件\n      FileSaver.saveAs(this.content, this.fileName);\n    }\n\n    if (this.onCompleted) {\n      this.onCompleted.call(this);\n    }\n  }\n\n  public downloadError(error: any) {\n    if (this.onError) {\n      this.onError.call(this, error);\n    }\n  }\n\n  public updateProgress() {\n    if (this.onProgress) {\n      this.onProgress.call(this, this.loaded, this.total, this.speed);\n    }\n  }\n}\n"
  },
  {
    "path": "src/service/extension.ts",
    "content": "import { EAction, EDataResultType } from \"@/interface/common\";\nimport { APP } from \"./api\";\n\nexport default class Extension {\n  public isExtensionMode: boolean = APP.isExtensionMode;\n\n  /**\n   * 向背景页发送指令\n   * @param action 需要执行的命令\n   * @param callback 回调函数\n   * @param data 附加数据\n   */\n  public sendRequest(\n    action: EAction,\n    callback?: any,\n    data?: any\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.isExtensionMode) {\n        try {\n          chrome.runtime.sendMessage(\n            {\n              action,\n              data\n            },\n            (result: any) => {\n              if (chrome.runtime.lastError) {\n                let message = chrome.runtime.lastError.message || \"\";\n                console.log(\n                  \"Extension.sendRequest.runtime\",\n                  action,\n                  data,\n                  chrome.runtime.lastError.message\n                );\n                if (/Could not establish connection/.test(message)) {\n                  APP.showNotifications({\n                    message: \"插件状态未知，当前操作可能失败，请刷新页面后再试\"\n                  });\n                  reject(chrome.runtime.lastError);\n                  return;\n                }\n\n                if (\n                  !/The message port closed before a response was received/.test(\n                    message\n                  )\n                ) {\n                  reject(chrome.runtime.lastError);\n                  return;\n                }\n              }\n\n              callback && callback(result.resolve || result.reject);\n\n              if (result.reject) {\n                reject(result.reject);\n              } else if (\n                result.resolve &&\n                (result.resolve.status === \"error\" ||\n                  result.resolve.success === false)\n              ) {\n                reject(result.resolve);\n              } else {\n                resolve(result.resolve);\n              }\n            }\n          );\n        } catch (error) {\n          // @see https://groups.google.com/a/chromium.org/forum/#!topic/chromium-extensions/QLC4gNlYjbA\n          if (\n            /Invocation of form runtime\\.connect|doesn't match definition runtime\\.connect|Extension context invalidated/.test(\n              error.message\n            )\n          ) {\n            // console.error(\n            //   \"Chrome extension, Actson has been reloaded. Please refresh the page\"\n            // );\n            reject({\n              type: EDataResultType.error,\n              msg: \"插件状态未知，当前操作可能失败，请刷新页面后再试\",\n              success: false\n            });\n          } else {\n            reject(error);\n          }\n        }\n\n        return;\n      }\n\n      /**\n       * 仅对调试界面时使用\n       */\n      if (process.env.NODE_ENV === \"test\") {\n        // 使用 import() 方法是为了在打包时减少不必要的代码依赖\n        import(\"@/background/service\")\n          .then((result: any) => {\n            console.log(result);\n            const PTService = new result.default(true);\n            PTService.requestMessage({\n              action,\n              data\n            })\n              .then((result: any) => {\n                callback && callback.call(this, result);\n                resolve(result);\n              })\n              .catch((error: any) => {\n                reject(error);\n              });\n          })\n          .catch(error => {\n            console.log(\"sendRequest.error\", error);\n          });\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/favicon.ts",
    "content": "import { Dictionary } from \"@/interface/common\";\nimport { filters } from \"./filters\";\nimport { FileDownloader } from \"./downloader\";\nconst StorageKey = \"Favicon-Cache\";\n// 1 像素透明\nconst NOIMAGE =\n  \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\";\n\nexport class Favicon {\n  private cache: Dictionary<any> = {};\n\n  constructor(public service?: any) {\n    this.loadCache();\n  }\n\n  private loadCache() {\n    let result = window.localStorage.getItem(StorageKey);\n    if (result) {\n      this.cache = JSON.parse(result) || {};\n    }\n  }\n\n  private saveCache() {\n    window.localStorage.setItem(StorageKey, JSON.stringify(this.cache));\n  }\n\n  public clear() {\n    this.cache = {};\n    this.saveCache();\n  }\n\n  public reset(): Promise<any> {\n    let _cache = JSON.parse(JSON.stringify(this.cache));\n    this.cache = {};\n    let urls: string[] = [];\n    for (const host in _cache) {\n      if (_cache.hasOwnProperty(host)) {\n        let item = _cache[host];\n        urls.push(item.origin);\n      }\n    }\n\n    return this.gets(urls);\n  }\n\n  public gets(urls: string[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let requests: any[] = [];\n      urls.forEach((url: string) => {\n        requests.push(this.get(url));\n      });\n      Promise.all(requests)\n        .then((results: any[]) => {\n          resolve(results);\n        })\n        .catch(e => {\n          reject(e);\n        });\n    });\n  }\n\n  /**\n   * 获取指定站点的图标\n   * @param url 站点地址\n   * @param reset 是否重置\n   */\n  public get(url: string, reset: boolean = false): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let URL = filters.parseURL(url);\n      let cache = this.cache[URL.host];\n      if (!cache || reset) {\n        this.cacheFavicon(URL.origin, URL.host)\n          .then((result: any) => {\n            resolve(result);\n          })\n          .catch(e => {\n            reject(e);\n          });\n        return;\n      }\n      return resolve(cache);\n    });\n  }\n\n  /**\n   * 缓存图标\n   * @param url 站点地址\n   * @param host 域名\n   */\n  private cacheFavicon(url: string, host: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.download(`${url}/favicon.ico`)\n        .then(result => {\n          if (result && result.size > 0) {\n            this.transformBlob(result, \"base64\")\n              .then(base64 => {\n                resolve(this.set(url, base64));\n              })\n              .catch(e => {\n                reject(e);\n              });\n          } else {\n            this.cacheFromIndex(url, host)\n              .then((result: any) => {\n                resolve(result);\n              })\n              .catch(e => {\n                reject(e);\n              });\n          }\n        })\n        .catch(() => {\n          this.cacheFromIndex(url, host)\n            .then((result: any) => {\n              resolve(result);\n            })\n            .catch(e => {\n              reject(e);\n            });\n        });\n    });\n  }\n\n  public set(url: string, data: string) {\n    let URL = filters.parseURL(url);\n    let item = {\n      origin: URL.origin,\n      host: URL.host,\n      data\n    };\n    this.cache[URL.host] = item;\n    this.saveCache();\n    return item;\n  }\n\n  /**\n   * 从站点首页内容中获取图标地址并缓存\n   * @param url\n   * @param host\n   */\n  private cacheFromIndex(url: string, host: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.download(url)\n        .then(result => {\n          if (result && /text/gi.test(result.type)) {\n            this.transformBlob(result, \"text\")\n              .then(text => {\n                try {\n                  const doc = new DOMParser().parseFromString(\n                    text,\n                    \"text/html\"\n                  );\n                  // 构造 jQuery 对象\n                  const head = $(doc).find(\"head\");\n                  let query = head.find(\"link[rel*=icon]:first\");\n                  if (query && query.length > 0) {\n                    let URL = filters.parseURL(url);\n                    let link = query.attr(\"href\") + \"\";\n\n                    if (link.substr(0, 2) === \"//\") {\n                      link = `${URL.protocol}:${link}`;\n                    } else if (link.substr(0, 4) !== \"http\") {\n                      link = link.startsWith('/') ? `${URL.origin}${link}` : `${URL.origin}/${link}`;\n                    }\n\n                    this.download(link)\n                      .then(result => {\n                        if (result && /image/gi.test(result.type)) {\n                          this.transformBlob(result, \"base64\")\n                            .then(base64 => {\n                              resolve(this.set(url, base64));\n                            })\n                            .catch(e => {\n                              this.debug(e);\n                              reject(e);\n                            });\n                        } else {\n                          resolve(this.set(url, NOIMAGE));\n                        }\n                      })\n                      .catch(() => {\n                        resolve(this.set(url, NOIMAGE));\n                      });\n                  } else {\n                    resolve(this.set(url, NOIMAGE));\n                  }\n                } catch (error) {\n                  console.log(error);\n                  this.debug(error);\n                  resolve(this.set(url, NOIMAGE));\n                }\n              })\n              .catch(e => {\n                this.debug(e);\n                reject(e);\n              });\n          } else {\n            reject();\n          }\n        })\n        .catch(() => {\n          resolve(this.set(url, NOIMAGE));\n        });\n    });\n  }\n\n  private transformBlob(blob: Blob, to: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      const reader = new FileReader();\n      reader.addEventListener(\"loadend\", () => {\n        if (reader.result) {\n          resolve(reader.result);\n        } else {\n          reject();\n        }\n      });\n\n      switch (to) {\n        case \"text\":\n          reader.readAsText(blob);\n          break;\n\n        case \"base64\":\n          reader.readAsDataURL(blob);\n          break;\n      }\n    });\n  }\n\n  private download(url: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let file = new FileDownloader({\n        url,\n        getDataOnly: true,\n        timeout: 5000\n      });\n\n      file.onCompleted = () => {\n        if (file.content) {\n          resolve(file.content);\n        } else {\n          reject();\n        }\n      };\n\n      file.onError = (e: any) => {\n        console.log(\"Favicon.download.error\", e);\n        this.debug(e);\n        reject(e);\n      };\n\n      file.start();\n    });\n  }\n\n  private debug(...msg: any[]) {\n    if (this.service) {\n      this.service.debug(...msg);\n    }\n  }\n}\n"
  },
  {
    "path": "src/service/filters.ts",
    "content": "interface IFilter {\n  formatNumber: (source: number, format?: string) => string;\n  formatSize: (bytes: any, zeroToEmpty?: boolean, type?: string) => string;\n  formatSizeWithNegative: (bytes: any, zeroToEmpty?: boolean, type?: string) => string;\n  formatSpeed: (bytes: any, zeroToEmpty: boolean) => string;\n  parseURL: (\n    url: string\n  ) => {\n    source: string;\n    protocol: string;\n    host: string;\n    port?: number;\n    query?: string;\n    params?: string[];\n    hash?: string;\n    path: string;\n    segments: string;\n    origin: string;\n  };\n  timeAgoToNumber: (source: string) => number;\n  [key: string]: any;\n  formatInteger:(source: number) => string;\n  formatIMDbId:(source: string) => string;\n}\n\n/**\n * 过滤器定义\n */\nexport const filters: IFilter = {\n  /**\n   * 格式化数字\n   * @param source 数字来源\n   * @param format 格式化格式\n   */\n  formatNumber(source: number, format: string = \"###,###,###,###.00\"): string {\n    if (source === undefined) {\n      return \"\";\n    }\n\n    const fStr = (sNumber: string, fmt?: any, p?: any) => {\n      if (sNumber === \"\" || sNumber === undefined) {\n        if (fmt === \"\" || fmt === undefined) {\n          return \"\";\n        } else {\n          return fmt;\n        }\n      }\n      let fc = \"\";\n      let s = \"\";\n      let r = \"\";\n      let pos = 0;\n\n      if (!p) {\n        sNumber = sNumber\n          .split(\"\")\n          .reverse()\n          .join(\"\");\n        fmt = fmt\n          .split(\"\")\n          .reverse()\n          .join(\"\");\n      }\n\n      let j = 0;\n      for (let i = 0; i < fmt.length; i++, j++) {\n        s = sNumber.charAt(j);\n        if (s === undefined) {\n          continue;\n        }\n        fc = fmt.charAt(i);\n        switch (fc) {\n          case \"#\":\n            r += s;\n            pos = i;\n            break;\n          case \"0\":\n            r = s || s === fc ? r + s : r + 0;\n            pos = i;\n            // 原方法,这里对小数点后的处理有点问题.\n            break;\n          case \".\":\n            r += s === fc ? s : (j--, fc);\n            break;\n          case \",\":\n            r += s === fc ? s : (j--, fc);\n            break;\n          default:\n            r += fc;\n            j--;\n        }\n      }\n      if (\n        j !== sNumber.length &&\n        fmt.charAt(fmt.length - 1) !== \"0\" &&\n        pos !== fmt.length &&\n        fmt.charAt(pos) !== \"0\"\n      ) {\n        r = r.substr(0, pos + 1) + sNumber.substr(j) + r.substr(pos + 1);\n      }\n\n      r = (p\n        ? r\n        : r\n            .split(\"\")\n            .reverse()\n            .join(\"\")\n      ).replace(/(^,)|(,$)|(,,+)/g, \"\");\n      if (r.substr(0, 1) === \",\") {\n        r = r.substr(1);\n      }\n      if (r.substr(0, 2) === \"-,\") {\n        r = \"-\" + r.substr(2);\n      }\n      return r;\n    };\n    const sourceString: string = source.toString();\n    if (sourceString.length === 0) {\n      return \"\";\n    }\n\n    let bytes = parseFloat(sourceString);\n    if (isNaN(bytes)) {\n      return sourceString;\n    }\n\n    if (!format) {\n      return sourceString;\n    }\n\n    const arrFormat = format.split(\".\");\n    const arrSource = sourceString.split(\".\");\n    return arrFormat.length > 1\n      ? fStr(arrSource[0], arrFormat[0]) +\n          \".\" +\n          fStr(arrSource[1], arrFormat[1], 1)\n      : fStr(arrSource[0], arrFormat[0]);\n  },\n\n  /**\n   *\n   * @param sourceBytes 需要格式的字节\n   * @param zeroToEmpty 是否需要将0转为空输出，默认为 false\n   * @param type 类型，可指定为 `speed` 为速度，会在后面加上 /s\n   */\n  formatSize(\n    sourceBytes: any,\n    zeroToEmpty: boolean = false,\n    type: string = \"\"\n  ): string {\n    let bytes = parseFloat(sourceBytes);\n    if (isNaN(bytes)) {\n      return \"\";\n    }\n\n    if (bytes < 0) {\n      return \"N/A\";\n    }\n\n    if (bytes === 0) {\n      if (zeroToEmpty) {\n        return \"\";\n      } else {\n        if (type === \"speed\") {\n          return \"0.00 KiB/s\";\n        } else {\n          return \"0.00\";\n        }\n      }\n    }\n    let r: number;\n    let u = \"KiB\";\n    let format = '###,###,###,###.00 ';\n    let format2 = '###,###,###,###.000 ';\n    if (bytes < 1000 * Math.pow(2, 10)) {\n      r = bytes / Math.pow(2, 10);\n      u = \"KiB\";\n    } else if (bytes < 1000 * Math.pow(2, 20)) {\n      r = bytes / Math.pow(2, 20);\n      u = \"MiB\";\n    } else if (bytes < 1000 * Math.pow(2, 30)) {\n      r = bytes / Math.pow(2, 30);\n      u = \"GiB\";\n    } else if (bytes < 1000 * Math.pow(2, 40)) {\n      r = bytes / Math.pow(2, 40);\n      u = \"TiB\";\n      format = format2;\n    } else if (bytes < 1000 * Math.pow(2, 50)) {\n      r = bytes / Math.pow(2, 50);\n      u = \"PiB\";\n      format = format2;\n    } else if (bytes < 1000 * Math.pow(2, 60)) {\n      r = bytes / Math.pow(2, 60);\n      u = \"EiB\";\n      format = format2;\n    } else {\n      r = bytes / Math.pow(2, 70);\n      u = \"ZiB\";\n      format = format2;\n    }\n\n    if (type === \"speed\") {\n      u += \"/s\";\n    }\n\n    return this.formatNumber(r, format) + u;\n  },\n\n  /**\n   * 支持负值\n   */\n  formatSizeWithNegative(\n    sourceBytes: any,\n    zeroToEmpty: boolean = false,\n    type: string = \"\"\n  ): string {\n    sourceBytes = parseFloat(sourceBytes)\n    let bytes = sourceBytes\n    if (sourceBytes < 0) {\n      bytes = - bytes\n    }\n    let result = this.formatSize(bytes, zeroToEmpty, type)\n    if (sourceBytes < 0) {\n      result = `- ${result}`\n    }\n    return result\n  },\n\n  /**\n   * 格式化速度\n   * @param bytes 需要格式化的字节数\n   * @param zeroToEmpty 是否需要将0转为空输出，默认为 false\n   */\n  formatSpeed(bytes: any, zeroToEmpty: boolean = false) {\n    return this.formatSize(bytes, zeroToEmpty, \"speed\");\n  },\n\n  /** \n  * @param {string} url 完整的URL地址 \n  * @returns {object} 自定义的对象 \n  * @description 用法示例：var myURL = parseURL('http://abc.com:8080/dir/index.html?id=255&m=hello#top');\n    myURL.file='index.html' \n\n    myURL.hash= 'top' \n\n    myURL.host= 'abc.com' \n\n    myURL.query= '?id=255&m=hello' \n\n    myURL.params= Object = { id: 255, m: hello } \n\n    myURL.path= '/dir/index.html' \n\n    myURL.segments= Array = ['dir', 'index.html'] \n\n    myURL.port= '8080' \n\n    myURL.protocol= 'http' \n\n    myURL.source= 'http://abc.com:8080/dir/index.html?id=255&m=hello#top' \n  */\n  parseURL(url: string): any {\n    var a = document.createElement(\"a\");\n    a.href = url;\n    return {\n      source: url,\n      protocol: a.protocol.replace(\":\", \"\"),\n      host: a.hostname,\n      port: a.port,\n      query: a.search,\n      params: (function() {\n        var ret: any = {},\n          seg = a.search.replace(/^\\?/, \"\").split(\"&\"),\n          len = seg.length,\n          i = 0,\n          s;\n        for (; i < len; i++) {\n          if (!seg[i]) {\n            continue;\n          }\n          s = seg[i].split(\"=\");\n          ret[s[0]] = s[1];\n        }\n        return ret;\n      })(),\n      hash: a.hash.replace(\"#\", \"\"),\n      path: a.pathname.replace(/^([^/])/, \"/$1\"),\n      segments: a.pathname.replace(/^\\//, \"\").split(\"/\"),\n      origin: `${a.protocol}//${a.hostname}` + (a.port ? `:${a.port}` : \"\")\n    };\n  },\n  /**\n   * 将数字转为正确的IMDbId\n   * @param source\n   */\n  formatIMDbId(imdbId: string): string {\n    if (Number(imdbId))\n    {\n      if (imdbId.length < 7)\n        imdbId = imdbId.padStart(7, '0');\n      \n      imdbId = \"tt\" + imdbId;\n    }\n    return imdbId;\n  },\n\n  /**\n   * 将多少时间之前的格式转为时间数字（大概时间）\n   * @param source\n   */\n  timeAgoToNumber(source: string): number {\n    /**\n     * 可以匹配以下情况：\n     * 1 year\n     * 1 year ago\n     * 2 yesrs ago\n     * 2.1 months ago\n     * 2.1 months ago by xxx\n     */\n    let rule = /^([\\d.]+).+?((year|month|week|day|hour|min|minute)s?)( +ago)?(.+)?$/i;\n\n    let matchs = source.trim().match(rule);\n    if (!matchs) {\n      return 0;\n    }\n\n    let now = new Date();\n    let interval = matchs[3];\n    let number = Math.round(parseFloat(matchs[1]));\n    let result: Date = new Date();\n\n    switch (interval.toLowerCase()) {\n      // 年份\n      case \"year\":\n        result = new Date(now.setFullYear(now.getFullYear() - number));\n        break;\n\n      // 月\n      case \"month\":\n        result = new Date(now.setMonth(now.getMonth() - number));\n        break;\n\n      // 日期\n      case \"day\":\n        result = new Date(now.setDate(now.getDate() - number));\n        break;\n\n      case \"week\":\n        result = new Date(now.setDate(now.getDate() - 7 * number));\n        break;\n\n      case \"hour\":\n        result = new Date(now.setHours(now.getHours() - number));\n        break;\n\n      case \"min\":\n      case \"minute\":\n        result = new Date(now.setMinutes(now.getMinutes() - number));\n        break;\n    }\n\n    return result.getTime();\n  },\n  formatInteger(source: number) : string {\n    return this.formatNumber(source, \"###,###,###,###\")\n  },\n};\n"
  },
  {
    "path": "src/service/localStorage.ts",
    "content": "import { EStorageType } from \"@/interface/common\";\nexport default class localStorage {\n  private isExtensionMode: boolean = false;\n  constructor() {\n    if (window.chrome && chrome.extension) {\n      this.isExtensionMode = true;\n    }\n  }\n\n  /**\n   * 设置指定的值\n   * @param key\n   * @param value\n   * @param type\n   */\n  public set(\n    key: any,\n    value?: any,\n    type: EStorageType = EStorageType.json\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.isExtensionMode) {\n        let data: any = {};\n        data[key] = value;\n        // console.log(\"save\", data);\n        chrome.storage.local.set(data, () => {\n          resolve();\n        });\n      } else {\n        // console.log(key);\n        if (typeof value !== \"string\" && type == EStorageType.json) {\n          value = JSON.stringify(value);\n        }\n        window.localStorage.setItem(key, value);\n        resolve();\n      }\n    });\n  }\n\n  public get(\n    key: string,\n    callback?: any,\n    type: EStorageType = EStorageType.json\n  ) {\n    if (this.isExtensionMode) {\n      chrome.storage.local.get(key, (result: any) => {\n        if (result && result[key]) {\n          callback(result[key]);\n        } else {\n          callback(null);\n        }\n      });\n    } else {\n      let result = window.localStorage.getItem(key);\n      if (result && type == EStorageType.json) {\n        result = JSON.parse(result);\n      }\n      callback && callback(result);\n    }\n  }\n}\n"
  },
  {
    "path": "src/service/logger.ts",
    "content": "import { EConfigKey, LogItem } from \"@/interface/common\";\nimport localStorage from \"@/service/localStorage\";\nimport { PPF } from \"./public\";\n\nexport class Logger {\n  public maxLength: number = 1000;\n  public items: any[] = [];\n  public storage: localStorage = new localStorage();\n  public configKey: string = EConfigKey.systemLogs;\n\n  constructor() {\n    this.load();\n  }\n\n  /**\n   * 获取记录\n   */\n  public load(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.storage.get(this.configKey, (result: any) => {\n        this.items = result || [];\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 添加日志\n   * @param data 日志信息\n   */\n  public add(data: LogItem): string {\n    let time: number = new Date().getTime();\n    let saveData: LogItem = Object.assign(\n      {\n        module: \"\",\n        time,\n        id: PPF.getNewId()\n      },\n      data\n    );\n    if (!this.items) {\n      this.load().then(() => {\n        this.items.push(saveData);\n        this.storage.set(this.configKey, this.items);\n      });\n    } else {\n      // 如果超出了最大值，则删除最早的记录\n      if (this.items.length >= this.maxLength) {\n        this.items.splice(0, 1);\n      }\n      this.items.push(saveData);\n      this.storage.set(this.configKey, this.items);\n    }\n    return saveData.id as string;\n  }\n\n  /**\n   * 删除历史记录\n   * @param items 需要删除的列表\n   */\n  public remove(items: any[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.load().then(() => {\n        for (let index = this.items.length - 1; index >= 0; index--) {\n          let item = this.items[index];\n          let findIndex = items.findIndex((data: any) => {\n            return data.id === item.id;\n          });\n          if (findIndex >= 0) {\n            this.items.splice(index, 1);\n          }\n        }\n        this.storage.set(this.configKey, this.items);\n        resolve(this.items);\n      });\n    });\n  }\n\n  /**\n   * 清除记录\n   */\n  public clear(): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.items = [];\n      this.storage.set(this.configKey, this.items);\n      resolve(this.items);\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/movieInfoService.ts",
    "content": "import { Dictionary } from \"@/interface/common\";\nimport { PPF } from './public';\n\nexport type MovieInfoCache = {\n  base: Dictionary<any>;\n  ratings: Dictionary<any>;\n  doubanToIMDb: Dictionary<any>;\n  search: Dictionary<any>;\n};\n\n/**\n * 电影信息\n */\nexport class MovieInfoService {\n  // 豆瓣标准接口\n  public doubanApiURL = \"https://api.douban.com/v2\";\n  // 豆瓣 frodo 接口\n  public doubanFrodoApi = \"https://frodo.douban.com/api/v2\";\n\n  public douban = {\n    frodo: {\n      apiKeys: [\n        \"054022eaeae0b00e0fc068c0c0a2102a\"\n      ],\n      entApiKeys: [\n        \"054022eaeae0b00e0fc068c0c0a2102a\"\n      ],\n      // 豆瓣 frodo 接口相关方法\n      methods: {\n        movie: {\n          search: `${this.doubanFrodoApi}/search?q=$key$&count=$count$&apiKey=$apikey$`,\n          /* \n            数据示例\n            request: https://movie.douban.com/j/subject_suggest?q=tt0120762\n            response:\n            [{\n              \"episode\": \"\",\n              \"img\": \"https://img9.doubanio.com\\/view\\/photo\\/s_ratio_poster\\/public\\/p2443062814.jpg\",\n              \"title\": \"花木兰\",\n              \"url\": \"https:\\/\\/movie.douban.com\\/subject\\/1294833\\/?suggest=tt0120762\",\n              \"type\": \"movie\",\n              \"year\": \"1998\",\n              \"sub_title\": \"Mulan\",\n              \"id\": \"1294833\"\n            }]\n          */\n          imdb: `https://omit.mkrobot.org/movie/infos/$imdbid$`,\n          subject: `https://omit.mkrobot.org/movie/infos/douban$id$`\n          // imdb: `https://movie.douban.com/j/subject_suggest?q=$imdbid$`,\n          // subject: `${this.doubanFrodoApi}/movie/$id$?apiKey=$apikey$`\n        },\n      }\n    },\n    common: {\n      apiKeys: [\n        \"02646d3fb69a52ff072d47bf23cef8fd\",\n        \"0b2bdeda43b5688921839c8ecb20399b\",\n        \"0dad551ec0f84ed02907ff5c42e8ec70\",\n        \"0df993c66c0c636e29ecbb5344252a4a\"\n      ],\n      entApiKeys: [\n        \"0dad551ec0f84ed02907ff5c42e8ec70\",\n        \"02646d3fb69a52ff072d47bf23cef8fd\"\n      ],\n      methods: {\n        movie: {\n          search: `${this.doubanApiURL}/movie/search?q=$key$&count=$count$&apikey=$apikey$`,\n          imdb: `${this.doubanApiURL}/movie/imdb/$imdbid$?apikey=$apikey$`,\n          subject: `${this.doubanApiURL}/movie/subject/$id$?apikey=$apikey$`\n        }\n      },\n    }\n  }\n\n  // 用于加载评分信息\n  public omdbApiURL = \"https://www.omdbapi.com\";\n  // 用于获取IMDbID\n  public omitApiURL = \"https://omit.mkrobot.org\";\n  // omdbapi 申请的Key列表\n  // 每个 key 一天有1000次请求限制\n  public omdbApiKeys = [\n    \"e0d3039d\",\n    \"a67d9bce\",\n    \"6be019fc\",\n    \"4ee790e0\",\n    \"d82cb888\",\n    \"d58193b6\",\n    \"15c0aa3f\",\n    \"53acf36d\",\n    \"3a864b75\",\n    \"2892ab46\",\n    \"b507af90\",\n    \"7cf67120\",\n    \"85b2a90c\",\n    \"2896ff0a\",\n    \"aa4b9983\",\n    \"c4e08870\",\n    \"5d5c0049\",\n    \"79a462f2\",\n    \"e4c3fce8\",\n    \"918d70df\",\n    \"e94cb667\",\n    \"eb84d6d7\",\n    \"e192b5a\",\n    \"d62b4cf5\",\n    \"5e6442a3\",\n    \"9b1468c6\"\n  ];\n  // 豆瓣 apikey\n  public doubanApiKeys = [\n    \"02646d3fb69a52ff072d47bf23cef8fd\",\n    \"0b2bdeda43b5688921839c8ecb20399b\",\n    \"0dad551ec0f84ed02907ff5c42e8ec70\",\n    \"0df993c66c0c636e29ecbb5344252a4a\"\n    // \"07c78782db00a121175696889101e363\"\n  ];\n\n  // 07c78782db00a121175696889101e363 已被禁用 2019.11.23\n\n  // 仅用于 search 接口\n  // 部分key无法用于 search 接口，故将key分开\n  public doubanEntApiKeys = [\n    \"0dad551ec0f84ed02907ff5c42e8ec70\",\n    \"02646d3fb69a52ff072d47bf23cef8fd\"\n    // \"07c78782db00a121175696889101e363\"\n  ];\n\n  // 用于获取特定接口数据\n  public omitApiKeys = [\"kiqMY6MC\"];\n\n  // 信息缓存\n  public cache: MovieInfoCache = {\n    base: {},\n    ratings: {},\n    doubanToIMDb: {},\n    search: {}\n  };\n\n  // 链接超时时间\n  public timeout = 3000;\n\n  // 豆瓣当前使用API\n  private doubanApi = this.douban.frodo;\n\n  private requsetQueue: Dictionary<any> = {};\n\n  public getInfos(key: string): Promise<any> {\n    if (/(douban\\d+)/.test(key)) {\n      return this.getInfoFromDoubanId(key.replace(\"douban\", \"\"));\n    }\n\n    if (/^(tt\\d+)$/.test(key)) {\n      return this.getInfoFromIMDb(key);\n    }\n\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      reject(\"暂未实现\");\n    });\n  }\n\n  /**\n   * 判断是否为 IMDbId\n   * @param IMDbId\n   */\n  public isIMDbId(IMDbId: string): boolean {\n    return /^(tt\\d+)$/.test(IMDbId);\n  }\n\n  /**\n   * 根据指定的 IMDbId 获取电影信息\n   * @param IMDbId\n   */\n  public getInfoFromIMDb(IMDbId: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.isIMDbId(IMDbId)) {\n        let cache = this.cache.base[IMDbId];\n        if (cache) {\n          resolve(cache);\n          return;\n        }\n        let url = PPF.replaceKeys(this.doubanApi.methods.movie.imdb, {\n          imdbid: IMDbId,\n          apikey: this.getDoubanApiKey()\n        });\n\n        $.ajax({\n          url: url,\n          timeout: this.timeout\n        })\n          .done(json => {\n            let result;\n            if (json) {\n              result = json.data || json;\n            }\n\n            this.cache.base[IMDbId] = result;\n            resolve(result);\n          })\n          .fail(error => {\n            reject(error);\n          });\n      } else {\n        reject(\"error IMDbId\");\n      }\n    });\n  }\n\n  /**\n   * 根据豆瓣ID获取影片信息\n   * @param id\n   */\n  public getInfoFromDoubanId(id: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (/^(\\d+)$/.test(id)) {\n        let cache = this.cache.base[id];\n        if (cache) {\n          resolve(cache);\n          return;\n        }\n        // let url = `${\n        //   this.doubanApiURL\n        // }/movie/subject/${id}?apikey=${this.getDoubanApiKey()}`;\n\n        let url = PPF.replaceKeys(this.doubanApi.methods.movie.subject, {\n          id,\n          apikey: this.getDoubanApiKey()\n        });\n\n        $.ajax({\n          url: url,\n          timeout: this.timeout\n        })\n          .done(json => {\n            let result;\n            if (json) {\n              result = json.data || json;\n            }\n\n            this.cache.base[id] = result;\n            resolve(result);\n          })\n          .fail(error => {\n            reject(error);\n          });\n      } else {\n        reject(\"error douban id\");\n      }\n    });\n  }\n\n  /**\n   * 获取评分信息\n   * @param IMDbId\n   */\n  public getRatings(IMDbId: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (this.isIMDbId(IMDbId)) {\n        let cache = this.cache.ratings[IMDbId];\n        if (cache) {\n          resolve(cache);\n          return;\n        }\n\n        let requestCount = 0;\n        const request = () => {\n          // 每个Key 1000 一天的限制\n          let apikey = this.getOmdbApiKey();\n          let url = `${this.omdbApiURL}/?i=${IMDbId}&apikey=${apikey}&tomatoes=true`;\n          $.ajax({\n            url: url,\n            timeout: this.timeout\n          })\n            .done(json => {\n              // 当发生错误时，更换Key进行重试\n              if (json && json.Error) {\n                requestCount++;\n                if (requestCount >= 5) {\n                  reject(json);\n                  return;\n                }\n                this.removeApiKey(\"omdb\", apikey);\n                request();\n                return;\n              }\n              this.cache.ratings[IMDbId] = json;\n              resolve(json);\n            })\n            .fail(error => {\n              reject(error);\n            });\n        };\n        request();\n      } else {\n        reject(\"error IMDbId\");\n      }\n    });\n  }\n\n  /**\n   * 从OMDb apikey列表中随机获取一个key\n   */\n  public getOmdbApiKey() {\n    // 随机获取一个key\n    return this.omdbApiKeys[\n      Math.floor(Math.random() * this.omdbApiKeys.length)\n    ];\n  }\n\n  /**\n   * 从豆瓣apikey列表中随机获取一个key\n   */\n  public getDoubanApiKey() {\n    // 随机获取一个key\n    return this.doubanApi.apiKeys[\n      Math.floor(Math.random() * this.doubanApi.apiKeys.length)\n    ];\n  }\n\n  /**\n   * 获取用于查询的apikey\n   */\n  public getDoubanEntApiKey() {\n    // 随机获取一个key\n    return this.doubanApi.entApiKeys[\n      Math.floor(Math.random() * this.doubanApi.entApiKeys.length)\n    ];\n  }\n\n  /**\n   * 根据指定的 doubanId 获取 IMDbId\n   * @param doubanId\n   */\n  public getIMDbIdFromDouban(doubanId: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let cache = this.cache.doubanToIMDb[doubanId];\n      if (cache) {\n        resolve(cache);\n        return;\n      }\n      let url = `${this.omitApiURL}/movie/${doubanId}/douban/imdb`;\n\n      if (this.requsetQueue[url]) {\n        reject();\n        return;\n      }\n\n      this.requsetQueue[url] = true;\n\n      $.ajax({\n        url: url,\n        timeout: this.timeout\n      })\n        .done(json => {\n          console.log(\"getIMDbIdFromDouban\", json);\n          if (json.data) {\n            this.cache.doubanToIMDb[doubanId] = json.data;\n            resolve(json.data);\n          } else {\n            reject(json);\n          }\n        })\n        .fail(error => {\n          reject(error);\n        })\n        .always(() => {\n          delete this.requsetQueue[url];\n        });\n    });\n  }\n\n  /**\n   * 查询指定关键的影片信息\n   * @param key\n   * @param count\n   */\n  public queryMovieInfoFromDouban(\n    key: string,\n    count: number = 5\n  ): Promise<any> {\n    if (this.isIMDbId(key)) {\n      return this.getInfoFromIMDb(key);\n    }\n    return new Promise<any>((resolve?: any, reject?: any) => {\n\n      let cache = this.cache.search[key];\n      if (cache) {\n        resolve(cache);\n        return;\n      }\n      let url = `${this.omitApiURL}/movie/search/${key}`;\n\n      if (this.requsetQueue[url]) {\n        reject();\n        return;\n      }\n\n      this.requsetQueue[url] = true;\n\n      $.ajax({\n        url: url,\n        timeout: this.timeout\n      })\n        .done(json => {\n          console.log(\"queryMovieInfoFromDouban\", json);\n          if (json.data) {\n            this.cache.search[key] = json.data;\n            resolve(json.data);\n          } else {\n            reject(json);\n          }\n        })\n        .fail(error => {\n          reject(error);\n        })\n        .always(() => {\n          delete this.requsetQueue[url];\n        });\n\n\n\n      // let url = `${this.doubanApiURL}/movie/search?q=${encodeURIComponent(\n      //   key\n      // )}&count=${count}&apikey=${this.getDoubanEntApiKey()}`;\n\n      // let url = PPF.replaceKeys(this.doubanApi.methods.movie.search, {\n      //   key: encodeURIComponent(key),\n      //   count,\n      //   apikey: this.getDoubanEntApiKey()\n      // });\n\n      // $.ajax({\n      //   url: url,\n      //   timeout: this.timeout\n      // })\n      //   .done((result: any) => {\n      //     console.log(\"query\", result);\n      //     if (result.subjects) {\n      //       resolve(result);\n      //     } else {\n      //       reject(result);\n      //     }\n      //   })\n      //   .fail(error => {\n      //     reject(error);\n      //   });\n    });\n  }\n\n  /**\n   * 追加API Key\n   * @param type\n   * @param keys\n   */\n  public appendApiKey(type: string = \"\", keys: string[]) {\n    let apiKeys: string[];\n    switch (type) {\n      case \"omdb\":\n        apiKeys = this.omdbApiKeys;\n        break;\n\n      case \"douban\":\n        apiKeys = this.doubanApiKeys;\n        break;\n    }\n\n    keys.forEach(key => {\n      if (key && !apiKeys.includes(key)) {\n        apiKeys.push(key);\n      }\n    });\n  }\n\n  /**\n   * 移除指定的Key\n   * @param type\n   * @param key\n   */\n  private removeApiKey(type: string = \"\", key: string) {\n    let apiKeys: string[] = [];\n    switch (type) {\n      case \"omdb\":\n        apiKeys = this.omdbApiKeys;\n        break;\n\n      case \"douban\":\n        apiKeys = this.doubanApiKeys;\n        break;\n    }\n\n    let index = apiKeys.findIndex(item => {\n      if (item === key) {\n        return true;\n      }\n    });\n\n    if (index !== -1) {\n      apiKeys.splice(index, 1);\n    }\n  }\n\n  /**\n   * 验证 OMDB API Key\n   * @param key\n   */\n  public verifyOmdbApiKey(key: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let url = `${this.omdbApiURL}/?i=tt0111161&apikey=${key}&tomatoes=true`;\n\n      $.ajax({\n        url: url,\n        timeout: this.timeout\n      })\n        .done(json => {\n          if (json && json.Error) {\n            reject(json.Error);\n            return;\n          }\n          resolve();\n        })\n        .fail(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 验证豆瓣Api Key\n   * @param key\n   */\n  public verifyDoubanApiKey(key: string): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      let url = `${this.doubanApiURL}/movie/imdb/tt0111161?apikey=${key}`;\n\n      $.ajax({\n        url: url,\n        timeout: this.timeout\n      })\n        .done(json => {\n          if (json && json.title) {\n            resolve();\n          } else {\n            reject(json.Error);\n          }\n        })\n        .fail(error => {\n          reject(error);\n        });\n    });\n  }\n\n  /**\n   * 获取热门搜索\n   * @param count 需要获取的数量，最多为100\n   */\n  public getTopSearches(count: number = 9): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      $.ajax({\n        url: `${this.omitApiURL}/movie/top/${count}?apikey=${this.omitApiKeys[0]}`,\n        timeout: this.timeout\n      })\n        .then(result => {\n          if (result && result.data) {\n            resolve(result.data);\n          } else {\n            reject();\n          }\n        })\n        .catch(error => {\n          reject(error);\n        });\n    });\n  }\n}\n"
  },
  {
    "path": "src/service/pathHandler.ts",
    "content": "import { Site, Dictionary } from \"@/interface/common\";\n\n/**\n * 保存路径处理程序\n */\nexport class PathHandler {\n  constructor() {}\n\n  /**\n   * 替换路径系统关键字\n   * 关键字列表：\n   * site.name 站点名称\n   * site.host 站点域名\n   * YYYY 年份\n   * MM 月份\n   * DD 日期\n   * @param {*} path\n   */\n  public replacePathKey(path: any, site: Site) {\n    if (!path) {\n      return path;\n    }\n    const now = new Date();\n    return this.replaceKeys(path, {\n      \"site.name\": site.name,\n      \"site.host\": site.host,\n      YYYY: now.getFullYear(),\n      MM: (\"0\" + (now.getMonth() + 1).toString()).substr(-2),\n      DD: (\"0\" + now.getDate().toString()).substr(-2)\n    });\n  }\n\n  /**\n   * 获取保存路径\n   * @param {*} sourcePath\n   */\n  public getSavePath(sourcePath: any, site: Site): any {\n    if (!sourcePath) {\n      return undefined;\n    }\n    let path = sourcePath;\n    let key = \"<...>\";\n\n    if (path) {\n      // 自定义路径\n      if (path.indexOf(key) >= 0) {\n        let tmp = window.prompt(\n          `当前为自定义路径：${path} \\n请输入路径中 ${key} 部分的内容: `\n        );\n        // 取消\n        if (tmp === null) {\n          return false;\n        }\n        path = path.replace(key, tmp);\n      }\n    }\n\n    return this.replacePathKey(path, site);\n  }\n\n  public replaceKeys(source: string, keys: Dictionary<any>): string {\n    let result: string = source;\n\n    for (const key in keys) {\n      if (keys.hasOwnProperty(key)) {\n        const value = keys[key];\n        result = result.replace(\"$\" + key + \"$\", value);\n      }\n    }\n    return result;\n  }\n}\n"
  },
  {
    "path": "src/service/public.ts",
    "content": "import md5 from \"blueimp-md5\";\nimport * as basicContext from \"basiccontext\";\nimport { Options, Site, Dictionary } from \"@/interface/common\";\nimport dayjs from \"dayjs\";\nimport { UAParser } from \"ua-parser-js\";\n\nclass HelpFunctions {\n  public isExtensionMode: boolean = false;\n  public browserName: string = \"\";\n  public manifest: chrome.runtime.Manifest = {\n    manifest_version: 2,\n    name: \"\",\n    version: \"\"\n  };\n  constructor() {\n    try {\n      this.isExtensionMode = !!(\n        chrome.runtime &&\n        chrome.extension &&\n        chrome.runtime.getManifest\n      );\n\n      this.manifest = chrome.runtime.getManifest();\n    } catch (error) {\n      console.log(\"HelpFunctions: is not extension mode.\", error);\n    }\n\n    this.browserName = new UAParser().getBrowser().name || \"\";\n  }\n\n  /**\n   * 获取当天日期的键值\n   */\n  public getToDay(time?: number): string {\n    let day = new Date();\n    if (time) {\n      day = new Date(time);\n    }\n    let yyyy = day.getFullYear();\n    let m = day.getMonth() + 1;\n    let mm = m < 10 ? \"0\" + m : m;\n\n    let d = day.getDate();\n    let dd = d < 10 ? \"0\" + d : d;\n\n    return `${yyyy}-${mm}-${dd}`;\n  }\n\n  /**\n   * 更新插件徽标提示\n   * @param count\n   */\n  public updateBadge(count: number) {\n    if (!this.isExtensionMode) return;\n    try {\n      if (count == 0) {\n        chrome.browserAction.setBadgeText({ text: \"\" });\n        chrome.browserAction.enable();\n      } else {\n        chrome.browserAction.setBadgeText({ text: count.toString() });\n        chrome.browserAction.setBadgeBackgroundColor({\n          color: \"#aabbcc\"\n        });\n        chrome.browserAction.disable();\n      }\n    } catch (error) {\n      console.log(error);\n    }\n  }\n\n  /**\n   * 获取当前版本\n   */\n  public getVersion() {\n    if (this.isExtensionMode) {\n      return \"v\" + (this.manifest.version_name || this.manifest.version);\n    } else {\n      return \"localVersion\";\n    }\n  }\n\n  /**\n   * 获取随机字符串\n   * @param  {number} length    长度，默认为32\n   * @param  {boolean} noSimilar 是否包含容易混淆的字符，默认为包含\n   * @return {string}           返回的内容\n   */\n  public getRandomString(\n    length: number = 32,\n    noSimilar: boolean = false\n  ): string {\n    // 是否包含容易混淆的字符[oO,Ll,9gq,Vv,Uu,I1]，默认为包含\n    let chars = noSimilar\n      ? \"abcdefhijkmnprstwxyz2345678ABCDEFGHJKMNPQRSTWXYZ\"\n      : \"abcdefghijkmnopqrstuvwxyz0123456789ABCDEFGHIJKMNOPQRSTUVWXYZ\";\n    let maxLength = chars.length;\n    let result = [];\n    for (let i = 0; i < length; i++) {\n      result.push(chars.charAt(Math.floor(Math.random() * maxLength)));\n    }\n\n    return result.join(\"\");\n  }\n\n  /**\n   * 获取一个编号\n   */\n  public getNewId(): string {\n    return md5(\n      new Date().getTime().toString() + this.getRandomString()\n    ).toString();\n  }\n\n  /**\n   * 显示系统提示信息\n   * @param options\n   */\n  public showNotifications(\n    options: chrome.notifications.NotificationOptions,\n    timeout: number = 3000\n  ) {\n    options = Object.assign(\n      {\n        type: \"basic\",\n        iconUrl: chrome.runtime.getURL(\"/assets/icon-128.png\"),\n        title: \"PT 助手 Plus\",\n        priority: 0,\n        message: \"\"\n      },\n      options\n    );\n\n    let id = Math.floor(Math.random() * 99999) + \"\";\n\n    chrome.notifications.create(id, options, function(myId) {\n      id = myId;\n    });\n\n    setTimeout(() => {\n      chrome.notifications.clear(id, () => {});\n    }, timeout);\n  }\n\n  /**\n   * 去除重复的查询字符串\n   * @param url\n   */\n  public removeDuplicateQueryString(url: string) {\n    let querys: string[] = [],\n      queryString = \"\",\n      rule = /([^&=]+)=([^&]*)/g,\n      m,\n      head = \"\",\n      index = url.indexOf(\"?\");\n\n    if (index !== -1) {\n      head = url.substr(0, index + 1);\n      queryString = url.substr(index + 1);\n      while ((m = rule.exec(queryString))) {\n        const v = m[1] + \"=\" + m[2];\n        if (!querys.includes(v)) {\n          querys.push(v);\n        }\n      }\n\n      return head + querys.join(\"&\");\n    }\n\n    return url;\n  }\n\n  /**\n   * 删除链接中指定值的查询字符串\n   * @param url\n   * @param value\n   */\n  public removeQueryStringFromValue(url: string, value: string) {\n    let querys: string[] = [],\n      queryString = \"\",\n      rule = /([^&=]+)=([^&]*)/g,\n      m,\n      head = \"\",\n      index = url.indexOf(\"?\");\n\n    if (index !== -1) {\n      head = url.substr(0, index + 1);\n      queryString = url.substr(index + 1);\n      while ((m = rule.exec(queryString))) {\n        const v = m[1] + \"=\" + m[2];\n        if (m[2] !== value) {\n          querys.push(v);\n        }\n      }\n\n      return head + querys.join(\"&\");\n    }\n\n    return url;\n  }\n\n  /**\n   * 删除链接中指定的字段\n   * @param url\n   * @param value\n   */\n  public removeQueryStringFields(url: string, fields: string[]) {\n    let querys: string[] = [],\n      queryString = \"\",\n      rule = /([^&=]+)=([^&]*)/g,\n      m,\n      head = \"\",\n      index = url.indexOf(\"?\");\n\n    if (index !== -1) {\n      head = url.substr(0, index + 1);\n      queryString = url.substr(index + 1);\n      while ((m = rule.exec(queryString))) {\n        const v = m[1] + \"=\" + m[2];\n        if (!fields.includes(m[1])) {\n          querys.push(v);\n        }\n      }\n\n      return head + querys.join(\"&\");\n    }\n\n    return url;\n  }\n\n  public clone(source: any) {\n    return JSON.parse(JSON.stringify(source));\n  }\n\n  /**\n   * 输出调试信息\n   * @param msg\n   */\n  public debug(...msg: any) {\n    console.log(new Date().toLocaleString(), ...msg);\n  }\n\n  /**\n   * 显示上下文菜单\n   * @param menus\n   * @param event\n   */\n  public showContextMenu(menus: any, event: any) {\n    try {\n      // 显示菜单\n      basicContext.show(menus, event);\n\n      // 修正偏移量\n      $(\".basicContext\").css({\n        left: \"-=20px\",\n        top: \"+=10px\"\n      });\n    } catch (error) {}\n  }\n\n  public getCleaningURL(url: string) {\n    return this.removeQueryStringFields(url, [\"hit\", \"cmtpage\", \"page\"]);\n  }\n\n  /**\n   * 检查权限\n   * @param permissions 需要检查的权限列表\n   */\n  public checkPermissions(permissions: string[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome && chrome.permissions) {\n        // 查询当前权限\n        chrome.permissions.contains(\n          {\n            permissions: permissions\n          },\n          result => {\n            if (result === true) {\n              resolve(true);\n            } else {\n              reject({\n                success: false\n              });\n            }\n          }\n        );\n      } else {\n        reject({\n          success: false\n        });\n      }\n    });\n  }\n\n  /**\n   * 申请权限\n   * @param permissions 需要申请的权限列表\n   */\n  public requestPermissions(permissions: string[]): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      if (chrome && chrome.permissions) {\n        chrome.permissions.request(\n          {\n            permissions: permissions\n          },\n          granted => {\n            if (granted === true) {\n              resolve(true);\n            } else {\n              reject({\n                success: false\n              });\n            }\n          }\n        );\n      } else {\n        reject({\n          success: false\n        });\n      }\n    });\n  }\n\n  /**\n   * 使用指定的使用\n   * @param permissions 权限列表\n   * @param needConfirm 是否需要确认，因为有些权限默认浏览器会弹出确认，有些不会弹出确认，增加此参数用于手工确认\n   */\n  public usePermissions(\n    permissions: string[],\n    needConfirm: boolean = false,\n    confirmMsg: string = \"\"\n  ): Promise<any> {\n    return new Promise<any>((resolve?: any, reject?: any) => {\n      this.checkPermissions(permissions)\n        .then(result => {\n          resolve(result);\n        })\n        .catch(() => {\n          let confirmed = true;\n          if (needConfirm) {\n            confirmed = confirm(confirmMsg);\n          }\n          if (!confirmed) {\n            reject({\n              success: false\n            });\n            return;\n          }\n          this.requestPermissions(permissions)\n            .then(result => {\n              resolve(result);\n            })\n            .catch(error => {\n              reject(error);\n            });\n        });\n    });\n  }\n\n  /**\n   * 根据指定的host获取已定义的站点信息\n   * @param host\n   */\n  public getSiteFromHost(host: string, options: Options) {\n    let sites: Site[] = [];\n    if (options.sites) {\n      sites.push(...options.sites);\n    }\n\n    if (options.system && options.system.publicSites) {\n      sites.push(...options.system.publicSites);\n    }\n\n    let site = sites.find((item: Site) => {\n      let cdn = [item.url].concat(item.cdn, item.formerHosts?.map(x => `//${x}`));\n      return item.host == host || cdn.join(\"\").indexOf(`//${host}`) > -1;\n    });\n\n    if (site) {\n      return this.clone(site);\n    }\n\n    return null;\n  }\n\n  public getNewBackupFileName(): string {\n    return (\n      \"PT-Plugin-Plus-Backup-\" + dayjs().format(\"YYYY-MM-DD_HH-mm-ss\") + \".zip\"\n    );\n  }\n\n  /**\n   * 替换指定的字符串列表\n   * @param source\n   * @param maps\n   */\n  public replaceKeys(\n    source: string,\n    maps: Dictionary<any>,\n    prefix: string = \"\"\n  ): string {\n    if (!source || typeof source !== 'string') {\n      return source;\n    }\n    let result: string = source;\n\n    for (const key in maps) {\n      if (maps.hasOwnProperty(key)) {\n        const value = maps[key];\n        let search = \"$\" + key + \"$\";\n        if (prefix) {\n          search = `$${prefix}.${key}$`;\n        }\n        result = result.replace(search, value);\n      }\n    }\n    return result;\n  }\n\n  /**\n   * 检查指定的可选权限是否有被声明\n   * @param key\n   */\n  public checkOptionalPermission(key: string): boolean {\n    if (\n      this.isExtensionMode &&\n      this.manifest &&\n      this.manifest.optional_permissions\n    ) {\n      return this.manifest.optional_permissions.includes(key);\n    }\n\n    return false;\n  }\n\n  /**\n   * 转换时间\n   * @param time 待转换的时间\n   * @param timezoneOffset 时区偏移量，用于解决时差问题，如：+08:00, -08:00, +0800, UTC+0800, UTC+08:00\n   * @see https://zh.wikipedia.org/wiki/各國時區列表\n   * @see https://zh.wikipedia.org/wiki/时区\n   * @see https://zh.wikipedia.org/wiki/ISO_8601\n   */\n  public transformTime(time?: number, timezoneOffset?: string) {\n    if (!timezoneOffset || !time) {\n      return time;\n    }\n    let result = time;\n    // 标准时间戳需要 * 1000\n    if (/^(\\d){10}$/.test(result + \"\")) {\n      result = parseInt(result + \"\") * 1000;\n    }\n    // 时间格式按 ISO 8601 标准设置，如：2020-01-01T00:00:01+0800\n    let datetime = dayjs(result).format(\"YYYY-MM-DDTHH:mm:ss\");\n    result = new Date(`${datetime}${timezoneOffset}`).getTime();\n    return result;\n  }\n}\n\n/**\n * 导出公用函数\n * PT Plugin Public Function\n */\nexport const PPF = new HelpFunctions();\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es2018\",\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"importHelpers\": true,\n    \"moduleResolution\": \"node\",\n    \"experimentalDecorators\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"sourceMap\": true,\n    \"baseUrl\": \".\",\n    // \"rootDir\": \"./src/options\",\n    \"types\": [\n      \"webpack-env\",\n      \"chrome\",\n      \"jquery\"\n    ],\n    \"paths\": {\n      \"@/*\": [\n        \"src/*\"\n      ]\n    },\n    \"lib\": [\n      \"esnext\",\n      \"dom\",\n      \"dom.iterable\",\n      \"scripthost\"\n    ]\n  },\n  \"include\": [\n    \"src/**/*.ts\",\n    \"src/**/*.vue\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "update/index.xml",
    "content": "<?xml version='1.0' encoding='UTF-8'?>\n<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>\n  <app appid='dmmjlmbkigbgpnjfiimhlnbnmppjhpea'>\n    <updatecheck codebase='https://github.com/pt-plugins/PT-Plugin-Plus/releases/download/v1.6.0/PT-Plugin-Plus-v1.6.0.crx' version='1.6.0' prodversionmin='1.0.0' />\n  </app>\n</gupdate>"
  },
  {
    "path": "vue.config.js",
    "content": "module.exports = {\n  pages: {\n    index: {\n      // page 的入口\n      entry: \"src/options/main.ts\",\n      title: \"PT-Plugin-Plus\"\n    },\n    // 调试页面\n    debugger: {\n      entry: \"src/debugger/index.ts\",\n      title: \"PT-Plugin-Plus Debugger\"\n    },\n    changelog: {\n      entry: \"src/changelog/index.ts\",\n      title: \"PT-Plugin-Plus ChangeLog\"\n    }\n  },\n  productionSourceMap: false,\n  configureWebpack: {\n    optimization: {\n      // 打包为 Chrome 商店版时不对代码进行压缩混淆\n      minimize: !process.env.CHROME_WEB_STORE\n    }\n  }\n};\n"
  },
  {
    "path": "webpack/common.js",
    "content": "const path = require(\"path\");\n// 因第三方库中可能会包含一些特殊字符，导致编译后 Chrome 无法加载的问题，故引入 terser-webpack-plugin 插件\n// Chrome 会报以下错误：无法为内容脚本加载“xxx.js”文件。该文件采用的不是 UTF-8 编码。\n// @see https://stackoverflow.com/questions/55601774/chrome-extension-has-a-content-js-error-about-utf-8\nconst TerserPlugin = require(\"terser-webpack-plugin\");\n\n// 用于替换 @ 符号的路径\nfunction resolve(dir) {\n  return path.join(__dirname, \"..\", dir);\n}\n\nmodule.exports = {\n  optimization: {\n    // 将第三方库和主程序分离\n    // 参考配置：https://yi-jy.com/2018/06/09/webpack-split-chunks/\n    splitChunks: {\n      chunks: \"all\",\n      minSize: 30000,\n      maxSize: 0,\n      minChunks: 1,\n      maxAsyncRequests: 5,\n      maxInitialRequests: 3,\n      name: true,\n      cacheGroups: {\n        // 第三方库\n        vendors: {\n          test: /[\\\\/]node_modules[\\\\/]/,\n          priority: -10,\n          name: \"libs\"\n        },\n        // 公用模块\n        default: {\n          minSize: 0,\n          minChunks: 2,\n          priority: -20,\n          reuseExistingChunk: true,\n          name: \"utils\"\n        }\n      }\n    },\n    // 打包为 Chrome 商店版时不对代码进行压缩混淆\n    minimize: !process.env.CHROME_WEB_STORE,\n    minimizer: [\n      new TerserPlugin({\n        // 防止因编码问题导致Chrome无法加载插件\n        terserOptions: {\n          output: { ascii_only: true }\n        }\n      })\n    ]\n  },\n  module: {\n    rules: [\n      {\n        test: /\\.tsx?$/,\n        use: \"ts-loader\",\n        exclude: /node_modules/\n      },\n      {\n        test: /\\.scss$/,\n        use: [\n          \"style-loader\",\n          \"css-loader\",\n          {\n            loader: \"sass-loader\",\n            options: {\n              implementation: require(\"sass\")\n            }\n          },\n          \"postcss-loader\"\n        ]\n      },\n      {\n        test: /\\.css$/,\n        use: [\"style-loader\", \"css-loader\", \"postcss-loader\"]\n      }\n    ]\n  },\n  resolve: {\n    extensions: [\".ts\", \".tsx\", \".js\"],\n    alias: {\n      \"@\": resolve(\"src\")\n    }\n  },\n  // 防止一些模块中使用了fs时无法编译的错误\n  node: {\n    fs: \"empty\"\n  }\n};\n"
  },
  {
    "path": "webpack/dev-background.js",
    "content": "const merge = require(\"webpack-merge\");\nconst common = require(\"./common.js\");\nconst path = require(\"path\");\nconst CopyWebpackPlugin = require(\"copy-webpack-plugin\");\n\n// 用于替换 @ 符号的路径\nfunction resolve(dir) {\n  return path.join(__dirname, \"..\", dir);\n}\n\nmodule.exports = merge(common, {\n  entry: {\n    background: path.join(__dirname, \"../src/background/index.ts\")\n  },\n  output: {\n    path: path.join(__dirname, \"../dist/js/background\"),\n    filename: \"[name].js\"\n  },\n  devtool: \"inline-source-map\",\n  mode: \"development\",\n  plugins: [\n    new CopyWebpackPlugin([\n      {\n        from: resolve(\"/resource/\"),\n        to: path.join(resolve(\"/dist/\"), \"resource\"),\n        ignore: [\".DS_Store\", \"README.md\", \"testSearchData.json\"]\n      }\n    ])\n  ]\n});\n"
  },
  {
    "path": "webpack/dev-content.js",
    "content": "const merge = require(\"webpack-merge\");\nconst common = require(\"./common.js\");\nconst path = require(\"path\");\n\nmodule.exports = merge(common, {\n  entry: {\n    content: path.join(__dirname, \"../src/content/index.ts\")\n  },\n  output: {\n    path: path.join(__dirname, \"../dist/js/content\"),\n    filename: \"[name].js\"\n  },\n  devtool: \"inline-source-map\",\n  mode: \"development\"\n});\n"
  },
  {
    "path": "webpack/prod-background.js",
    "content": "const merge = require(\"webpack-merge\");\nconst common = require(\"./common.js\");\nconst path = require(\"path\");\nconst CopyWebpackPlugin = require(\"copy-webpack-plugin\");\nconst git = require('git-rev-sync');\n\n// 用于替换 @ 符号的路径\nfunction resolve(dir) {\n  return path.join(__dirname, \"..\", dir);\n}\n\nmodule.exports = merge(common, {\n  entry: {\n    background: path.join(__dirname, \"../src/background/index.ts\")\n  },\n  output: {\n    path: path.join(__dirname, \"../dist/js/background\"),\n    filename: \"[name].js\"\n  },\n  mode: \"production\",\n  plugins: [\n    new CopyWebpackPlugin([\n      {\n        from: resolve(\"/resource/\"),\n        to: path.join(resolve(\"/dist/\"), \"resource\"),\n        ignore: [\".DS_Store\", \"README.md\", \"testSearchData.json\"]\n      }\n    ]),\n    new CopyWebpackPlugin([\n      {\n        from: path.join(resolve('public'), 'manifest.json'),\n        to: path.join(resolve('dist'), \"manifest.json\"),\n        transform (content, path) {\n          var manifest = JSON.parse(content.toString());\n\n          // rewrite version to add Build number (simple from git count)\n          const build_number = git.count() % 65535;\n          manifest.version = `${manifest.version}.${build_number}`;\n\n          return JSON.stringify(manifest);\n        }\n      }\n    ])\n  ]\n});\n"
  },
  {
    "path": "webpack/prod-content.js",
    "content": "const merge = require(\"webpack-merge\");\nconst common = require(\"./common.js\");\nconst path = require(\"path\");\n\nmodule.exports = merge(common, {\n  entry: {\n    content: path.join(__dirname, \"../src/content/index.ts\")\n  },\n  output: {\n    path: path.join(__dirname, \"../dist/js/content\"),\n    filename: \"[name].js\"\n  },\n  mode: \"production\"\n});\n"
  }
]