Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/fumiama/unibase2n v0.0.0-20240530074540-ec743fd5a6d6
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0
github.com/google/uuid v1.6.0
github.com/guohuiyuan/music-lib v1.0.1
github.com/jinzhu/gorm v1.9.16
github.com/jozsefsallai/gophersauce v1.0.1
github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopxl/beep/v2 v2.1.1 h1:6FYIYMm2qPAdWkjX+7xwKrViS1x0Po5kDMdRkq8NVbU=
github.com/gopxl/beep/v2 v2.1.1/go.mod h1:ZAm9TGQ9lvpoiFLd4zf5B1IuyxZhgRACMId1XJbaW0E=
github.com/guohuiyuan/music-lib v1.0.1 h1:RE1/0o7cMD+zU4vWtau3P6Czl9fW1a1G3dbjbgTA5H0=
github.com/guohuiyuan/music-lib v1.0.1/go.mod h1:0qJM2Ug7Ow7hmbX5M5jPBRnZ1UN2No9GX156LDAaJ80=
github.com/jfreymuth/oggvorbis v1.0.5 h1:u+Ck+R0eLSRhgq8WTmffYnrVtSztJcYrl588DM4e3kQ=
github.com/jfreymuth/oggvorbis v1.0.5/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=
Expand Down
326 changes: 138 additions & 188 deletions plugin/music/selecter.go
Original file line number Diff line number Diff line change
@@ -1,239 +1,189 @@
// Package music QQ音乐、网易云、酷狗、酷我、咪咕 点歌
// Package music 整合多平台音乐点播能力(基于 music-lib 重构)
package music

import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/FloatTech/floatbox/web"

ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/control"
ctrl "github.com/FloatTech/zbpctrl" // 别名 zbpctrl 为 ctrl
"github.com/FloatTech/zbputils/control" // 保持 control 原名
"github.com/FloatTech/zbputils/ctxext"
"github.com/tidwall/gjson"
"github.com/guohuiyuan/music-lib/kugou"
"github.com/guohuiyuan/music-lib/kuwo"
"github.com/guohuiyuan/music-lib/migu"
"github.com/guohuiyuan/music-lib/netease"
"github.com/guohuiyuan/music-lib/qq"
zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message"
)

var (
longZhuURL = "https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg=%v"
)
// 平台映射:指令前缀 -> 平台名称
var platformMap = map[string]string{
"咪咕": "migu",
"酷我": "kuwo",
"酷狗": "kugou",
"网易": "netease",
"qq": "qq",
"": "kuwo", // 默认点歌指向酷我
}

