Skip to content
Open
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.2-0.20260121020416-53f6cb24629d
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.2-0.20260121020416-53f6cb24629d h1:6Cw52c4JaYvq55yAa9ZgUQeBL6b3ZWErQqkbeMZiAYw=
github.com/guohuiyuan/music-lib v1.0.2-0.20260121020416-53f6cb24629d/go.mod h1:D/6kQDwhQFDNZEMjN8y760DQSVYpOGlQXrYzhKz0rCQ=
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
318 changes: 131 additions & 187 deletions plugin/music/selecter.go
Original file line number Diff line number Diff line change
@@ -1,239 +1,183 @@
// Package music QQ音乐、网易云、酷狗、酷我、咪咕 点歌
// Package music 整合多平台音乐点播能力
package music

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

"github.com/FloatTech/floatbox/web"

ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/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"
"github.com/pkg/errors"
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]func(string) (message.Segment, error){
"咪咕": getMiguMusic,
"酷我": getKuwoMusic,
"酷狗": getKugouMusic,
"网易": getNeteaseMusic,
"qq": getQQMusic,
"": getKuwoMusic, // 默认点歌指向酷我
}

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]

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

seg, err := processFunc(keyword)
if err != nil {
ctx.SendChain(message.Text("点歌失败:", err))
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)
}
}
func getMiguMusic(keyword string) (message.Segment, error) {
songs, err := migu.Search(keyword)
if err != nil {
return message.Segment{}, errors.Wrap(err, "咪咕音乐搜索失败")
}

results = gjson.GetBytes(data, "#.full_track").Array()
if len(results) > 0 {
if musicURL := results[0].String(); musicURL != "" {
return message.Record(musicURL)
}
if len(songs) == 0 {
return message.Segment{}, errors.New("咪咕音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

return message.Text("点歌失败, 找不到 ", keyword, " 的相关结果")
}
playURL, err := migu.GetDownloadURL(&song)
if err != nil {
return message.Segment{}, errors.Wrap(err, "获取咪咕播放链接失败")
}
if playURL == "" {
return message.Segment{}, errors.New("获取咪咕播放链接失败:链接为空")
}

// 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"},
}
// 搜索音乐信息 第一首歌
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="},
}
// 搜索音乐信息 第一首歌
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))
// 返回音乐卡片
func getKuwoMusic(keyword string) (message.Segment, error) {
songs, err := kuwo.Search(keyword)
if err != nil {
return message.Segment{}, errors.Wrap(err, "酷我音乐搜索失败")
}
if len(songs) == 0 {
return message.Segment{}, errors.New("酷我音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

playURL, err := kuwo.GetDownloadURL(&song)
if err != nil {
return message.Segment{}, errors.Wrap(err, "获取酷我播放链接失败")
}
if playURL == "" {
return message.Segment{}, errors.New("获取酷我播放链接失败:链接为空")
}

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"},
}
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"},
}
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")
// 返回音乐卡片
func getKugouMusic(keyword string) (message.Segment, error) {
songs, err := kugou.Search(keyword)
if err != nil {
return message.Segment{}, errors.Wrap(err, "酷狗音乐搜索失败")
}
if len(songs) == 0 {
return message.Segment{}, errors.New("酷狗音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

playURL, err := kugou.GetDownloadURL(&song)
if err != nil {
return message.Segment{}, errors.Wrap(err, "获取酷狗播放链接失败")
}
if playURL == "" {
return message.Segment{}, errors.New("获取酷狗播放链接失败:链接为空")
}

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)
func getNeteaseMusic(keyword string) (message.Segment, error) {
songs, err := netease.Search(keyword)
if err != nil {
msg = message.Text("ERROR: ", err)
return
return message.Segment{}, errors.Wrap(err, "网易云音乐搜索失败")
}
msg = message.Music("163", gjson.ParseBytes(data).Get("result.songs.0.id").Int())
return
}
if len(songs) == 0 {
return message.Segment{}, errors.New("网易云音乐未找到相关歌曲:" + keyword)
}
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)
playURL, err := netease.GetDownloadURL(&song)
if err != nil {
msg = message.Text("ERROR: ", err)
return
return message.Segment{}, errors.Wrap(err, "获取网易云播放链接失败")
}
if playURL == "" {
return message.Segment{}, errors.New("获取网易云播放链接失败:链接为空")
}
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
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)
func getQQMusic(keyword string) (message.Segment, error) {
songs, err := qq.Search(keyword)
if err != nil {
return message.Segment{}, errors.Wrap(err, "QQ音乐搜索失败")
}
if len(songs) == 0 {
return message.Segment{}, errors.New("QQ音乐未找到相关歌曲:" + keyword)
}
song := songs[0]

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

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