|
1 | | -// Package music QQ音乐、网易云、酷狗、酷我、咪咕 点歌 |
| 1 | +// Package music 整合多平台音乐点播能力(基于 music-lib 重构) |
2 | 2 | package music |
3 | 3 |
|
4 | 4 | import ( |
5 | | - "crypto/md5" |
6 | | - "encoding/hex" |
| 5 | + "errors" |
7 | 6 | "fmt" |
8 | | - "io" |
9 | | - "net/http" |
10 | | - "net/url" |
11 | | - "strings" |
12 | | - "time" |
13 | 7 |
|
14 | | - "github.com/FloatTech/floatbox/web" |
15 | | - |
16 | | - ctrl "github.com/FloatTech/zbpctrl" |
17 | | - "github.com/FloatTech/zbputils/control" |
| 8 | + ctrl "github.com/FloatTech/zbpctrl" // 别名 zbpctrl 为 ctrl |
| 9 | + "github.com/FloatTech/zbputils/control" // 保持 control 原名 |
18 | 10 | "github.com/FloatTech/zbputils/ctxext" |
19 | | - "github.com/tidwall/gjson" |
| 11 | + "github.com/guohuiyuan/music-lib/kugou" |
| 12 | + "github.com/guohuiyuan/music-lib/kuwo" |
| 13 | + "github.com/guohuiyuan/music-lib/migu" |
| 14 | + "github.com/guohuiyuan/music-lib/netease" |
| 15 | + "github.com/guohuiyuan/music-lib/qq" |
20 | 16 | zero "github.com/wdvxdr1123/ZeroBot" |
21 | 17 | "github.com/wdvxdr1123/ZeroBot/message" |
22 | 18 | ) |
23 | 19 |
|
24 | | -var ( |
25 | | - longZhuURL = "https://www.hhlqilongzhu.cn/api/joox/juhe_music.php?msg=%v" |
26 | | -) |
| 20 | +// 平台映射:指令前缀 -> 平台名称 |
| 21 | +var platformMap = map[string]string{ |
| 22 | + "咪咕": "migu", |
| 23 | + "酷我": "kuwo", |
| 24 | + "酷狗": "kugou", |
| 25 | + "网易": "netease", |
| 26 | + "qq": "qq", |
| 27 | + "": "kuwo", // 默认点歌指向酷我 |
| 28 | +} |
27 | 29 |
|
28 | 30 | func init() { |
| 31 | + // 注册指令处理器 |
29 | 32 | control.AutoRegister(&ctrl.Options[*zero.Ctx]{ |
30 | 33 | DisableOnDefault: false, |
31 | 34 | Brief: "点歌", |
32 | | - Help: "- 点歌[xxx]\n" + |
| 35 | + Help: "- 点歌[xxx] (默认酷我)\n" + |
33 | 36 | "- 网易点歌[xxx]\n" + |
34 | 37 | "- 酷我点歌[xxx]\n" + |
35 | 38 | "- 酷狗点歌[xxx]\n" + |
36 | 39 | "- 咪咕点歌[xxx]\n" + |
37 | 40 | "- qq点歌[xxx]\n", |
38 | 41 | }).OnRegex(`^(.{0,2})点歌\s?(.{1,25})$`).SetBlock(true).Limit(ctxext.LimitByUser). |
39 | 42 | Handle(func(ctx *zero.Ctx) { |
40 | | - // switch 平台 |
41 | | - switch ctx.State["regex_matched"].([]string)[1] { |
42 | | - case "咪咕": |
43 | | - ctx.SendChain(migu(ctx.State["regex_matched"].([]string)[2])) |
44 | | - case "酷我": |
45 | | - ctx.SendChain(kuwo(ctx.State["regex_matched"].([]string)[2])) |
46 | | - case "酷狗": |
47 | | - ctx.SendChain(kugou(ctx.State["regex_matched"].([]string)[2])) |
48 | | - case "网易": |
49 | | - ctx.SendChain(cloud163(ctx.State["regex_matched"].([]string)[2])) |
50 | | - case "qq": |
51 | | - ctx.SendChain(qqmusic(ctx.State["regex_matched"].([]string)[2])) |
52 | | - default: // 默认聚合点歌 |
53 | | - ctx.SendChain(longzhu(ctx.State["regex_matched"].([]string)[2])) |
| 43 | + matches := ctx.State["regex_matched"].([]string) |
| 44 | + platformPrefix := matches[1] |
| 45 | + keyword := matches[2] |
| 46 | + |
| 47 | + // 获取目标平台 |
| 48 | + targetPlatform, ok := platformMap[platformPrefix] |
| 49 | + if !ok { |
| 50 | + ctx.SendChain(message.Text("不支持的点播平台:", platformPrefix)) |
| 51 | + return |
| 52 | + } |
| 53 | + |
| 54 | + // 执行点播并返回结果 |
| 55 | + seg, err := getMusicSegment(targetPlatform, keyword) |
| 56 | + if err != nil { |
| 57 | + ctx.SendChain(message.Text("点歌失败:", err.Error())) |
| 58 | + return |
54 | 59 | } |
| 60 | + ctx.SendChain(seg) |
55 | 61 | }) |
56 | 62 | } |
57 | 63 |
|
58 | | -// longzhu 聚合平台 |
59 | | -func longzhu(keyword string) message.Segment { |
60 | | - data, _ := web.GetData(fmt.Sprintf(longZhuURL, url.QueryEscape(keyword))) |
61 | | - // 假设 data 是包含整个 JSON 数组的字节切片 |
62 | | - results := gjson.ParseBytes(data).Array() |
63 | | - for _, result := range results { |
64 | | - if strings.Contains(strings.ToLower(result.Get("title").String()), strings.ToLower(keyword)) { |
65 | | - if musicURL := result.Get("full_track").String(); musicURL != "" { |
66 | | - return message.Record(musicURL) |
67 | | - } |
68 | | - } |
| 64 | +// getMusicSegment 根据平台和关键词获取音乐消息段 |
| 65 | +func getMusicSegment(platform, keyword string) (message.Segment, error) { |
| 66 | + switch platform { |
| 67 | + case "migu": |
| 68 | + return getMiguMusic(keyword) |
| 69 | + case "kuwo": |
| 70 | + return getKuwoMusic(keyword) |
| 71 | + case "kugou": |
| 72 | + return getKugouMusic(keyword) |
| 73 | + case "netease": |
| 74 | + return getNeteaseMusic(keyword) |
| 75 | + case "qq": |
| 76 | + return getQQMusic(keyword) |
| 77 | + default: |
| 78 | + return message.Segment{}, errors.New("未知的音乐平台:" + platform) |
69 | 79 | } |
| 80 | +} |
70 | 81 |
|
71 | | - results = gjson.GetBytes(data, "#.full_track").Array() |
72 | | - if len(results) > 0 { |
73 | | - if musicURL := results[0].String(); musicURL != "" { |
74 | | - return message.Record(musicURL) |
75 | | - } |
76 | | - } |
| 82 | +// --- 各平台适配层(基于 music-lib 实现) --- |
77 | 83 |
|
78 | | - return message.Text("点歌失败, 找不到 ", keyword, " 的相关结果") |
79 | | -} |
| 84 | +// getMiguMusic 咪咕音乐点播 |
| 85 | +func getMiguMusic(keyword string) (message.Segment, error) { |
| 86 | + songs, err := migu.Search(keyword) |
| 87 | + if err != nil || len(songs) == 0 { |
| 88 | + return message.Segment{}, errors.New("咪咕音乐未找到相关歌曲:" + keyword) |
| 89 | + } |
| 90 | + song := songs[0] |
80 | 91 |
|
81 | | -// migu 返回咪咕音乐卡片 |
82 | | -func migu(keyword string) message.Segment { |
83 | | - headers := http.Header{ |
84 | | - "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"}, |
85 | | - "csrf": []string{"LWKACV45JSQ"}, |
86 | | - "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"}, |
87 | | - "Referer": []string{"http://m.music.migu.cn"}, |
88 | | - "proxy": []string{"false"}, |
| 92 | + // 传入 &song (指针) |
| 93 | + playURL, err := migu.GetDownloadURL(&song) |
| 94 | + if err != nil || playURL == "" { |
| 95 | + return message.Segment{}, errors.New("获取咪咕播放链接失败:" + err.Error()) |
89 | 96 | } |
90 | | - // 搜索音乐信息 第一首歌 |
91 | | - search, _ := url.Parse("http://m.music.migu.cn/migu/remoting/scr_search_tag") |
92 | | - search.RawQuery = url.Values{ |
93 | | - "keyword": []string{keyword}, |
94 | | - "type": []string{"2"}, |
95 | | - "pgc": []string{"1"}, |
96 | | - "rows": []string{"10"}, |
97 | | - }.Encode() |
98 | | - info := gjson.ParseBytes(netGet(search.String(), headers)).Get("musics.0") |
99 | | - // 返回音乐卡片 |
| 97 | + |
100 | 98 | return message.CustomMusic( |
101 | | - fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", info.Get("copyrightId").String()), |
102 | | - info.Get("mp3").String(), |
103 | | - info.Get("songName").String(), |
104 | | - ).Add("content", info.Get("artist").Str).Add("image", info.Get("cover").Str).Add("subtype", "migu") |
| 99 | + fmt.Sprintf("https://music.migu.cn/v3/music/song/%s", song.ID), |
| 100 | + playURL, |
| 101 | + song.Name, |
| 102 | + ).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "migu"), nil |
105 | 103 | } |
106 | 104 |
|
107 | | -// kuwo 返回酷我音乐卡片 |
108 | | -func kuwo(keyword string) message.Segment { |
109 | | - headers := http.Header{ |
110 | | - "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"}, |
111 | | - "csrf": []string{"LWKACV45JSQ"}, |
112 | | - "User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"}, |
113 | | - "Referer": []string{"https://www.kuwo.cn/search/list?key="}, |
| 105 | +// getKuwoMusic 酷我音乐点播 |
| 106 | +func getKuwoMusic(keyword string) (message.Segment, error) { |
| 107 | + songs, err := kuwo.Search(keyword) |
| 108 | + if err != nil || len(songs) == 0 { |
| 109 | + return message.Segment{}, errors.New("酷我音乐未找到相关歌曲:" + keyword) |
114 | 110 | } |
115 | | - // 搜索音乐信息 第一首歌 |
116 | | - search, _ := url.Parse("https://www.kuwo.cn/api/www/search/searchMusicBykeyWord") |
117 | | - search.RawQuery = url.Values{ |
118 | | - "key": []string{keyword}, |
119 | | - "pn": []string{"1"}, |
120 | | - "rn": []string{"1"}, |
121 | | - "httpsStatus": []string{"1"}, |
122 | | - }.Encode() |
123 | | - info := gjson.ParseBytes(netGet(search.String(), headers)).Get("data.list.0") |
124 | | - // 获得音乐直链 |
125 | | - music, _ := url.Parse("http://www.kuwo.cn/api/v1/www/music/playUrl") |
126 | | - music.RawQuery = url.Values{ |
127 | | - "mid": []string{fmt.Sprintf("%d", info.Get("rid").Int())}, |
128 | | - "type": []string{"convert_url3"}, |
129 | | - "br": []string{"320kmp3"}, |
130 | | - "httpsStatus": []string{"1"}, |
131 | | - }.Encode() |
132 | | - audio := gjson.ParseBytes(netGet(music.String(), headers)) |
133 | | - // 返回音乐卡片 |
| 111 | + song := songs[0] |
| 112 | + |
| 113 | + // 传入 &song (指针) |
| 114 | + playURL, err := kuwo.GetDownloadURL(&song) |
| 115 | + if err != nil || playURL == "" { |
| 116 | + return message.Segment{}, errors.New("获取酷我播放链接失败:" + err.Error()) |
| 117 | + } |
| 118 | + |
134 | 119 | return message.CustomMusic( |
135 | | - fmt.Sprintf("https://www.kuwo.cn/play_detail/%d", info.Get("rid").Int()), |
136 | | - audio.Get("data.url").Str, |
137 | | - info.Get("name").Str, |
138 | | - ).Add("content", info.Get("artist").Str).Add("image", info.Get("pic").Str).Add("subtype", "kuwo") |
| 120 | + fmt.Sprintf("https://www.kuwo.cn/play_detail/%s", song.ID), |
| 121 | + playURL, |
| 122 | + song.Name, |
| 123 | + ).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kuwo"), nil |
139 | 124 | } |
140 | 125 |
|
141 | | -// kugou 返回酷狗音乐卡片 |
142 | | -func kugou(keyword string) message.Segment { |
143 | | - stamp := time.Now().UnixNano() / 1e6 |
144 | | - hash := md5str( |
145 | | - fmt.Sprintf( |
146 | | - "NVPh5oo715z5DIWAeQlhMDsWXXQV4hwtbitrate=0callback=callback123clienttime=%dclientver=2000dfid=-inputtype=0iscorrection=1isfuzzy=0keyword=%smid=%dpage=1pagesize=30platform=WebFilterprivilege_filter=0srcappid=2919tag=emuserid=-1uuid=%dNVPh5oo715z5DIWAeQlhMDsWXXQV4hwt", |
147 | | - stamp, keyword, stamp, stamp, |
148 | | - ), |
149 | | - ) |
150 | | - // 搜索音乐信息 第一首歌 |
151 | | - h1 := http.Header{ |
152 | | - "User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"}, |
| 126 | +// getKugouMusic 酷狗音乐点播 |
| 127 | +func getKugouMusic(keyword string) (message.Segment, error) { |
| 128 | + songs, err := kugou.Search(keyword) |
| 129 | + if err != nil || len(songs) == 0 { |
| 130 | + return message.Segment{}, errors.New("酷狗音乐未找到相关歌曲:" + keyword) |
153 | 131 | } |
154 | | - search, _ := url.Parse("https://complexsearch.kugou.com/v2/search/song") |
155 | | - search.RawQuery = url.Values{ |
156 | | - "callback": []string{"callback123"}, |
157 | | - "keyword": []string{keyword}, |
158 | | - "page": []string{"1"}, |
159 | | - "pagesize": []string{"30"}, |
160 | | - "bitrate": []string{"0"}, |
161 | | - "isfuzzy": []string{"0"}, |
162 | | - "tag": []string{"em"}, |
163 | | - "inputtype": []string{"0"}, |
164 | | - "platform": []string{"WebFilter"}, |
165 | | - "userid": []string{"-1"}, |
166 | | - "clientver": []string{"2000"}, |
167 | | - "iscorrection": []string{"1"}, |
168 | | - "privilege_filter": []string{"0"}, |
169 | | - "srcappid": []string{"2919"}, |
170 | | - "clienttime": []string{fmt.Sprintf("%d", stamp)}, |
171 | | - "mid": []string{fmt.Sprintf("%d", stamp)}, |
172 | | - "uuid": []string{fmt.Sprintf("%d", stamp)}, |
173 | | - "dfid": []string{"-"}, |
174 | | - "signature": []string{hash}, |
175 | | - }.Encode() |
176 | | - res := netGet(search.String(), h1) |
177 | | - info := gjson.ParseBytes(res[12 : len(res)-2]).Get("data.lists.0") |
178 | | - // 获得音乐直链 |
179 | | - h2 := http.Header{ |
180 | | - "Cookie": []string{"kg_mid=d8e70a262c93d47599c6196c612d6f4f; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1610278505,1611631363,1611722252; kg_dfid=33ZWee1kircl0jcJ1h0WF1fX; Hm_lpvt_aedee6983d4cfc62f509129360d6bb3d=1611727348; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e"}, |
181 | | - "Host": []string{"wwwapi.kugou.com"}, |
182 | | - "TE": []string{"Trailers"}, |
183 | | - "User-Agent": []string{"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0"}, |
| 132 | + song := songs[0] |
| 133 | + |
| 134 | + // 传入 &song (指针) |
| 135 | + playURL, err := kugou.GetDownloadURL(&song) |
| 136 | + if err != nil || playURL == "" { |
| 137 | + return message.Segment{}, errors.New("获取酷狗播放链接失败:" + err.Error()) |
184 | 138 | } |
185 | | - music := "https://wwwapi.kugou.com/yy/index.php?r=play%2Fgetdata&hash=" + info.Get("FileHash").Str + "&album_id=" + info.Get("AlbumID").Str |
186 | | - audio := gjson.ParseBytes(netGet(music, h2)).Get("data") |
187 | | - // 返回音乐卡片 |
| 139 | + |
188 | 140 | return message.CustomMusic( |
189 | | - "https://www.kugou.com/song/#hash="+audio.Get("hash").Str+"&album_id="+audio.Get("album_id").Str, |
190 | | - strings.ReplaceAll(audio.Get("play_backup_url").Str, "\\/", "/"), |
191 | | - audio.Get("audio_name").Str, |
192 | | - ).Add("content", audio.Get("author_name").Str).Add("image", audio.Get("img").Str).Add("subtype", "kugou") |
| 141 | + "https://www.kugou.com/", |
| 142 | + playURL, |
| 143 | + song.Name, |
| 144 | + ).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "kugou"), nil |
193 | 145 | } |
194 | 146 |
|
195 | | -// cloud163 返回网易云音乐卡片 |
196 | | -func cloud163(keyword string) (msg message.Segment) { |
197 | | - requestURL := "http://music.163.com/api/search/get/web?type=1&limit=1&s=" + url.QueryEscape(keyword) |
198 | | - data, err := web.GetData(requestURL) |
199 | | - if err != nil { |
200 | | - msg = message.Text("ERROR: ", err) |
201 | | - return |
| 147 | +// getNeteaseMusic 网易云音乐点播 |
| 148 | +func getNeteaseMusic(keyword string) (message.Segment, error) { |
| 149 | + songs, err := netease.Search(keyword) |
| 150 | + if err != nil || len(songs) == 0 { |
| 151 | + return message.Segment{}, errors.New("网易云音乐未找到相关歌曲:" + keyword) |
202 | 152 | } |
203 | | - msg = message.Music("163", gjson.ParseBytes(data).Get("result.songs.0.id").Int()) |
204 | | - return |
205 | | -} |
| 153 | + song := songs[0] |
206 | 154 |
|
207 | | -// qqmusic 返回QQ音乐卡片 |
208 | | -func qqmusic(keyword string) (msg message.Segment) { |
209 | | - requestURL := "https://c.y.qq.com/splcloud/fcgi-bin/smartbox_new.fcg?platform=yqq.json&key=" + url.QueryEscape(keyword) |
210 | | - data, err := web.RequestDataWith(web.NewDefaultClient(), requestURL, "GET", "", web.RandUA(), nil) |
211 | | - if err != nil { |
212 | | - msg = message.Text("ERROR: ", err) |
213 | | - return |
| 155 | + // 获取播放直链 |
| 156 | + playURL, err := netease.GetDownloadURL(&song) |
| 157 | + if err != nil || playURL == "" { |
| 158 | + return message.Segment{}, errors.New("获取网易云播放链接失败:" + err.Error()) |
214 | 159 | } |
215 | | - msg = message.Music("qq", gjson.ParseBytes(data).Get("data.song.itemlist.0.id").Int()) |
216 | | - return |
217 | | -} |
218 | 160 |
|
219 | | -// md5str 返回字符串 MD5 |
220 | | -func md5str(s string) string { |
221 | | - h := md5.New() |
222 | | - h.Write([]byte(s)) |
223 | | - result := strings.ToUpper(hex.EncodeToString(h.Sum(nil))) |
224 | | - return result |
| 161 | + // 构造 CustomMusic |
| 162 | + return message.CustomMusic( |
| 163 | + fmt.Sprintf("https://music.163.com/#/song?id=%s", song.ID), |
| 164 | + playURL, |
| 165 | + song.Name, |
| 166 | + ).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "163"), nil |
225 | 167 | } |
226 | 168 |
|
227 | | -// netGet 返回请求数据 |
228 | | -func netGet(url string, header http.Header) []byte { |
229 | | - client := &http.Client{} |
230 | | - request, _ := http.NewRequest("GET", url, nil) |
231 | | - request.Header = header |
232 | | - res, err := client.Do(request) |
233 | | - if err != nil { |
234 | | - return nil |
| 169 | +// getQQMusic QQ音乐点播 |
| 170 | +func getQQMusic(keyword string) (message.Segment, error) { |
| 171 | + songs, err := qq.Search(keyword) |
| 172 | + if err != nil || len(songs) == 0 { |
| 173 | + return message.Segment{}, errors.New("QQ音乐未找到相关歌曲:" + keyword) |
| 174 | + } |
| 175 | + song := songs[0] |
| 176 | + |
| 177 | + // 获取播放直链 |
| 178 | + playURL, err := qq.GetDownloadURL(&song) |
| 179 | + if err != nil || playURL == "" { |
| 180 | + return message.Segment{}, errors.New("获取QQ音乐播放链接失败:" + err.Error()) |
235 | 181 | } |
236 | | - defer res.Body.Close() |
237 | | - result, _ := io.ReadAll(res.Body) |
238 | | - return result |
| 182 | + |
| 183 | + // 构造 CustomMusic |
| 184 | + return message.CustomMusic( |
| 185 | + fmt.Sprintf("https://y.qq.com/n/ryqq/songDetail/%s", song.ID), |
| 186 | + playURL, |
| 187 | + song.Name, |
| 188 | + ).Add("content", song.Artist).Add("image", song.Cover).Add("subtype", "qq"), nil |
239 | 189 | } |
0 commit comments