取消多个API依赖以提高稳定性,优化用户本地/API歌单可读性和自定义性 (#315)

This commit is contained in:
方柳煜 2022-07-19 11:28:05 +08:00 committed by GitHub
parent 9182d214af
commit 4a1d4644ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 802 additions and 333 deletions

View File

@ -581,22 +581,32 @@ print("run[CQ:image,file="+j["img"]+"]")
`import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic"` `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic"`
- 猜歌插件该插件依赖ffmpeg - 猜歌插件该插件依赖ffmpeg
- [x] 个人猜歌
- [x] 团队猜歌
- [x] 设置猜歌缓存歌库路径 [绝对路径] - [x] 设置猜歌缓存歌库路径 [绝对路径]
- [x] 设置猜歌本地 [true/false]
- [x] 设置猜歌Api [true/false]
- 注:默认歌库为网易云热歌榜 - [x] 设置猜歌[本地/Api] [true/false]
- 1.可在后面添加“-动漫”进行动漫歌猜歌(这个只能猜歌名和歌手) - [x] 登录网易云
- 2.可在后面添加“-动漫2”进行动漫歌猜歌(这个可以猜番名,但歌手经常“未知”) - 注不登陆也能用API有几率返回400
- [x] 添加歌单 [网易云歌单ID] [歌单名称]
- 注:[歌单名称]可为空,默认原标题
- [x] 删除歌单 [网易云歌单ID/API歌单名称]
- [x] 获取歌单列表
- [x] [网易云歌单ID/API歌单名称]歌单信息
- [x] [个人/团队]猜歌
- 注默认歌库为网易云ACG动画榜
- 可在后面添加[-歌单名称]进行指定歌单猜歌
- 歌单的歌曲命名规则为:歌名 - 歌手 - 其他(歌曲出处之类)
</details> </details>
<details> <details>

View File

@ -15,6 +15,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/sirupsen/logrus"
"github.com/wdvxdr1123/ZeroBot/utils/helper"
"github.com/pkg/errors" "github.com/pkg/errors"
zero "github.com/wdvxdr1123/ZeroBot" zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message" "github.com/wdvxdr1123/ZeroBot/message"
@ -25,6 +28,10 @@ import (
"github.com/FloatTech/zbputils/file" "github.com/FloatTech/zbputils/file"
"github.com/FloatTech/zbputils/web" "github.com/FloatTech/zbputils/web"
"github.com/wdvxdr1123/ZeroBot/extension/single" "github.com/wdvxdr1123/ZeroBot/extension/single"
// 图片输出
"github.com/FloatTech/zbputils/img/text"
) )
const ( const (
@ -32,26 +39,31 @@ const (
) )
var ( var (
cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒) catlist = make(map[string]int64, 100)
cfg = config{ // 默认 config filelist []string
MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改 cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒)
Local: true, // 是否使用本地音乐库 cfg config
API: true, // 是否使用 Api
}
) )
func init() { // 插件主体 func init() { // 插件主体
engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{ engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{
DisableOnDefault: false, DisableOnDefault: false,
Help: "猜歌插件该插件依赖ffmpeg\n" + Help: "猜歌插件该插件依赖ffmpeg\n" +
"- 个人猜歌\n" + "------bot主人指令------\n" +
"- 团队猜歌\n" +
"- 设置猜歌缓存歌库路径 [绝对路径]\n" + "- 设置猜歌缓存歌库路径 [绝对路径]\n" +
"- 设置猜歌本地 [true/false]\n" + "- 设置猜歌[本地/Api] [true/false]\n" +
"- 设置猜歌Api [true/false]\n" + "- 登录网易云\n" +
"注:默认歌库为网易云热歌榜\n- 本地歌榜歌库歌曲命名规格“歌名 - 歌手”\n" + "- 添加歌单 [网易云歌单ID] [歌单名称]\n" +
"1.可在后面添加“-动漫”进行动漫歌猜歌\n- 这个只能猜歌名和歌手\n- 本地动漫歌库歌曲命名规格“歌名 - 歌手”\n" + "- 删除歌单 [网易云歌单ID/API歌单名称]\n" +
"2.可在后面添加“-动漫2”进行动漫歌猜歌\n- 这个可以猜番名,但歌手经常“未知”\n- 本地动漫2歌库歌曲命名规格“歌名 - 歌手 - 番名”", "注:\n1.不登陆也能用API有几率返回400\n" +
"2.[歌单名称]可为空,默认原标题\n" +
"------公 用 指 令------\n" +
"- 获取歌单列表\n" +
"- [网易云歌单ID/API歌单名称]歌单信息\n" +
"- [个人/团队]猜歌\n" +
"注默认歌库为网易云ACG动画榜\n" +
"可在后面添加[-歌单名称]进行指定歌单猜歌\n" +
"歌单的歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)",
PrivateDataFolder: "guessmusic", PrivateDataFolder: "guessmusic",
}).ApplySingle(single.New( }).ApplySingle(single.New(
single.WithKeyFn(func(ctx *zero.Ctx) int64 { return ctx.Event.GroupID }), single.WithKeyFn(func(ctx *zero.Ctx) int64 { return ctx.Event.GroupID }),
@ -84,11 +96,28 @@ func init() { // 插件主体
panic(err) panic(err)
} }
} else { } else {
var plist = []listRaw{
{
Name: "动画榜",
ID: 3001835560,
},
}
cfg = config{ // 默认 config
MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改
Local: true, // 是否使用本地音乐库
API: true, // 是否使用 Api
Cookie: "",
Playlist: plist,
}
err = saveConfig(cfgFile) err = saveConfig(cfgFile)
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
err = getcatlist(cfg.MusicPath)
if err != nil {
logrus.Infof("[guessmusic2]无法获取歌单列表,[error]%s", err)
}
engine.OnRegex(`^设置猜歌(缓存歌库路径|本地|Api)\s*(.*)$`, func(ctx *zero.Ctx) bool { engine.OnRegex(`^设置猜歌(缓存歌库路径|本地|Api)\s*(.*)$`, func(ctx *zero.Ctx) bool {
if !zero.SuperUserPermission(ctx) { if !zero.SuperUserPermission(ctx) {
ctx.SendChain(message.Text("只有bot主人可以设置")) ctx.SendChain(message.Text("只有bot主人可以设置"))
@ -132,15 +161,259 @@ func init() { // 插件主体
ctx.SendChain(message.Text("ERROR:", err)) ctx.SendChain(message.Text("ERROR:", err))
} }
}) })
engine.OnRegex(`^(个人|团队)猜歌(-动漫|-动漫2)?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup). engine.OnFullMatch("登录网易云", zero.SuperUserPermission, func(ctx *zero.Ctx) bool {
if !zero.OnlyPrivate(ctx) {
ctx.SendChain(message.Text("为了保护登录过程请bot主人私聊。"))
return false
}
return true
}).SetBlock(true).
Handle(func(ctx *zero.Ctx) { Handle(func(ctx *zero.Ctx) {
mode := ctx.State["regex_matched"].([]string)[2] keyURL := "https://music.cyrilstudio.top/login/qr/key"
gid := strconv.FormatInt(ctx.Event.GroupID, 10) data, err := web.GetData(keyURL)
if mode == "-动漫2" { if err != nil {
ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|番剧|提示|取消]”\n一共3段语音6次机会")) ctx.SendChain(message.Text("获取网易云key失败, ERROR:", err))
} else { return
ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|提示|取消]”\n一共3段语音6次机会"))
} }
var keyInfo keyInfo
err = json.Unmarshal(data, &keyInfo)
if err != nil {
ctx.SendChain(message.Text("解析网易云key失败, ERROR:", err))
return
}
qrURL := "https://music.cyrilstudio.top/login/qr/create?key=" + keyInfo.Data.Unikey + "&qrimg=1"
data, err = web.GetData(qrURL)
if err != nil {
ctx.SendChain(message.Text("获取网易云二维码失败, ERROR:", err))
return
}
var qrInfo qrInfo
err = json.Unmarshal(data, &qrInfo)
if err != nil {
ctx.SendChain(message.Text("解析网易云二维码失败, ERROR:", err))
return
}
ctx.SendChain(message.Text("[请使用手机APP扫描二维码或者进入网页扫码登录]\n", qrInfo.Data.Qrurl), message.Image("base64://"+strings.ReplaceAll(qrInfo.Data.Qrimg, "data:image/png;base64,", "")), message.Text("二维码有效时间为6分钟"))
i := 0
for range time.NewTicker(10 * time.Second).C {
apiURL := "https://music.cyrilstudio.top/login/qr/check?key=" + url.QueryEscape(keyInfo.Data.Unikey)
referer := "https://music.cyrilstudio.top"
data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua)
if err != nil {
ctx.SendChain(message.Text("无法获取登录状态, ERROR:", err))
return
}
var cookiesInfo cookyInfo
err = json.Unmarshal(data, &cookiesInfo)
if err != nil {
ctx.SendChain(message.Text("解析登录状态失败, ERROR:", err))
return
}
switch cookiesInfo.Code {
case 803:
cfg.Cookie = cookiesInfo.Cookie
err = saveConfig(cfgFile)
if err == nil {
ctx.SendChain(message.Text("成功!"))
} else {
ctx.SendChain(message.Text("ERROR:", err))
}
return
case 801:
i++
if i%6 == 0 { // 每1分钟才提醒一次,减少提示(380/60=6次)
ctx.SendChain(message.Text("状态:", cookiesInfo.Message))
}
continue
case 800:
ctx.SendChain(message.Text("状态:", cookiesInfo.Message))
return
default:
ctx.SendChain(message.Text("状态:", cookiesInfo.Message))
continue
}
}
})
engine.OnRegex(`^添加歌单\s?(\d*)(\s(.*))?$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
listID := ctx.State["regex_matched"].([]string)[1]
listName := ctx.State["regex_matched"].([]string)[3]
apiURL := "https://music.cyrilstudio.top/playlist/detail?id=" + listID + "&cookie=" + url.QueryEscape(cfg.Cookie)
referer := "https://music.cyrilstudio.top"
data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua)
if err != nil {
ctx.SendChain(message.Text("无法连接歌单,[error]", err))
return
}
var parsed topList
err = json.Unmarshal(data, &parsed)
if err != nil {
ctx.SendChain(message.Text("无法解析歌单ID内容,[error]", err))
return
}
if listName == "" {
listName = parsed.Playlist.Name
}
playID, _ := strconv.ParseInt(listID, 10, 64)
catlist[listName] = playID
cfg.Playlist = append(cfg.Playlist, listRaw{
Name: listName,
ID: playID,
})
err = saveConfig(cfgFile)
if err == nil {
ctx.SendChain(message.Text("成功!"))
} else {
ctx.SendChain(message.Text("ERROR:", err))
}
})
engine.OnRegex(`^删除歌单\s?(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
delList := ctx.State["regex_matched"].([]string)[1]
var playlist []listRaw
var newCatList = make(map[string]int64)
var ok = false
for name, musicID := range catlist {
if delList == name || delList == strconv.FormatInt(musicID, 10) {
ok = true
continue
}
newCatList[name] = musicID
playlist = append(playlist, listRaw{
Name: name,
ID: musicID,
})
}
if !ok {
ctx.SendChain(message.Text("目标歌单未找到,请确认是否正确"))
return
}
catlist = newCatList
cfg.Playlist = playlist
err = saveConfig(cfgFile)
if err == nil {
ctx.SendChain(message.Text("成功!"))
} else {
ctx.SendChain(message.Text("ERROR:", err))
}
})
engine.OnFullMatch("获取歌单列表").SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
var msg []string
// 获取网易云歌单列表
if cfg.API {
catlist = make(map[string]int64, 100)
msg = append(msg, "\n当前添加的API歌单含有以下\n")
for i, listInfo := range cfg.Playlist {
catlist[listInfo.Name] = listInfo.ID
msg = append(msg, strconv.Itoa(i)+":"+listInfo.Name)
if i%3 == 2 {
msg = append(msg, "\n")
}
}
}
// 获取本地歌单列表*/
if cfg.Local {
err = os.MkdirAll(cfg.MusicPath, 0755)
if err == nil {
files, err := ioutil.ReadDir(cfg.MusicPath)
if err == nil {
msg = append(msg, "\n当前本地歌单含有以下\n")
i := 0
for _, name := range files {
if !name.IsDir() {
continue
}
filelist[i] = strconv.Itoa(i) + ":" + name.Name()
msg = append(msg, filelist[i])
if i%3 == 2 {
msg = append(msg, "\n")
}
i++
}
} else {
ctx.SendChain(message.Text("[读取本地列表错误]ERROR:", err))
}
} else {
ctx.SendChain(message.Text("[生成文件夹错误]ERROR:", err))
}
}
if msg == nil {
ctx.SendChain(message.Text("本地和API均未开启"))
return
}
msgs, err := text.RenderToBase64(strings.Join(msg, " "), text.FontFile, 400, 20)
if err != nil {
ctx.SendChain(message.Text("生成列表图片失败,请重试"))
return
}
if id := ctx.SendChain(message.Image("base64://" + helper.BytesToString(msgs))); id.ID() == 0 {
ctx.SendChain(message.Text("ERROR: 可能被风控了"))
}
})
engine.OnSuffix("歌单信息").SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
list := ctx.State["args"].(string)
var listIDStr string
for listName, listID := range catlist {
if list == listName || list == strconv.FormatInt(listID, 10) {
listIDStr = strconv.FormatInt(listID, 10)
break
}
}
if listIDStr == "" {
_, err := strconv.ParseInt(list, 10, 64)
if err != nil {
ctx.SendChain(message.Text("仅支持歌单ID查询"))
return
}
listIDStr = list
}
apiURL := "https://music.cyrilstudio.top/playlist/detail?id=" + listIDStr + "&cookie=" + url.QueryEscape(cfg.Cookie)
referer := "https://music.cyrilstudio.top"
data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua)
if err != nil {
ctx.SendChain(message.Text("无法连接歌单,[error]", err))
return
}
var parsed topList
err = json.Unmarshal(data, &parsed)
if err != nil {
ctx.SendChain(message.Text("无法解析歌单ID内容,[error]", err))
return
}
ctx.SendChain(
message.Image(parsed.Playlist.CoverImgURL),
message.Text(
"歌单名称:", parsed.Playlist.Name,
"\n歌单ID", parsed.Playlist.ID,
"\n创建人", parsed.Playlist.Creator.Nickname,
"\n创建时间", time.Unix(parsed.Playlist.CreateTime/1000, 0).Format("2006-01-02"),
"\n标签", strings.Join(parsed.Playlist.Tags, ";"),
"\n歌曲数量", parsed.Playlist.TrackCount,
"\n歌单简介:\n", parsed.Playlist.Description,
"\n更新时间", time.Unix(parsed.Playlist.UpdateTime/1000, 0).Format("2006-01-02"),
))
})
engine.OnRegex(`^(个人|团队)猜歌(-(.*))?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
mode := ctx.State["regex_matched"].([]string)[3]
if mode == "" {
mode = "动画榜"
}
_, ok := catlist[mode]
switch {
// 如果API没有开本地也不存在这个歌单
case !cfg.API && !strings.Contains(strings.Join(filelist, " "), mode):
ctx.SendChain(message.Text("歌单名称错误,可以发送“获取歌单列表”获取歌单名称"))
return
// 如果本地没有开,网易云也不存在这个歌单
case !cfg.Local && !ok:
ctx.SendChain(message.Text("歌单名称错误,可以发送“获取歌单列表”获取歌单名称"))
return
}
gid := strconv.FormatInt(ctx.Event.GroupID, 10)
ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲信息(歌名歌手等)|提示|取消]”\n一共3段语音6次机会"))
// 随机抽歌 // 随机抽歌
musicName, pathOfMusic, err := musicLottery(mode, cfg.MusicPath) musicName, pathOfMusic, err := musicLottery(mode, cfg.MusicPath)
if err != nil { if err != nil {
@ -154,9 +427,17 @@ func init() { // 插件主体
ctx.SendChain(message.Text(err)) ctx.SendChain(message.Text(err))
return return
} }
// 解析歌曲信息
musicInfo := strings.Split(musicName, " - ")
infoNum := len(musicInfo)
answerString := "歌名:" + musicInfo[0] + "\n歌手:" + musicInfo[1]
musicAlia := ""
if infoNum > 2 {
musicAlia = musicInfo[2]
answerString += "\n其他信息:\n" + strings.ReplaceAll(musicAlia, "&", "\n")
}
// 进行猜歌环节 // 进行猜歌环节
ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + "0.wav")) ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + "0.wav"))
answerString := strings.Split(musicName, " - ")
var next *zero.FutureEvent var next *zero.FutureEvent
if ctx.State["regex_matched"].([]string)[1] == "个人" { if ctx.State["regex_matched"].([]string)[1] == "个人" {
next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), ctx.CheckSession()) next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), ctx.CheckSession())
@ -175,15 +456,8 @@ func init() { // 插件主体
case <-tick.C: case <-tick.C:
ctx.SendChain(message.Text("猜歌游戏你还有15s作答时间")) ctx.SendChain(message.Text("猜歌游戏你还有15s作答时间"))
case <-after.C: case <-after.C:
msg := make(message.Message, 0, 3) ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID,
msg = append(msg, message.Reply(ctx.Event.MessageID)) message.Text("时间超时,猜歌结束,公布答案:\n", answerString)))
msg = append(msg, message.Text("猜歌超时,游戏结束\n答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return return
case <-wait.C: case <-wait.C:
wait.Reset(40 * time.Second) wait.Reset(40 * time.Second)
@ -207,15 +481,8 @@ func init() { // 插件主体
wait.Stop() wait.Stop()
tick.Stop() tick.Stop()
after.Stop() after.Stop()
msg := make(message.Message, 0, 3) ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID,
msg = append(msg, message.Reply(c.Event.MessageID)) message.Text("游戏已取消,猜歌答案是\n", answerString)))
msg = append(msg, message.Text("游戏已取消,猜歌答案是",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return return
} }
ctx.Send( ctx.Send(
@ -241,53 +508,28 @@ func init() { // 插件主体
), ),
) )
ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav"))
case strings.Contains(answerString[0], answer) || strings.EqualFold(answerString[0], answer): case strings.Contains(musicInfo[0], answer) || strings.EqualFold(musicInfo[0], answer):
wait.Stop() wait.Stop()
tick.Stop() tick.Stop()
after.Stop() after.Stop()
msg := make(message.Message, 0, 3) ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
msg = append(msg, message.Reply(c.Event.MessageID)) message.Text("太棒了,你猜对歌曲名了!答案是\n", answerString)))
msg = append(msg, message.Text("太棒了,你猜对歌曲名了!答案是",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return return
case answerString[1] == "未知" && answer == "未知": case strings.Contains(musicInfo[1], answer) || strings.EqualFold(musicInfo[1], answer):
ctx.Send(
message.ReplyWithMessage(c.Event.MessageID,
message.Text("该模式禁止回答“未知”"),
),
)
case strings.Contains(answerString[1], answer) || strings.EqualFold(answerString[1], answer):
wait.Stop() wait.Stop()
tick.Stop() tick.Stop()
after.Stop() after.Stop()
msg := make(message.Message, 0, 3) ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
msg = append(msg, message.Reply(c.Event.MessageID)) message.Text("太棒了,你猜对歌手名了!答案是\n", answerString)))
msg = append(msg, message.Text("太棒了,你猜对歌手名了!答案是", return
"\n歌名:", answerString[0], case strings.Contains(musicAlia, answer) || strings.EqualFold(musicAlia, answer):
"\n歌手:", answerString[1])) wait.Stop()
if mode == "-动漫2" { tick.Stop()
msg = append(msg, message.Text("\n歌曲出自:", answerString[2])) after.Stop()
} ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
ctx.Send(msg) message.Text("太棒了,你猜对出处了!答案是\n", answerString)))
return return
default: default:
if mode == "-动漫2" && (strings.Contains(answerString[2], answer) || strings.EqualFold(answerString[2], answer)) {
wait.Stop()
tick.Stop()
after.Stop()
ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
message.Text("太棒了,你猜对番剧名了!答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1],
"\n歌曲出自:", answerString[2]),
))
return
}
musicCount++ musicCount++
switch { switch {
case musicCount > 2 && answerCount < 6: case musicCount > 2 && answerCount < 6:
@ -302,15 +544,8 @@ func init() { // 插件主体
wait.Stop() wait.Stop()
tick.Stop() tick.Stop()
after.Stop() after.Stop()
msg := make(message.Message, 0, 3) ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
msg = append(msg, message.Reply(c.Event.MessageID)) message.Text("次数到了,没能猜出来。答案是\n", answerString)))
msg = append(msg, message.Text("次数到了,你没能猜出来。\n答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return return
default: default:
wait.Reset(40 * time.Second) wait.Reset(40 * time.Second)
@ -340,16 +575,30 @@ func saveConfig(cfgFile string) (err error) {
return nil return nil
} }
func getcatlist(pathOfMusic string) error {
catlist = make(map[string]int64, 100)
for _, listInfo := range cfg.Playlist {
catlist[listInfo.Name] = listInfo.ID
}
err := os.MkdirAll(pathOfMusic, 0755)
if err != nil {
err = errors.Errorf("[生成文件夹错误]ERROR:%s", err)
return err
}
files, err := ioutil.ReadDir(pathOfMusic)
if err != nil {
err = errors.Errorf("[读取本地列表错误]ERROR:%s", err)
return err
}
for i, name := range files {
filelist = append(filelist, strconv.Itoa(i)+":"+name.Name())
}
return nil
}
// 随机抽取音乐 // 随机抽取音乐
func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err error) { func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err error) {
switch mode { pathOfMusic = musicPath + mode + "/"
case "-动漫":
pathOfMusic = musicPath + "动漫/"
case "-动漫2":
pathOfMusic = musicPath + "动漫2/"
default:
pathOfMusic = musicPath + "歌榜/"
}
err = os.MkdirAll(pathOfMusic, 0755) err = os.MkdirAll(pathOfMusic, 0755)
if err != nil { if err != nil {
err = errors.Errorf("[生成文件夹错误]ERROR:%s", err) err = errors.Errorf("[生成文件夹错误]ERROR:%s", err)
@ -360,21 +609,27 @@ func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err er
err = errors.Errorf("[读取本地列表错误]ERROR:%s", err) err = errors.Errorf("[读取本地列表错误]ERROR:%s", err)
return return
} }
listID, ok := catlist[mode]
listIDstr := strconv.FormatInt(listID, 10)
if cfg.Local && cfg.API { if cfg.Local && cfg.API {
switch { switch {
case len(files) == 0: case len(files) == 0:
if !ok {
// 如果歌单是本地歌单
err = errors.New("本地歌单数据为0")
return
}
// 如果没有任何本地就下载歌曲 // 如果没有任何本地就下载歌曲
musicName, err = getAPIMusic(mode, pathOfMusic) musicName, err = getListMusic(listIDstr, pathOfMusic)
if err != nil { if err != nil {
err = errors.Errorf("[本地数据为0歌曲下载错误]ERROR:%s", err) err = errors.Errorf("[本地数据为0歌曲下载错误]ERROR:%s", err)
return return
} }
case rand.Intn(2) == 0: case rand.Intn(2) == 0 || !ok:
// [0,1)只会取到0rand不允许的 // 1/2概率抽本地或者歌单只有本地有时
musicName = getLocalMusic(files) musicName = getLocalMusic(files)
default: default:
musicName, err = getAPIMusic(mode, pathOfMusic) musicName, err = getListMusic(listIDstr, pathOfMusic)
if err != nil { if err != nil {
// 如果下载失败就从本地抽一个歌曲 // 如果下载失败就从本地抽一个歌曲
musicName = getLocalMusic(files) musicName = getLocalMusic(files)
@ -391,27 +646,15 @@ func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err er
musicName = getLocalMusic(files) musicName = getLocalMusic(files)
return return
} }
if cfg.API { if cfg.API && ok {
musicName, err = getAPIMusic(mode, pathOfMusic) musicName, err = getListMusic(listIDstr, pathOfMusic)
if err != nil { if err != nil {
err = errors.Errorf("[获取API失败未开启本地数据] ERROR:%s", err) err = errors.Errorf("[获取API失败未开启本地数据] ERROR:%s", err)
return return
} }
return return
} }
err = errors.New("[未开启API以及本地数据]") err = errors.New("[请确认本地和API设置已开启或歌单存在]")
return
}
func getAPIMusic(mode string, musicPath string) (musicName string, err error) {
switch mode {
case "-动漫":
musicName, err = getPaugramData(musicPath)
case "-动漫2":
musicName, err = getAnimeData(musicPath)
default:
musicName, err = getNetEaseData(musicPath)
}
return return
} }
@ -424,28 +667,43 @@ func getLocalMusic(files []fs.FileInfo) (musicName string) {
return return
} }
// 下载保罗API的歌曲 // 下载网易云歌单音乐
func getPaugramData(musicPath string) (musicName string, err error) { func getListMusic(listID, pathOfMusic string) (musicName string, err error) {
api := "https://api.paugram.com/acgm/?list=1" apiURL := "https://music.cyrilstudio.top/playlist/track/all?id=" + listID + "&cookie=" + url.QueryEscape(cfg.Cookie)
referer := "https://api.paugram.com/" referer := "https://music.cyrilstudio.top"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua) data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua)
if err != nil { if err != nil {
return return
} }
var parsed paugramData var parsed topMusicInfo
err = json.Unmarshal(data, &parsed) err = json.Unmarshal(data, &parsed)
if err != nil { if err != nil {
return return
} }
name := parsed.Title listlen := len(parsed.Songs)
artistsName := parsed.Artist randidx := rand.Intn(listlen)
musicURL := parsed.Link // 将"/"符号去除,不然无法生成文件
if name == "" || artistsName == "" { name := strings.ReplaceAll(parsed.Songs[randidx].Name, "/", "·")
musicID := parsed.Songs[randidx].ID
artistName := ""
for i, ARInfo := range parsed.Songs[randidx].Ar {
if i != 0 {
artistName += "&" + ARInfo.Name
} else {
artistName += ARInfo.Name
}
}
cource := ""
if parsed.Songs[randidx].Alia != nil {
cource = strings.Join(parsed.Songs[randidx].Alia, "&")
// 将"/"符号去除,不然无法下载
cource = strings.ReplaceAll(cource, "/", "&")
}
if name == "" || musicID == 0 {
err = errors.New("无法获API取歌曲信息") err = errors.New("无法获API取歌曲信息")
return return
} }
musicName = name + " - " + artistsName musicURL := "http://music.163.com/song/media/outer/url?id=" + strconv.Itoa(musicID)
downMusic := musicPath + "/" + musicName + ".mp3"
response, err := http.Head(musicURL) response, err := http.Head(musicURL)
if err != nil { if err != nil {
err = errors.Errorf("下载音乐失败, ERROR: %s", err) err = errors.Errorf("下载音乐失败, ERROR: %s", err)
@ -455,109 +713,12 @@ func getPaugramData(musicPath string) (musicName string, err error) {
err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode) err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode)
return return
} }
if file.IsNotExist(downMusic) { if cource != "" {
data, err = web.GetData(musicURL) musicName = name + " - " + artistName + " - " + cource
if err != nil { } else {
return musicName = name + " - " + artistName
}
err = os.WriteFile(downMusic, data, 0666)
if err != nil {
return
}
} }
return downMusic := pathOfMusic + musicName + ".mp3"
}
// 下载animeMusic API的歌曲
func getAnimeData(musicPath string) (musicName string, err error) {
api := "https://anime-music.jijidown.com/api/v2/music"
referer := "https://anime-music.jijidown.com/"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua)
if err != nil {
return
}
var parsed animeData
err = json.Unmarshal(data, &parsed)
if err != nil {
return
}
name := parsed.Res.Title
artistName := parsed.Res.Author
acgName := parsed.Res.AnimeInfo.Title
//musicURL := parsed.Res.PlayURL
if name == "" || artistName == "" {
err = errors.New("无法获API取歌曲信息")
return
}
requestURL := "https://music.cyrilstudio.top/search?keywords=" + url.QueryEscape(name+" "+artistName) + "&limit=1"
if artistName == "未知" {
requestURL = "https://music.cyrilstudio.top/search?keywords=" + url.QueryEscape(acgName+" "+name) + "&limit=1"
}
data, err = web.GetData(requestURL)
if err != nil {
err = errors.Errorf("API歌曲查询失败, ERROR: %s", err)
return
}
var autumnfish autumnfishData
err = json.Unmarshal(data, &autumnfish)
if err != nil {
return
}
if autumnfish.Code != 200 {
err = errors.Errorf("下载音乐失败, Status Code: %d", autumnfish.Code)
return
}
musicID := strconv.Itoa(autumnfish.Result.Songs[0].ID)
if artistName == "未知" {
artistName = strings.ReplaceAll(autumnfish.Result.Songs[0].Artists[0].Name, " - ", "-")
}
musicName = name + " - " + artistName + " - " + acgName
downMusic := musicPath + "/" + musicName + ".mp3"
musicURL := "http://music.163.com/song/media/outer/url?id=" + musicID
response, err := http.Head(musicURL)
if err != nil {
err = errors.Errorf("下载音乐失败, ERROR: %s", err)
return
}
if response.StatusCode != 200 {
err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode)
return
}
if file.IsNotExist(downMusic) {
data, err = web.GetData(musicURL)
if err != nil {
return
}
err = os.WriteFile(downMusic, data, 0666)
if err != nil {
return
}
}
return
}
// 下载网易云热歌榜音乐
func getNetEaseData(musicPath string) (musicName string, err error) {
api := "https://api.uomg.com/api/rand.music?sort=%E7%83%AD%E6%AD%8C%E6%A6%9C&format=json"
referer := "https://api.uomg.com/api/rand.music"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua)
if err != nil {
return
}
var parsed netEaseData
err = json.Unmarshal(data, &parsed)
if err != nil {
return
}
name := parsed.Data.Name
musicURL := parsed.Data.URL
artistsName := parsed.Data.Artistsname
if name == "" || artistsName == "" {
err = errors.New("无法获API取歌曲信息")
return
}
musicName = name + " - " + artistsName
downMusic := musicPath + "/" + musicName + ".mp3"
if file.IsNotExist(downMusic) { if file.IsNotExist(downMusic) {
data, err = web.GetData(musicURL) data, err = web.GetData(musicURL)
if err != nil { if err != nil {

View File

@ -1,108 +1,406 @@
package guessmusic package guessmusic
type listRaw struct {
Name string `json:"name"`
ID int64 `json:"id"`
}
type config struct { type config struct {
MusicPath string `json:"musicPath"` MusicPath string `json:"musicPath"`
Local bool `json:"local"` Local bool `json:"local"`
API bool `json:"api"` API bool `json:"api"`
Cookie string `json:"cookie"`
Playlist []listRaw `json:"playlist"`
} }
type paugramData struct { type keyInfo struct {
ID int `json:"id"` Data struct {
Title string `json:"title"` Code int `json:"code"`
Artist string `json:"artist"` Unikey string `json:"unikey"`
Album string `json:"album"` } `json:"data"`
Cover string `json:"cover"`
Lyric string `json:"lyric"`
SubLyric string `json:"sub_lyric"`
Link string `json:"link"`
Cached bool `json:"cached"`
}
type animeData struct {
Msg string `json:"msg"`
Res struct {
ID string `json:"id"`
AnimeInfo struct {
Desc string `json:"desc"`
ID string `json:"id"`
Atime int `json:"atime"`
Logo string `json:"logo"`
Year int `json:"year"`
Bg string `json:"bg"`
Title string `json:"title"`
Month int `json:"month"`
} `json:"anime_info"`
PlayURL string `json:"play_url"`
Atime int `json:"atime"`
Title string `json:"title"`
Author string `json:"author"`
Type string `json:"type"`
Recommend bool `json:"recommend"`
} `json:"res"`
Code int `json:"code"` Code int `json:"code"`
} }
type cookyInfo struct {
type netEaseData struct { Code int `json:"code"`
Message string `json:"message"`
Cookie string `json:"cookie"`
}
type qrInfo struct {
Code int `json:"code"` Code int `json:"code"`
Data struct { Data struct {
Name string `json:"name"` Qrurl string `json:"qrurl"`
URL string `json:"url"` Qrimg string `json:"qrimg"`
Picurl string `json:"picurl"`
Artistsname string `json:"artistsname"`
} `json:"data"` } `json:"data"`
} }
type topList struct {
Code int `json:"code"`
RelatedVideos interface{} `json:"relatedVideos"`
Playlist struct {
ID int64 `json:"id"`
Name string `json:"name"`
CoverImgID int64 `json:"coverImgId"`
CoverImgURL string `json:"coverImgUrl"`
CoverImgIDStr string `json:"coverImgId_str"`
AdType int `json:"adType"`
UserID int `json:"userId"`
CreateTime int64 `json:"createTime"`
Status int `json:"status"`
OpRecommend bool `json:"opRecommend"`
HighQuality bool `json:"highQuality"`
NewImported bool `json:"newImported"`
UpdateTime int64 `json:"updateTime"`
TrackCount int `json:"trackCount"`
SpecialType int `json:"specialType"`
Privacy int `json:"privacy"`
TrackUpdateTime int64 `json:"trackUpdateTime"`
CommentThreadID string `json:"commentThreadId"`
PlayCount int `json:"playCount"`
TrackNumberUpdateTime int64 `json:"trackNumberUpdateTime"`
SubscribedCount int `json:"subscribedCount"`
CloudTrackCount int `json:"cloudTrackCount"`
Ordered bool `json:"ordered"`
Description string `json:"description"`
Tags []string `json:"tags"`
UpdateFrequency interface{} `json:"updateFrequency"`
BackgroundCoverID int `json:"backgroundCoverId"`
BackgroundCoverURL interface{} `json:"backgroundCoverUrl"`
TitleImage int `json:"titleImage"`
TitleImageURL interface{} `json:"titleImageUrl"`
EnglishTitle interface{} `json:"englishTitle"`
OfficialPlaylistType interface{} `json:"officialPlaylistType"`
Subscribers []struct {
DefaultAvatar bool `json:"defaultAvatar"`
Province int `json:"province"`
AuthStatus int `json:"authStatus"`
Followed bool `json:"followed"`
AvatarURL string `json:"avatarUrl"`
AccountStatus int `json:"accountStatus"`
Gender int `json:"gender"`
City int `json:"city"`
Birthday int `json:"birthday"`
UserID int `json:"userId"`
UserType int `json:"userType"`
Nickname string `json:"nickname"`
Signature string `json:"signature"`
Description string `json:"description"`
DetailDescription string `json:"detailDescription"`
AvatarImgID int64 `json:"avatarImgId"`
BackgroundImgID int64 `json:"backgroundImgId"`
BackgroundURL string `json:"backgroundUrl"`
Authority int `json:"authority"`
Mutual bool `json:"mutual"`
ExpertTags interface{} `json:"expertTags"`
Experts interface{} `json:"experts"`
DjStatus int `json:"djStatus"`
VipType int `json:"vipType"`
RemarkName interface{} `json:"remarkName"`
AuthenticationTypes int `json:"authenticationTypes"`
AvatarDetail interface{} `json:"avatarDetail"`
Anchor bool `json:"anchor"`
BackgroundImgIDStr string `json:"backgroundImgIdStr"`
AvatarImgIDStr string `json:"avatarImgIdStr"`
AvatarImgIDString string `json:"AvatarImgIDString"`
} `json:"subscribers"`
Subscribed interface{} `json:"subscribed"`
Creator struct {
DefaultAvatar bool `json:"defaultAvatar"`
Province int `json:"province"`
AuthStatus int `json:"authStatus"`
Followed bool `json:"followed"`
AvatarURL string `json:"avatarUrl"`
AccountStatus int `json:"accountStatus"`
Gender int `json:"gender"`
City int `json:"city"`
Birthday int `json:"birthday"`
UserID int `json:"userId"`
UserType int `json:"userType"`
Nickname string `json:"nickname"`
Signature string `json:"signature"`
Description string `json:"description"`
DetailDescription string `json:"detailDescription"`
AvatarImgID int64 `json:"avatarImgId"`
BackgroundImgID int64 `json:"backgroundImgId"`
BackgroundURL string `json:"backgroundUrl"`
Authority int `json:"authority"`
Mutual bool `json:"mutual"`
ExpertTags interface{} `json:"expertTags"`
Experts interface{} `json:"experts"`
DjStatus int `json:"djStatus"`
VipType int `json:"vipType"`
RemarkName interface{} `json:"remarkName"`
AuthenticationTypes int `json:"authenticationTypes"`
AvatarDetail struct {
UserType int `json:"userType"`
IdentityLevel int `json:"identityLevel"`
IdentityIconURL string `json:"identityIconUrl"`
} `json:"avatarDetail"`
Anchor bool `json:"anchor"`
BackgroundImgIDStr string `json:"backgroundImgIdStr"`
AvatarImgIDStr string `json:"avatarImgIdStr"`
AvatarImgIDString string `json:"AvatarImgIDString"`
} `json:"creator"`
Tracks []struct {
Name string `json:"name"`
ID int `json:"id"`
Pst int `json:"pst"`
T int `json:"t"`
Ar []struct {
ID int `json:"id"`
Name string `json:"name"`
Tns []interface{} `json:"tns"`
Alias []interface{} `json:"alias"`
} `json:"ar"`
Alia []string `json:"alia"`
Pop int `json:"pop"`
St int `json:"st"`
Rt string `json:"rt"`
Fee int `json:"fee"`
V int `json:"v"`
Crbt interface{} `json:"crbt"`
Cf string `json:"cf"`
Al struct {
ID int `json:"id"`
Name string `json:"name"`
PicURL string `json:"picUrl"`
Tns []interface{} `json:"tns"`
PicStr string `json:"pic_str"`
Pic int64 `json:"pic"`
} `json:"al"`
Dt int `json:"dt"`
H struct {
Br int `json:"br"`
Fid int `json:"fid"`
Size int `json:"size"`
Vd int `json:"vd"`
Sr int `json:"sr"`
} `json:"h"`
M struct {
Br int `json:"br"`
Fid int `json:"fid"`
Size int `json:"size"`
Vd int `json:"vd"`
Sr int `json:"sr"`
} `json:"m"`
L struct {
Br int `json:"br"`
Fid int `json:"fid"`
Size int `json:"size"`
Vd int `json:"vd"`
Sr int `json:"sr"`
} `json:"l"`
Sq interface{} `json:"sq"`
Hr interface{} `json:"hr"`
A interface{} `json:"a"`
Cd string `json:"cd"`
No int `json:"no"`
RtURL interface{} `json:"rtUrl"`
Ftype int `json:"ftype"`
RtUrls []interface{} `json:"rtUrls"`
DjID int `json:"djId"`
Copyright int `json:"copyright"`
SID int `json:"s_id"`
Mark int `json:"mark"`
OriginCoverType int `json:"originCoverType"`
OriginSongSimpleData interface{} `json:"originSongSimpleData"`
TagPicList interface{} `json:"tagPicList"`
ResourceState bool `json:"resourceState"`
Version int `json:"version"`
SongJumpInfo interface{} `json:"songJumpInfo"`
EntertainmentTags interface{} `json:"entertainmentTags"`
Single int `json:"single"`
NoCopyrightRcmd interface{} `json:"noCopyrightRcmd"`
Alg interface{} `json:"alg"`
Rtype int `json:"rtype"`
Rurl interface{} `json:"rurl"`
Mst int `json:"mst"`
Cp int `json:"cp"`
Mv int `json:"mv"`
PublishTime int64 `json:"publishTime"`
Tns []string `json:"tns,omitempty"`
} `json:"tracks"`
VideoIds interface{} `json:"videoIds"`
Videos interface{} `json:"videos"`
TrackIds []struct {
ID int `json:"id"`
V int `json:"v"`
T int `json:"t"`
At int64 `json:"at"`
Alg interface{} `json:"alg"`
UID int `json:"uid"`
RcmdReason string `json:"rcmdReason"`
Sc interface{} `json:"sc"`
Lr int `json:"lr,omitempty"`
} `json:"trackIds"`
ShareCount int `json:"shareCount"`
CommentCount int `json:"commentCount"`
RemixVideo interface{} `json:"remixVideo"`
SharedUsers interface{} `json:"sharedUsers"`
HistorySharedUsers interface{} `json:"historySharedUsers"`
GradeStatus string `json:"gradeStatus"`
Score interface{} `json:"score"`
AlgTags interface{} `json:"algTags"`
} `json:"playlist"`
Urls interface{} `json:"urls"`
Privileges []struct {
ID int `json:"id"`
Fee int `json:"fee"`
Payed int `json:"payed"`
RealPayed int `json:"realPayed"`
St int `json:"st"`
Pl int `json:"pl"`
Dl int `json:"dl"`
Sp int `json:"sp"`
Cp int `json:"cp"`
Subp int `json:"subp"`
Cs bool `json:"cs"`
Maxbr int `json:"maxbr"`
Fl int `json:"fl"`
Pc interface{} `json:"pc"`
Toast bool `json:"toast"`
Flag int `json:"flag"`
PaidBigBang bool `json:"paidBigBang"`
PreSell bool `json:"preSell"`
PlayMaxbr int `json:"playMaxbr"`
DownloadMaxbr int `json:"downloadMaxbr"`
MaxBrLevel string `json:"maxBrLevel"`
PlayMaxBrLevel string `json:"playMaxBrLevel"`
DownloadMaxBrLevel string `json:"downloadMaxBrLevel"`
PlLevel string `json:"plLevel"`
DlLevel string `json:"dlLevel"`
FlLevel string `json:"flLevel"`
Rscl int `json:"rscl"`
FreeTrialPrivilege struct {
ResConsumable bool `json:"resConsumable"`
UserConsumable bool `json:"userConsumable"`
ListenType interface{} `json:"listenType"`
} `json:"freeTrialPrivilege"`
ChargeInfoList []struct {
Rate int `json:"rate"`
ChargeURL interface{} `json:"chargeUrl"`
ChargeMessage interface{} `json:"chargeMessage"`
ChargeType int `json:"chargeType"`
} `json:"chargeInfoList"`
} `json:"privileges"`
SharedPrivilege interface{} `json:"sharedPrivilege"`
ResEntrance interface{} `json:"resEntrance"`
}
type autumnfishData struct { type topMusicInfo struct {
Result struct { Songs []struct {
Songs []struct { Name string `json:"name"`
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Pst int `json:"pst"`
Artists []struct { T int `json:"t"`
ID int `json:"id"` Ar []struct {
Name string `json:"name"` ID int `json:"id"`
PicURL interface{} `json:"picUrl"` Name string `json:"name"`
Alias []interface{} `json:"alias"` Tns []interface{} `json:"tns"`
AlbumSize int `json:"albumSize"` Alias []interface{} `json:"alias"`
PicID int `json:"picId"` } `json:"ar"`
Img1V1URL string `json:"img1v1Url"` Alia []string `json:"alia"`
Img1V1 int `json:"img1v1"` Pop int `json:"pop"`
Trans interface{} `json:"trans"` St int `json:"st"`
} `json:"artists"` Rt string `json:"rt"`
Album struct { Fee int `json:"fee"`
ID int `json:"id"` V int `json:"v"`
Name string `json:"name"` Crbt interface{} `json:"crbt"`
Artist struct { Cf string `json:"cf"`
ID int `json:"id"` Al struct {
Name string `json:"name"` ID int `json:"id"`
PicURL interface{} `json:"picUrl"` Name string `json:"name"`
Alias []interface{} `json:"alias"` PicURL string `json:"picUrl"`
AlbumSize int `json:"albumSize"` Tns []interface{} `json:"tns"`
PicID int `json:"picId"` PicStr string `json:"pic_str"`
Img1V1URL string `json:"img1v1Url"` Pic int64 `json:"pic"`
Img1V1 int `json:"img1v1"` } `json:"al"`
Trans interface{} `json:"trans"` Dt int `json:"dt"`
} `json:"artist"` H struct {
PublishTime int64 `json:"publishTime"` Br int `json:"br"`
Size int `json:"size"` Fid int `json:"fid"`
CopyrightID int `json:"copyrightId"` Size int `json:"size"`
Status int `json:"status"` Vd float32 `json:"vd"`
PicID int64 `json:"picId"` Sr int `json:"sr"`
Mark int `json:"mark"` } `json:"h"`
} `json:"album"` M struct {
Duration int `json:"duration"` Br int `json:"br"`
CopyrightID int `json:"copyrightId"` Fid int `json:"fid"`
Status int `json:"status"` Size int `json:"size"`
Alias []interface{} `json:"alias"` Vd float32 `json:"vd"`
Rtype int `json:"rtype"` Sr int `json:"sr"`
Ftype int `json:"ftype"` } `json:"m"`
TransNames []string `json:"transNames"` L struct {
Mvid int `json:"mvid"` Br int `json:"br"`
Fee int `json:"fee"` Fid int `json:"fid"`
RURL interface{} `json:"rUrl"` Size int `json:"size"`
Mark int `json:"mark"` Vd float32 `json:"vd"`
} `json:"songs"` Sr int `json:"sr"`
HasMore bool `json:"hasMore"` } `json:"l"`
SongCount int `json:"songCount"` Sq interface{} `json:"sq"`
} `json:"result"` Hr interface{} `json:"hr"`
A interface{} `json:"a"`
Cd string `json:"cd"`
No int `json:"no"`
RtURL interface{} `json:"rtUrl"`
Ftype int `json:"ftype"`
RtUrls []interface{} `json:"rtUrls"`
DjID int `json:"djId"`
Copyright int `json:"copyright"`
SID int `json:"s_id"`
Mark int `json:"mark"`
OriginCoverType int `json:"originCoverType"`
OriginSongSimpleData interface{} `json:"originSongSimpleData"`
TagPicList interface{} `json:"tagPicList"`
ResourceState bool `json:"resourceState"`
Version int `json:"version"`
SongJumpInfo interface{} `json:"songJumpInfo"`
EntertainmentTags interface{} `json:"entertainmentTags"`
AwardTags interface{} `json:"awardTags"`
Single int `json:"single"`
NoCopyrightRcmd interface{} `json:"noCopyrightRcmd"`
Rtype int `json:"rtype"`
Rurl interface{} `json:"rurl"`
Mst int `json:"mst"`
Cp int `json:"cp"`
Mv int `json:"mv"`
PublishTime int64 `json:"publishTime"`
Tns []string `json:"tns,omitempty"`
} `json:"songs"`
Privileges []struct {
ID int `json:"id"`
Fee int `json:"fee"`
Payed int `json:"payed"`
St int `json:"st"`
Pl int `json:"pl"`
Dl int `json:"dl"`
Sp int `json:"sp"`
Cp int `json:"cp"`
Subp int `json:"subp"`
Cs bool `json:"cs"`
Maxbr int `json:"maxbr"`
Fl int `json:"fl"`
Toast bool `json:"toast"`
Flag int `json:"flag"`
PreSell bool `json:"preSell"`
PlayMaxbr int `json:"playMaxbr"`
DownloadMaxbr int `json:"downloadMaxbr"`
MaxBrLevel string `json:"maxBrLevel"`
PlayMaxBrLevel string `json:"playMaxBrLevel"`
DownloadMaxBrLevel string `json:"downloadMaxBrLevel"`
PlLevel string `json:"plLevel"`
DlLevel string `json:"dlLevel"`
FlLevel string `json:"flLevel"`
Rscl int `json:"rscl"`
FreeTrialPrivilege struct {
ResConsumable bool `json:"resConsumable"`
UserConsumable bool `json:"userConsumable"`
ListenType interface{} `json:"listenType"`
} `json:"freeTrialPrivilege"`
ChargeInfoList []struct {
Rate int `json:"rate"`
ChargeURL interface{} `json:"chargeUrl"`
ChargeMessage interface{} `json:"chargeMessage"`
ChargeType int `json:"chargeType"`
} `json:"chargeInfoList"`
} `json:"privileges"`
Code int `json:"code"` Code int `json:"code"`
} }