├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── release-drafter.yml └── workflows │ ├── greetings.yml │ ├── pypi-publish.yml │ ├── release-drafter.yml │ └── stale.yml ├── .gitignore ├── LICENSE ├── README.md ├── nonebot-plugin-resolver ├── __init__.py ├── config.py ├── constants │ ├── __init__.py │ ├── bili.py │ ├── common.py │ ├── kugou.py │ ├── ncm.py │ ├── tiktok.py │ ├── weibo.py │ └── xhs.py └── core │ ├── __init__.py │ ├── a-bogus.js │ ├── acfun.py │ ├── bili23.py │ ├── common.py │ ├── tiktok.py │ ├── weibo.py │ └── ytdlp.py ├── npr_install.sh └── pyproject.toml /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug 反馈 2 | description: 当你在代码中发现了一个 Bug,导致应用崩溃或抛出异常,或者有一个组件存在问题,或者某些地方看起来不对劲。 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢对项目的支持与关注。在提出问题之前,请确保你已查看相关开发或使用文档: 10 | - https://... 11 | - type: checkboxes 12 | attributes: 13 | label: 这个问题是否已经存在? 14 | options: 15 | - label: 我已经搜索过现有的问题 (https://gitee.com/../../issues) 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: 如何复现 20 | description: 请详细告诉我们如何复现你遇到的问题,如涉及代码,可提供一个最小代码示例,并使用反引号```附上它 21 | placeholder: | 22 | 1. ... 23 | 2. ... 24 | 3. ... 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: 预期结果 30 | description: 请告诉我们你预期会发生什么。 31 | validations: 32 | required: false 33 | - type: textarea 34 | attributes: 35 | label: 实际结果 36 | description: 请告诉我们实际发生了什么。 37 | validations: 38 | required: true 39 | - type: textarea 40 | attributes: 41 | label: 截图或视频 42 | description: 如果可以的话,上传任何关于 bug 的截图。 43 | value: | 44 | [在这里上传图片] 45 | - type: textarea 46 | attributes: 47 | label: 插件版本号 48 | description: 当前你使用的 zhiyu1998/nonebot-plugin-resolver 版本号 49 | validations: 50 | required: true 51 | - type: dropdown 52 | id: adapterType 53 | attributes: 54 | label: 适配器类型 55 | description: 你当前使用的适配器类型? 56 | options: 57 | - napcat 58 | - lagrange 59 | - LLOneBot 60 | - go-cqhttp 61 | - icqq或者其他 62 | validations: 63 | required: true 64 | - type: dropdown 65 | id: pythonId 66 | attributes: 67 | label: Python版本 68 | description: 你当前使用的Python版本? 69 | options: 70 | - Python3.11以上 71 | - Python3.9~3.12之间 72 | - Python3.9以下 73 | validations: 74 | required: true 75 | - type: dropdown 76 | id: system 77 | attributes: 78 | label: 系统 79 | description: 你当前使用的系统? 80 | options: 81 | - windows 82 | - linux 83 | - macos 84 | validations: 85 | required: true -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: R插件文档 4 | url: https://gitee.com/kyrzy0416/rconsole-plugin 5 | about: 专门为朋友们写的Yunzai-Bot插件,专注图片视频分享、生活、健康和学习的插件! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 功能建议 2 | description: 对本项目提出一个功能建议 3 | title: "[功能建议]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | 感谢提出功能建议,我们将仔细考虑! 10 | - type: textarea 11 | id: related-problem 12 | attributes: 13 | label: 你的功能建议是否和某个问题相关? 14 | description: 清晰并简洁地描述问题是什么,例如,当我...时,我总是感到困扰。 15 | validations: 16 | required: false 17 | - type: textarea 18 | id: desired-solution 19 | attributes: 20 | label: 你希望看到什么解决方案? 21 | description: 清晰并简洁地描述你希望发生的事情。 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: alternatives 26 | attributes: 27 | label: 你考虑过哪些替代方案? 28 | description: 清晰并简洁地描述你考虑过的任何替代解决方案或功能。 29 | validations: 30 | required: false 31 | - type: textarea 32 | id: additional-context 33 | attributes: 34 | label: 你有其他上下文或截图吗? 35 | description: 在此处添加有关功能请求的任何其他上下文或截图。 36 | validations: 37 | required: false 38 | - type: checkboxes 39 | attributes: 40 | label: 意向参与贡献 41 | options: 42 | - label: 我有意向参与具体功能的开发实现并将代码贡献回到上游社区 43 | required: false -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$RESOLVED_VERSION 🌈' 3 | tag-template: 'v$RESOLVED_VERSION' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'feat' 8 | - 'feature' 9 | - 'enhancement' 10 | - 'kind/feature' 11 | - title: '🐛 Bug Fixes' 12 | labels: 13 | - 'fix' 14 | - 'bugfix' 15 | - 'bug' 16 | - 'regression' 17 | - 'kind/bug' 18 | - title: 📝 Documentation updates 19 | labels: 20 | - docs 21 | - documentation 22 | - 'kind/doc' 23 | - title: 👻 Maintenance 24 | labels: 25 | - chore 26 | - dependencies 27 | - 'kind/chore' 28 | - 'kind/dep' 29 | - title: 🚦 Tests 30 | labels: 31 | - test 32 | - tests 33 | - title: 🦄 Reactor 34 | labels: 35 | - reactor 36 | - reactors 37 | - title: 🎨 Style 38 | labels: 39 | - style 40 | - styles 41 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 42 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 43 | version-resolver: 44 | major: 45 | labels: 46 | - 'major' 47 | minor: 48 | labels: 49 | - 'minor' 50 | patch: 51 | labels: 52 | - 'patch' 53 | default: patch 54 | template: | 55 | ## What’s Changed 56 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.ACCESS_TOKEN }} 15 | issue-message: "👋 你好!感谢你提交了第一个问题!🔍 你的细心帮助我们发现了新的改进机会,我们会尽快处理。🚀 如果你有更多的想法或疑问,请继续分享,我们非常乐意听取你的声音!🌟 感谢你的支持!" 16 | pr-message: "🙌 你好,贡献者!感谢你提交的第一个拉取请求!🛠️ 你正在帮助我们打造更好的项目,我们会认真审查并与你一起优化它。💡 如果你有任何问题或需要帮助,随时联系!✨ 很高兴与你合作,欢迎成为我们社区的一员!" -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | pypi-publish: 11 | name: Upload release to PyPI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@master 15 | - name: Set up Python 16 | uses: actions/setup-python@v1 17 | with: 18 | python-version: "3.x" 19 | - name: Install pypa/build 20 | run: >- 21 | python -m 22 | pip install 23 | build 24 | --user 25 | - name: Build a binary wheel and a source tarball 26 | run: >- 27 | python -m 28 | build 29 | --sdist 30 | --wheel 31 | --outdir dist/ 32 | . 33 | - name: Publish distribution to PyPI 34 | uses: pypa/gh-action-pypi-publish@release/v1 35 | with: 36 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | # branches to consider in the event; optional, defaults to all 6 | branches: 7 | - master 8 | # pull_request event is required only for autolabeler 9 | pull_request: 10 | # Only following types are handled by the action, but one can default to all as well 11 | types: [opened, reopened, synchronize] 12 | # pull_request_target event is required for autolabeler to support PRs from forks 13 | # pull_request_target: 14 | # types: [opened, reopened, synchronize] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | update_release_draft: 21 | permissions: 22 | # write permission is required to create a github release 23 | contents: write 24 | # write permission is required for autolabeler 25 | # otherwise, read permission is required at least 26 | pull-requests: write 27 | runs-on: ubuntu-latest 28 | steps: 29 | # (Optional) GitHub Enterprise requires GHE_HOST variable set 30 | #- name: Set GHE_HOST 31 | # run: | 32 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV 33 | 34 | # Drafts your next Release notes as Pull Requests are merged into "master" 35 | - uses: release-drafter/release-drafter@v6 36 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 37 | # with: 38 | # config-name: my-config.yml 39 | # disable-autolabeler: true 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '24 5 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v5 22 | with: 23 | repo-token: ${{ secrets.ACCESS_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | venv 3 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 木兰宽松许可证, 第2版 2 | 3 | 木兰宽松许可证, 第2版 4 | 2020年1月 http://license.coscl.org.cn/MulanPSL2 5 | 6 | 7 | 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: 8 | 9 | 0. 定义 10 | 11 | “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 12 | 13 | “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 14 | 15 | “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 16 | 17 | “法人实体”是指提交贡献的机构及其“关联实体”。 18 | 19 | “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 20 | 21 | 1. 授予版权许可 22 | 23 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 24 | 25 | 2. 授予专利许可 26 | 27 | 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 28 | 29 | 3. 无商标许可 30 | 31 | “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 32 | 33 | 4. 分发限制 34 | 35 | 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 36 | 37 | 5. 免责声明与责任限制 38 | 39 | “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 40 | 41 | 6. 语言 42 | “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 43 | 44 | 条款结束 45 | 46 | 如何将木兰宽松许可证,第2版,应用到您的软件 47 | 48 | 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: 49 | 50 | 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; 51 | 52 | 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; 53 | 54 | 3, 请将如下声明文本放入每个源文件的头部注释中。 55 | 56 | Copyright (c) [Year] [name of copyright holder] 57 | [Software Name] is licensed under Mulan PSL v2. 58 | You can use this software according to the terms and conditions of the Mulan PSL v2. 59 | You may obtain a copy of Mulan PSL v2 at: 60 | http://license.coscl.org.cn/MulanPSL2 61 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 62 | See the Mulan PSL v2 for more details. 63 | 64 | 65 | Mulan Permissive Software License,Version 2 66 | 67 | Mulan Permissive Software License,Version 2 (Mulan PSL v2) 68 | January 2020 http://license.coscl.org.cn/MulanPSL2 69 | 70 | Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 71 | 72 | 0. Definition 73 | 74 | Software means the program and related documents which are licensed under this License and comprise all Contribution(s). 75 | 76 | Contribution means the copyrightable work licensed by a particular Contributor under this License. 77 | 78 | Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. 79 | 80 | Legal Entity means the entity making a Contribution and all its Affiliates. 81 | 82 | Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. 83 | 84 | 1. Grant of Copyright License 85 | 86 | Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. 87 | 88 | 2. Grant of Patent License 89 | 90 | Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. 91 | 92 | 3. No Trademark License 93 | 94 | No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. 95 | 96 | 4. Distribution Restriction 97 | 98 | You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. 99 | 100 | 5. Disclaimer of Warranty and Limitation of Liability 101 | 102 | THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 103 | 104 | 6. Language 105 | 106 | THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. 107 | 108 | END OF THE TERMS AND CONDITIONS 109 | 110 | How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software 111 | 112 | To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: 113 | 114 | i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; 115 | 116 | ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; 117 | 118 | iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. 119 | 120 | 121 | Copyright (c) [Year] [name of copyright holder] 122 | [Software Name] is licensed under Mulan PSL v2. 123 | You can use this software according to the terms and conditions of the Mulan PSL v2. 124 | You may obtain a copy of Mulan PSL v2 at: 125 | http://license.coscl.org.cn/MulanPSL2 126 | THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. 127 | See the Mulan PSL v2 for more details. 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | NoneBotPluginLogo 3 |
4 |

NoneBotPluginText

