diff --git a/README.md b/README.md index d020d2d5..5120f9cb 100644 --- a/README.md +++ b/README.md @@ -581,22 +581,32 @@ print("run[CQ:image,file="+j["img"]+"]") `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic"` - 猜歌插件(该插件依赖ffmpeg) - - - [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动画榜 + + - 可在后面添加[-歌单名称]进行指定歌单猜歌 + + - 歌单的歌曲命名规则为:歌名 - 歌手 - 其他(歌曲出处之类)
diff --git a/plugin/guessmusic/main.go b/plugin/guessmusic/main.go index d3f27fc5..03a6c96f 100644 --- a/plugin/guessmusic/main.go +++ b/plugin/guessmusic/main.go @@ -15,6 +15,9 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" + "github.com/wdvxdr1123/ZeroBot/utils/helper" + "github.com/pkg/errors" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/message" @@ -25,6 +28,10 @@ import ( "github.com/FloatTech/zbputils/file" "github.com/FloatTech/zbputils/web" "github.com/wdvxdr1123/ZeroBot/extension/single" + + // 图片输出 + + "github.com/FloatTech/zbputils/img/text" ) const ( @@ -32,26 +39,31 @@ const ( ) var ( - cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒) - cfg = config{ // 默认 config - MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改 - Local: true, // 是否使用本地音乐库 - API: true, // 是否使用 Api - } + catlist = make(map[string]int64, 100) + filelist []string + cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒) + cfg config ) func init() { // 插件主体 engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Help: "猜歌插件(该插件依赖ffmpeg)\n" + - "- 个人猜歌\n" + - "- 团队猜歌\n" + + "------bot主人指令------\n" + "- 设置猜歌缓存歌库路径 [绝对路径]\n" + - "- 设置猜歌本地 [true/false]\n" + - "- 设置猜歌Api [true/false]\n" + - "注:默认歌库为网易云热歌榜\n- 本地歌榜歌库歌曲命名规格“歌名 - 歌手”\n" + - "1.可在后面添加“-动漫”进行动漫歌猜歌\n- 这个只能猜歌名和歌手\n- 本地动漫歌库歌曲命名规格“歌名 - 歌手”\n" + - "2.可在后面添加“-动漫2”进行动漫歌猜歌\n- 这个可以猜番名,但歌手经常“未知”\n- 本地动漫2歌库歌曲命名规格“歌名 - 歌手 - 番名”", + "- 设置猜歌[本地/Api] [true/false]\n" + + "- 登录网易云\n" + + "- 添加歌单 [网易云歌单ID] [歌单名称]\n" + + "- 删除歌单 [网易云歌单ID/API歌单名称]\n" + + "注:\n1.不登陆也能用,API有几率返回400\n" + + "2.[歌单名称]可为空,默认原标题\n" + + "------公 用 指 令------\n" + + "- 获取歌单列表\n" + + "- [网易云歌单ID/API歌单名称]歌单信息\n" + + "- [个人/团队]猜歌\n" + + "注:默认歌库为网易云ACG动画榜\n" + + "可在后面添加[-歌单名称]进行指定歌单猜歌\n" + + "歌单的歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)", PrivateDataFolder: "guessmusic", }).ApplySingle(single.New( single.WithKeyFn(func(ctx *zero.Ctx) int64 { return ctx.Event.GroupID }), @@ -84,11 +96,28 @@ func init() { // 插件主体 panic(err) } } 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) if err != nil { panic(err) } } + err = getcatlist(cfg.MusicPath) + if err != nil { + logrus.Infof("[guessmusic2]无法获取歌单列表,[error]:%s", err) + } engine.OnRegex(`^设置猜歌(缓存歌库路径|本地|Api)\s*(.*)$`, func(ctx *zero.Ctx) bool { if !zero.SuperUserPermission(ctx) { ctx.SendChain(message.Text("只有bot主人可以设置!")) @@ -132,15 +161,259 @@ func init() { // 插件主体 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) { - mode := ctx.State["regex_matched"].([]string)[2] - gid := strconv.FormatInt(ctx.Event.GroupID, 10) - if mode == "-动漫2" { - ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|番剧|提示|取消]”\n一共3段语音,6次机会")) - } else { - ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|提示|取消]”\n一共3段语音,6次机会")) + keyURL := "https://music.cyrilstudio.top/login/qr/key" + data, err := web.GetData(keyURL) + if err != nil { + ctx.SendChain(message.Text("获取网易云key失败, ERROR:", err)) + return } + 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) if err != nil { @@ -154,9 +427,17 @@ func init() { // 插件主体 ctx.SendChain(message.Text(err)) 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")) - answerString := strings.Split(musicName, " - ") var next *zero.FutureEvent if ctx.State["regex_matched"].([]string)[1] == "个人" { next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), ctx.CheckSession()) @@ -175,15 +456,8 @@ func init() { // 插件主体 case <-tick.C: ctx.SendChain(message.Text("猜歌游戏,你还有15s作答时间")) case <-after.C: - msg := make(message.Message, 0, 3) - msg = append(msg, message.Reply(ctx.Event.MessageID)) - 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) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, + message.Text("时间超时,猜歌结束,公布答案:\n", answerString))) return case <-wait.C: wait.Reset(40 * time.Second) @@ -207,15 +481,8 @@ func init() { // 插件主体 wait.Stop() tick.Stop() after.Stop() - msg := make(message.Message, 0, 3) - msg = append(msg, message.Reply(c.Event.MessageID)) - 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) + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, + message.Text("游戏已取消,猜歌答案是\n", answerString))) return } ctx.Send( @@ -241,53 +508,28 @@ func init() { // 插件主体 ), ) 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() tick.Stop() after.Stop() - msg := make(message.Message, 0, 3) - msg = append(msg, message.Reply(c.Event.MessageID)) - 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) + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("太棒了,你猜对歌曲名了!答案是\n", answerString))) return - case answerString[1] == "未知" && answer == "未知": - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("该模式禁止回答“未知”"), - ), - ) - case strings.Contains(answerString[1], answer) || strings.EqualFold(answerString[1], answer): + case strings.Contains(musicInfo[1], answer) || strings.EqualFold(musicInfo[1], answer): wait.Stop() tick.Stop() after.Stop() - msg := make(message.Message, 0, 3) - msg = append(msg, message.Reply(c.Event.MessageID)) - 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) + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("太棒了,你猜对歌手名了!答案是\n", answerString))) + return + case strings.Contains(musicAlia, answer) || strings.EqualFold(musicAlia, answer): + wait.Stop() + tick.Stop() + after.Stop() + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("太棒了,你猜对出处了!答案是\n", answerString))) return 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++ switch { case musicCount > 2 && answerCount < 6: @@ -302,15 +544,8 @@ func init() { // 插件主体 wait.Stop() tick.Stop() after.Stop() - msg := make(message.Message, 0, 3) - msg = append(msg, message.Reply(c.Event.MessageID)) - 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) + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("次数到了,没能猜出来。答案是\n", answerString))) return default: wait.Reset(40 * time.Second) @@ -340,16 +575,30 @@ func saveConfig(cfgFile string) (err error) { 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) { - switch mode { - case "-动漫": - pathOfMusic = musicPath + "动漫/" - case "-动漫2": - pathOfMusic = musicPath + "动漫2/" - default: - pathOfMusic = musicPath + "歌榜/" - } + pathOfMusic = musicPath + mode + "/" err = os.MkdirAll(pathOfMusic, 0755) if err != nil { 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) return } - + listID, ok := catlist[mode] + listIDstr := strconv.FormatInt(listID, 10) if cfg.Local && cfg.API { switch { case len(files) == 0: + if !ok { + // 如果歌单是本地歌单 + err = errors.New("本地歌单数据为0") + return + } // 如果没有任何本地就下载歌曲 - musicName, err = getAPIMusic(mode, pathOfMusic) + musicName, err = getListMusic(listIDstr, pathOfMusic) if err != nil { err = errors.Errorf("[本地数据为0,歌曲下载错误]ERROR:%s", err) return } - case rand.Intn(2) == 0: - // [0,1)只会取到0,rand不允许的 + case rand.Intn(2) == 0 || !ok: + // 1/2概率抽本地或者歌单只有本地有时 musicName = getLocalMusic(files) default: - musicName, err = getAPIMusic(mode, pathOfMusic) + musicName, err = getListMusic(listIDstr, pathOfMusic) if err != nil { // 如果下载失败就从本地抽一个歌曲 musicName = getLocalMusic(files) @@ -391,27 +646,15 @@ func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err er musicName = getLocalMusic(files) return } - if cfg.API { - musicName, err = getAPIMusic(mode, pathOfMusic) + if cfg.API && ok { + musicName, err = getListMusic(listIDstr, pathOfMusic) if err != nil { err = errors.Errorf("[获取API失败,未开启本地数据] ERROR:%s", err) return } return } - 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) - } + err = errors.New("[请确认本地和API设置已开启或歌单存在]") return } @@ -424,28 +667,43 @@ func getLocalMusic(files []fs.FileInfo) (musicName string) { return } -// 下载保罗API的歌曲 -func getPaugramData(musicPath string) (musicName string, err error) { - api := "https://api.paugram.com/acgm/?list=1" - referer := "https://api.paugram.com/" - data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua) +// 下载网易云歌单音乐 +func getListMusic(listID, pathOfMusic string) (musicName string, err error) { + apiURL := "https://music.cyrilstudio.top/playlist/track/all?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 { return } - var parsed paugramData + var parsed topMusicInfo err = json.Unmarshal(data, &parsed) if err != nil { return } - name := parsed.Title - artistsName := parsed.Artist - musicURL := parsed.Link - if name == "" || artistsName == "" { + listlen := len(parsed.Songs) + randidx := rand.Intn(listlen) + // 将"/"符号去除,不然无法生成文件 + 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取歌曲信息") return } - musicName = name + " - " + artistsName - downMusic := musicPath + "/" + musicName + ".mp3" + musicURL := "http://music.163.com/song/media/outer/url?id=" + strconv.Itoa(musicID) response, err := http.Head(musicURL) if err != nil { 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) return } - if file.IsNotExist(downMusic) { - data, err = web.GetData(musicURL) - if err != nil { - return - } - err = os.WriteFile(downMusic, data, 0666) - if err != nil { - return - } + if cource != "" { + musicName = name + " - " + artistName + " - " + cource + } else { + musicName = name + " - " + artistName } - return -} - -// 下载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" + downMusic := pathOfMusic + musicName + ".mp3" if file.IsNotExist(downMusic) { data, err = web.GetData(musicURL) if err != nil { diff --git a/plugin/guessmusic/struct.go b/plugin/guessmusic/struct.go index 216f9f96..0f585e30 100644 --- a/plugin/guessmusic/struct.go +++ b/plugin/guessmusic/struct.go @@ -1,108 +1,406 @@ package guessmusic +type listRaw struct { + Name string `json:"name"` + ID int64 `json:"id"` +} + type config struct { - MusicPath string `json:"musicPath"` - Local bool `json:"local"` - API bool `json:"api"` + MusicPath string `json:"musicPath"` + Local bool `json:"local"` + API bool `json:"api"` + Cookie string `json:"cookie"` + Playlist []listRaw `json:"playlist"` } -type paugramData struct { - ID int `json:"id"` - Title string `json:"title"` - Artist string `json:"artist"` - Album string `json:"album"` - 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"` +type keyInfo struct { + Data struct { + Code int `json:"code"` + Unikey string `json:"unikey"` + } `json:"data"` Code int `json:"code"` } - -type netEaseData struct { +type cookyInfo struct { + Code int `json:"code"` + Message string `json:"message"` + Cookie string `json:"cookie"` +} +type qrInfo struct { Code int `json:"code"` Data struct { - Name string `json:"name"` - URL string `json:"url"` - Picurl string `json:"picurl"` - Artistsname string `json:"artistsname"` + Qrurl string `json:"qrurl"` + Qrimg string `json:"qrimg"` } `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 { - Result struct { - Songs []struct { - ID int `json:"id"` - Name string `json:"name"` - Artists []struct { - ID int `json:"id"` - Name string `json:"name"` - PicURL interface{} `json:"picUrl"` - Alias []interface{} `json:"alias"` - AlbumSize int `json:"albumSize"` - PicID int `json:"picId"` - Img1V1URL string `json:"img1v1Url"` - Img1V1 int `json:"img1v1"` - Trans interface{} `json:"trans"` - } `json:"artists"` - Album struct { - ID int `json:"id"` - Name string `json:"name"` - Artist struct { - ID int `json:"id"` - Name string `json:"name"` - PicURL interface{} `json:"picUrl"` - Alias []interface{} `json:"alias"` - AlbumSize int `json:"albumSize"` - PicID int `json:"picId"` - Img1V1URL string `json:"img1v1Url"` - Img1V1 int `json:"img1v1"` - Trans interface{} `json:"trans"` - } `json:"artist"` - PublishTime int64 `json:"publishTime"` - Size int `json:"size"` - CopyrightID int `json:"copyrightId"` - Status int `json:"status"` - PicID int64 `json:"picId"` - Mark int `json:"mark"` - } `json:"album"` - Duration int `json:"duration"` - CopyrightID int `json:"copyrightId"` - Status int `json:"status"` - Alias []interface{} `json:"alias"` - Rtype int `json:"rtype"` - Ftype int `json:"ftype"` - TransNames []string `json:"transNames"` - Mvid int `json:"mvid"` - Fee int `json:"fee"` - RURL interface{} `json:"rUrl"` - Mark int `json:"mark"` - } `json:"songs"` - HasMore bool `json:"hasMore"` - SongCount int `json:"songCount"` - } `json:"result"` +type topMusicInfo struct { + Songs []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 float32 `json:"vd"` + Sr int `json:"sr"` + } `json:"h"` + M struct { + Br int `json:"br"` + Fid int `json:"fid"` + Size int `json:"size"` + Vd float32 `json:"vd"` + Sr int `json:"sr"` + } `json:"m"` + L struct { + Br int `json:"br"` + Fid int `json:"fid"` + Size int `json:"size"` + Vd float32 `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"` + 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"` }