koishi-plugin-music-link
Version:
/*音乐下载*/🎵搜索音乐资源🤩提供QQ、网易云平台的音乐下载,付费的也可以欸?[点我查看使用方法](https://github.com/shangxueink/koishi-shangxue-apps/tree/main/plugins/music-link)
1,199 lines (1,108 loc) • 114 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.apply = exports.Config = exports.usage = exports.inject = exports.name = void 0;
const { Schema, Logger, h } = require("koishi");
const fs = require('node:fs/promises');
const crypto = require('node:crypto');
const path = require('node:path');
const url = require('node:url');
const name = 'music-link';
const inject = {
required: ['http', "i18n"],
optional: ['puppeteer'],
};
const logger = new Logger('music-link');
const usage = `
<hr>
<details>
<summary><h3>使用方法 (点击展开)</h3></summary>
<p>安装并配置插件后,使用下述命令搜索和下载音乐:</p>
<hr>
<h3>使用星之阁API搜索QQ、网易云音乐</h3>
<pre><code>下载音乐 [keywords]</code></pre>
<p><b>(不推荐)</b> 星之阁API,需要加群申请API Key,且API Key可能存在失效风险。支持QQ音乐和网易云音乐,速度较慢,稳定性一般。</p>
<hr>
<h3>使用星之阁-酷狗API搜索酷狗音乐</h3>
<pre><code>酷狗音乐 [keywords]</code></pre>
<p><b>(不推荐)</b> 星之阁-酷狗API,需要加群申请API Key,且API Key可能存在失效风险。仅支持酷狗音乐,速度较慢,稳定性一般。</p>
<hr>
<h3>使用music.gdstudio.xyz网站搜索各大音乐平台</h3>
<pre><code>歌曲搜索 [keywords]</code></pre>
<p><b>(比较推荐)</b> music.gdstudio.xyz 网站,无需API Key,但需要 <b>puppeteer</b> 服务支持进行网页爬取,速度还行。默认使用网易云音乐搜索,支持多平台选择。</p>
<hr>
<h3>使用api.injahow.cn网站搜索网易云音乐</h3>
<pre><code>网易点歌 [歌曲名称/歌曲ID]</code></pre>
<p><b>(很推荐)</b> api.injahow.cn 网站,API请求快速且稳定,无需 puppeteer 服务,推荐QQ官方机器人使用此后端,使用这个后端VIP歌曲只能听45秒,但这个指令还有一个后端可以都听。很好用哦<b>仅支持网易云音乐</b>,可以通过歌曲名称或歌曲ID进行搜索。</p>
<hr>
<h3>使用dev.iw233.cn网站搜索网易云音乐</h3>
<pre><code>音乐搜索器 [keywords]</code></pre>
<p><b>(推荐)</b> dev.iw233.cn 网站,无需API Key,但需要 <b>puppeteer</b> 服务支持进行网页爬取,速度较慢。支持网易云音乐搜索。</p>
<hr>
<h3>使用www.hhlqilongzhu.cn网站API搜索QQ + 网易云音乐</h3>
<pre><code>龙珠搜索 [keywords]</code></pre>
<p><b>(一般推荐)</b> www.hhlqilongzhu.cn 网站的点歌API,江苏的确可能访问性较差(江苏反诈)。支持网易云音乐平台搜索。(好像QQ音乐有点死掉了)</p>
<hr>
</details>
---
<h3>如何返回语音/视频/群文件消息</h3>
<p>可以修改对应指令的<code>返回字段表</code>中的 <code>下载链接</code> 对应的 <code>字段发送类型</code> 字段,
把 <code>text</code> 更改为 <code>audio</code> 就是返回 语音,
改为 <code>video</code> 就是返回 视频消息,
改为 <code>file</code> 就是返回 群文件。</p>
<hr>
<p>⚠️需要注意的是,当配置返回格式为音频/视频的时候,请自行检查是否安装了 <code>silk</code>、<code>ffmpeg</code> 等服务。</p>
<p>⚠️如果你选择了 <code>file</code> 类型,请确保平台支持!目前仅实测了 <code>onebot</code> 平台的部分协议端支持!</p>
<hr>
<h3>使用 <code>-n 1</code> 直接返回内容</h3>
<p>在使用命令时,可以通过添加 <code>-n 1</code> 选项直接返回指定序号的歌曲内容。这对于快速获取特定歌曲非常有用。</p>
<p>例如,使用以下命令可以直接获取第一首歌曲的详细信息:</p>
<pre><code>歌曲搜索 -n 1 蔚蓝档案</code></pre>
---
## 重要提示⚠️
### 目前 星之阁API的key已经失效,如需使用请自行前往注册
### 目前 推荐使用<code>api.injahow.cn(网易云点歌)</code>的服务,请确保<code>puppeteer</code>服务可用
---
| 后端推荐度 | 名称 | 备注 |
| :--------: | :-------------------------------: | :---: |
| **ⅰ** | \`api.injahow.cn\` (歌曲搜索) | 较高 |
| **ⅱ** | \`dev.iw233.cn\` (音乐搜索器) | 中等 |
| *......* | 其他 | 中等 |
| **ⅳ** | \`星之阁API\` (下载音乐/酷狗音乐) | 较低 |
---
目前基本QQ音乐都死翘翘了 (腾讯太小气了
`;
const command1_return_qqdata_Field_default = [
{
"data": "songname",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "subtitle",
"describe": "标题",
"type": "text",
"enable": false
},
{
"data": "name",
"describe": "歌手",
"type": "text",
},
{
"data": "album",
"describe": "专辑",
"type": "text",
"enable": false
},
{
"data": "pay",
"describe": "付费情况",
"type": "text",
"enable": false
},
{
"data": "song_type",
"describe": "歌曲类型",
"type": "text",
"enable": false
},
{
"data": "type",
"describe": "类型",
"type": "text",
"enable": false
},
{
"data": "songid",
"describe": "歌曲ID",
"type": "text",
"enable": false
},
{
"data": "mid",
"describe": "mid",
"type": "text",
"enable": false
},
{
"data": "time",
"describe": "发行时间",
"type": "text",
"enable": false
},
{
"data": "bpm",
"describe": "bpm",
"type": "text",
"enable": false
},
{
"data": "quality",
"describe": "音质",
"type": "text"
},
{
"data": "interval",
"describe": "时长",
"type": "text",
"enable": false
},
{
"data": "size",
"describe": "大小",
"type": "text"
},
{
"data": "kbps",
"describe": "分辨率",
"type": "text",
"enable": false
},
{
"data": "cover",
"describe": "封面",
"type": "image"
},
{
"data": "songurl",
"describe": "歌曲链接",
"type": "text",
"enable": false
},
{
"data": "src",
"describe": "下载链接",
"type": "text"
}
];
const command1_return_wyydata_Field_default = [
{
"data": "songname",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "name",
"describe": "歌手",
"type": "text"
},
{
"data": "album",
"describe": "专辑",
"type": "text",
"enable": false
},
{
"data": "pay",
"describe": "付费情况",
"enable": false,
"type": "text"
},
{
"data": "id",
"describe": "歌曲ID",
"enable": false,
"type": "text"
},
{
"data": "quality",
"describe": "音质",
"type": "text"
},
{
"data": "interval",
"describe": "时长",
"enable": false,
"type": "text"
},
{
"data": "size",
"describe": "大小",
"type": "text"
},
{
"data": "kbps",
"describe": "分辨率",
"enable": false,
"type": "text"
},
{
"data": "cover",
"describe": "封面",
"type": "image"
},
{
"data": "songurl",
"describe": "歌曲链接",
"type": "text",
"enable": false
},
{
"data": "src",
"describe": "下载链接",
"type": "text"
}
];
const command4_return_data_Field_default = [
{
"data": "songname",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "name",
"describe": "歌手",
"type": "text"
},
{
"data": "album",
"describe": "专辑",
"type": "text"
},
{
"data": "quality",
"describe": "音质",
"type": "text"
},
{
"data": "interval",
"describe": "时长",
"type": "text",
"enable": false
},
{
"data": "size",
"describe": "大小",
"type": "text",
"enable": null
},
{
"data": "kbps",
"describe": "分辨率",
"type": "text",
"enable": false
},
{
"data": "cover",
"describe": "封面",
"type": "image"
},
{
"data": "src",
"describe": "下载链接",
"type": "text"
},
{
"data": "songurl",
"describe": "跳转链接",
"type": "text",
"enable": false
}
];
const command5_return_data_Field_default = [
{
"data": "name",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "artist",
"describe": "歌手",
"type": "text"
},
{
"data": "album",
"describe": "专辑",
"type": "text",
"enable": false
},
{
"data": "source",
"describe": "来源平台",
"enable": false,
"type": "text"
},
{
"data": "fileSize",
"describe": "文件大小",
"type": "text"
},
{
"data": "br",
"describe": "比特率",
"type": "text",
"enable": false
},
{
"data": "coverUrl",
"describe": "封面链接",
"type": "image"
},
{
"data": "musicUrl",
"describe": "下载链接",
"type": "text"
},
{
"data": "lyric",
"describe": "歌词",
"type": "text",
"enable": false
}
];
const command6_return_data_Field_default = [
{
"data": "name",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "id",
"describe": "歌曲ID",
"type": "text"
},
{
"data": "artist",
"describe": "歌手",
"type": "text"
},
{
"data": "url",
"describe": "下载链接",
"type": "text"
},
{
"data": "pic",
"describe": "封面链接",
"type": "image"
},
{
"data": "lrc",
"describe": "歌词",
"type": "text",
"enable": false
}
];
const command7_return_data_Field_default = [
{
"type": "text",
"data": "type",
"describe": "平台名称",
"enable": false
},
{
"data": "link",
"describe": "音乐地址",
"type": "text",
"enable": false
},
{
"data": "songid",
"describe": "歌曲ID",
"type": "text",
"enable": false
},
{
"data": "title",
"describe": "歌曲名称",
"type": "text",
"enable": null
},
{
"data": "author",
"describe": "歌手",
"type": "text"
},
{
"data": "lrc",
"describe": "歌词",
"type": "text",
"enable": false
},
{
"data": "url",
"describe": "下载链接",
"type": "text"
},
{
"data": "pic",
"describe": "封面链接",
"type": "image"
}
];
const command8_return_wyydata_Field_default = [
{
"data": "title",
"describe": "歌曲名称",
"type": "text"
},
{
"data": "singer",
"describe": "歌手",
"type": "text"
},
{
"data": "id",
"describe": "音质",
"type": "text",
"enable": null
},
{
"data": "cover",
"describe": "封面",
"type": "image"
},
{
"data": "link",
"describe": "歌曲链接",
"type": "text",
"enable": false
},
{
"data": "music_url",
"describe": "下载链接",
"type": "text"
},
{
"data": "lrc",
"describe": "歌词",
"type": "text",
"enable": false
}
];
const platformMap = {
'网易云': 'netease',
'QQ': 'tencent',
'酷我': 'kuwo',
'Tidal': 'tidal',
'Qobuz': 'qobuz',
'喜马FM': 'ximalaya',
'咪咕': 'migu',
'酷狗': 'kugou',
'油管': 'ytmusic',
'Spotify': 'spotify',
};
const Config = Schema.intersect([
Schema.object({
waitTimeout: Schema.natural().role('s').description('允许用户返回选择序号的等待时间').default(45),
exitCommand: Schema.string().default('0, 不听了').description('退出选择指令,多个指令间请用逗号分隔开'), // 兼容中文逗号、英文逗号
menuExitCommandTip: Schema.boolean().default(false).description('是否在歌单内容的后面,加上退出选择指令的文字提示'),
}).description('基础设置'),
Schema.object({
imageMode: Schema.boolean().default(true).description('开启后返回图片歌单(需要puppeteer服务),关闭后返回文本歌单(部分指令必须使用puppeteer)'),
darkMode: Schema.boolean().default(true).description('是否开启暗黑模式(黑底菜单)')
}).description('图片歌单设置'),
Schema.object({
serverSelect: Schema.union([
Schema.const('command1').description('command1:星之阁API (需加群申请APIkey) (QQ + 网易云)'),
Schema.const('command4').description('command4:星之阁-酷狗API (需加群申请APIkey) (酷狗)'),
Schema.const('command5').description('command5:`music.gdstudio.xyz` 网站 (需puppeteer爬取 较慢,但访问性好) (多平台)'),
Schema.const('command6').description('command6:`api.injahow.cn`网站 (API 请求快 + 稳定 推荐QQ官方机器人使用) (网易云)'),
Schema.const('command7').description('command7:`dev.iw233.cn` 网站 (需puppeteer爬取 较慢) (网易云)'),
Schema.const('command8').description('command8:`www.hhlqilongzhu.cn` 龙珠API (API,江苏可能访问不了) (网易云 + QQ点歌)(好像QQ音乐有点死掉了)'),
]).role('radio').default("command6").description('选择使用的后端<br>➣ 推荐度:`api.injahow.cn` ≥ `music.gdstudio.xyz` ≥ `dev.iw233.cn` ≥ `www.hhlqilongzhu.cn` > `星之阁API`'),
}).description('后端选择'),
Schema.union([
Schema.object({
serverSelect: Schema.const('command1').required(),
xingzhigeAPIkey: Schema.string().role('secret').description('星之阁的音乐API的请求key<br>(默认值是作者自己的哦,如果失效了请你自己获取一个)<br>请前往 QQ群 905188643 <br>添加QQ好友 3556898686 <br>私聊发送 `/getapikey` 获得你的APIkey以填入此处 ')
.default("xhsP7Q4MulpzDU6BVwHSKB-j-NfvBxaqiT37hx8djyE="),
command1: Schema.string().default('下载音乐').description('星之阁API的指令名称'),
command1_wyy_Quality: Schema.number().default(2).description('网易云音乐默认下载音质。默认2,其余自己试 `不建议更改,可能会导致无音源`'),
command1_qq_Quality: Schema.number().default(2).description('QQ音乐默认下载音质。音质11为最高 `不建议更改,可能会导致无音源`'),
command1_qq_uin: Schema.string().description('QQ音乐搜索:提供skey的账号(当站长提供的cookie失效时必填,届时生效)'),
command1_qq_skey: Schema.string().description('QQ音乐搜索:提供开通有绿钻特权的skey可获取vip歌曲(当站长提供的cookie失效时必填,届时生效)为空默认获取站长提供的skey'),
command1_return_qqdata_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用')
})).role('table').default(command1_return_qqdata_Field_default).description('歌曲返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](https://api.xingzhige.com/API/QQmusicVIP/?songid=499449053&br=2&uin=2&skey=2&key=)'),
command1_return_wyydata_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用')
})).role('table').default(command1_return_wyydata_Field_default).description('歌曲返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](https://api.xingzhige.com/API/NetEase_CloudMusic_new/?name=%E8%94%9A%E8%93%9D%E6%A1%88&n=1&key=)'),
}).description('星之阁API返回设置'),
Schema.object({
serverSelect: Schema.const('command4').required(),
xingzhigeAPIkey: Schema.string().role('secret').description('星之阁的音乐API的请求key<br>(默认值是作者自己的哦,如果失效了请你自己获取一个)<br>请前往 QQ群 905188643 <br>添加QQ好友 3556898686 <br>私聊发送 `/getapikey` 获得你的APIkey以填入此处 ')
.default("xhsP7Q4MulpzDU6BVwHSKB-j-NfvBxaqiT37hx8djyE="),
command4: Schema.string().default('酷狗音乐').description('酷狗-星之阁API的指令名称'),
command4_kugouQuality: Schema.number().default(1).description('音乐默认下载音质。音质,默认为1'),
command4_return_data_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用')
})).role('table').default(command4_return_data_Field_default).description('歌曲返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](https://api.xingzhige.com/API/Kugou_GN_new/?name=蔚蓝档案&pagesize=20&br=2&key=)'),
}).description('酷狗-星之阁API返回设置'),
Schema.object({
serverSelect: Schema.const('command5').required(),
command5: Schema.string().default('歌曲搜索').description('`music.gdstudio.xyz`的指令名称'),
command5_defaultPlatform: Schema.union([
Schema.const('网易云').description('网易云'),
Schema.const('QQ').description('QQ'),
Schema.const('酷我').description('酷我'),
Schema.const('Tidal').description('Tidal'),
Schema.const('Qobuz').description('Qobuz'),
Schema.const('喜马FM').description('喜马FM'),
Schema.const('咪咕').description('咪咕'),
Schema.const('酷狗').description('酷狗'),
Schema.const('油管').description('油管'),
Schema.const('Spotify').description('Spotify'),
]).description('音乐 **默认**使用的平台。').default('网易云'),
/*
command5_defaultQuality: Schema.union([
Schema.const('128K').description('128K标准 [ 全部音乐源 ]<br>192K较高 [ 网易云 / QQ / Spotify / 咪咕 / 油管 ]'),
Schema.const('320K').description('320K高品 [ 全部音乐源 ]'),
Schema.const('16bit').description('16bit无损 [ 网易云 / QQ / 酷我 / Tidal / Qobuz / 咪咕 ]'),
Schema.const('24bit').description('24bit无损 [ 网易云 / QQ / Tidal / Qobuz ]'),
]).role('radio').description('音乐 **默认**下载音质。').default('320K'),
*/
command5_searchList: Schema.number().default(20).min(1).max(20).description('歌曲搜索的列表长度。返回的候选项个数。'),
command5_page_setTimeout: Schema.number().default(15).min(1).description('等待页面完全加载的等待时间(秒)'),
command5_return_data_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用'),
})).role('table').description('歌曲返回信息的字段选择<br>').default(command5_return_data_Field_default),
}).description('`music.gdstudio.xyz`返回设置'),
Schema.object({
serverSelect: Schema.const('command6'),
command6: Schema.string().default('网易点歌').description('`网易点歌`的指令名称<br>输入歌曲ID,返回歌曲'),
command6_searchList: Schema.number().default(20).min(1).max(50).description('歌曲搜索的列表长度。返回的候选项个数。'),
maxDuration: Schema.natural().description('歌曲最长持续时间,单位为:秒').default(900),
command6_usedAPI: Schema.union([
Schema.const('api.injahow.cn').description('稳定、黑胶只能30秒的`api.injahow.cn`后端(适合官方bot)'),
Schema.const('www.byfuns.top').description('稳定性未知、全部可听的`www.byfuns.top`后端').experimental(),
]).description("选择 获取音乐直链的后端API").default("www.byfuns.top"),
command6_return_data_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用'),
})).role('table').description('歌曲返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](http://music.163.com/api/search/get/web?csrf_token=hlpretag=&hlposttag=&s=蔚蓝档案&type=1&offset=0&total=true&limit=10)').default(command6_return_data_Field_default),
}).description('`网易点歌`返回设置'),
Schema.object({
serverSelect: Schema.const('command7').required(),
command7: Schema.string().default('音乐搜索器').description('`音乐搜索器`的指令名称<br>使用 dev.iw233.cn 提供的网站'),
command7_searchList: Schema.number().default(10).min(1).step(1).max(10).description('歌曲搜索的列表长度。返回的候选项个数。<br>为`网易云音乐`的组合'),
command7_return_data_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用'),
})).role('table').description('歌曲返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](https://dev.iw233.cn/Music1/?name=%E8%94%9A%E8%93%9D%E6%A1%A3%E6%A1%88&type=netease) 需F12 网络标签页 预览响应 `Music1/`').default(command7_return_data_Field_default),
}).description('`dev.iw233.cn`返回设置'),
Schema.object({
serverSelect: Schema.const('command8').required(),
command8: Schema.string().default('龙珠搜索').description('龙珠API的指令名称'),
command8_wyyQuality: Schema.number().default(1).description('网易云音乐默认下载音质。`找不到对应音质,会自动使用标准音质`<br>1(标准音质)/2(极高音质)/3(无损音质)/4(Hi-Res音质)/5(高清环绕声)/6(沉浸环绕声)/7(超清母带)'),
command8_searchList: Schema.number().default(20).min(1).max(50).description('歌曲搜索的列表长度。返回的候选项个数。'),
command8_return_wyydata_Field: Schema.array(Schema.object({
data: Schema.string().description('返回的字段'),
describe: Schema.string().description('对该字段的中文描述'),
type: Schema.union([
Schema.const('text').description('文本(text)'),
Schema.const('image').description('图片(image)'),
Schema.const('audio').description('语音(audio)'),
Schema.const('video').description('视频(video)'),
Schema.const('file').description('文件(file)'),
]).description('字段发送类型'),
enable: Schema.boolean().default(true).description('是否启用')
})).role('table').default(command8_return_wyydata_Field_default).description('网易云歌曲 返回信息的字段选择<br>[➣ 点我查看该API返回内容示例](https://www.hhlqilongzhu.cn/api/dg_wyymusic.php?gm=蔚蓝档案&type=json&num=10&n=1)'),
}).description('龙珠API返回设置'),
Schema.object({
}).description('↑ 请选择后端服务 ↑'),
]),
Schema.object({
enablemiddleware: Schema.boolean().description("是否自动解析JSON音乐卡片").default(false),
middleware: Schema.boolean().description("`enablemiddleware`是否使用前置中间件监听<br>`中间件无法接受到消息可以考虑开启`").default(false),
used_id: Schema.number().default(1).min(0).max(10).description("在歌单里默认选择的序号<br>范围`0-10`,无需考虑11-20,会自动根据JSON卡片的平台选择。若音乐平台不匹配 则在搜索项前十个进行选择。"),
}).description('JSON卡片解析设置'),
Schema.object({
isfigure: Schema.boolean().default(false).description("`图片、文本`元素 使用合并转发,其余单独发送<br>`仅支持 onebot 适配器` 其他平台开启 无效").experimental(),
isuppercase: Schema.boolean().default(false).description("将链接域名进行大写置换,仅适用于qq官方平台").experimental(),
data_Field_Mode: Schema.union([
Schema.const('text').description('富媒体置底:文字 > 图片 > 语音 ≥ 视频 ≥ 文件 (默认)'),
Schema.const('image').description('仅图片置顶的 富媒体置底:图片 > 文字 ≥ 语音 ≥ 视频 ≥ 文件 (仅官方机器人考虑使用)'),
Schema.const('raw').description('严格按照 `command_return_data_Field` 表格的顺序 (严格按照配置项表格的上下顺序)'),
]).role('radio').default("text").description('对 `command*_return_data_Field`配置项 排序的控制<br>优先级越高,顺序越靠前<br>[➣点我查看此配置项 效果预览图](https://i0.hdslb.com/bfs/article/6e8b901f9b9daa57f082bf0cece36102312276085.png)'),
deleteTempTime: Schema.number().default(20).description('对于`file`类型的`Temp`临时文件的删除时间<br>若干`秒`后 删除下载的本地临时文件').experimental(),
}).description('高级进阶设置'),
Schema.object({
loggerinfo: Schema.boolean().default(false).description('日志调试开关'),
}).description('调试模式'),
]);
function apply(ctx, config) {
const tempDir = path.join(__dirname, 'temp'); // h.file的临时存储 用于解决部分协议端必须上传本地URL
let isTempDirInitialized = false;
const tempFiles = new Set(); // 用于跟踪临时文件路径
ctx.on('ready', async () => {
ctx.i18n.define("zh-CN", {
commands: {
[config.command1]: {
description: `搜索歌曲`,
messages: {
"nokeyword": `请输入歌曲相关信息。\n➣示例:/${config.command1} 蔚蓝档案`,
"songlisterror": "无法获取歌曲列表,请稍后再试。",
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
}
},
[config.command4]: {
description: `搜索酷狗音乐`,
messages: {
"nokeyword": `请输入歌曲相关信息。\n➣示例:/${config.command4} 蔚蓝档案`,
"songlisterror": "获取酷狗音乐数据时发生错误,请稍后再试。",
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
}
},
[config.command5]: {
description: `歌曲搜索`,
messages: {
"nopuppeteer": "没有开启puppeteer服务",
"nokeyword": `请输入歌曲相关信息。\n➣示例:/${config.command5} 蔚蓝档案`,
"invalidplatform": "`不支持的平台: {0}`;",
"songlisterror": "无法获取歌曲列表,请稍后再试。",
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
"noSearchResults": "没有找到相关的歌曲,请尝试更换关键词或平台。",
}
},
[config.command6]: {
description: `网易云点歌`,
messages: {
"nopuppeteer": "没有开启puppeteer服务",
"nokeyword": `请输入网易云歌曲的 名称 或 ID。\n➣示例:/${config.command6} 蔚蓝档案\n➣示例:/${config.command6} 2608813264`,
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
"songlisterror": "无法获取歌曲列表,请稍后再试。",
"maxsongDuration": "歌曲持续时间超出限制,允许的单曲最大时长为 {0} 秒。",
}
},
[config.command7]: {
description: `音乐搜索器`,
messages: {
"nopuppeteer": "没有开启puppeteer服务",
"nokeyword": `请输入歌曲相关信息。\n➣示例:/${config.command7} 蔚蓝档案`,
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
"songlisterror": "无法获取歌曲列表,请稍后再试。",
}
},
[config.command8]: {
description: `龙珠音乐`,
messages: {
"nopuppeteer": "没有开启puppeteer服务",
"nokeyword": `请输入歌曲相关信息。\n➣示例:/${config.command8} 蔚蓝档案`,
"invalidNumber": "序号输入错误,已退出歌曲选择。",
"waitTime": "请在{0}秒内,\n输入歌曲对应的序号:\n➣示例:@机器人 1",
"waitTimeout": "输入超时,已取消点歌。",
"exitprompt": "已退出歌曲选择。",
"noplatform": "获取歌曲失败。",
"somerror": "解析歌曲详情时发生错误",
"songlisterror": "无法获取歌曲列表,请稍后再试。",
}
}
}
});
if (config.enablemiddleware) {
ctx.middleware(async (session, next) => {
try {
// 解析消息内容
const messageElements = await h.parse(session.content);
// 遍历解析后的消息元素
for (const element of messageElements) {
// 确保元素类型为 'json' 并且有数据
if (element.type === 'json' && element.attrs && element.attrs.data) {
const jsonData = JSON.parse(element.attrs.data);
logInfo(JSON.stringify(jsonData, null, 2));
// 检查是否存在 musicMeta 和 tag
const musicMeta = jsonData?.meta?.music || jsonData?.meta?.news; // 尝试兼容两种结构
const tag = musicMeta?.tag;
if (musicMeta && tag.includes("音乐")) {
const title = musicMeta.title;
const desc = musicMeta.desc;
logInfo("↡--------------中间件解析--------------↡");
logInfo(tag);
logInfo(title);
logInfo(desc);
logInfo("↟--------------中间件解析--------------↟");
// 获取配置的指令名称
let command = config.serverSelect;
let commandName = config[command]; // 直接使用 config[command] 获取配置项的值
logInfo(commandName);
if (!commandName) {
commandName = '歌曲搜索'; // 默认值,以防配置项不存在
logger.error(`未找到配置项 ${command} 对应的指令名称,使用默认指令名称 '歌曲搜索'`);
}
// 如果选择了 command6 并且是网易云音乐卡片
if (command === 'command6' && tag === '网易云音乐') {
// 直接提取歌曲 ID
const jumpUrl = musicMeta.jumpUrl;
const match = jumpUrl?.match(/id=(\d+)/); // 使用 ?. 确保 jumpUrl 不为 null 或 undefined
if (match && match[1]) {
const songId = match[1];
logInfo(`提取到网易云音乐 ID: ${songId}`);
// 执行 command6 指令
await session.execute(`${commandName} ${songId}`);
return; // 结束当前中间件处理
} else {
logger.error('未能在 jumpUrl 中找到歌曲 ID');
}
} else {
// 其他情况,按照原逻辑处理
let usedId = config.used_id;
if (tag === '网易云音乐') {
if (config.serverSelect === "command1") { // command1 的网易云音乐是后 10 个
usedId += 10;
}
}
logInfo(`使用指令: ${command} ,选择序号:${usedId}`)
if (command) {
// 更通用的获取指令名称方式
logInfo(`${commandName} -n ${usedId} “${title} ${desc}”`)
await session.execute(`${commandName} -n ${usedId} “${title} ${desc}”`);
}
}
}
}
}
} catch (error) {
ctx.logger.error(error);
await session.send('处理消息时出错。');
}
// 如果没有匹配到任何 json 数据,继续下一个中间件
return next();
}, config.middleware);
}
if (config.serverSelect === "command1") {
ctx.command(`${config.command1} <keyword:text>`)
.option('quality', '-q <value:number> 品质因数')
.option('number', '-n <number:number> 歌曲序号')
.action(async ({ session, options }, keyword) => {
if (!keyword) return h.text(session.text(".nokeyword"));
let qq, netease;
try {
let res = await searchQQ(ctx.http, keyword);
if (typeof res === 'string') res = JSON.parse(res);
const item = res.request?.data?.body?.item_song;
qq = {
code: res.code,
msg: '',
data: Array.isArray(item) ? item.map(v => ({
songname: v.title.replaceAll('<em>', '').replaceAll('</em>', ''),
album: v.album.name,
songid: v.id,
songurl: `https://y.qq.com/n/ryqq/songDetail/${v.mid}`,
name: v.singer.map(v => v.name).join('/')
})) : []
};
logInfo(qq)
} catch (e) {
logger.error('获取QQ音乐数据时发生错误', e);
}
try {
netease = await searchXZG(ctx.http, 'NetEase Music',
{
name: keyword,
key: config.xingzhigeAPIkey
});
} catch (e) {
logger.error('获取网易云音乐数据时发生错误', e);
}
const qqData = qq?.data;
const neteaseData = netease?.data;
if (!qqData?.length && !neteaseData?.length) return h.text(session.text(`.songlisterror`));
const totalQQSongs = qqData?.length ?? 0;
const totalNetEaseSongs = neteaseData?.length ?? 0;
// 检查是不是可用序号
let serialNumber = options.number;
if (serialNumber) {
serialNumber = Number(serialNumber);
if (Number.isNaN(serialNumber) || serialNumber < 1 || serialNumber > (totalQQSongs + totalNetEaseSongs)) {
return h.text(session.text(`.invalidNumber`));
}
} else {
// 给用户选择序号
const qqListText = qqData?.length ? formatSongList(qqData, 'QQ Music', 0, 10) : '<b>QQ Music</b>: 无法获取歌曲列表';
const neteaseListText = neteaseData?.length ? formatSongList(neteaseData, 'NetEase Music', qqData?.length ? 0 : 10, 20) : '<b>NetEase Music</b>: 无法获取歌曲列表';
const listText = `${qqListText}<br /><br />${neteaseListText}`;
const exitCommands = config.exitCommand.split(/[,,]/).map(cmd => cmd.trim());
const exitCommandTip = config.menuExitCommandTip ? `退出选择请发[${exitCommands}]中的任意内容<br /><br />` : '';
let quoteId = session.messageId;
if (config.imageMode) {
const imageBuffer = await generateSongListImage(ctx.puppeteer, listText);
const payload = [
h.image(imageBuffer, 'image/png'),
h.text(`${exitCommandTip.replaceAll('<br />', '\n')}${h.text(session.text(`.waitTime`, [config.waitTimeout]))}`),
];
const msg = await session.send(payload);
quoteId = msg.at(-1);
} else {
const msg = await session.send(`${listText}<br /><br />${exitCommandTip}${h.text(session.text(`.waitTime`, [config.waitTimeout]))}`);
quoteId = msg.at(-1);
}
const input = await session.prompt(config.waitTimeout * 1000);
if (!input) {
return quoteId ? h.quote(quoteId) : '' + h.text(session.text(`.waitTimeout`));
}
if (exitCommands.includes(input)) {
return h.text(session.text(`.exitprompt`));
}
serialNumber = +input;
if (Number.isNaN(serialNumber) || serialNumber < 1 || serialNumber > (totalQQSongs + totalNetEaseSongs)) {
return h.text(session.text(`.songlisterror`));
}
}
let platform, songid, br, uin, skey;
let selected;
if (serialNumber <= totalQQSongs) {
selected = qqData[serialNumber - 1];
platform = 'QQ Music';
songid = selected.songid;
br = config.command1_qq_Quality;
uin = config.command1_qq_uin;
skey = config.command1_qq_skey;
} else {
selected = neteaseData[serialNumber - totalQQSongs - 1];
platform = 'NetEase Music';
songid = selected.id;
br = config.command1_wyy_Quality;
uin = 'onlyqq';
skey = 'onlyqq';
}
if (options.quality) {
br = options.quality;
}
if (!platform) return h.text(session.text(`.noplatform`));
const song = await searchXZG(ctx.http, platform, {
songid,
br,
uin,
skey,
key: config.xingzhigeAPIkey
});
if (song.code === 0) {
const data = song.data;
try {
let songDetails;
if (serialNumber <= totalQQSongs) {
songDetails = generateResponse(session, data, config.command1_return_qqdata_Field);
} else {
songDetails = generateResponse(session, data, config.command1_return_wyydata_Field);
}
logInfo(songDetails);
return songDetails;
} catch (e) {
logger.error(e);
return h.text(session.text(`.somerror`));
}
} else {
logger.error(`获取歌曲失败:${JSON.stringify(song)}`);
return '获取歌曲失败:' + song.msg;
}
});
}
if (config.serverSelect === "command4") {
ctx.command(`${config.command4} <keyword:text>`)
.option('quality', '-q <value:number> 音质因数')
.option('number', '-n <number:number> 歌曲序号')
.action(async ({ session, options }, keyword) => {
if (!keyword) return h.text(session.text(`.nokeyword`));
let kugou;
try {
kugou = await searchKugou(ctx.http, keyword, options.quality || config.command4_kugouQuality);
if (kugou.code !== 200) {
logger.error(kugou);
return h.text(`获取酷狗音乐数据时发生错误`);
}
} catch (e) {
logger.error('获取酷狗音乐数据时发生错误', e);
return h.text(session.text(`.songlisterror`));
}
const kugouData = kugou?.data;
if (!kugouData?.length) return h.text(session.text(`.songlisterror`));
const totalKugouSongs = kugouData.length;
// 检查是不是可用序号
let serialNumber = options.number;
if (serialNumber) {
serialNumber = Number(serialNumber);
if (Number.isNaN(serialNumber) || serialNumber < 1 || serialNumber > totalKugouSongs) {
return h.text(session.text(`.invalidNumber`));
}
} else {
// 给用户选择序号
const kugouListText = formatSongList(kugouData, '酷狗音乐', 0, 20);
const exitCommands = config.exitCommand.split(/[,,]/).map(cmd => cmd.trim());
const exitCommandTip = config.menuExitCommandTip ? `退出选择请发[${exitCommands}]中的任意内容<br /><br />` : '';
let quoteId = session.messageId;
if (config.imageMode) {
const imageBuffer = await generateSongListImage(ctx.puppeteer, kugouListText);
const payload = [
h.image(imageBuffer, 'image/png'),
h.text(`${exitCommandTip.replaceAll('<br />', '\n')}${h.text(session.text(`.waitTime`, [config.waitTimeout]))}`),
];
const msg = await session.send(payload);
quoteId = msg.at(-1);
} else {
const msg = await session.send(`${kugouListText}<br /><br />${exitCommandTip}${h.text(session.text(`.waitTime`, [config.waitTimeout]))}`);
quoteId = msg.at(-1);
}
const input = await session.prompt(config.waitTimeout * 1000);
if (!input) {
return `${quoteId ? h.quote(quoteId) : ''}输入超时,已取消点歌。`;
}
if (exitCommands.includes(input)) {
return h.text(session.text(`.exitprompt`));
}
serialNumber = +input;
if (Number.isNaN(serialNumber) || serialNumber < 1 || serialNumber > totalKugouSongs) {
return h.text(session.text(`.invalidNumber`));
}
}
//const selected = kugouData[serialNumber - 1];
//const songid = serialNumber;
//logInfo(songid);
const br = options.quality || config.command4_kugouQuality;
const song = await searchKugouSong(ctx.http, keyword, br, serialNumber);
if (song.code === 0) {
const data = song.data;
try {
logInfo(song);
logInfo(data);
const songDetails = generateResponse(session, data, config.command4_return_data_Field);
logInfo(songDetails);
return songDetails;
} catch (e) {
logger.error(e);
return h.text(session.text(`.somerror`));
}
} else {
logger.error(`获取歌曲失败:${JSON.stringify(song)}`);
return '获取歌曲失败:' + song.msg;
}
});
}
if (config.serverSelect === "command5") {
ctx.command(`${config.command5} <keyword:text>`)
.option('platform', '-p <platform:string> 平台名称')
.option('number', '-n <number:number> 歌曲序号')
.example("歌曲搜索 -p QQ -n 1 蔚蓝档案")
.action(async ({ session, options }, keyword) => {
if (!ctx.puppeteer) {
await session.send(h.text(session.text(`.nopuppeteer`)));
return;
}
if (!keyword) return h.text(session.text(`.nokeyword`));
const page = await ctx.puppeteer.page(); // 主页面,用于搜索和双击
let searchResults = [];
let songDetails = {
musicUrl: undefined,
coverUrl: undefined,
lyric: undefined,
musicSize: undefined,
musicBr: undefined
};
// let timeoutId; // 移除 timeoutId
const exitCommands = config.exitCommand.split(/[,,]/).map(cmd => cmd.trim()); // 定义在action函数内
// 错误处理函数,用于处理 API 响应解析错误
const handleApiResponse = (text, type) => {
try {
const match = text.match(/^jQuery\w+\((.*)\)$/);
let jsonData;
if (match) {
jsonData = JSON.parse(match[1]);
} else {
jsonData = JSON.parse(text); // 尝试直接解析,可能不是 jQuery 回调
}
if (!jsonData) {
ctx.logger.warn(`无法解析 ${type} API 响应: 没有 JSON 数据`);
return null;
}
return jsonData;
} catch (error) {
ctx.logger.error(`解析 ${type} API 响应失败:`, error, text);
return null;
}
};
// 得放到page.on外面,不然没有本地化
// [W] i18n Error: missing scope for ".waitTime"
const exitCommandTip = config.menuExitCommandTip ? `退出选择请发[${exitCommands}]中的任意内容\n\n` : '';
const promptText = `${exitCommandTip}${h.text(session.text(`.waitTime`, [config.waitTimeout]))}`;
const waitTimeout = session.text(`.waitTimeout`)
const exitprompt = session.text(`.exitprompt`)
const invalidNumber = session.text(`.invalidNumber`)
let popupError;
async function checkAndHandlePopup(page) {
// layui-layer layui-layer-dialog layui-layer-border layui-layer-msg layui-layer-hui
const alert = await page.$('.layui-layer.layui-layer-msg.layui-layer-hui, .layui-layer-dialog.layui-layer-msg');
if (alert) {
// 修改 page.evaluate,从 alert 元素本身查找 .layui-layer-content
const alertText = await page.evaluate(alertElement => {
// 直接在 alertElement 中查询 .layui-layer-content
const alertContent = alertElement.querySelector('.layui-layer-content');
return alertContent ? alertContent.innerText : null;
}, alert);
if (a