5 |
6 | 7 |
8 | 9 | # nonebot-plugin-resolver 10 | 11 | _✨ NoneBot2 链接分享解析器插件 ✨_ 12 | 13 | 14 | 15 | license 16 | 17 | 18 | pypi 19 | 20 | python 21 | 22 |
23 | 24 | ## 📖 介绍 25 | 26 | 适用于NoneBot2的解析视频、图片链接/小程序插件,tiktok、bilibili、twitter等实时发送! 27 | ## 💿 安装 28 | 29 | 1. 使用 nb-cli 安装,不需要手动添加入口,更新使用 pip 30 | 31 | ```sh 32 | nb plugin install nonebot-plugin-resolver 33 | ``` 34 | 35 | 2. 使用 pip 安装和更新,初次安装需要手动添加入口 36 | 37 | ```sh 38 | pip install --upgrade nonebot-plugin-resolver 39 | ``` 40 | 3. 🚀【高级 / 进阶 / 推荐】使用脚本进行安装,**优点就是及时更新** | ⚠️在可以执行`nb run`那个目录执行即可 41 | 42 | ```shell 43 | curl -fsSL https://raw.gitmirror.com/zhiyu1998/nonebot-plugin-resolver/master/npr_install.sh > npr_install.sh && chmod 755 npr_install.sh && ./npr_install.sh 44 | ``` 45 | 46 | 4. 【必要】安装必要组件 FFmpeg 47 | 48 | ```shell 49 | # ubuntu 50 | sudo apt-get install ffmpeg 51 | # 其他linux参考(群友推荐):https://gitee.com/baihu433/ffmpeg 52 | # Windows 参考:https://www.jianshu.com/p/5015a477de3c 53 | ``` 54 | > [!IMPORTANT] 55 | > 推荐两个ffmpeg全编译版本: 56 | > - https://github.com/yt-dlp/FFmpeg-Builds 57 | > - https://github.com/BtbN/FFmpeg-Builds 58 | 59 | 5. 【可选】安装`TikTok`&`YouTube`解析必要依赖 不建议直接使用`apt`不是最新版 60 | 61 | ```shell 62 | pip install yt-dlp 63 | ``` 64 | ## ⚙️ 配置 65 | 66 | 在 nonebot2 项目的`.env`文件中添加下表中的可选配置 67 | 68 | ``` 69 | XHS_CK='' #xhs cookie 70 | DOUYIN_CK='' # douyin's cookie, 格式:odin_tt=xxx;passport_fe_beating_status=xxx;sid_guard=xxx;uid_tt=xxx;uid_tt_ss=xxx;sid_tt=xxx;sessionid=xxx;sessionid_ss=xxx;sid_ucp_v1=xxx;ssid_ucp_v1=xxx;passport_assist_user=xxx;ttwid=xxx; 71 | IS_OVERSEA=False # 是否是海外服务器部署 72 | RESOLVER_PROXY = "http://127.0.0.1:7890" # 代理 73 | R_GLOBAL_NICKNAME="" # 解析前缀名 74 | BILI_SESSDATA='' # bilibili sessdata 填写后可附加: 总结等功能 75 | VIDEO_DURATION_MAXIMUM=480 # 视频最大解析长度,默认480s为8分钟,计算公式为480s/60s=8mins 76 | GLOBAL_RESOLVE_CONTROLLER="" # 全局禁止的解析,示例 GLOBAL_RESOLVE_CONTROLLER="bilibili,dy" 表示禁止了哔哩哔哩和抖,GLOBAL_RESOLVE_CONTROLLER=""说明都不禁止,(大部分是缩写)请严格遵守选项: bilibili,dy,tiktok,ac,twitter,xiaohongshu,youtube.netease,kugou,wb 77 | ``` 78 | 79 | ## 🕹️ 开启 & 关闭解析 80 | 81 | 使用以下命令可以控制对当前群是否开启/关闭解析: 82 | ```shell 83 | @机器人 开启解析 84 | @机器人 关闭解析 85 | 查看关闭解析 86 | ``` 87 | 88 | ## 🤳🏿 在线观看如何获取 Cookie 89 | 90 | > 由群友 `@麦满分` 提供 91 | 92 | https://github.com/user-attachments/assets/7ead6d62-a36c-4e8d-bb5d-6666749dfb26 93 | 94 | ## youtube 解析可能存在的问题 95 | - 网络问题, 自行解决 96 | - 解析失败可能是因为人机检测,建议先自行使用 `yt-dlp` 测试,确定后将 youtube 的 cookies 以 **Netscape** 的格式导出为 `ytb_cookies.txt`,放到 nonebot 工作目录 97 | 98 | ## 🎉 使用 & 效果图 99 | 100 | 101 | 102 | 103 | 104 | 105 | ## 开发 && 发版 106 | 107 | 发版 Action: 108 | ```shell 109 | git tag 110 | 111 | git push origin --tags 112 | ``` 113 | 114 | ## 贡献 115 | 116 | 同时感谢以下开发者对 `Nonebot - R插件` 作出的贡献: 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import os.path 3 | from functools import wraps 4 | from typing import cast, Iterable, Union 5 | from urllib.parse import parse_qs 6 | 7 | from bilibili_api import video, Credential, live, article 8 | from bilibili_api.favorite_list import get_video_favorite_list_content 9 | from bilibili_api.opus import Opus 10 | from bilibili_api.video import VideoDownloadURLDataDetecter 11 | from nonebot import on_regex, get_driver, on_command 12 | from nonebot.adapters.onebot.v11 import Message, Event, Bot, MessageSegment, GROUP_ADMIN, GROUP_OWNER 13 | from nonebot.adapters.onebot.v11.event import GroupMessageEvent, PrivateMessageEvent 14 | from nonebot.matcher import current_bot 15 | from nonebot.permission import SUPERUSER 16 | from nonebot.plugin import PluginMetadata 17 | from nonebot.rule import to_me 18 | 19 | from .config import Config 20 | # noinspection PyUnresolvedReferences 21 | from .constants import COMMON_HEADER, URL_TYPE_CODE_DICT, DOUYIN_VIDEO, GENERAL_REQ_LINK, XHS_REQ_LINK, DY_TOUTIAO_INFO, \ 22 | BILIBILI_HEADER, NETEASE_API_CN, NETEASE_TEMP_API, VIDEO_MAX_MB, \ 23 | WEIBO_SINGLE_INFO, KUGOU_TEMP_API 24 | from .core.acfun import parse_url, download_m3u8_videos, parse_m3u8, merge_ac_file_to_mp4 25 | from .core.bili23 import download_b_file, merge_file_to_mp4, extra_bili_info 26 | from .core.common import * 27 | from .core.tiktok import generate_x_bogus_url, dou_transfer_other 28 | from .core.weibo import mid2id 29 | from .core.ytdlp import get_video_title, download_ytb_video 30 | 31 | __plugin_meta__ = PluginMetadata( 32 | name="链接分享解析器", 33 | description="NoneBot2链接分享解析器插件。解析视频、图片链接/小程序插件,tiktok、bilibili、twitter等实时发送!", 34 | usage="分享链接即可体验到效果", 35 | type="application", 36 | homepage="https://github.com/zhiyu1998/nonebot-plugin-resolver", 37 | config=Config, 38 | supported_adapters={ "~onebot.v11" } 39 | ) 40 | 41 | # 配置加载 42 | global_config = Config.parse_obj(get_driver().config.dict()) 43 | # 全局名称 44 | GLOBAL_NICKNAME: str = str(getattr(global_config, "r_global_nickname", "")) 45 | # 🪜地址 46 | resolver_proxy: str = getattr(global_config, "resolver_proxy", "http://127.0.0.1:7890") 47 | # 是否是海外服务器 48 | IS_OVERSEA: bool = bool(getattr(global_config, "is_oversea", False)) 49 | # 哔哩哔哩限制的最大视频时长(默认8分钟),单位:秒 50 | VIDEO_DURATION_MAXIMUM: int = int(getattr(global_config, "video_duration_maximum", 480)) 51 | # 全局解析内容控制 52 | GLOBAL_RESOLVE_CONTROLLER: list = split_and_strip(str(getattr(global_config, "global_resolve_controller", "[]")), ",") 53 | # 哔哩哔哩的 SESSDATA 54 | BILI_SESSDATA: str = str(getattr(global_config, "bili_sessdata", "")) 55 | # 构建哔哩哔哩的Credential 56 | credential = Credential(sessdata=BILI_SESSDATA) 57 | 58 | bili23 = on_regex( 59 | r"(bilibili.com|b23.tv|bili2233.cn|^BV[0-9a-zA-Z]{10}$)", priority=1 60 | ) 61 | douyin = on_regex( 62 | r"(v.douyin.com)", priority=1 63 | ) 64 | tik = on_regex( 65 | r"(www.tiktok.com|vt.tiktok.com|vm.tiktok.com)", priority=1 66 | ) 67 | acfun = on_regex(r"(acfun.cn)") 68 | twit = on_regex( 69 | r"(x.com)", priority=1 70 | ) 71 | xhs = on_regex( 72 | r"(xhslink.com|xiaohongshu.com)", priority=1 73 | ) 74 | y2b = on_regex( 75 | r"(youtube.com|youtu.be)", priority=1 76 | ) 77 | ncm = on_regex( 78 | r"(music.163.com|163cn.tv)" 79 | ) 80 | weibo = on_regex( 81 | r"(weibo.com|m.weibo.cn)" 82 | ) 83 | kg = on_regex( 84 | r"(kugou.com)" 85 | ) 86 | 87 | enable_resolve = on_command('开启解析', rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 88 | disable_resolve = on_command('关闭解析', rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER) 89 | check_resolve = on_command('查看关闭解析', permission=SUPERUSER) 90 | 91 | # 内存中关闭解析的名单,第一次先进行初始化 92 | resolve_shutdown_list_in_memory: list = load_or_initialize_list() 93 | 94 | 95 | def resolve_handler(func): 96 | """ 97 | 解析控制装饰器 98 | :param func: 99 | :return: 100 | """ 101 | 102 | @wraps(func) 103 | async def wrapper(*args, **kwargs): 104 | # 假设 `event` 是通过被装饰函数的参数传入的 105 | event = kwargs.get('event') or args[1] # 根据位置参数或者关键字参数获取 event 106 | send_id = get_id_both(event) 107 | 108 | if send_id not in resolve_shutdown_list_in_memory: 109 | return await func(*args, **kwargs) 110 | else: 111 | logger.info(f"发送者/群 {send_id} 已关闭解析,不再执行") 112 | return None 113 | 114 | return wrapper 115 | 116 | 117 | @enable_resolve.handle() 118 | async def enable(bot: Bot, event: Event): 119 | """ 120 | 开启解析 121 | :param bot: 122 | :param event: 123 | :return: 124 | """ 125 | send_id = get_id_both(event) 126 | if send_id in resolve_shutdown_list_in_memory: 127 | resolve_shutdown_list_in_memory.remove(send_id) 128 | save_sub_user(resolve_shutdown_list_in_memory) 129 | logger.info(resolve_shutdown_list_in_memory) 130 | await enable_resolve.finish('解析已开启') 131 | else: 132 | await enable_resolve.finish('解析已开启,无需重复开启') 133 | 134 | 135 | @disable_resolve.handle() 136 | async def disable(bot: Bot, event: Event): 137 | """ 138 | 关闭解析 139 | :param bot: 140 | :param event: 141 | :return: 142 | """ 143 | send_id = get_id_both(event) 144 | if send_id not in resolve_shutdown_list_in_memory: 145 | resolve_shutdown_list_in_memory.append(send_id) 146 | save_sub_user(resolve_shutdown_list_in_memory) 147 | logger.info(resolve_shutdown_list_in_memory) 148 | await disable_resolve.finish('解析已关闭') 149 | else: 150 | await disable_resolve.finish('解析已关闭,无需重复关闭') 151 | 152 | 153 | @check_resolve.handle() 154 | async def check_disable(bot: Bot, event: Event): 155 | """ 156 | 查看关闭解析 157 | :param bot: 158 | :param event: 159 | :return: 160 | """ 161 | memory_disable_list = [str(item) + "--" + (await bot.get_group_info(group_id=item))['group_name'] for item in 162 | resolve_shutdown_list_in_memory] 163 | memory_disable_list = "1. 在【内存】中的名单有:\n" + '\n'.join(memory_disable_list) 164 | persistence_disable_list = [str(item) + "--" + (await bot.get_group_info(group_id=item))['group_name'] for item in 165 | list(load_sub_user())] 166 | persistence_disable_list = "2. 在【持久层】中的名单有:\n" + '\n'.join(persistence_disable_list) 167 | 168 | await check_resolve.send(Message("已经发送到私信了~")) 169 | await bot.send_private_msg(user_id=event.user_id, message=Message( 170 | "[nonebot-plugin-resolver 关闭名单如下:]" + "\n\n" + memory_disable_list + '\n\n' + persistence_disable_list + "\n\n" + "🌟 温馨提示:如果想关闭解析需要艾特我然后输入: 关闭解析")) 171 | 172 | 173 | def resolve_controller(func): 174 | """ 175 | 将装饰器应用于函数,通过装饰器自动判断是否允许执行函数 176 | :param func: 177 | :return: 178 | """ 179 | 180 | logger.debug( 181 | f"[nonebot-plugin-resolver][解析全局控制] 加载 {func.__name__} {'禁止' if func.__name__ in GLOBAL_RESOLVE_CONTROLLER else '允许'}") 182 | 183 | @wraps(func) 184 | async def wrapper(*args, **kwargs): 185 | # 判断函数名是否在允许列表中 186 | if func.__name__ not in GLOBAL_RESOLVE_CONTROLLER: 187 | logger.info(f"[nonebot-plugin-resolver][解析全局控制] {func.__name__}...") 188 | return await func(*args, **kwargs) 189 | else: 190 | logger.warning(f"[nonebot-plugin-resolver][解析全局控制] {func.__name__} 被禁止执行") 191 | return None 192 | 193 | return wrapper 194 | 195 | 196 | @bili23.handle() 197 | @resolve_handler 198 | @resolve_controller 199 | async def bilibili(bot: Bot, event: Event) -> None: 200 | """ 201 | 哔哩哔哩解析 202 | :param bot: 203 | :param event: 204 | :return: 205 | """ 206 | # 消息 207 | url: str = str(event.message).strip() 208 | # 正则匹配 209 | url_reg = r"(http:|https:)\/\/(space|www|live).bilibili.com\/[A-Za-z\d._?%&+\-=\/#]*" 210 | b_short_rex = r"(https?://(?:b23\.tv|bili2233\.cn)/[A-Za-z\d._?%&+\-=\/#]+)" 211 | # BV处理 212 | if re.match(r'^BV[1-9a-zA-Z]{10}$', url): 213 | url = 'https://www.bilibili.com/video/' + url 214 | # 处理短号、小程序问题 215 | if "b23.tv" in url or "bili2233.cn" in url or "QQ小程序" in url: 216 | b_short_url = re.search(b_short_rex, url.replace("\\", ""))[0] 217 | resp = httpx.get(b_short_url, headers=BILIBILI_HEADER, follow_redirects=True) 218 | url: str = str(resp.url) 219 | else: 220 | url: str = re.search(url_reg, url).group(0) 221 | # ===============发现解析的是动态,转移一下=============== 222 | if ('t.bilibili.com' in url or '/opus' in url) and BILI_SESSDATA != '': 223 | # 去除多余的参数 224 | if '?' in url: 225 | url = url[:url.index('?')] 226 | dynamic_id = int(re.search(r'[^/]+(?!.*/)', url)[0]) 227 | dynamic_info = await Opus(dynamic_id, credential).get_info() 228 | # 这里比较复杂,暂时不用管,使用下面这个算法即可实现哔哩哔哩动态转发 229 | if dynamic_info is not None: 230 | title = dynamic_info['item']['basic']['title'] 231 | paragraphs = [] 232 | for module in dynamic_info['item']['modules']: 233 | if 'module_content' in module: 234 | paragraphs = module['module_content']['paragraphs'] 235 | break 236 | desc = paragraphs[0]['text']['nodes'][0]['word']['words'] 237 | pics = paragraphs[1]['pic']['pics'] 238 | await bili23.send(Message(f"{GLOBAL_NICKNAME}识别:B站动态,{title}\n{desc}")) 239 | send_pics = [] 240 | for pic in pics: 241 | img = pic['url'] 242 | send_pics.append(make_node_segment(bot.self_id, MessageSegment.image(img))) 243 | # 发送异步后的数据 244 | await send_forward_both(bot, event, send_pics) 245 | return 246 | # 直播间识别 247 | if 'live' in url: 248 | # https://live.bilibili.com/30528999?hotRank=0 249 | room_id = re.search(r'\/(\d+)$', url).group(1) 250 | room = live.LiveRoom(room_display_id=int(room_id)) 251 | room_info = (await room.get_room_info())['room_info'] 252 | title, cover, keyframe = room_info['title'], room_info['cover'], room_info['keyframe'] 253 | await bili23.send(Message([MessageSegment.image(cover), MessageSegment.image(keyframe), 254 | MessageSegment.text(f"{GLOBAL_NICKNAME}识别:哔哩哔哩直播,{title}")])) 255 | return 256 | # 专栏识别 257 | if 'read' in url: 258 | read_id = re.search(r'read\/cv(\d+)', url).group(1) 259 | ar = article.Article(read_id) 260 | # 如果专栏为公开笔记,则转换为笔记类 261 | # NOTE: 笔记类的函数与专栏类的函数基本一致 262 | if ar.is_note(): 263 | ar = ar.turn_to_note() 264 | # 加载内容 265 | await ar.fetch_content() 266 | markdown_path = f'{os.getcwd()}/article.md' 267 | with open(markdown_path, 'w', encoding='utf8') as f: 268 | f.write(ar.markdown()) 269 | await bili23.send(Message(f"{GLOBAL_NICKNAME}识别:哔哩哔哩专栏")) 270 | await bili23.send(Message(MessageSegment(type="file", data={ "file": markdown_path }))) 271 | return 272 | # 收藏夹识别 273 | if 'favlist' in url and BILI_SESSDATA != '': 274 | # https://space.bilibili.com/22990202/favlist?fid=2344812202 275 | fav_id = re.search(r'favlist\?fid=(\d+)', url).group(1) 276 | fav_list = (await get_video_favorite_list_content(fav_id))['medias'][:10] 277 | favs = [] 278 | for fav in fav_list: 279 | title, cover, intro, link = fav['title'], fav['cover'], fav['intro'], fav['link'] 280 | logger.info(title, cover, intro) 281 | favs.append( 282 | [MessageSegment.image(cover), 283 | MessageSegment.text(f'🧉 标题:{title}\n📝 简介:{intro}\n🔗 链接:{link}')]) 284 | await bili23.send(f'{GLOBAL_NICKNAME}识别:哔哩哔哩收藏夹,正在为你找出相关链接请稍等...') 285 | await bili23.send(make_node_segment(bot.self_id, favs)) 286 | return 287 | # 获取视频信息 288 | video_id = re.search(r"video\/[^\?\/ ]+", url)[0].split('/')[1] 289 | v = video.Video(video_id, credential=credential) 290 | video_info = await v.get_info() 291 | if video_info is None: 292 | await bili23.send(Message(f"{GLOBAL_NICKNAME}识别:B站,出错,无法获取数据!")) 293 | return 294 | video_title, video_cover, video_desc, video_duration = video_info['title'], video_info['pic'], video_info['desc'], \ 295 | video_info['duration'] 296 | # 校准 分p 的情况 297 | page_num = 0 298 | if 'pages' in video_info: 299 | # 解析URL 300 | parsed_url = urlparse(url) 301 | # 检查是否有查询字符串 302 | if parsed_url.query: 303 | # 解析查询字符串中的参数 304 | query_params = parse_qs(parsed_url.query) 305 | # 获取指定参数的值,如果参数不存在,则返回None 306 | page_num = int(query_params.get('p', [1])[0]) - 1 307 | else: 308 | page_num = 0 309 | if 'duration' in video_info['pages'][page_num]: 310 | video_duration = video_info['pages'][page_num].get('duration', video_info.get('duration')) 311 | else: 312 | # 如果索引超出范围,使用 video_info['duration'] 或者其他默认值 313 | video_duration = video_info.get('duration', 0) 314 | # 删除特殊字符 315 | video_title = delete_boring_characters(video_title) 316 | # 截断下载时间比较长的视频 317 | online = await v.get_online() 318 | online_str = f'🏄‍♂️ 总共 {online["total"]} 人在观看,{online["count"]} 人在网页端观看' 319 | if video_duration <= VIDEO_DURATION_MAXIMUM: 320 | await bili23.send(Message(MessageSegment.image(video_cover)) + Message( 321 | f"\n{GLOBAL_NICKNAME}识别:B站,{video_title}\n{extra_bili_info(video_info)}\n📝 简介:{video_desc}\n{online_str}")) 322 | else: 323 | return await bili23.finish( 324 | Message(MessageSegment.image(video_cover)) + Message( 325 | f"\n{GLOBAL_NICKNAME}识别:B站,{video_title}\n{extra_bili_info(video_info)}\n简介:{video_desc}\n{online_str}\n---------\n⚠️ 当前视频时长 {video_duration // 60} 分钟,超过管理员设置的最长时间 {VIDEO_DURATION_MAXIMUM // 60} 分钟!")) 326 | # 获取下载链接 327 | logger.info(page_num) 328 | download_url_data = await v.get_download_url(page_index=page_num) 329 | detecter = VideoDownloadURLDataDetecter(download_url_data) 330 | streams = detecter.detect_best_streams() 331 | video_url, audio_url = streams[0].url, streams[1].url 332 | # 下载视频和音频 333 | path = os.getcwd() + "/" + video_id 334 | try: 335 | await asyncio.gather( 336 | download_b_file(video_url, f"{path}-video.m4s", logger.info), 337 | download_b_file(audio_url, f"{path}-audio.m4s", logger.info)) 338 | await merge_file_to_mp4(f"{path}-video.m4s", f"{path}-audio.m4s", f"{path}-res.mp4") 339 | finally: 340 | remove_res = remove_files([f"{path}-video.m4s", f"{path}-audio.m4s"]) 341 | logger.info(remove_res) 342 | # 发送出去 343 | # await bili23.send(Message(MessageSegment.video(f"{path}-res.mp4"))) 344 | await auto_video_send(event, f"{path}-res.mp4") 345 | # 这里是总结内容,如果写了cookie就可以 346 | if BILI_SESSDATA != '': 347 | ai_conclusion = await v.get_ai_conclusion(await v.get_cid(0)) 348 | if ai_conclusion['model_result']['summary'] != '': 349 | send_forword_summary = make_node_segment(bot.self_id, ["bilibili AI总结", 350 | ai_conclusion['model_result']['summary']]) 351 | await bili23.send(Message(send_forword_summary)) 352 | 353 | 354 | @douyin.handle() 355 | @resolve_handler 356 | @resolve_controller 357 | async def dy(bot: Bot, event: Event) -> None: 358 | """ 359 | 抖音解析 360 | :param bot: 361 | :param event: 362 | :return: 363 | """ 364 | # 消息 365 | msg: str = str(event.message).strip() 366 | logger.info(msg) 367 | # 正则匹配 368 | reg = r"(http:|https:)\/\/v.douyin.com\/[A-Za-z\d._?%&+\-=#]*" 369 | dou_url = re.search(reg, msg, re.I)[0] 370 | dou_url_2 = httpx.get(dou_url).headers.get('location') 371 | 372 | # 实况图集临时解决方案,eg. https://v.douyin.com/iDsVgJKL/ 373 | if "share/slides" in dou_url_2: 374 | cover, author, title, images = await dou_transfer_other(dou_url) 375 | # 如果第一个不为None 大概率是成功 376 | if author is not None: 377 | await douyin.send(MessageSegment.image(cover) + Message(f"{GLOBAL_NICKNAME}识别:【抖音】\n作者:{author}\n标题:{title}")) 378 | await send_forward_both(bot, event, make_node_segment(bot.self_id, [MessageSegment.image(url) for url in images])) 379 | # 截断后续操作 380 | return 381 | # logger.error(dou_url_2) 382 | reg2 = r".*(video|note)\/(\d+)\/(.*?)" 383 | # 获取到ID 384 | dou_id = re.search(reg2, dou_url_2, re.I)[2] 385 | # logger.info(dou_id) 386 | # 如果没有设置dy的ck就结束,因为获取不到 387 | douyin_ck = getattr(global_config, "douyin_ck", "") 388 | if douyin_ck == "": 389 | logger.error(global_config) 390 | await douyin.send(Message(f"{GLOBAL_NICKNAME}识别:抖音,无法获取到管理员设置的抖音ck!")) 391 | return 392 | # API、一些后续要用到的参数 393 | headers = { 394 | 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2', 395 | 'referer': f'https://www.douyin.com/video/{dou_id}', 396 | 'cookie': douyin_ck 397 | } | COMMON_HEADER 398 | api_url = DOUYIN_VIDEO.replace("{}", dou_id) 399 | api_url = generate_x_bogus_url(api_url, headers) # 如果请求失败直接返回 400 | async with aiohttp.ClientSession() as session: 401 | async with session.get(api_url, headers=headers, timeout=10) as response: 402 | detail = await response.json() 403 | if detail is None: 404 | await douyin.send(Message(f"{GLOBAL_NICKNAME}识别:抖音,解析失败!")) 405 | return 406 | # 获取信息 407 | detail = detail['aweme_detail'] 408 | # 判断是图片还是视频 409 | url_type_code = detail['aweme_type'] 410 | url_type = URL_TYPE_CODE_DICT.get(url_type_code, 'video') 411 | await douyin.send(Message(f"{GLOBAL_NICKNAME}识别:抖音,{detail.get('desc')}")) 412 | # 根据类型进行发送 413 | if url_type == 'video': 414 | # 识别播放地址 415 | player_uri = detail.get("video").get("play_addr")['uri'] 416 | player_real_addr = DY_TOUTIAO_INFO.replace("{}", player_uri) 417 | # 发送视频 418 | # logger.info(player_addr) 419 | # await douyin.send(Message(MessageSegment.video(player_addr))) 420 | await auto_video_send(event, player_real_addr) 421 | elif url_type == 'image': 422 | # 无水印图片列表/No watermark image list 423 | no_watermark_image_list = [] 424 | # 有水印图片列表/With watermark image list 425 | watermark_image_list = [] 426 | # 遍历图片列表/Traverse image list 427 | for i in detail['images']: 428 | # 无水印图片列表 429 | # no_watermark_image_list.append(i['url_list'][0]) 430 | no_watermark_image_list.append(MessageSegment.image(i['url_list'][0])) 431 | # 有水印图片列表 432 | # watermark_image_list.append(i['download_url_list'][0]) 433 | # 异步发送 434 | # logger.info(no_watermark_image_list) 435 | # imgList = await asyncio.gather([]) 436 | await send_forward_both(bot, event, make_node_segment(bot.self_id, no_watermark_image_list)) 437 | 438 | 439 | @tik.handle() 440 | @resolve_handler 441 | @resolve_controller 442 | async def tiktok(event: Event) -> None: 443 | """ 444 | tiktok解析 445 | :param event: 446 | :return: 447 | """ 448 | # 消息 449 | url: str = str(event.message).strip() 450 | 451 | # 海外服务器判断 452 | proxy = None if IS_OVERSEA else resolver_proxy 453 | 454 | url_reg = r"(http:|https:)\/\/www.tiktok.com\/[A-Za-z\d._?%&+\-=\/#@]*" 455 | url_short_reg = r"(http:|https:)\/\/vt.tiktok.com\/[A-Za-z\d._?%&+\-=\/#]*" 456 | url_short_reg2 = r"(http:|https:)\/\/vm.tiktok.com\/[A-Za-z\d._?%&+\-=\/#]*" 457 | 458 | if "vt.tiktok" in url: 459 | temp_url = re.search(url_short_reg, url)[0] 460 | temp_resp = httpx.get(temp_url, follow_redirects=True, proxies=proxy) 461 | url = temp_resp.url 462 | elif "vm.tiktok" in url: 463 | temp_url = re.search(url_short_reg2, url)[0] 464 | temp_resp = httpx.get(temp_url, headers={ "User-Agent": "facebookexternalhit/1.1" }, follow_redirects=True, 465 | proxies=proxy) 466 | url = str(temp_resp.url) 467 | # logger.info(url) 468 | else: 469 | url = re.search(url_reg, url)[0] 470 | title = await get_video_title(url, IS_OVERSEA, resolver_proxy, 'tiktok') 471 | 472 | await tik.send(Message(f"{GLOBAL_NICKNAME}识别:TikTok,{title}\n")) 473 | 474 | target_tik_video_path = await download_ytb_video(url, IS_OVERSEA, os.getcwd(), resolver_proxy, 'tiktok') 475 | 476 | await auto_video_send(event, target_tik_video_path) 477 | 478 | 479 | @acfun.handle() 480 | @resolve_handler 481 | @resolve_controller 482 | async def ac(event: Event) -> None: 483 | """ 484 | acfun解析 485 | :param event: 486 | :return: 487 | """ 488 | # 消息 489 | inputMsg: str = str(event.message).strip() 490 | 491 | # 短号处理 492 | if "m.acfun.cn" in inputMsg: 493 | inputMsg = f"https://www.acfun.cn/v/ac{re.search(r'ac=([^&?]*)', inputMsg)[1]}" 494 | 495 | url_m3u8s, video_name = parse_url(inputMsg) 496 | await acfun.send(Message(f"{GLOBAL_NICKNAME}识别:猴山,{video_name}")) 497 | m3u8_full_urls, ts_names, output_folder_name, output_file_name = parse_m3u8(url_m3u8s) 498 | # logger.info(output_folder_name, output_file_name) 499 | await asyncio.gather(*[download_m3u8_videos(url, i) for i, url in enumerate(m3u8_full_urls)]) 500 | merge_ac_file_to_mp4(ts_names, output_file_name) 501 | # await acfun.send(Message(MessageSegment.video(f"{os.getcwd()}/{output_file_name}"))) 502 | await auto_video_send(event, f"{os.getcwd()}/{output_file_name}") 503 | 504 | 505 | @twit.handle() 506 | @resolve_handler 507 | @resolve_controller 508 | async def twitter(bot: Bot, event: Event): 509 | """ 510 | X解析 511 | :param bot: 512 | :param event: 513 | :return: 514 | """ 515 | msg: str = str(event.message).strip() 516 | x_url = re.search(r"https?:\/\/x.com\/[0-9-a-zA-Z_]{1,20}\/status\/([0-9]*)", msg)[0] 517 | 518 | x_url = GENERAL_REQ_LINK.replace("{}", x_url) 519 | 520 | # 内联一个请求 521 | def x_req(url): 522 | return httpx.get(url, headers={ 523 | 'Accept': 'ext/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,' 524 | 'application/signed-exchange;v=b3;q=0.7', 525 | 'Accept-Encoding': 'gzip, deflate', 526 | 'Accept-Language': 'zh-CN,zh;q=0.9', 527 | 'Host': '47.99.158.118', 528 | 'Proxy-Connection': 'keep-alive', 529 | 'Upgrade-Insecure-Requests': '1', 530 | 'Sec-Fetch-User': '?1', 531 | **COMMON_HEADER 532 | }) 533 | 534 | x_data: object = x_req(x_url).json()['data'] 535 | 536 | if x_data is None: 537 | x_url = x_url + '/photo/1' 538 | logger.info(x_url) 539 | x_data = x_req(x_url).json()['data'] 540 | 541 | x_url_res = x_data['url'] 542 | 543 | await twit.send(Message(f"{GLOBAL_NICKNAME}识别:小蓝鸟学习版")) 544 | 545 | # 海外服务器判断 546 | proxy = None if IS_OVERSEA else resolver_proxy 547 | 548 | # 图片 549 | if x_url_res.endswith(".jpg") or x_url_res.endswith(".png"): 550 | res = await download_img(x_url_res, '', proxy) 551 | else: 552 | # 视频 553 | res = await download_video(x_url_res, proxy) 554 | aio_task_res = auto_determine_send_type(int(bot.self_id), res) 555 | 556 | # 发送异步后的数据 557 | await send_forward_both(bot, event, aio_task_res) 558 | 559 | # 清除垃圾 560 | os.unlink(res) 561 | 562 | 563 | @xhs.handle() 564 | @resolve_handler 565 | @resolve_controller 566 | async def xiaohongshu(bot: Bot, event: Event): 567 | """ 568 | 小红书解析 569 | :param event: 570 | :return: 571 | """ 572 | msg_url = re.search(r"(http:|https:)\/\/(xhslink|(www\.)xiaohongshu).com\/[A-Za-z\d._?%&+\-=\/#@]*", 573 | str(event.message).replace("&", "&").strip())[0] 574 | # 如果没有设置xhs的ck就结束,因为获取不到 575 | xhs_ck = getattr(global_config, "xhs_ck", "") 576 | if xhs_ck == "": 577 | logger.error(global_config) 578 | await xhs.send(Message(f"{GLOBAL_NICKNAME}识别内容来自:【小红书】\n无法获取到管理员设置的小红书ck!")) 579 | return 580 | # 请求头 581 | headers = { 582 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,' 583 | 'application/signed-exchange;v=b3;q=0.9', 584 | 'cookie': xhs_ck, 585 | } | COMMON_HEADER 586 | if "xhslink" in msg_url: 587 | msg_url = httpx.get(msg_url, headers=headers, follow_redirects=True).url 588 | msg_url = str(msg_url) 589 | xhs_id = re.search(r'/explore/(\w+)', msg_url) 590 | if not xhs_id: 591 | xhs_id = re.search(r'/discovery/item/(\w+)', msg_url) 592 | if not xhs_id: 593 | xhs_id = re.search(r'source=note¬eId=(\w+)', msg_url) 594 | xhs_id = xhs_id[1] 595 | # 解析 URL 参数 596 | parsed_url = urlparse(msg_url) 597 | params = parse_qs(parsed_url.query) 598 | # 提取 xsec_source 和 xsec_token 599 | xsec_source = params.get('xsec_source', [None])[0] or "pc_feed" 600 | xsec_token = params.get('xsec_token', [None])[0] 601 | 602 | html = httpx.get(f'{XHS_REQ_LINK}{xhs_id}?xsec_source={xsec_source}&xsec_token={xsec_token}', headers=headers).text 603 | # response_json = re.findall('window.__INITIAL_STATE__=(.*?)', html)[0] 604 | try: 605 | response_json = re.findall('window.__INITIAL_STATE__=(.*?)', html)[0] 606 | except IndexError: 607 | await xhs.send( 608 | Message(f"{GLOBAL_NICKNAME}识别内容来自:【小红书】\n当前ck已失效,请联系管理员重新设置的小红书ck!")) 609 | return 610 | response_json = response_json.replace("undefined", "null") 611 | response_json = json.loads(response_json) 612 | note_data = response_json['note']['noteDetailMap'][xhs_id]['note'] 613 | type = note_data['type'] 614 | note_title = note_data['title'] 615 | note_desc = note_data['desc'] 616 | await xhs.send(Message( 617 | f"{GLOBAL_NICKNAME}识别:小红书,{note_title}\n{note_desc}")) 618 | 619 | aio_task = [] 620 | if type == 'normal': 621 | image_list = note_data['imageList'] 622 | # 批量下载 623 | async with aiohttp.ClientSession() as session: 624 | for index, item in enumerate(image_list): 625 | aio_task.append(asyncio.create_task( 626 | download_img(item['urlDefault'], f'{os.getcwd()}/{str(index)}.jpg', session=session))) 627 | links_path = await asyncio.gather(*aio_task) 628 | elif type == 'video': 629 | # 这是一条解析有水印的视频 630 | logger.info(note_data['video']) 631 | 632 | video_url = note_data['video']['media']['stream']['h264'][0]['masterUrl'] 633 | 634 | # ⚠️ 废弃,解析无水印视频video.consumer.originVideoKey 635 | # video_url = f"http://sns-video-bd.xhscdn.com/{note_data['video']['consumer']['originVideoKey']}" 636 | path = await download_video(video_url) 637 | # await xhs.send(Message(MessageSegment.video(path))) 638 | await auto_video_send(event, path) 639 | return 640 | # 发送图片 641 | links = make_node_segment(bot.self_id, 642 | [MessageSegment.image(f"file://{link}") for link in links_path]) 643 | # 发送异步后的数据 644 | await send_forward_both(bot, event, links) 645 | # 清除图片 646 | for temp in links_path: 647 | os.unlink(temp) 648 | 649 | 650 | @y2b.handle() 651 | @resolve_handler 652 | @resolve_controller 653 | async def youtube(bot: Bot, event: Event): 654 | msg_url = re.search( 655 | r"(?:https?:\/\/)?(www\.)?youtube\.com\/[A-Za-z\d._?%&+\-=\/#]*|(?:https?:\/\/)?youtu\.be\/[A-Za-z\d._?%&+\-=\/#]*", 656 | str(event.message).strip())[0] 657 | 658 | # 海外服务器判断 659 | proxy = None if IS_OVERSEA else resolver_proxy 660 | 661 | title = await get_video_title(msg_url, IS_OVERSEA, proxy) 662 | 663 | await y2b.send(Message(f"{GLOBAL_NICKNAME}识别:油管,{title}\n")) 664 | 665 | target_ytb_video_path = await download_ytb_video(msg_url, IS_OVERSEA, os.getcwd(), proxy) 666 | 667 | await auto_video_send(event, target_ytb_video_path) 668 | 669 | 670 | @ncm.handle() 671 | @resolve_handler 672 | @resolve_controller 673 | async def netease(bot: Bot, event: Event): 674 | message = str(event.message) 675 | # 识别短链接 676 | if "163cn.tv" in message: 677 | message = re.search(r"(http:|https:)\/\/163cn\.tv\/([a-zA-Z0-9]+)", message).group(0) 678 | message = str(httpx.head(message, follow_redirects=True).url) 679 | 680 | ncm_id = re.search(r"id=(\d+)", message).group(1) 681 | if ncm_id is None: 682 | await ncm.finish(Message(f"❌ {GLOBAL_NICKNAME}识别:网易云,获取链接失败")) 683 | # 拼接获取信息的链接 684 | # ncm_detail_url = f'{NETEASE_API_CN}/song/detail?ids={ncm_id}' 685 | # ncm_detail_resp = httpx.get(ncm_detail_url, headers=COMMON_HEADER) 686 | # # 获取歌曲名 687 | # ncm_song = ncm_detail_resp.json()['songs'][0] 688 | # ncm_title = f'{ncm_song["name"]}-{ncm_song["ar"][0]["name"]}'.replace(r'[\/\?<>\\:\*\|".… ]', "") 689 | 690 | # 对接临时接口 691 | ncm_vip_data = httpx.get(f"{NETEASE_TEMP_API.replace('{}', ncm_id)}", headers=COMMON_HEADER).json() 692 | ncm_url = ncm_vip_data['music_url'] 693 | ncm_cover = ncm_vip_data['cover'] 694 | ncm_singer = ncm_vip_data['singer'] 695 | ncm_title = ncm_vip_data['title'] 696 | await ncm.send(Message( 697 | [MessageSegment.image(ncm_cover), MessageSegment.text(f'{GLOBAL_NICKNAME}识别:网易云音乐,{ncm_title}-{ncm_singer}')])) 698 | # 下载音频文件后会返回一个下载路径 699 | ncm_music_path = await download_audio(ncm_url) 700 | # 发送语音 701 | await ncm.send(Message(MessageSegment.record(ncm_music_path))) 702 | # 发送群文件 703 | await upload_both(bot, event, ncm_music_path, f'{ncm_title}-{ncm_singer}.{ncm_music_path.split(".")[-1]}') 704 | if os.path.exists(ncm_music_path): 705 | os.unlink(ncm_music_path) 706 | 707 | 708 | @kg.handle() 709 | @resolve_handler 710 | @resolve_controller 711 | async def kugou(bot: Bot, event: Event): 712 | message = str(event.message) 713 | # logger.info(message) 714 | reg1 = r"https?://.*?kugou\.com.*?(?=\s|$|\n)" 715 | reg2 = r'jumpUrl":\s*"(https?:\\/\\/[^"]+)"' 716 | reg3 = r'jumpUrl":\s*"(https?://[^"]+)"' 717 | # 处理卡片问题 718 | if 'com.tencent.structmsg' in message: 719 | match = re.search(reg2, message) 720 | if match: 721 | get_url = match.group(1) 722 | else: 723 | match = re.search(reg3, message) 724 | if match: 725 | get_url = match.group(1) 726 | else: 727 | await kg.send(Message(f"{GLOBAL_NICKNAME}\n来源:【酷狗音乐】\n获取链接失败")) 728 | get_url = None 729 | return 730 | if get_url: 731 | url = json.loads('"' + get_url + '"') 732 | else: 733 | match = re.search(reg1, message) 734 | url = match.group() 735 | 736 | # 使用 httpx 获取 URL 的标题 737 | response = httpx.get(url, follow_redirects=True) 738 | if response.status_code == 200: 739 | title = response.text 740 | get_name = r"(.*?)_高音质在线试听" 741 | name = re.search(get_name, title) 742 | if name: 743 | kugou_title = name.group(1) # 只输出歌曲名和歌手名的部分 744 | kugou_vip_data = httpx.get(f"{KUGOU_TEMP_API.replace('{}', kugou_title)}", headers=COMMON_HEADER).json() 745 | # logger.info(kugou_vip_data) 746 | kugou_url = kugou_vip_data.get('music_url') 747 | kugou_cover = kugou_vip_data.get('cover') 748 | kugou_name = kugou_vip_data.get('title') 749 | kugou_singer = kugou_vip_data.get('singer') 750 | await kg.send(Message( 751 | [MessageSegment.image(kugou_cover), 752 | MessageSegment.text(f'{GLOBAL_NICKNAME}\n来源:【酷狗音乐】\n歌曲:{kugou_name}-{kugou_singer}')])) 753 | # 下载音频文件后会返回一个下载路径 754 | kugou_music_path = await download_audio(kugou_url) 755 | # 发送语音 756 | await kg.send(Message(MessageSegment.record(kugou_music_path))) 757 | # 发送群文件 758 | await upload_both(bot, event, kugou_music_path, 759 | f'{kugou_name}-{kugou_singer}.{kugou_music_path.split(".")[-1]}') 760 | if os.path.exists(kugou_music_path): 761 | os.unlink(kugou_music_path) 762 | else: 763 | await kg.send(Message(f"{GLOBAL_NICKNAME}\n来源:【酷狗音乐】\n不支持当前外链,请重新分享再试")) 764 | else: 765 | await kg.send(Message(f"{GLOBAL_NICKNAME}\n来源:【酷狗音乐】\n获取链接失败")) 766 | 767 | 768 | @weibo.handle() 769 | @resolve_handler 770 | @resolve_controller 771 | async def wb(bot: Bot, event: Event): 772 | message = str(event.message) 773 | weibo_id = None 774 | reg = r'(jumpUrl|qqdocurl)": ?"(.*?)"' 775 | 776 | # 处理卡片问题 777 | if 'com.tencent.structmsg' or 'com.tencent.miniapp' in message: 778 | match = re.search(reg, message) 779 | print(match) 780 | if match: 781 | get_url = match.group(2) 782 | print(get_url) 783 | if get_url: 784 | message = json.loads('"' + get_url + '"') 785 | else: 786 | message = message 787 | # logger.info(message) 788 | # 判断是否包含 "m.weibo.cn" 789 | if "m.weibo.cn" in message: 790 | # https://m.weibo.cn/detail/4976424138313924 791 | match = re.search(r'(?<=detail/)[A-Za-z\d]+', message) or re.search(r'(?<=m.weibo.cn/)[A-Za-z\d]+/[A-Za-z\d]+', 792 | message) 793 | weibo_id = match.group(0) if match else None 794 | 795 | # 判断是否包含 "weibo.com/tv/show" 且包含 "mid=" 796 | elif "weibo.com/tv/show" in message and "mid=" in message: 797 | # https://weibo.com/tv/show/1034:5007449447661594?mid=5007452630158934 798 | match = re.search(r'(?<=mid=)[A-Za-z\d]+', message) 799 | if match: 800 | weibo_id = mid2id(match.group(0)) 801 | 802 | # 判断是否包含 "weibo.com" 803 | elif "weibo.com" in message: 804 | # https://weibo.com/1707895270/5006106478773472 805 | match = re.search(r'(?<=weibo.com/)[A-Za-z\d]+/[A-Za-z\d]+', message) 806 | weibo_id = match.group(0) if match else None 807 | 808 | # 无法获取到id则返回失败信息 809 | if not weibo_id: 810 | await weibo.finish(Message("解析失败:无法获取到wb的id")) 811 | # 最终获取到的 id 812 | weibo_id = weibo_id.split("/")[1] if "/" in weibo_id else weibo_id 813 | logger.info(weibo_id) 814 | # 请求数据 815 | resp = httpx.get(WEIBO_SINGLE_INFO.replace('{}', weibo_id), headers={ 816 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 817 | "cookie": "_T_WM=40835919903; WEIBOCN_FROM=1110006030; MLOGIN=0; XSRF-TOKEN=4399c8", 818 | "Referer": f"https://m.weibo.cn/detail/{id}", 819 | } | COMMON_HEADER).json() 820 | weibo_data = resp['data'] 821 | logger.info(weibo_data) 822 | text, status_title, source, region_name, pics, page_info = (weibo_data.get(key, None) for key in 823 | ['text', 'status_title', 'source', 'region_name', 824 | 'pics', 'page_info']) 825 | # 发送消息 826 | await weibo.send( 827 | Message( 828 | f"{GLOBAL_NICKNAME}识别:微博,{re.sub(r'<[^>]+>', '', text)}\n{status_title}\n{source}\t{region_name if region_name else ''}")) 829 | if pics: 830 | pics = map(lambda x: x['url'], pics) 831 | download_img_funcs = [asyncio.create_task(download_img(item, '', headers={ 832 | "Referer": "http://blog.sina.com.cn/" 833 | } | COMMON_HEADER)) for item in pics] 834 | links_path = await asyncio.gather(*download_img_funcs) 835 | # 发送图片 836 | links = make_node_segment(bot.self_id, 837 | [MessageSegment.image(f"file://{link}") for link in links_path]) 838 | # 发送异步后的数据 839 | await send_forward_both(bot, event, links) 840 | # 清除图片 841 | for temp in links_path: 842 | os.unlink(temp) 843 | if page_info: 844 | video_url = page_info.get('urls', '').get('mp4_720p_mp4', '') or page_info.get('urls', '').get('mp4_hd_mp4', '') 845 | if video_url: 846 | path = await download_video(video_url, ext_headers={ 847 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", 848 | "referer": "https://weibo.com/" 849 | }) 850 | await auto_video_send(event, path) 851 | 852 | 853 | def auto_determine_send_type(user_id: int, task: str): 854 | """ 855 | 判断是视频还是图片然后发送最后删除,函数在 twitter 这类可以图、视频混合发送的媒体十分有用 856 | :param user_id: 857 | :param task: 858 | :return: 859 | """ 860 | if task.endswith("jpg") or task.endswith("png"): 861 | return MessageSegment.node_custom(user_id=user_id, nickname=GLOBAL_NICKNAME, 862 | content=Message(MessageSegment.image(task))) 863 | elif task.endswith("mp4"): 864 | return MessageSegment.node_custom(user_id=user_id, nickname=GLOBAL_NICKNAME, 865 | content=Message(MessageSegment.video(task))) 866 | 867 | 868 | def make_node_segment(user_id, segments: Union[MessageSegment, List]) -> Union[ 869 | MessageSegment, Iterable[MessageSegment]]: 870 | """ 871 | 将消息封装成 Segment 的 Node 类型,可以传入单个也可以传入多个,返回一个封装好的转发类型 872 | :param user_id: 可以通过event获取 873 | :param segments: 一般为 MessageSegment.image / MessageSegment.video / MessageSegment.text 874 | :return: 875 | """ 876 | if isinstance(segments, list): 877 | return [MessageSegment.node_custom(user_id=user_id, nickname=GLOBAL_NICKNAME, 878 | content=Message(segment)) for segment in segments] 879 | return MessageSegment.node_custom(user_id=user_id, nickname=GLOBAL_NICKNAME, 880 | content=Message(segments)) 881 | 882 | 883 | async def send_forward_both(bot: Bot, event: Event, segments: Union[MessageSegment, List]) -> None: 884 | """ 885 | 自动判断message是 List 还是单个,然后发送{转发},允许发送群和个人 886 | :param bot: 887 | :param event: 888 | :param segments: 889 | :return: 890 | """ 891 | if isinstance(event, GroupMessageEvent): 892 | await bot.send_group_forward_msg(group_id=event.group_id, 893 | messages=segments) 894 | else: 895 | await bot.send_private_forward_msg(user_id=event.user_id, 896 | messages=segments) 897 | 898 | 899 | async def send_both(bot: Bot, event: Event, segments: MessageSegment) -> None: 900 | """ 901 | 自动判断message是 List 还是单个,发送{单个消息},允许发送群和个人 902 | :param bot: 903 | :param event: 904 | :param segments: 905 | :return: 906 | """ 907 | if isinstance(event, GroupMessageEvent): 908 | await bot.send_group_msg(group_id=event.group_id, 909 | message=Message(segments)) 910 | elif isinstance(event, PrivateMessageEvent): 911 | await bot.send_private_msg(user_id=event.user_id, 912 | message=Message(segments)) 913 | 914 | 915 | async def upload_both(bot: Bot, event: Event, file_path: str, name: str) -> None: 916 | """ 917 | 上传文件,不限于群和个人 918 | :param bot: 919 | :param event: 920 | :param file_path: 921 | :param name: 922 | :return: 923 | """ 924 | if isinstance(event, GroupMessageEvent): 925 | # 上传群文件 926 | await bot.upload_group_file(group_id=event.group_id, file=file_path, name=name) 927 | elif isinstance(event, PrivateMessageEvent): 928 | # 上传私聊文件 929 | await bot.upload_private_file(user_id=event.user_id, file=file_path, name=name) 930 | 931 | 932 | def get_id_both(event: Event): 933 | if isinstance(event, GroupMessageEvent): 934 | return event.group_id 935 | elif isinstance(event, PrivateMessageEvent): 936 | return event.user_id 937 | 938 | 939 | async def auto_video_send(event: Event, data_path: str): 940 | """ 941 | 自动判断视频类型并进行发送,支持群发和私发 942 | :param event: 943 | :param data_path: 944 | :return: 945 | """ 946 | try: 947 | bot: Bot = cast(Bot, current_bot.get()) 948 | 949 | # 如果data以"http"开头,先下载视频 950 | if data_path is not None and data_path.startswith("http"): 951 | data_path = await download_video(data_path) 952 | 953 | # 检测文件大小 954 | file_size_in_mb = get_file_size_mb(data_path) 955 | # 如果视频大于 100 MB 自动转换为群文件 956 | if file_size_in_mb > VIDEO_MAX_MB: 957 | await bot.send(event, Message( 958 | f"当前解析文件 {file_size_in_mb} MB 大于 {VIDEO_MAX_MB} MB,尝试改用文件方式发送,请稍等...")) 959 | await upload_both(bot, event, data_path, data_path.split('/')[-1]) 960 | return 961 | # 根据事件类型发送不同的消息 962 | await send_both(bot, event, MessageSegment.video(f'file://{data_path}')) 963 | except Exception as e: 964 | logger.error(f"解析发送出现错误,具体为\n{e}") 965 | finally: 966 | # 删除临时文件 967 | if os.path.exists(data_path): 968 | os.unlink(data_path) 969 | if os.path.exists(data_path + '.jpg'): 970 | os.unlink(data_path + '.jpg') 971 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Extra 2 | from typing import Optional 3 | 4 | 5 | class Config(BaseModel, extra=Extra.ignore): 6 | xhs_ck: Optional[str] = '' 7 | douyin_ck: Optional[str] = '' 8 | is_oversea: Optional[bool] = False 9 | bili_sessdata: Optional[str] = '' 10 | r_global_nickname: Optional[str] = '' 11 | resolver_proxy: Optional[str] = 'http://127.0.0.1:7890' 12 | video_duration_maximum: Optional[int] = 480 13 | global_resolve_controller: Optional[str] = "" 14 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import pkgutil 4 | # from nonebot import logger 5 | 6 | # 获取当前包下的所有模块 7 | package_dir = os.path.dirname(__file__) 8 | 9 | # 模块列表,用于存储导入的模块 10 | modules = [] 11 | 12 | # 遍历并导入包中的所有模块 13 | for module_info in pkgutil.iter_modules([package_dir]): 14 | module_name = module_info.name 15 | # logger.info(f"Importing module: {module_name}") 16 | module = importlib.import_module(f".{module_name}", package=__name__) 17 | modules.append(module) 18 | 19 | # logger.info(f"Imported modules: {[module.__name__ for module in modules]}") 20 | 21 | # 动态导出每个模块中定义的常量 22 | __all__ = [] 23 | 24 | for module in modules: 25 | # logger.info(f"Checking module: {module.__name__}") 26 | for name in dir(module): 27 | if name.isupper(): # 假设常量以大写字母命名 28 | # logger.info(f"Found constant: {name} in module {module.__name__}") 29 | globals()[name] = getattr(module, name) 30 | __all__.append(name) 31 | 32 | # logger.info(f"Exported constants: {__all__}") 33 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/bili.py: -------------------------------------------------------------------------------- 1 | """ 2 | 哔哩哔哩的头请求 3 | """ 4 | BILIBILI_HEADER = { 5 | 'User-Agent': 6 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 ' 7 | 'Safari/537.36', 8 | 'referer': 'https://www.bilibili.com', 9 | } -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | 通用解析 3 | """ 4 | GENERAL_REQ_LINK = "http://47.99.158.118/video-crack/v2/parse?content={}" 5 | 6 | """ 7 | 通用头请求 8 | """ 9 | COMMON_HEADER = { 10 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 ' 11 | 'UBrowser/6.2.4098.3 Safari/537.36' 12 | } 13 | 14 | """ 15 | 视频最大大小(MB) 16 | """ 17 | VIDEO_MAX_MB = 100 18 | 19 | # 插件名字 20 | PLUGIN_NAME = "nonebot-plugin-resolver" 21 | 22 | # 解析列表文件名 23 | RESOLVE_SHUTDOWN_LIST_NAME = "resolver_shutdown_list" 24 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/kugou.py: -------------------------------------------------------------------------------- 1 | """ 2 | KG临时接口 3 | """ 4 | KUGOU_TEMP_API = "https://www.hhlqilongzhu.cn/api/dg_kugouSQ.php?msg={}&n=1&type=json" 5 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/ncm.py: -------------------------------------------------------------------------------- 1 | """ 2 | NCM获取歌曲信息链接 3 | """ 4 | NETEASE_API_CN = 'https://www.markingchen.ink' 5 | 6 | """ 7 | NCM临时接口 8 | """ 9 | NETEASE_TEMP_API = "https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?id={}&br=7&type=json" 10 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/tiktok.py: -------------------------------------------------------------------------------- 1 | """以下为抖音/TikTok类型代码/Type code for Douyin/TikTok""" 2 | URL_TYPE_CODE_DICT = { 3 | # 抖音/Douyin 4 | 2: 'image', 5 | 4: 'video', 6 | 68: 'image', 7 | # TikTok 8 | 0: 'video', 9 | 51: 'video', 10 | 55: 'video', 11 | 58: 'video', 12 | 61: 'video', 13 | 150: 'image' 14 | } 15 | 16 | """ 17 | dy视频信息 18 | """ 19 | DOUYIN_VIDEO = "https://www.douyin.com/aweme/v1/web/aweme/detail/?device_platform=webapp&aid=6383&channel=channel_pc_web&aweme_id={}&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1344&screen_height=756&browser_language=zh-CN&browser_platform=Win32&browser_name=Firefox&browser_version=118.0&browser_online=true&engine_name=Gecko&engine_version=109.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=&platform=PC" 20 | 21 | """ 22 | 今日头条 DY API 23 | """ 24 | DY_TOUTIAO_INFO = "https://aweme.snssdk.com/aweme/v1/play/?video_id={}&ratio=1080p&line=0" 25 | 26 | """ 27 | tiktok视频信息 28 | """ 29 | TIKTOK_VIDEO = "https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/" -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/weibo.py: -------------------------------------------------------------------------------- 1 | WEIBO_SINGLE_INFO = "https://m.weibo.cn/statuses/show?id={}" -------------------------------------------------------------------------------- /nonebot-plugin-resolver/constants/xhs.py: -------------------------------------------------------------------------------- 1 | """ 2 | 小红书下载链接 3 | """ 4 | XHS_REQ_LINK = "https://www.xiaohongshu.com/explore/" -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhiyu1998/nonebot-plugin-resolver/1c4c47b9c0f6acc5e2d89c45558beac7c5ee87ed/nonebot-plugin-resolver/core/__init__.py -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/a-bogus.js: -------------------------------------------------------------------------------- 1 | // All the content in this article is only for learning and communication use, not for any other purpose, strictly prohibited for commercial use and illegal use, otherwise all the consequences are irrelevant to the author! 2 | function rc4_encrypt(plaintext, key) { 3 | var s = [] 4 | for (var i = 0; i < 256; i++) { 5 | s[i] = i 6 | } 7 | var j = 0 8 | for (var i = 0; i < 256; i++) { 9 | j = (j + s[i] + key.charCodeAt(i % key.length)) % 256 10 | var temp = s[i] 11 | s[i] = s[j] 12 | s[j] = temp 13 | } 14 | 15 | var i = 0 16 | var j = 0 17 | var cipher = [] 18 | for (var k = 0; k < plaintext.length; k++) { 19 | i = (i + 1) % 256 20 | j = (j + s[i]) % 256 21 | var temp = s[i] 22 | s[i] = s[j] 23 | s[j] = temp 24 | var t = (s[i] + s[j]) % 256 25 | cipher.push(String.fromCharCode(s[t] ^ plaintext.charCodeAt(k))) 26 | } 27 | return cipher.join('') 28 | } 29 | 30 | function le(e, r) { 31 | return ((e << (r %= 32)) | (e >>> (32 - r))) >>> 0 32 | } 33 | 34 | function de(e) { 35 | return 0 <= e && e < 16 ? 2043430169 : 16 <= e && e < 64 ? 2055708042 : void console['error']('invalid j for constant Tj') 36 | } 37 | 38 | function pe(e, r, t, n) { 39 | return 0 <= e && e < 16 40 | ? (r ^ t ^ n) >>> 0 41 | : 16 <= e && e < 64 42 | ? ((r & t) | (r & n) | (t & n)) >>> 0 43 | : (console['error']('invalid j for bool function FF'), 0) 44 | } 45 | 46 | function he(e, r, t, n) { 47 | return 0 <= e && e < 16 ? (r ^ t ^ n) >>> 0 : 16 <= e && e < 64 ? ((r & t) | (~r & n)) >>> 0 : (console['error']('invalid j for bool function GG'), 0) 48 | } 49 | 50 | function reset() { 51 | ;(this.reg[0] = 1937774191), 52 | (this.reg[1] = 1226093241), 53 | (this.reg[2] = 388252375), 54 | (this.reg[3] = 3666478592), 55 | (this.reg[4] = 2842636476), 56 | (this.reg[5] = 372324522), 57 | (this.reg[6] = 3817729613), 58 | (this.reg[7] = 2969243214), 59 | (this['chunk'] = []), 60 | (this['size'] = 0) 61 | } 62 | 63 | function write(e) { 64 | let n 65 | var a = 66 | 'string' == typeof e 67 | ? (function (e) { 68 | ;(n = encodeURIComponent(e)['replace'](/%([0-9A-F]{2})/g, function (e, r) { 69 | return String['fromCharCode']('0x' + r) 70 | })), 71 | (a = new Array(n['length'])) 72 | return ( 73 | Array['prototype']['forEach']['call'](n, function (e, r) { 74 | a[r] = e.charCodeAt(0) 75 | }), 76 | a 77 | ) 78 | })(e) 79 | : e 80 | this.size += a.length 81 | var f = 64 - this['chunk']['length'] 82 | if (a['length'] < f) this['chunk'] = this['chunk'].concat(a) 83 | else 84 | for (this['chunk'] = this['chunk'].concat(a.slice(0, f)); this['chunk'].length >= 64; ) 85 | this['_compress'](this['chunk']), f < a['length'] ? (this['chunk'] = a['slice'](f, Math['min'](f + 64, a['length']))) : (this['chunk'] = []), (f += 64) 86 | } 87 | 88 | function sum(e, t) { 89 | e && (this['reset'](), this['write'](e)), this['_fill']() 90 | for (var f = 0; f < this.chunk['length']; f += 64) this._compress(this['chunk']['slice'](f, f + 64)) 91 | var i = null 92 | if (t == 'hex') { 93 | i = '' 94 | for (f = 0; f < 8; f++) i += se(this['reg'][f]['toString'](16), 8, '0') 95 | } else 96 | for (i = new Array(32), f = 0; f < 8; f++) { 97 | var c = this.reg[f] 98 | ;(i[4 * f + 3] = (255 & c) >>> 0), 99 | (c >>>= 8), 100 | (i[4 * f + 2] = (255 & c) >>> 0), 101 | (c >>>= 8), 102 | (i[4 * f + 1] = (255 & c) >>> 0), 103 | (c >>>= 8), 104 | (i[4 * f] = (255 & c) >>> 0) 105 | } 106 | return this['reset'](), i 107 | } 108 | 109 | function _compress(t) { 110 | if (t < 64) console.error('compress error: not enough data') 111 | else { 112 | for ( 113 | var f = (function (e) { 114 | for (var r = new Array(132), t = 0; t < 16; t++) 115 | (r[t] = e[4 * t] << 24), (r[t] |= e[4 * t + 1] << 16), (r[t] |= e[4 * t + 2] << 8), (r[t] |= e[4 * t + 3]), (r[t] >>>= 0) 116 | for (var n = 16; n < 68; n++) { 117 | var a = r[n - 16] ^ r[n - 9] ^ le(r[n - 3], 15) 118 | ;(a = a ^ le(a, 15) ^ le(a, 23)), (r[n] = (a ^ le(r[n - 13], 7) ^ r[n - 6]) >>> 0) 119 | } 120 | for (n = 0; n < 64; n++) r[n + 68] = (r[n] ^ r[n + 4]) >>> 0 121 | return r 122 | })(t), 123 | i = this['reg'].slice(0), 124 | c = 0; 125 | c < 64; 126 | c++ 127 | ) { 128 | var o = le(i[0], 12) + i[4] + le(de(c), c), 129 | s = ((o = le((o = (4294967295 & o) >>> 0), 7)) ^ le(i[0], 12)) >>> 0, 130 | u = pe(c, i[0], i[1], i[2]) 131 | u = (4294967295 & (u = u + i[3] + s + f[c + 68])) >>> 0 132 | var b = he(c, i[4], i[5], i[6]) 133 | ;(b = (4294967295 & (b = b + i[7] + o + f[c])) >>> 0), 134 | (i[3] = i[2]), 135 | (i[2] = le(i[1], 9)), 136 | (i[1] = i[0]), 137 | (i[0] = u), 138 | (i[7] = i[6]), 139 | (i[6] = le(i[5], 19)), 140 | (i[5] = i[4]), 141 | (i[4] = (b ^ le(b, 9) ^ le(b, 17)) >>> 0) 142 | } 143 | for (var l = 0; l < 8; l++) this['reg'][l] = (this['reg'][l] ^ i[l]) >>> 0 144 | } 145 | } 146 | 147 | function _fill() { 148 | var a = 8 * this['size'], 149 | f = this['chunk']['push'](128) % 64 150 | for (64 - f < 8 && (f -= 64); f < 56; f++) this.chunk['push'](0) 151 | for (var i = 0; i < 4; i++) { 152 | var c = Math['floor'](a / 4294967296) 153 | this['chunk'].push((c >>> (8 * (3 - i))) & 255) 154 | } 155 | for (i = 0; i < 4; i++) this['chunk']['push']((a >>> (8 * (3 - i))) & 255) 156 | } 157 | 158 | function SM3() { 159 | this.reg = [] 160 | this.chunk = [] 161 | this.size = 0 162 | this.reset() 163 | } 164 | SM3.prototype.reset = reset 165 | SM3.prototype.write = write 166 | SM3.prototype.sum = sum 167 | SM3.prototype._compress = _compress 168 | SM3.prototype._fill = _fill 169 | 170 | function result_encrypt(long_str, num = null) { 171 | let s_obj = { 172 | s0: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=', 173 | s1: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25+WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=', 174 | s2: 'Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=', 175 | s3: 'ckdp1h4ZKsUB80/Mfvw36XIgR25+WQAlEi7NLboqYTOPuzmFjJnryx9HVGDaStCe', 176 | s4: 'Dkdpgh2ZmsQB80/MfvV36XI1R45-WUAlEixNLwoqYTOPuzKFjJnry79HbGcaStCe', 177 | } 178 | let constant = { 179 | 0: 16515072, 180 | 1: 258048, 181 | 2: 4032, 182 | str: s_obj[num], 183 | } 184 | 185 | let result = '' 186 | let lound = 0 187 | let long_int = get_long_int(lound, long_str) 188 | for (let i = 0; i < (long_str.length / 3) * 4; i++) { 189 | if (Math.floor(i / 4) !== lound) { 190 | lound += 1 191 | long_int = get_long_int(lound, long_str) 192 | } 193 | let key = i % 4 194 | switch (key) { 195 | case 0: 196 | temp_int = (long_int & constant['0']) >> 18 197 | result += constant['str'].charAt(temp_int) 198 | break 199 | case 1: 200 | temp_int = (long_int & constant['1']) >> 12 201 | result += constant['str'].charAt(temp_int) 202 | break 203 | case 2: 204 | temp_int = (long_int & constant['2']) >> 6 205 | result += constant['str'].charAt(temp_int) 206 | break 207 | case 3: 208 | temp_int = long_int & 63 209 | result += constant['str'].charAt(temp_int) 210 | break 211 | default: 212 | break 213 | } 214 | } 215 | return result 216 | } 217 | 218 | function get_long_int(round, long_str) { 219 | round = round * 3 220 | return (long_str.charCodeAt(round) << 16) | (long_str.charCodeAt(round + 1) << 8) | long_str.charCodeAt(round + 2) 221 | } 222 | 223 | function gener_random(random, option) { 224 | return [ 225 | (random & 255 & 170) | (option[0] & 85), // 163 226 | (random & 255 & 85) | (option[0] & 170), //87 227 | ((random >> 8) & 255 & 170) | (option[1] & 85), //37 228 | ((random >> 8) & 255 & 85) | (option[1] & 170), //41 229 | ] 230 | } 231 | 232 | ////////////////////////////////////////////// 233 | function generate_rc4_bb_str(url_search_params, user_agent, window_env_str, suffix = 'cus', Arguments = [0, 1, 14]) { 234 | let sm3 = new SM3() 235 | let start_time = Date.now() 236 | /** 237 | * 进行3次加密处理 238 | * 1: url_search_params两次sm3之的结果 239 | * 2: 对后缀两次sm3之的结果 240 | * 3: 对ua处理之后的结果 241 | */ 242 | // url_search_params两次sm3之的结果 243 | let url_search_params_list = sm3.sum(sm3.sum(url_search_params + suffix)) 244 | // 对后缀两次sm3之的结果 245 | let cus = sm3.sum(sm3.sum(suffix)) 246 | // 对ua处理之后的结果 247 | let ua = sm3.sum(result_encrypt(rc4_encrypt(user_agent, String.fromCharCode.apply(null, [0.00390625, 1, 14])), 's3')) 248 | // 249 | let end_time = Date.now() 250 | // b 251 | let b = { 252 | 8: 3, // 固定 253 | 10: end_time, //3次加密结束时间 254 | 15: { 255 | aid: 6383, 256 | pageId: 6241, 257 | boe: false, 258 | ddrt: 7, 259 | paths: { 260 | include: [{}, {}, {}, {}, {}, {}, {}], 261 | exclude: [], 262 | }, 263 | track: { 264 | mode: 0, 265 | delay: 300, 266 | paths: [], 267 | }, 268 | dump: true, 269 | rpU: '', 270 | }, 271 | 16: start_time, //3次加密开始时间 272 | 18: 44, //固定 273 | 19: [1, 0, 1, 5], 274 | } 275 | 276 | //3次加密开始时间 277 | b[20] = (b[16] >> 24) & 255 278 | b[21] = (b[16] >> 16) & 255 279 | b[22] = (b[16] >> 8) & 255 280 | b[23] = b[16] & 255 281 | b[24] = (b[16] / 256 / 256 / 256 / 256) >> 0 282 | b[25] = (b[16] / 256 / 256 / 256 / 256 / 256) >> 0 283 | 284 | // 参数Arguments [0, 1, 14, ...] 285 | // let Arguments = [0, 1, 14] 286 | b[26] = (Arguments[0] >> 24) & 255 287 | b[27] = (Arguments[0] >> 16) & 255 288 | b[28] = (Arguments[0] >> 8) & 255 289 | b[29] = Arguments[0] & 255 290 | 291 | b[30] = (Arguments[1] / 256) & 255 292 | b[31] = Arguments[1] % 256 & 255 293 | b[32] = (Arguments[1] >> 24) & 255 294 | b[33] = (Arguments[1] >> 16) & 255 295 | 296 | b[34] = (Arguments[2] >> 24) & 255 297 | b[35] = (Arguments[2] >> 16) & 255 298 | b[36] = (Arguments[2] >> 8) & 255 299 | b[37] = Arguments[2] & 255 300 | 301 | // (url_search_params + "cus") 两次sm3之的结果 302 | /**let url_search_params_list = [ 303 | 91, 186, 35, 86, 143, 253, 6, 76, 304 | 34, 21, 167, 148, 7, 42, 192, 219, 305 | 188, 20, 182, 85, 213, 74, 213, 147, 306 | 37, 155, 93, 139, 85, 118, 228, 213 307 | ]*/ 308 | b[38] = url_search_params_list[21] 309 | b[39] = url_search_params_list[22] 310 | 311 | // ("cus") 对后缀两次sm3之的结果 312 | /** 313 | * let cus = [ 314 | 136, 101, 114, 147, 58, 77, 207, 201, 315 | 215, 162, 154, 93, 248, 13, 142, 160, 316 | 105, 73, 215, 241, 83, 58, 51, 43, 317 | 255, 38, 168, 141, 216, 194, 35, 236 318 | ]*/ 319 | b[40] = cus[21] 320 | b[41] = cus[22] 321 | 322 | // 对ua处理之后的结果 323 | /** 324 | * let ua = [ 325 | 129, 190, 70, 186, 86, 196, 199, 53, 326 | 99, 38, 29, 209, 243, 17, 157, 69, 327 | 147, 104, 53, 23, 114, 126, 66, 228, 328 | 135, 30, 168, 185, 109, 156, 251, 88 329 | ]*/ 330 | b[42] = ua[23] 331 | b[43] = ua[24] 332 | 333 | //3次加密结束时间 334 | b[44] = (b[10] >> 24) & 255 335 | b[45] = (b[10] >> 16) & 255 336 | b[46] = (b[10] >> 8) & 255 337 | b[47] = b[10] & 255 338 | b[48] = b[8] 339 | b[49] = (b[10] / 256 / 256 / 256 / 256) >> 0 340 | b[50] = (b[10] / 256 / 256 / 256 / 256 / 256) >> 0 341 | 342 | // object配置项 343 | b[51] = b[15]['pageId'] 344 | b[52] = (b[15]['pageId'] >> 24) & 255 345 | b[53] = (b[15]['pageId'] >> 16) & 255 346 | b[54] = (b[15]['pageId'] >> 8) & 255 347 | b[55] = b[15]['pageId'] & 255 348 | 349 | b[56] = b[15]['aid'] 350 | b[57] = b[15]['aid'] & 255 351 | b[58] = (b[15]['aid'] >> 8) & 255 352 | b[59] = (b[15]['aid'] >> 16) & 255 353 | b[60] = (b[15]['aid'] >> 24) & 255 354 | 355 | // 中间进行了环境检测 356 | // 代码索引: 2496 索引值: 17 (索引64关键条件) 357 | // '1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32'.charCodeAt()得到65位数组 358 | /** 359 | * let window_env_list = [49, 53, 51, 54, 124, 55, 52, 55, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 48, 124, 51, 360 | * 48, 124, 48, 124, 48, 124, 49, 53, 51, 54, 124, 56, 51, 52, 124, 49, 53, 51, 54, 124, 56, 361 | * 54, 52, 124, 49, 53, 50, 53, 124, 55, 52, 55, 124, 50, 52, 124, 50, 52, 124, 87, 105, 110, 362 | * 51, 50] 363 | */ 364 | let window_env_list = [] 365 | for (let index = 0; index < window_env_str.length; index++) { 366 | window_env_list.push(window_env_str.charCodeAt(index)) 367 | } 368 | b[64] = window_env_list.length 369 | b[65] = b[64] & 255 370 | b[66] = (b[64] >> 8) & 255 371 | 372 | b[69] = [].length 373 | b[70] = b[69] & 255 374 | b[71] = (b[69] >> 8) & 255 375 | 376 | b[72] = 377 | b[18] ^ 378 | b[20] ^ 379 | b[26] ^ 380 | b[30] ^ 381 | b[38] ^ 382 | b[40] ^ 383 | b[42] ^ 384 | b[21] ^ 385 | b[27] ^ 386 | b[31] ^ 387 | b[35] ^ 388 | b[39] ^ 389 | b[41] ^ 390 | b[43] ^ 391 | b[22] ^ 392 | b[28] ^ 393 | b[32] ^ 394 | b[36] ^ 395 | b[23] ^ 396 | b[29] ^ 397 | b[33] ^ 398 | b[37] ^ 399 | b[44] ^ 400 | b[45] ^ 401 | b[46] ^ 402 | b[47] ^ 403 | b[48] ^ 404 | b[49] ^ 405 | b[50] ^ 406 | b[24] ^ 407 | b[25] ^ 408 | b[52] ^ 409 | b[53] ^ 410 | b[54] ^ 411 | b[55] ^ 412 | b[57] ^ 413 | b[58] ^ 414 | b[59] ^ 415 | b[60] ^ 416 | b[65] ^ 417 | b[66] ^ 418 | b[70] ^ 419 | b[71] 420 | let bb = [ 421 | b[18], 422 | b[20], 423 | b[52], 424 | b[26], 425 | b[30], 426 | b[34], 427 | b[58], 428 | b[38], 429 | b[40], 430 | b[53], 431 | b[42], 432 | b[21], 433 | b[27], 434 | b[54], 435 | b[55], 436 | b[31], 437 | b[35], 438 | b[57], 439 | b[39], 440 | b[41], 441 | b[43], 442 | b[22], 443 | b[28], 444 | b[32], 445 | b[60], 446 | b[36], 447 | b[23], 448 | b[29], 449 | b[33], 450 | b[37], 451 | b[44], 452 | b[45], 453 | b[59], 454 | b[46], 455 | b[47], 456 | b[48], 457 | b[49], 458 | b[50], 459 | b[24], 460 | b[25], 461 | b[65], 462 | b[66], 463 | b[70], 464 | b[71], 465 | ] 466 | bb = bb.concat(window_env_list).concat(b[72]) 467 | return rc4_encrypt(String.fromCharCode.apply(null, bb), String.fromCharCode.apply(null, [121])) 468 | } 469 | 470 | function generate_random_str() { 471 | let random_str_list = [] 472 | random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [3, 45])) 473 | random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 0])) 474 | random_str_list = random_str_list.concat(gener_random(Math.random() * 10000, [1, 5])) 475 | return String.fromCharCode.apply(null, random_str_list) 476 | } 477 | 478 | function generate_a_bogus(url_search_params, user_agent) { 479 | /** 480 | * url_search_params:"device_platform=webapp&aid=6383&channel=channel_pc_web&update_version_code=170400&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Chrome&browser_version=123.0.0.0&browser_online=true&engine_name=Blink&engine_version=123.0.0.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7362810250930783783&msToken=VkDUvz1y24CppXSl80iFPr6ez-3FiizcwD7fI1OqBt6IICq9RWG7nCvxKb8IVi55mFd-wnqoNkXGnxHrikQb4PuKob5Q-YhDp5Um215JzlBszkUyiEvR" 481 | * user_agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" 482 | */ 483 | let result_str = 484 | generate_random_str() + generate_rc4_bb_str(url_search_params, user_agent, '1536|747|1536|834|0|30|0|0|1536|834|1536|864|1525|747|24|24|Win32') 485 | return result_encrypt(result_str, 's4') + '=' 486 | } 487 | module.exports = { 488 | generate_a_bogus, 489 | } 490 | 491 | //测试调用 492 | // console.log(generate_a_bogus( 493 | // "device_platform=webapp&aid=6383&channel=channel_pc_web&update_version_code=170400&pc_client_type=1&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1536&screen_height=864&browser_language=zh-CN&browser_platform=Win32&browser_name=Chrome&browser_version=123.0.0.0&browser_online=true&engine_name=Blink&engine_version=123.0.0.0&os_name=Windows&os_version=10&cpu_core_num=16&device_memory=8&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7362810250930783783&msToken=VkDUvz1y24CppXSl80iFPr6ez-3FiizcwD7fI1OqBt6IICq9RWG7nCvxKb8IVi55mFd-wnqoNkXGnxHrikQb4PuKob5Q-YhDp5Um215JzlBszkUyiEvR", 494 | // "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" 495 | // )); -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/acfun.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | import subprocess 4 | import os 5 | 6 | import httpx 7 | 8 | headers = { 9 | 'referer': 'https://www.acfun.cn/', 10 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83' 11 | } 12 | 13 | 14 | def parse_url(url: str): 15 | """ 16 | 解析acfun链接 17 | :param url: 18 | :return: 19 | """ 20 | url_suffix = "?quickViewId=videoInfo_new&ajaxpipe=1" 21 | url = url + url_suffix 22 | # print(url) 23 | 24 | raw = httpx.get(url, headers=headers).text 25 | strs_remove_header = raw.split("window.pageInfo = window.videoInfo =") 26 | strs_remove_tail = strs_remove_header[1].split("</script>") 27 | str_json = strs_remove_tail[0] 28 | str_json_escaped = escape_special_chars(str_json) 29 | video_info = json.loads(str_json_escaped) 30 | # print(video_info) 31 | video_name = parse_video_name_fixed(video_info) 32 | ks_play_json = video_info['currentVideoInfo']['ksPlayJson'] 33 | ks_play = json.loads(ks_play_json) 34 | representations = ks_play['adaptationSet'][0]['representation'] 35 | # 这里[d['url'] for d in representations],从4k~360,此处默认720p 36 | url_m3u8s = [d['url'] for d in representations][3] 37 | # print([d['url'] for d in representations]) 38 | return url_m3u8s, video_name 39 | 40 | 41 | def parse_m3u8(m3u8_url: str): 42 | """ 43 | 解析m3u8链接 44 | :param m3u8_url: 45 | :return: 46 | """ 47 | m3u8_file = httpx.get(m3u8_url, headers=headers).text 48 | # 分离ts文件链接 49 | raw_pieces = re.split(r"\n#EXTINF:.{8},\n", m3u8_file) 50 | # print(raw_pieces) 51 | # 过滤头部\ 52 | m3u8_relative_links = raw_pieces[1:] 53 | # print(m3u8_relative_links) 54 | # 修改尾部 去掉尾部多余的结束符 55 | patched_tail = m3u8_relative_links[-1].split("\n")[0] 56 | m3u8_relative_links[-1] = patched_tail 57 | # print(m3u8_relative_links) 58 | 59 | # 完整链接,直接加m3u8Url的通用前缀 60 | m3u8_prefix = "/".join(m3u8_url.split("/")[0:-1]) 61 | m3u8_full_urls = [m3u8_prefix + "/" + d for d in m3u8_relative_links] 62 | # aria2c下载的文件名,就是取url最后一段,去掉末尾url参数(?之后是url参数) 63 | ts_names = [d.split("?")[0] for d in m3u8_relative_links] 64 | # print(ts_names) 65 | output_folder_name = ts_names[0][:-9] 66 | output_file_name = output_folder_name + ".mp4" 67 | # print(output_file_name) 68 | return m3u8_full_urls, ts_names, output_folder_name, output_file_name 69 | 70 | 71 | async def download_m3u8_videos(m3u8_full_url, i): 72 | """ 73 | 批量下载m3u8 74 | :param m3u8_full_urls: 75 | :return: 76 | """ 77 | async with httpx.AsyncClient() as client: 78 | async with client.stream("GET", m3u8_full_url, headers=headers) as resp: 79 | with open(f"{i}.ts", "wb") as f: 80 | async for chunk in resp.aiter_bytes(): 81 | f.write(chunk) 82 | 83 | 84 | def escape_special_chars(str_json): 85 | return str_json.replace('\\\\"', '\\"').replace('\\"', '"') 86 | 87 | 88 | def parse_video_name(video_info: json): 89 | """ 90 | 获取视频信息 91 | :param video_info: 92 | :return: 93 | """ 94 | ac_id = "ac" + video_info['dougaId'] if video_info['dougaId'] is not None else "" 95 | title = video_info['title'] if video_info['title'] is not None else "" 96 | author = video_info['user']['name'] if video_info['user']['name'] is not None else "" 97 | upload_time = video_info['createTime'] if video_info['createTime'] is not None else "" 98 | desc = video_info['description'] if video_info['description'] is not None else "" 99 | 100 | raw = '_'.join([ac_id, title, author, upload_time, desc])[:101] 101 | return raw 102 | 103 | 104 | def merge_ac_file_to_mp4(ts_names, full_file_name, should_delete=True): 105 | concat_str = '\n'.join([f"file {i}.ts" for i, d in enumerate(ts_names)]) 106 | # print(concat_str) 107 | with open('file.txt', 'w') as f: 108 | f.write(concat_str) 109 | 110 | subprocess.call(f'ffmpeg -y -f concat -safe 0 -i "file.txt" -c copy "{full_file_name}"', shell=True, 111 | stdout=subprocess.DEVNULL, 112 | stderr=subprocess.DEVNULL, 113 | ) 114 | if should_delete: 115 | os.unlink('file.txt') 116 | # os.unlink(full_file_name) 117 | for i in range(len(ts_names)): 118 | os.unlink(f'{i}.ts') 119 | 120 | 121 | def parse_video_name_fixed(video_info: json): 122 | """ 123 | 校准文件名 124 | :param video_info: 125 | :return: 126 | """ 127 | f = parse_video_name(video_info) 128 | t = f.replace(" ", "-") 129 | return t 130 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/bili23.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import platform 3 | import subprocess 4 | 5 | import aiofiles 6 | import httpx 7 | from nonebot import logger 8 | 9 | from ..constants import BILIBILI_HEADER 10 | 11 | 12 | async def download_b_file(url, full_file_name, progress_callback): 13 | """ 14 | 下载视频文件和音频文件 15 | :param url: 16 | :param full_file_name: 17 | :param progress_callback: 18 | :return: 19 | """ 20 | async with httpx.AsyncClient(transport=httpx.AsyncHTTPTransport(local_address="0.0.0.0")) as client: 21 | async with client.stream("GET", url, headers=BILIBILI_HEADER) as resp: 22 | current_len = 0 23 | total_len = int(resp.headers.get('content-length', 0)) 24 | print(total_len) 25 | async with aiofiles.open(full_file_name, "wb") as f: 26 | async for chunk in resp.aiter_bytes(): 27 | current_len += len(chunk) 28 | await f.write(chunk) 29 | progress_callback(f'下载进度:{round(current_len / total_len, 3)}') 30 | 31 | 32 | async def merge_file_to_mp4(v_full_file_name: str, a_full_file_name: str, output_file_name: str, log_output: bool = False): 33 | """ 34 | 合并视频文件和音频文件 35 | :param v_full_file_name: 视频文件路径 36 | :param a_full_file_name: 音频文件路径 37 | :param output_file_name: 输出文件路径 38 | :param log_output: 是否显示 ffmpeg 输出日志,默认忽略 39 | :return: 40 | """ 41 | logger.info(f'正在合并:{output_file_name}') 42 | 43 | # 构建 ffmpeg 命令 44 | command = f'ffmpeg -y -i "{v_full_file_name}" -i "{a_full_file_name}" -c copy "{output_file_name}"' 45 | stdout = None if log_output else subprocess.DEVNULL 46 | stderr = None if log_output else subprocess.DEVNULL 47 | 48 | if platform.system() == "Windows": 49 | # Windows 下使用 run_in_executor 50 | loop = asyncio.get_event_loop() 51 | await loop.run_in_executor( 52 | None, 53 | lambda: subprocess.call(command, shell=True, stdout=stdout, stderr=stderr) 54 | ) 55 | else: 56 | # 其他平台使用 create_subprocess_shell 57 | process = await asyncio.create_subprocess_shell( 58 | command, 59 | shell=True, 60 | stdout=stdout, 61 | stderr=stderr 62 | ) 63 | await process.communicate() 64 | 65 | 66 | def extra_bili_info(video_info): 67 | """ 68 | 格式化视频信息 69 | """ 70 | video_state = video_info['stat'] 71 | video_like, video_coin, video_favorite, video_share, video_view, video_danmaku, video_reply = video_state['like'], \ 72 | video_state['coin'], video_state['favorite'], video_state['share'], video_state['view'], video_state['danmaku'], \ 73 | video_state['reply'] 74 | 75 | video_data_map = { 76 | "点赞": video_like, 77 | "硬币": video_coin, 78 | "收藏": video_favorite, 79 | "分享": video_share, 80 | "总播放量": video_view, 81 | "弹幕数量": video_danmaku, 82 | "评论": video_reply 83 | } 84 | 85 | video_info_result = "" 86 | for key, value in video_data_map.items(): 87 | if int(value) > 10000: 88 | formatted_value = f"{value / 10000:.1f}万" 89 | else: 90 | formatted_value = value 91 | video_info_result += f"{key}: {formatted_value} | " 92 | 93 | return video_info_result 94 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/common.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import time 5 | from typing import List, Dict, Any 6 | from urllib.parse import urlparse 7 | from nonebot import require, logger 8 | 9 | require("nonebot_plugin_localstore") 10 | 11 | import nonebot_plugin_localstore as store 12 | 13 | import aiofiles 14 | import aiohttp 15 | import httpx 16 | 17 | from ..constants import COMMON_HEADER, PLUGIN_NAME, RESOLVE_SHUTDOWN_LIST_NAME 18 | 19 | 20 | async def download_video(url, proxy: str = None, ext_headers=None) -> str: 21 | """ 22 | 异步下载(httpx)视频,并支持通过代理下载。 23 | 文件名将使用时间戳生成,以确保唯一性。 24 | 如果提供了代理地址,则会通过该代理下载视频。 25 | 26 | :param ext_headers: 27 | :param url: 要下载的视频的URL。 28 | :param proxy: 可选,下载视频时使用的代理服务器的URL。 29 | :return: 保存视频的路径。 30 | """ 31 | # 使用时间戳生成文件名,确保唯一性 32 | path = os.path.join(os.getcwd(), f"{int(time.time())}.mp4") 33 | 34 | # 判断 ext_headers 是否为 None 35 | if ext_headers is None: 36 | headers = COMMON_HEADER 37 | else: 38 | # 使用 update 方法合并两个字典 39 | headers = COMMON_HEADER.copy() # 先复制 COMMON_HEADER 40 | headers.update(ext_headers) # 然后更新 ext_headers 41 | 42 | # 配置代理 43 | client_config = { 44 | 'headers': headers, 45 | 'timeout': httpx.Timeout(60, connect=5.0), 46 | 'follow_redirects': True 47 | } 48 | if proxy: 49 | client_config['proxies'] = { 'https': proxy } 50 | 51 | # 下载文件 52 | try: 53 | async with httpx.AsyncClient(**client_config) as client: 54 | async with client.stream("GET", url) as resp: 55 | async with aiofiles.open(path, "wb") as f: 56 | async for chunk in resp.aiter_bytes(): 57 | await f.write(chunk) 58 | return path 59 | except Exception as e: 60 | print(f"下载视频错误原因是: {e}") 61 | return None 62 | 63 | 64 | async def download_img(url: str, path: str = '', proxy: str = None, session=None, headers=None) -> str: 65 | """ 66 | 异步下载(aiohttp)网络图片,并支持通过代理下载。 67 | 如果未指定path,则图片将保存在当前工作目录并以图片的文件名命名。 68 | 如果提供了代理地址,则会通过该代理下载图片。 69 | 70 | :param url: 要下载的图片的URL。 71 | :param path: 图片保存的路径。如果为空,则保存在当前目录。 72 | :param proxy: 可选,下载图片时使用的代理服务器的URL。 73 | :return: 保存图片的路径。 74 | """ 75 | if path == '': 76 | path = os.path.join(os.getcwd(), url.split('/').pop()) 77 | # 单个文件下载 78 | if session is None: 79 | async with aiohttp.ClientSession() as session: 80 | async with session.get(url, proxy=proxy, headers=headers) as response: 81 | if response.status == 200: 82 | data = await response.read() 83 | with open(path, 'wb') as f: 84 | f.write(data) 85 | # 多个文件异步下载 86 | else: 87 | async with session.get(url, proxy=proxy, headers=headers) as response: 88 | if response.status == 200: 89 | data = await response.read() 90 | with open(path, 'wb') as f: 91 | f.write(data) 92 | return path 93 | 94 | 95 | async def download_audio(url): 96 | # 从URL中提取文件名 97 | parsed_url = urlparse(url) 98 | file_name = parsed_url.path.split('/')[-1] 99 | # 去除可能存在的请求参数 100 | file_name = file_name.split('?')[0] 101 | 102 | path = os.path.join(os.getcwd(), file_name) 103 | 104 | async with httpx.AsyncClient() as client: 105 | response = await client.get(url) 106 | response.raise_for_status() # 检查请求是否成功 107 | 108 | async with aiofiles.open(path, 'wb') as file: 109 | await file.write(response.content) 110 | return path 111 | 112 | 113 | def delete_boring_characters(sentence): 114 | """ 115 | 去除标题的特殊字符 116 | :param sentence: 117 | :return: 118 | """ 119 | return re.sub(r'[0-9’!"∀〃#$%&\'()*+,-./:;<=>?@,。?★、…【】《》?“”‘’![\\]^_`{|}~~\s]+', "", sentence) 120 | 121 | 122 | def remove_files(file_paths: List[str]) -> Dict[str, str]: 123 | """ 124 | 根据路径删除文件 125 | 126 | Parameters: 127 | *file_paths (str): 要删除的一个或多个文件路径 128 | 129 | Returns: 130 | dict: 一个以文件路径为键、删除状态为值的字典 131 | """ 132 | results = { } 133 | 134 | for file_path in file_paths: 135 | if os.path.exists(file_path): 136 | try: 137 | os.remove(file_path) 138 | results[file_path] = 'remove' 139 | except Exception as e: 140 | results[file_path] = f'error: {e}' 141 | else: 142 | results[file_path] = 'don\'t exist' 143 | 144 | return results 145 | 146 | 147 | def get_file_size_mb(file_path): 148 | """ 149 | 判断当前文件的大小是多少MB 150 | :param file_path: 151 | :return: 152 | """ 153 | # 获取文件大小(以字节为单位) 154 | file_size_bytes = os.path.getsize(file_path) 155 | 156 | # 将字节转换为 MB 并取整 157 | file_size_mb = int(file_size_bytes / (1024 * 1024)) 158 | 159 | return file_size_mb 160 | 161 | 162 | def load_or_initialize_list() -> List[Any]: 163 | data_file = store.get_data_file(PLUGIN_NAME, RESOLVE_SHUTDOWN_LIST_NAME) 164 | # 判断是否存在 165 | if not data_file.exists(): 166 | data_file.write_text(json.dumps([])) 167 | return list(json.loads(data_file.read_text())) 168 | 169 | 170 | def save_sub_user(sub_group): 171 | """ 172 | 使用pickle将对象保存到文件 173 | :return: None 174 | """ 175 | data_file = store.get_data_file(PLUGIN_NAME, RESOLVE_SHUTDOWN_LIST_NAME) 176 | data_file.write_text(json.dumps(sub_group)) 177 | 178 | 179 | def load_sub_user(): 180 | """ 181 | 从文件中加载对象 182 | :return: 订阅用户列表 183 | """ 184 | data_file = store.get_data_file(PLUGIN_NAME, RESOLVE_SHUTDOWN_LIST_NAME) 185 | # 判断是否存在 186 | if not data_file.exists(): 187 | data_file.write_text(json.dumps([])) 188 | return json.loads(data_file.read_text()) 189 | 190 | 191 | def split_and_strip(text, sep=None) -> List[str]: 192 | # 先去除两边的空格,然后按指定分隔符分割 193 | split_text = text.strip().split(sep) 194 | # 去除每个子字符串两边的空格 195 | return [sub_text.strip() for sub_text in split_text] 196 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/tiktok.py: -------------------------------------------------------------------------------- 1 | import random 2 | import execjs 3 | import urllib.parse 4 | import os 5 | 6 | import httpx 7 | 8 | header = { 9 | 'User-Agent': "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36 Edg/87.0.664.66" 10 | } 11 | 12 | 13 | def generate_x_bogus_url(url, headers): 14 | """ 15 | 生成抖音A-Bogus签名 16 | :param url: 视频链接 17 | :return: 包含X-Bogus签名的URL 18 | """ 19 | # 调用JavaScript函数 20 | query = urllib.parse.urlparse(url).query 21 | abogus_file_path = f'{os.path.dirname(os.path.abspath(__file__))}/a-bogus.js' 22 | with open(abogus_file_path, 'r', encoding='utf-8') as abogus_file: 23 | abogus_file_path_transcoding = abogus_file.read() 24 | abogus = execjs.compile(abogus_file_path_transcoding).call('generate_a_bogus', query, headers['User-Agent']) 25 | # logger.info('生成的A-Bogus签名为: {}'.format(abogus)) 26 | return url + "&a_bogus=" + abogus 27 | 28 | 29 | def generate_random_str(self, randomlength=16): 30 | """ 31 | 根据传入长度产生随机字符串 32 | param :randomlength 33 | return:random_str 34 | """ 35 | random_str = '' 36 | base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789=' 37 | length = len(base_str) - 1 38 | for _ in range(randomlength): 39 | random_str += base_str[random.randint(0, length)] 40 | return random_str 41 | 42 | 43 | async def dou_transfer_other(dou_url): 44 | """ 45 | 图集临时解决方案 46 | :param dou_url: 47 | :return: 48 | """ 49 | douyin_temp_data = httpx.get(f"https://api.xingzhige.com/API/douyin/?url={dou_url}").json() 50 | data = douyin_temp_data.get("data", { }) 51 | item_id = data.get("jx", { }).get("item_id") 52 | item_type = data.get("jx", { }).get("type") 53 | 54 | if not item_id or not item_type: 55 | raise ValueError("备用 API 未返回 item_id 或 type") 56 | 57 | # 备用API成功解析图集,直接处理 58 | if item_type == "图集": 59 | item = data.get("item", { }) 60 | cover = item.get("cover", "") 61 | images = item.get("images", []) 62 | # 只有在有图片的情况下才发送 63 | if images: 64 | author = data.get("author", { }).get("name", "") 65 | title = data.get("item", { }).get("title", "") 66 | return cover, author, title, images 67 | 68 | return None, None, None, None 69 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/weibo.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | # 定义 base62 编码字符表 4 | ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' 5 | 6 | 7 | def base62_encode(number): 8 | """将数字转换为 base62 编码""" 9 | if number == 0: 10 | return '0' 11 | 12 | result = '' 13 | while number > 0: 14 | result = ALPHABET[number % 62] + result 15 | number //= 62 16 | 17 | return result 18 | 19 | 20 | def mid2id(mid): 21 | mid = str(mid)[::-1] # 反转输入字符串 22 | size = math.ceil(len(mid) / 7) # 计算每个块的大小 23 | result = [] 24 | 25 | for i in range(size): 26 | # 对每个块进行处理并反转 27 | s = mid[i * 7:(i + 1) * 7][::-1] 28 | # 将字符串转为整数后进行 base62 编码 29 | s = base62_encode(int(s)) 30 | # 如果不是最后一个块并且长度不足4位,进行左侧补零操作 31 | if i < size - 1 and len(s) < 4: 32 | s = '0' * (4 - len(s)) + s 33 | result.append(s) 34 | 35 | result.reverse() # 反转结果数组 36 | return ''.join(result) # 将结果数组连接成字符串 37 | -------------------------------------------------------------------------------- /nonebot-plugin-resolver/core/ytdlp.py: -------------------------------------------------------------------------------- 1 | import yt_dlp, asyncio 2 | 3 | from nonebot import logger 4 | 5 | async def get_video_title(url: str, is_oversea: bool, my_proxy=None, video_type='youtube') -> str: 6 | ydl_opts = { 7 | 'quiet': True, 8 | 'skip_download': True, 9 | 'force_generic_extractor': True, 10 | } 11 | if not is_oversea and my_proxy: 12 | ydl_opts['proxy'] = my_proxy 13 | if video_type == 'youtube': 14 | ydl_opts['cookiefile'] = 'ytb_cookies.txt' 15 | 16 | try: 17 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 18 | info_dict = await asyncio.to_thread(ydl.extract_info, url, download=False) 19 | return info_dict.get('title', '-') 20 | except Exception as e: 21 | logger.error(f"Error: {e}") 22 | return '-' 23 | 24 | async def download_ytb_video(url, is_oversea, path, my_proxy=None, video_type='youtube'): 25 | ydl_opts = { 26 | 'outtmpl': f'{path}/temp.%(ext)s', 27 | 'merge_output_format': 'mp4', 28 | } 29 | if video_type == 'youtube': 30 | ydl_opts['cookiefile'] = 'ytb_cookies.txt' 31 | if not 'shorts' in url: 32 | ydl_opts['format'] = 'bv*[width=1280][height=720]+ba' 33 | if not is_oversea and my_proxy: 34 | ydl_opts['proxy'] = my_proxy 35 | 36 | try: 37 | with yt_dlp.YoutubeDL(ydl_opts) as ydl: 38 | await asyncio.to_thread(ydl.download, [url]) 39 | return f"{path}/temp.mp4" 40 | except Exception as e: 41 | print(f"Error: {e}") 42 | return None 43 | 44 | 45 | -------------------------------------------------------------------------------- /npr_install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # ----------------------------------------------------------------------------- 3 | # Script Name: nonebot-plugin-resolver-install.sh 4 | # Description: 下载并设置onebot-plugin-resolver插件,检查和删除已存在的插件文件夹。 5 | # Author: zhiyu1998 6 | # Created: 2024-08-12 7 | # Last Modified: 2024-08-12 8 | # Version: 1.0 9 | # Repository: https://github.com/zhiyu1998/nonebot-plugin-resolver 10 | # ----------------------------------------------------------------------------- 11 | 12 | # 检查src目录是否存在 13 | if [ -d "src" ]; then 14 | cd src 15 | # 检查src下的plugins目录是否存在 16 | if [ ! -d "plugins" ]; then 17 | echo "src目录下没有plugins目录,检查当前目录..." 18 | cd .. 19 | if [ ! -d "plugins" ]; then 20 | echo "当前目录下也没有plugins目录,脚本结束。" 21 | exit 1 22 | else 23 | echo "在当前目录下找到plugins目录。" 24 | fi 25 | else 26 | echo "在src目录下找到plugins目录。" 27 | fi 28 | else 29 | echo "当前目录下没有src目录,检查是否有plugins目录..." 30 | if [ ! -d "plugins" ]; then 31 | echo "当前目录下也没有plugins目录,脚本结束。" 32 | exit 1 33 | fi 34 | fi 35 | 36 | # 进入plugins目录 37 | cd plugins 38 | 39 | # 检查并删除已存在的nonebot-plugin-resolver文件夹 40 | if [ -d "nonebot-plugin-resolver" ]; then 41 | echo "[nonebot-plugin-resolver] 存在已存在的nonebot-plugin-resolver文件夹,正在删除..." 42 | rm -rf nonebot-plugin-resolver 43 | fi 44 | 45 | # 使用git clone下载指定的GitHub仓库 46 | echo "[nonebot-plugin-resolver] 开始克隆GitHub仓库..." 47 | git clone https://ghproxy.net/https://github.com/zhiyu1998/nonebot-plugin-resolver.git 48 | 49 | # 进入下载的仓库文件夹 50 | cd nonebot-plugin-resolver 51 | 52 | # 删除除了nonebot-plugin-resolver文件夹以外的所有文件 53 | echo "[nonebot-plugin-resolver] 删除不需要的文件..." 54 | find . -maxdepth 1 ! -name 'nonebot-plugin-resolver' -type f -exec rm -f {} + 55 | 56 | # 将nonebot-plugin-resolver文件夹中的内容移动到当前目录 57 | echo "[nonebot-plugin-resolver] 移动文件..." 58 | mv nonebot-plugin-resolver/* ./ 59 | rmdir nonebot-plugin-resolver 60 | 61 | # 返回之前的目录 62 | cd .. 63 | 64 | echo "[nonebot-plugin-resolver] 安装完成,请使用nb run启动Nonebot!" 65 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot-plugin-resolver" 3 | version = "1.2.31" 4 | description = "NoneBot2链接分享解析器插件。解析视频、图片链接/小程序插件,tiktok、bilibili、twitter等实时发送!" 5 | authors = ["zhiyu1998 <renzhiyu0416@qq.com>"] 6 | readme = "README.md" 7 | packages = [{include = "nonebot-plugin-resolver"}] 8 | repository = "https://github.com/zhiyu1998/nonebot-plugin-resolver" 9 | keywords = ["nonebot", "nonebot2", "resolver"] 10 | 11 | [tool.poetry.dependencies] 12 | python = ">=3.9" 13 | aiohttp = "^3.7" 14 | httpx = ">=0.23" 15 | PyExecJS = "^1.5.1" 16 | bilibili-api-python = ">=16.2.0" 17 | aiofiles = ">=0.8.0" 18 | yt-dlp = ">=2024.11.18" 19 | nonebot2 = ">=2.0.0-beta.5" 20 | nonebot-adapter-onebot = "^2.0.0-beta.1" 21 | nonebot_plugin_localstore = ">=0.6.0" 22 | 23 | [build-system] 24 | requires = ["poetry-core"] 25 | build-backend = "poetry.core.masonry.api" --------------------------------------------------------------------------------