func init() {
// 注册指令处理器
control.AutoRegister(&ctrl.Options[*zero.Ctx]{
DisableOnDefault: false,
Brief: "点歌",
Help: "- 点歌[xxx]\n" +
Help: "- 点歌[xxx] (默认酷我)\n" +
"- 网易点歌[xxx]\n" +
"- 酷我点歌[xxx]\n" +
"- 酷狗点歌[xxx]\n" +
"- 咪咕点歌[xxx]\n" +
"- qq点歌[xxx]\n",
}).OnRegex(`^(.{0,2})点歌\s?(.{1,25})$`).SetBlock(true).Limit(ctxext.LimitByUser).
Handle(func(ctx *zero.Ctx) {
// switch 平台
switch ctx.State["regex_matched"].([]string)[1] {
case "咪咕":
ctx.SendChain(migu(ctx.State["regex_matched"].([]string)[2]))
case "酷我":
ctx.SendChain(kuwo(ctx.State["regex_matched"].([]string)[2]))
case "酷狗":
ctx.SendChain(kugou(ctx.State["regex_matched"].([]string)[2]))
case "网易":
ctx.SendChain(cloud163(ctx.State["regex_matched"].([]string)[2]))
case "qq":
ctx.SendChain(qqmusic(ctx.State["regex_matched"].([]string)[2]))
default: // 默认聚合点歌
ctx.SendChain(longzhu(ctx.State["regex_matched"].([]string)[2]))
matches := ctx.State["regex_matched"].([]string)
platformPrefix := matches[1]
keyword := matches[2]

// 获取目标平台
targetPlatform, ok := platformMap[platformPrefix]
if !ok {
ctx.SendChain(message.Text("不支持的点播平台:", platformPrefix))
return
}

// 执行点播并返回结果
seg, err := getMusicSegment(targetPlatform, keyword)
if err != nil {
ctx.SendChain(message.Text("点歌失败:", err.Error()))
return
}
ctx.SendChain(seg)
})
}

// longzhu 聚合平台
func longzhu(keyword string) message.Segment {
data, _ := web.GetData(fmt.Sprintf(longZhuURL, url.QueryEscape(keyword)))
// 假设 data 是包含整个 JSON 数组的字节切片
results := gjson.ParseBytes(data).Array()
for _, result := range results {
if strings.Contains(strings.ToLower(result.Get("title").String()), strings.ToLower(keyword)) {
if musicURL := result.Get("full_track").String(); musicURL != "" {
return message.Record(musicURL)
}
}
// getMusicSegment 根据平台和关键词获取音乐消息段
func getMusicSegment(platform, keyword string) (message.Segment, error) {
switch platform {
case "migu":
return getMiguMusic(keyword)
case "kuwo":
return getKuwoMusic(keyword)
case "kugou":
return getKugouMusic(keyword)
case "netease":
return getNeteaseMusic(keyword)
case "qq":
return getQQMusic(keyword)
default:
return message.Segment{}, errors.New("未知的音乐平台:" + platform)
}
}

results = gjson.GetBytes(data, "#.full_track").Array()
if len(results) > 0 {
if musicURL := results[0].String(); musicURL != "" {
return message.Record(musicURL)
}
}
// --- 各平台适配层(基于 music-lib 实现) ---

return message.Text("点歌失败, 找不到 ", keyword, " 的相关结果")
}
// getMiguMusic 咪咕音乐点播
func getMiguMusic(keyword string) (message.Segment, error) {
songs, err := migu.Search(keyword)
if err != nil || len(songs) == 0 {
return message.Segment{}, errors.New("咪咕音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

// migu 返回咪咕音乐卡片
func migu(keyword string) message.Segment {
headers := http.Header{
"Cookie": []string{"audioplayer_exist=1; audioplayer_open=0; migu_cn_cookie_id=3ad476db-f021-4bda-ab91-c485ac3d56a0; Hm_lvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; Hm_lpvt_ec5a5474d9d871cb3d82b846d861979d=1671119573; WT_FPC=id=279ef92eaf314cbb8d01671116477485:lv=1671119583092:ss=1671116477485"},
"csrf": []string{"LWKACV45JSQ"},
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"},
"Referer": []string{"http://m.music.migu.cn"},
"proxy": []string{"false"},
// 传入 &song (指针)
playURL, err := migu.GetDownloadURL(&song)
if err != nil || playURL == "" {
return message.Segment{}, errors.New("获取咪咕播放链接失败:" + err.Error())
}
// 搜索音乐信息 第一首歌
search, _ := url.Parse("http://m.music.migu.cn/migu/remoting/scr_search_tag")
search.RawQuery = url.Values{
"keyword": []string{keyword},
"type": []string{"2"},
"pgc": []string{"1"},
"rows": []string{"10"},
}.Encode()
info := gjson.ParseBytes(netGet(search.String(), headers)).Get("musics.0")
// 返回音乐卡片

return message.CustomMusic(
fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", info.Get("copyrightId").String()),
info.Get("mp3").String(),
info.Get("songName").String(),
).Add("content", info.Get("artist").Str).Add("image", info.Get("cover").Str).Add("subtype", "migu")
fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", song.ID),
playURL,
song.Name,
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "migu"), nil
}

// kuwo 返回酷我音乐卡片
func kuwo(keyword string) message.Segment {
headers := http.Header{
"Cookie": []string{"Hm_lvt_cdb524f42f0ce19b169a8071123a4797=1610284708,1610699237; _ga=GA1.2.1289529848.1591618534; kw_token=LWKACV45JSQ; Hm_lpvt_cdb524f42f0ce19b169a8071123a4797=1610699468; _gid=GA1.2.1868980507.1610699238; _gat=1"},
"csrf": []string{"LWKACV45JSQ"},
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
"Referer": []string{"https://www.kuwo.cn/search/list?key="},
// getKuwoMusic 酷我音乐点播
func getKuwoMusic(keyword string) (message.Segment, error) {
songs, err := kuwo.Search(keyword)
if err != nil || len(songs) == 0 {
return message.Segment{}, errors.New("酷我音乐未找到相关歌曲:" + keyword)
}
// 搜索音乐信息 第一首歌
search, _ := url.Parse("https://www.kuwo.cn/api/www/search/searchMusicBykeyWord")
search.RawQuery = url.Values{
"key": []string{keyword},
"pn": []string{"1"},
"rn": []string{"1"},
"httpsStatus": []string{"1"},
}.Encode()
info := gjson.ParseBytes(netGet(search.String(), headers)).Get("data.list.0")
// 获得音乐直链
music, _ := url.Parse("http://www.kuwo.cn/api/v1/www/music/playUrl")
music.RawQuery = url.Values{
"mid": []string{fmt.Sprintf("%d", info.Get("rid").Int())},
"type": []string{"convert_url3"},
"br": []string{"320kmp3"},
"httpsStatus": []string{"1"},
}.Encode()
audio := gjson.ParseBytes(netGet(music.String(), headers))
// 返回音乐卡片
song := songs[0]

// 传入 &song (指针)
playURL, err := kuwo.GetDownloadURL(&song)
if err != nil || playURL == "" {
return message.Segment{}, errors.New("获取酷我播放链接失败:" + err.Error())
}

return message.CustomMusic(
fmt.Sprintf("https://www.kuwo.cn/play_detail/%d", info.Get("rid").Int()),
audio.Get("data.url").Str,
info.Get("name").Str,
).Add("content", info.Get("artist").Str).Add("image", info.Get("pic").Str).Add("subtype", "kuwo")
fmt.Sprintf("https://www.kuwo.cn/play_detail/%s", song.ID),
playURL,
song.Name,
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kuwo"), nil
}

// kugou 返回酷狗音乐卡片
func kugou(keyword string) message.Segment {
stamp := time.Now().UnixNano() / 1e6
hash := md5str(
fmt.Sprintf(
"NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtbitrate=0callback=callback123clienttime=%dclientver=2000dfid=-inputtype=0iscorrection=1isfuzzy=0keyword=%smid=%dpage=1pagesize=30platform=WebFilterprivilege_filter=0srcappid=2919tag=emuserid=-1uuid=%dNVPh5oo715z5DIWAeQlhMDsWXXQV4hwt",
stamp, keyword, stamp, stamp,
),
)
// 搜索音乐信息 第一首歌
h1 := http.Header{
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
// getKugouMusic 酷狗音乐点播
func getKugouMusic(keyword string) (message.Segment, error) {
songs, err := kugou.Search(keyword)
if err != nil || len(songs) == 0 {
return message.Segment{}, errors.New("酷狗音乐未找到相关歌曲:" + keyword)
}
search, _ := url.Parse("https://complexsearch.kugou.com/v2/search/song")
search.RawQuery = url.Values{
"callback": []string{"callback123"},
"keyword": []string{keyword},
"page": []string{"1"},
"pagesize": []string{"30"},
"bitrate": []string{"0"},
"isfuzzy": []string{"0"},
"tag": []string{"em"},
"inputtype": []string{"0"},
"platform": []string{"WebFilter"},
"userid": []string{"-1"},
"clientver": []string{"2000"},
"iscorrection": []string{"1"},
"privilege_filter": []string{"0"},
"srcappid": []string{"2919"},
"clienttime": []string{fmt.Sprintf("%d", stamp)},
"mid": []string{fmt.Sprintf("%d", stamp)},
"uuid": []string{fmt.Sprintf("%d", stamp)},
"dfid": []string{"-"},
"signature": []string{hash},
}.Encode()
res := netGet(search.String(), h1)
info := gjson.ParseBytes(res[12 : len(res)-2]).Get("data.lists.0")
// 获得音乐直链
h2 := http.Header{
"Cookie": []string{"kg_mid=d8e70a262c93d47599c6196c612d6f4f; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1610278505,1611631363,1611722252; kg_dfid=33ZWee1kircl0jcJ1h0WF1fX; Hm_lpvt_aedee6983d4cfc62f509129360d6bb3d=1611727348; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e"},
"Host": []string{"wwwapi.kugou.com"},
"TE": []string{"Trailers"},
"User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"},
song := songs[0]

// 传入 &song (指针)
playURL, err := kugou.GetDownloadURL(&song)
if err != nil || playURL == "" {
return message.Segment{}, errors.New("获取酷狗播放链接失败:" + err.Error())
}
music := "https://wwwapi.kugou.com/yy/index.php?r=play%2Fgetdata&hash=" + info.Get("FileHash").Str + "&album_id=" + info.Get("AlbumID").Str
audio := gjson.ParseBytes(netGet(music, h2)).Get("data")
// 返回音乐卡片

return message.CustomMusic(
"https://www.kugou.com/song/#hash="+audio.Get("hash").Str+"&album_id="+audio.Get("album_id").Str,
strings.ReplaceAll(audio.Get("play_backup_url").Str, "\\/", "/"),
audio.Get("audio_name").Str,
).Add("content", audio.Get("author_name").Str).Add("image", audio.Get("img").Str).Add("subtype", "kugou")
"https://www.kugou.com/",
playURL,
song.Name,
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kugou"), nil
}

// cloud163 返回网易云音乐卡片
func cloud163(keyword string) (msg message.Segment) {
requestURL := "http://music.163.com/api/search/get/web?type=1&limit=1&s=" + url.QueryEscape(keyword)
data, err := web.GetData(requestURL)
if err != nil {
msg = message.Text("ERROR: ", err)
return
// getNeteaseMusic 网易云音乐点播
func getNeteaseMusic(keyword string) (message.Segment, error) {
songs, err := netease.Search(keyword)
if err != nil || len(songs) == 0 {
return message.Segment{}, errors.New("网易云音乐未找到相关歌曲:" + keyword)
}
msg = message.Music("163", gjson.ParseBytes(data).Get("result.songs.0.id").Int())
return
}
song := songs[0]

// qqmusic 返回QQ音乐卡片
func qqmusic(keyword string) (msg message.Segment) {
requestURL := "https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?platform=yqq.json&key=" + url.QueryEscape(keyword)
data, err := web.RequestDataWith(web.NewDefaultClient(), requestURL, "GET", "", web.RandUA(), nil)
if err != nil {
msg = message.Text("ERROR: ", err)
return
// 获取播放直链
playURL, err := netease.GetDownloadURL(&song)
if err != nil || playURL == "" {
return message.Segment{}, errors.New("获取网易云播放链接失败:" + err.Error())
}
msg = message.Music("qq", gjson.ParseBytes(data).Get("data.song.itemlist.0.id").Int())
return
}

// md5str 返回字符串 MD5
func md5str(s string) string {
h := md5.New()
h.Write([]byte(s))
result := strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
return result
// 构造 CustomMusic
return message.CustomMusic(
fmt.Sprintf("https://music.163.com/#/song?id=%s", song.ID),
playURL,
song.Name,
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "163"), nil
}

// netGet 返回请求数据
func netGet(url string, header http.Header) []byte {
client := &http.Client{}
request, _ := http.NewRequest("GET", url, nil)
request.Header = header
res, err := client.Do(request)
if err != nil {
return nil
// getQQMusic QQ音乐点播
func getQQMusic(keyword string) (message.Segment, error) {
songs, err := qq.Search(keyword)
if err != nil || len(songs) == 0 {
return message.Segment{}, errors.New("QQ音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

// 获取播放直链
playURL, err := qq.GetDownloadURL(&song)
if err != nil || playURL == "" {
return message.Segment{}, errors.New("获取QQ音乐播放链接失败:" + err.Error())
}
defer res.Body.Close()
result, _ := io.ReadAll(res.Body)
return result

// 构造 CustomMusic
return message.CustomMusic(
fmt.Sprintf("https://y.qq.com/n/ryqq/songDetail/%s", song.ID),
playURL,
song.Name,
).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "qq"), nil
}
Loading