├── .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 |

3 |
4 |

5 |
6 |
7 |
8 |
9 | # nonebot-plugin-resolver
10 |
11 | _✨ NoneBot2 链接分享解析器插件 ✨_
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |

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