diff --git a/README.md b/README.md index 47bdf7b3..f61238c7 100644 --- a/README.md +++ b/README.md @@ -734,37 +734,32 @@ print("run[CQ:image,file="+j["img"]+"]") `import _ "github.com/FloatTech/ZeroBot-Plugin/plugin/guessmusic"` - - 猜歌插件(该插件依赖ffmpeg) - - - 因为API不可抗因素,更改为了本地猜歌,但仍支持歌曲下载(VIP歌曲无法下载,黑胶可以) - + 猜歌插件(该插件依赖ffmpeg) + + ---------主 人 指 令--------- - [x] 设置猜歌歌库路径 [绝对路径] - - - [x] 猜歌[开启/关闭][歌单/歌词]自动下载 - - - 现只有歌词指令有效 - - - [ ] 添加歌单 [网易云歌单链接/ID] [歌单名称] - - - [x] 下载歌曲 [歌曲名称/网易云歌曲ID] [歌单名称] - - - [x] 删除歌单 [网易云歌单ID/歌单名称] - - - 注:删除网易云歌单ID仅只是解除绑定,删除歌单名称是将本地数据全部删除! - + - [x] [创建/删除]歌单 [歌单名称] + - [x] 下载歌曲[歌曲名称/网易云歌曲ID]到[歌单名称] + + -------管 理 员 指 令-------- - [x] 设置猜歌默认歌单 [歌单名称] - + - [x] 上传歌曲[群文件的音乐名]到[歌单名称] + + ------公 用 指 令------ - [x] 歌单列表 - - [x] [个人/团队]猜歌 - - - 注:默认歌库为歌单列表第一个,如果设置了默认歌单变为指定的歌单 - - - 可在“[个人/团队]猜歌指令”后面添加[-歌单名称]进行指定歌单猜歌 - - - 猜歌内容必须以[-]开头才会识别 - - - 本地歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类) + + ------插 件 扩 展------ + +NeteaseCloudMusicApi项目地址:https://binaryify.github.io/NeteaseCloudMusicApi/#/ + - [x] 设置猜歌API帮助 + - [x] 设置猜歌API [API首页网址] + - [x] 猜歌[开启/关闭][歌单/歌词]自动下载 + - [ ] 登录网易云 + - [x] 歌单信息 [网易云歌单链接/ID] + - [x] [歌单名称]绑定网易云[网易云歌单链接/ID] + - [x] 下载歌单[网易云歌单链接/ID]到[歌单名称] + - [x] 解除绑定 [歌单名称]
diff --git a/plugin/guessmusic/apiservice.go b/plugin/guessmusic/apiservice.go new file mode 100644 index 00000000..25789fc0 --- /dev/null +++ b/plugin/guessmusic/apiservice.go @@ -0,0 +1,484 @@ +package guessmusic + +import ( + "encoding/json" + "math/rand" + "net/url" + "os" + "strconv" + "strings" + "time" + + wyy "github.com/FloatTech/AnimeAPI/neteasemusic" + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/floatbox/web" + "github.com/FloatTech/zbputils/ctxext" + "github.com/pkg/errors" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +func init() { + // API配置 + engine.OnPrefix("设置猜歌API", zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + option := ctx.State["args"].(string) + if option == "帮助" { + ctx.SendChain(message.Text( + "项目地址:binaryify.github.io/NeteaseCloudMusicApi" + + "\n网上有基于该框架的API,可以自行搜索白嫖。\n" + + "添加API指令:\n设置猜歌API [API首页网址]")) + return + } + if !strings.HasSuffix(option, "/") { + option += "/" + } + cfg.APIURL = option + err := saveConfig(cfgFile) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) + // API配置 + engine.OnRegex(`^猜歌(开启|关闭)(歌单|歌词)自动下载`, zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + swtich := ctx.State["regex_matched"].([]string)[1] + option := ctx.State["regex_matched"].([]string)[2] + chose := true + if swtich == "关闭" { + chose = false + } + if option == "歌单" { + cfg.API = chose + } else { + cfg.Local = chose + } + err := saveConfig(cfgFile) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) + 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) { + keyURL := cfg.APIURL + "login/qr/key" + data, err := web.GetData(keyURL) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "获取网易云key失败,", err)) + return + } + var keyInfo keyInfo + err = json.Unmarshal(data, &keyInfo) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "解析网易云key失败,", err)) + return + } + qrURL := cfg.APIURL + "login/qr/create?key=" + keyInfo.Data.Unikey + "&qrimg=1" + data, err = web.GetData(qrURL) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "获取网易云二维码失败,", err)) + return + } + var qrInfo qrInfo + err = json.Unmarshal(data, &qrInfo) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "解析网易云二维码失败,", 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分钟,登陆后请耐心等待结果,获取cookie过程有些漫长。")) + i := 0 + for range time.NewTicker(10 * time.Second).C { + APIURL := cfg.APIURL + "login/qr/check?key=" + url.QueryEscape(keyInfo.Data.Unikey) + data, err := web.GetData(APIURL) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "无法获取登录状态,", err)) + return + } + var cookiesInfo cookyInfo + err = json.Unmarshal(data, &cookiesInfo) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "解析登录状态失败,", 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(serviceErr, 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*((https:\/\/music\.163\.com\/#\/playlist\?id=)?(\d+)|http:\/\/music\.163\.com\/playlist\/(\d+).*)$`).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + listID := ctx.State["regex_matched"].([]string)[3] + ctx.State["regex_matched"].([]string)[4] + _, err := strconv.ParseInt(listID, 10, 64) + if err != nil { + ctx.SendChain(message.Text("请输入正确的歌单ID或者歌单连接")) + return + } + APIURL := cfg.APIURL + "playlist/detail?id=" + listID + data, err := web.GetData(APIURL) + if err != nil { + ctx.SendChain(message.Text("无法连接歌单,", err)) + return + } + var parsed listInfoOfAPI + err = json.Unmarshal(data, &parsed) + if err != nil { + ctx.SendChain(message.Text("无法解析歌单ID内容,", 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"), + )) + }) + // 本地绑定网易云歌单ID + engine.OnRegex(`^(.*)绑定网易云\s*((https:\/\/music\.163\.com\/#\/playlist\?id=)?(\d+)|http:\/\/music\.163\.com\/playlist\/(\d+).*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + listName := ctx.State["regex_matched"].([]string)[1] + listID := ctx.State["regex_matched"].([]string)[4] + ctx.State["regex_matched"].([]string)[5] + ctx.SendChain(message.Text("正在校验歌单信息,请稍等")) + pathOfMusic := cfg.MusicPath + listName + "/" + if file.IsNotExist(pathOfMusic) { + ctx.SendChain(message.Text(serviceErr, "歌单不存在于本地")) + return + } + // 是否存在该歌单 + APIURL := cfg.APIURL + "playlist/track/all?id=" + listID + data, err := web.GetData(APIURL) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + var parsed musicListOfApI + err = json.Unmarshal(data, &parsed) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "无法解析歌单ID内容,", err)) + return + } + if parsed.Code != 200 { + ctx.SendChain(message.Text(serviceErr, parsed.Code)) + return + } + mid, _ := strconv.ParseInt(listID, 10, 64) + cfg.Playlist = append(cfg.Playlist, listRaw{ + Name: listName, + ID: mid, + }) + err = saveConfig(cfgFile) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) + engine.OnPrefix("解除绑定", zero.SuperUserPermission).SetBlock(true). + Handle(func(ctx *zero.Ctx) { + delList := ctx.State["args"].(string) + filelist, err := getlist(cfg.MusicPath) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + var playID int64 + for _, listinfo := range filelist { + if delList == listinfo.Name { + playID = listinfo.ID + break + } + } + // 删除ID + if playID == 0 { // 如果ID没有且没删除文件 + ctx.SendChain(message.Text("歌单名称错误或者该歌单并没有绑定网易云,可以发送“歌单列表”获取歌单名称")) + return + } + index := -1 + for i, list := range cfg.Playlist { + if playID == list.ID { + index = i + break + } + } + if index == -1 { + ctx.SendChain(message.Text("歌单名称错误或者该歌单并没有绑定网易云,可以发送“歌单列表”获取歌单名称")) + return + } + cfg.Playlist = append(cfg.Playlist[:index], cfg.Playlist[index+1:]...) + err = saveConfig(cfgFile) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) + // 下载歌曲到对应的歌单里面 + engine.OnRegex(`^下载歌单\s*((https:\/\/music\.163\.com\/#\/playlist\?id=)?(\d+)|http:\/\/music\.163\.com\/playlist\/(\d+).*[^\s$])\s*到\s*(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + keyword := ctx.State["regex_matched"].([]string)[3] + ctx.State["regex_matched"].([]string)[4] + listName := ctx.State["regex_matched"].([]string)[5] + ctx.SendChain(message.Text("正在校验歌单信息,请稍等")) + // 是否存在该歌单 + filelist, err := getlist(cfg.MusicPath) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "获取歌单列表ERROR:", err)) + return + } + ok := true + for _, listinfo := range filelist { + if listName == listinfo.Name { + ok = false + break + } + } + if ok { + ctx.SendChain(message.Text("歌单不存在,是否创建?(是/否)")) + next := zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`(是|否)`), ctx.CheckSession()) + recv, cancel := next.Repeat() + defer cancel() + wait := time.NewTimer(120 * time.Second) + answer := "" + for { + select { + case <-wait.C: + wait.Stop() + ctx.SendChain(message.Text("等待超时,取消下载")) + return + case c := <-recv: + wait.Stop() + answer = c.Event.Message.String() + } + if answer == "否" { + ctx.SendChain(message.Text("下载已经取消")) + return + } + if answer != "" { + break + } + } + err = os.MkdirAll(cfg.MusicPath+listName, 0755) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + } + listID, err := strconv.ParseInt(keyword, 10, 64) + if err == nil { + err = downloadlist(listID, cfg.MusicPath+listName+"/") + } + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) +} + +// 随机从歌单下载歌曲(歌单ID, 音乐保存路径) +func drawByAPI(playlistID int64, musicPath string) (musicName string, err error) { + APIURL := cfg.APIURL + "playlist/track/all?id=" + strconv.FormatInt(playlistID, 10) + data, err := web.GetData(APIURL) + if err != nil { + err = errors.Errorf("无法获取歌单列表\n%s", err) + return + } + var parsed musicListOfApI + err = json.Unmarshal(data, &parsed) + if err != nil { + err = errors.Errorf("无法读取歌单列表\n%s", err) + return + } + 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 + } + if cource != "" { + name += " - " + artistName + " - " + cource + } else { + name += " - " + artistName + } + // 下载歌曲 + err = wyy.DownloadMusic(musicID, name, musicPath) + if err == nil { + musicName = name + ".mp3" + if cfg.Local { + // 下载歌词 + _ = wyy.DownloadLrc(musicID, name, musicPath+"歌词/") + } + } + return +} + +// 下载歌单歌曲(歌单ID, 音乐保存路径) +func downloadlist(playlistID int64, musicPath string) error { + APIURL := cfg.APIURL + "playlist/track/all?id=" + strconv.FormatInt(playlistID, 10) + data, err := web.GetData(APIURL) + if err != nil { + return err + } + var parsed musicListOfApI + err = json.Unmarshal(data, &parsed) + if err != nil { + return err + } + if parsed.Code != 200 { + err = errors.Errorf("requset code : %d", parsed.Code) + return err + } + for _, info := range parsed.Songs { + // 将"/"符号去除,不然无法生成文件 + musicName := strings.ReplaceAll(info.Name, "/", "·") + musicID := info.ID + artistName := "" + for i, ARInfo := range info.Ar { + if i != 0 { + artistName += "&" + ARInfo.Name + } else { + artistName += ARInfo.Name + } + } + cource := "" + if info.Alia != nil { + cource = strings.Join(info.Alia, "&") + // 将"/"符号去除,不然无法下载 + cource = strings.ReplaceAll(cource, "/", "&") + } + if musicName == "" || musicID == 0 { + err = errors.New("无法获API取歌曲信息") + return err + } + if cource != "" { + musicName += " - " + artistName + " - " + cource + } else { + musicName += " - " + artistName + } + // 下载歌曲 + err = wyy.DownloadMusic(musicID, musicName, musicPath) + if err == nil { + if cfg.Local { + // 下载歌词 + _ = wyy.DownloadLrc(musicID, musicName, musicPath+"歌词/") + } + } + } + return nil +} + +/*****************************************************************/ +/**************************独角兽API*******************************/ +/*****************************************************************/ +// 下载从独角兽抽到的歌曲ID(歌单ID, 音乐保存路径, 歌词保存路径) +func downloadByOvooa(playlistID int64, musicPath string) (musicName string, err error) { + // 抽取歌曲 + mid, err := drawByOvooa(playlistID) + if err != nil { + err = errors.Errorf("API%s", err) + return + } + // 获取完成的歌名 + musiclist, err := wyy.SearchMusic(strconv.Itoa(mid), 1) + if err != nil { + err = errors.Errorf("API歌曲下载ERROR: %s", err) + return + } + // 歌曲ID理论是唯一的 + mun := len(musiclist) + if mun == 1 { + // 拉取歌名 + musicList := make([]string, mun) + i := 0 + for musicName := range musiclist { + musicList[i] = musicName + } + name := musicList[0] + // 下载歌曲 + err = wyy.DownloadMusic(mid, name, musicPath) + if err == nil { + musicName = name + ".mp3" + if cfg.Local { + // 下载歌词 + _ = wyy.DownloadLrc(mid, name, musicPath+"歌词/") + } + } + } else { + err = errors.Errorf("music IDThis music ID sreached munber is %d", mun) + } + return +} + +// 通过独角兽API随机抽取歌单歌曲ID(参数:歌单ID) +func drawByOvooa(playlistID int64) (musicID int, err error) { + APIURL := "https://ovooa.com/API/163_Music_Rand/api.php?id=" + strconv.FormatInt(playlistID, 10) + data, err := web.GetData(APIURL) + if err != nil { + return + } + var parsed ovooaData + err = json.Unmarshal(data, &parsed) + if err != nil { + return + } + if parsed.Code != 1 { + return + } + return parsed.Data.ID, nil +} diff --git a/plugin/guessmusic/guessmusic.go b/plugin/guessmusic/guessmusic.go new file mode 100644 index 00000000..2f129eab --- /dev/null +++ b/plugin/guessmusic/guessmusic.go @@ -0,0 +1,336 @@ +package guessmusic + +import ( + "bytes" + "io/fs" + "math/rand" + "os" + "os/exec" + "strconv" + "strings" + "time" + + "github.com/FloatTech/floatbox/file" + "github.com/FloatTech/zbputils/ctxext" + "github.com/pkg/errors" + zero "github.com/wdvxdr1123/ZeroBot" + "github.com/wdvxdr1123/ZeroBot/message" +) + +var cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒) + +func init() { + engine.OnRegex(`^(个人|团队)猜歌(-(.*))?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup). + Handle(func(ctx *zero.Ctx) { + mode := ctx.State["regex_matched"].([]string)[3] + gid := ctx.Event.GroupID + // 获取本地列表 + filelist, err := getlist(cfg.MusicPath) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + // 加载默认歌单 + if mode == "" { + index := -1 + for i, dlist := range cfg.Defaultlist { + if dlist.GroupID == gid { + index = i + break + } + } + if index == -1 { + // 如果没有设置就默认第一个文件夹 + mode = filelist[0].Name + } else { + mode = cfg.Defaultlist[index].Name + ok := true + for _, listinfo := range filelist { + if mode == listinfo.Name { + ok = false + break + } + } + // 如果默认的歌单不存在了清空设置 + if ok { + cfg.Defaultlist = append(cfg.Defaultlist[:index], cfg.Defaultlist[index+1:]...) + _ = saveConfig(cfgFile) + mode = filelist[0].Name + } + } + } + ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲信息(歌名歌手等)|提示|取消]”\n一共3段语音,6次机会")) + // 随机抽歌 + pathOfMusic, musicName, err := musicLottery(cfg.MusicPath, mode) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + // 解析歌曲信息 + music := strings.Split(musicName, ".") + // 获取音乐后缀 + musictype := music[len(music)-1] + if !strings.Contains(musictypelist, musictype) { + ctx.SendChain(message.Text("抽取到了歌曲:\n", + musicName, "\n该歌曲不是音乐后缀,请联系bot主人修改")) + return + } + // 获取音乐信息 + musicInfo := strings.Split(strings.ReplaceAll(musicName, "."+musictype, ""), " - ") + infoNum := len(musicInfo) + if infoNum == 1 { + ctx.SendChain(message.Text("抽取到了歌曲:\n", + musicName, "\n该歌曲命名不符合命名规则,请联系bot主人修改")) + return + } + answerString := "歌名:" + musicInfo[0] + "\n歌手:" + musicInfo[1] + musicAlia := "" + if infoNum > 2 { + musicAlia = musicInfo[2] + answerString += "\n其他信息:\n" + strings.ReplaceAll(musicAlia, "&", "\n") + } + // 切割音频,生成3个10秒的音频 + outputPath := cachePath + strconv.FormatInt(gid, 10) + "/" + err = cutMusic(musicName, pathOfMusic, outputPath) + if err != nil { + ctx.SendChain(message.Text(err)) + return + } + // 进行猜歌环节 + ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + "0.wav")) + 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()) + } else { + next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), zero.CheckGroup(ctx.Event.GroupID)) + } + var musicCount = 0 // 音频数量 + var answerCount = 0 // 问答次数 + recv, cancel := next.Repeat() + defer cancel() + wait := time.NewTimer(40 * time.Second) + tick := time.NewTimer(105 * time.Second) + after := time.NewTimer(120 * time.Second) + for { + select { + case <-tick.C: + ctx.SendChain(message.Text("猜歌游戏,你还有15s作答时间")) + case <-after.C: + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, + message.Text("时间超时,猜歌结束,公布答案:\n", answerString))) + return + case <-wait.C: + wait.Reset(40 * time.Second) + musicCount++ + if musicCount > 2 { + wait.Stop() + continue + } + ctx.SendChain( + message.Text("好像有些难度呢,再听这段音频,要仔细听哦"), + ) + ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) + case c := <-recv: + wait.Reset(40 * time.Second) + tick.Reset(105 * time.Second) + after.Reset(120 * time.Second) + answer := strings.Replace(c.Event.Message.String(), "-", "", 1) + switch { + case answer == "取消": + if c.Event.UserID == ctx.Event.UserID { + wait.Stop() + tick.Stop() + after.Stop() + ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, + message.Text("游戏已取消,猜歌答案是\n", answerString, "\n\n\n下面欣赏猜歌的歌曲"))) + ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) + return + } + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("你无权限取消"), + ), + ) + case answer == "提示": + musicCount++ + if musicCount > 2 { + wait.Stop() + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("已经没有提示了哦"), + ), + ) + continue + } + wait.Reset(40 * time.Second) + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("再听这段音频,要仔细听哦"), + ), + ) + ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) + case strings.Contains(musicInfo[0], answer) || strings.EqualFold(musicInfo[0], answer): + wait.Stop() + tick.Stop() + after.Stop() + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("太棒了,你猜对歌曲名了!答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) + ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) + return + case strings.Contains(musicInfo[1], answer) || strings.EqualFold(musicInfo[1], answer): + wait.Stop() + tick.Stop() + after.Stop() + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("太棒了,你猜对歌手名了!答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) + ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) + 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, "\n\n下面欣赏猜歌的歌曲"))) + ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) + return + default: + musicCount++ + switch { + case musicCount > 2 && answerCount < 6: + wait.Stop() + answerCount++ + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("答案不对哦,加油啊~"), + ), + ) + case musicCount > 2: + wait.Stop() + tick.Stop() + after.Stop() + ctx.Send(message.ReplyWithMessage(c.Event.MessageID, + message.Text("次数到了,没能猜出来。答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) + ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) + return + default: + wait.Reset(40 * time.Second) + answerCount++ + ctx.Send( + message.ReplyWithMessage(c.Event.MessageID, + message.Text("答案不对,再听这段音频,要仔细听哦"), + ), + ) + ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) + } + } + } + } + }) +} + +// 随机抽取音乐 +func musicLottery(musicPath, listName string) (pathOfMusic, musicName string, err error) { + // 读取歌单文件 + pathOfMusic = musicPath + listName + "/" + if file.IsNotExist(pathOfMusic) { + err = errors.New("指定的歌单不存在") + return + } + files, err := os.ReadDir(pathOfMusic) + if err != nil { + return + } + // 获取绑定的网易云 + var playlistID int64 + for _, listinfo := range cfg.Playlist { + if listinfo.Name == listName { + playlistID = listinfo.ID + } + } + // 如果本地列表为空 + if len(files) == 0 { + if playlistID == 0 || !cfg.API { + err = errors.New("本地歌单数据为0") + return + } + // 如果绑定了歌单ID + if cfg.APIURL == "" { + // 如果没有配置过API地址,尝试连接独角兽 + musicName, err = downloadByOvooa(playlistID, pathOfMusic) + if err != nil { + err = errors.Errorf("本地歌单数据为0,API下载歌曲失败\n%s", err) + } + } else { + // 从API中抽取歌曲 + musicName, err = drawByAPI(playlistID, pathOfMusic) + if err != nil { + err = errors.Errorf("本地歌单数据为0,API下载歌曲失败\n%s", err) + } + } + return + } + // 进行随机抽取 + if playlistID == 0 || !cfg.API { + musicName = getLocalMusic(files) + } else { + switch rand.Intn(3) { // 三分二概率抽取API的 + case 1: + musicName = getLocalMusic(files) + default: + if cfg.APIURL == "" { + // 如果没有配置过API地址,尝试连接独角兽 + musicName, err = downloadByOvooa(playlistID, pathOfMusic) + } else { + // 从API中抽取歌曲 + musicName, err = drawByAPI(playlistID, pathOfMusic) + } + if err != nil { + musicName = getLocalMusic(files) + err = nil + return + } + } + } + return +} + +// 从本地列表中随机抽取一首 +func getLocalMusic(files []fs.DirEntry) (musicName string) { + if len(files) > 1 { + music := files[rand.Intn(len(files))] + // 如果是文件夹就递归 + if music.IsDir() { + musicName = getLocalMusic(files) + } else { + musicName = music.Name() + } + } else { + music := files[0] + if !music.IsDir() { + musicName = files[0].Name() + } + } + return +} + +// 切割音乐成三个10s音频 +func cutMusic(musicName, pathOfMusic, outputPath string) (err error) { + err = os.MkdirAll(outputPath, 0755) + if err != nil { + err = errors.Errorf("[生成歌曲目录错误]ERROR: %s", err) + return + } + var stderr bytes.Buffer + cmdArguments := []string{"-y", "-i", pathOfMusic + musicName, + "-ss", cuttime[0], "-t", "10", file.BOTPATH + "/" + outputPath + "0.wav", + "-ss", cuttime[1], "-t", "10", file.BOTPATH + "/" + outputPath + "1.wav", + "-ss", cuttime[2], "-t", "10", file.BOTPATH + "/" + outputPath + "2.wav", "-hide_banner"} + cmd := exec.Command("ffmpeg", cmdArguments...) + cmd.Stderr = &stderr + err = cmd.Run() + if err != nil { + err = errors.Errorf("[生成歌曲错误]ERROR: %s", stderr.String()) + return + } + return +} diff --git a/plugin/guessmusic/main.go b/plugin/guessmusic/main.go index d9249c98..5a6a0680 100644 --- a/plugin/guessmusic/main.go +++ b/plugin/guessmusic/main.go @@ -2,29 +2,21 @@ package guessmusic import ( - "bytes" "encoding/json" - "io/fs" - "math/rand" "os" - "os/exec" "strconv" "strings" "time" - "github.com/FloatTech/floatbox/web" + wyy "github.com/FloatTech/AnimeAPI/neteasemusic" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" "github.com/pkg/errors" - "github.com/sirupsen/logrus" zero "github.com/wdvxdr1123/ZeroBot" "github.com/wdvxdr1123/ZeroBot/extension/single" "github.com/wdvxdr1123/ZeroBot/message" - // 网易云插件 - wyy "github.com/FloatTech/AnimeAPI/neteasemusic" - // 图片输出 "github.com/Coloured-glaze/gg" "github.com/FloatTech/floatbox/file" @@ -32,39 +24,50 @@ import ( "github.com/FloatTech/zbputils/img/text" ) -var ( - filelist []listinfo - musictypelist = "mp3;MP3;wav;WAV;amr;AMR;3gp;3GP;3gpp;3GPP;acc;ACC" - cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒) - cfg config -) +const serviceErr = "[guessmusic]error:" -func init() { // 插件主体 - engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{ +var ( + // 用户数据 + cfg config + // 插件主体 + engine = control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Brief: "猜歌插件", - Help: "由于不可抗因素无法获取网易云歌单内容, 插件改为本地猜歌了, 但保留了下歌功能\n" + - "------bot主人指令------\n" + + Help: "------bot主人指令------\n" + "- 设置猜歌歌库路径 [绝对路径]\n" + - "- (指令仅歌词有效) 猜歌[开启/关闭][歌单/歌词]自动下载\n" + - "- (指令已失效) 添加歌单 [网易云歌单链接/ID] [歌单名称]\n" + - "- 下载歌曲 [歌曲名称/网易云歌曲ID] [歌单名称]\n" + - "- 删除歌单 [网易云歌单ID/歌单名称]\n" + - "注: 删除网易云歌单ID仅只是解除绑定\n删除歌单名称是将本地数据全部删除, 慎用\n" + + "- [创建/删除]歌单 [歌单名称]\n" + + "- 下载歌曲[歌曲名称/网易云歌曲ID]到[歌单名称]\n" + "------管 理 员 指 令------\n" + "- 设置猜歌默认歌单 [歌单名称]\n" + + "- 上传歌曲[群文件的音乐名]到[歌单名称]\n" + "------公 用 指 令------\n" + "- 歌单列表\n" + "- [个人/团队]猜歌\n" + - "注: 默认歌库为歌单列表第一个, 如果设置了默认歌单变为指定的歌单\n" + - "可在\"[个人/团队]猜歌指令\"后面添加[-歌单名称]进行指定歌单猜歌\n" + - "猜歌内容必须以[-]开头才会识别\n" + - "本地歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)\n" + - "重要事项: 本插件依赖ffmpeg", + "\n------重 要 事 项------\n" + + "1.本插件依赖ffmpeg\n" + + "2.\"删除[歌单名称]\"是将本地歌单数据全部删除, 慎用\n" + + "3.不支持下载VIP歌曲,如有需求请用群文件上传\n" + + "4.未设置默认歌单的场合,猜歌歌单为歌单列表第一个。\n" + + "此外可在\"[个人/团队]猜歌\"指令后面添加[-歌单名称]进行指定歌单猜歌\n" + + "5.猜歌内容必须以[-]开头才会识别\n" + + "6.歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)" + + "\n------插 件 扩 展------\n" + + "内置了独角兽API,但API不保证可靠性。\n" + + "可以自行搭建或寻找NeteaseCloudMusicApi框架的API,本插件支持该API以下指令\n" + + "NeteaseCloudMusicApi项目地址:\nhttps://binaryify.github.io/NeteaseCloudMusicApi/#/\n" + + "- 设置猜歌API帮助\n" + + "- 设置猜歌API [API首页网址]\n" + + "- 猜歌[开启/关闭][歌单/歌词]自动下载\n" + + "- 登录网易云(这个指令目前不知道能干嘛,总之先保留了)\n" + + "- 歌单信息 [网易云歌单链接/ID]\n" + + "- [歌单名称]绑定网易云[网易云歌单链接/ID]\n" + + "- 下载歌单[网易云歌单链接/ID]到[歌单名称]\n" + + "- 解除绑定 [歌单名称]", PrivateDataFolder: "guessmusic", }).ApplySingle(single.New( single.WithKeyFn(func(ctx *zero.Ctx) int64 { return ctx.Event.GroupID }), single.WithPostFn[int64](func(ctx *zero.Ctx) { + ctx.Break() ctx.Send( message.ReplyWithMessage(ctx.Event.MessageID, message.Text("已经有正在进行的游戏..."), @@ -72,226 +75,130 @@ func init() { // 插件主体 ) }), )) - serviceErr := "[guessmusic]" - // 用于存放歌曲三个片段的文件夹 - cachePath := engine.DataFolder() + "cache/" - err := os.MkdirAll(cachePath, 0777) + // 用于存放歌曲三个片段的缓存文件夹 + cachePath = engine.DataFolder() + "cache/" + // 用于存放用户的配置 + cfgFile = engine.DataFolder() + "config.json" + // ffmpeg支持的格式 + musictypelist = "mp3;MP3;wav;WAV;amr;AMR;3gp;3GP;3gpp;3GPP;acc;ACC" +) + +func init() { + // 新建缓存文件夹 + err := os.MkdirAll(cachePath, 0755) if err != nil { - panic(serviceErr + "ERROR:" + err.Error()) + panic(serviceErr + err.Error()) } - // 获取用户的配置 - cfgFile := engine.DataFolder() + "config.json" + // 载入用户配置 if file.IsExist(cfgFile) { reader, err := os.Open(cfgFile) if err == nil { err = json.NewDecoder(reader).Decode(&cfg) } if err != nil { - panic(serviceErr + "ERROR:" + err.Error()) + panic(serviceErr + err.Error()) } err = reader.Close() if err != nil { - panic(serviceErr + "ERROR:" + err.Error()) + panic(serviceErr + err.Error()) } } else { - cfg = config{ // 配置默认 config + // 配置默认 config + cfg = config{ MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改 - API: true, - Local: true, Playlist: []listRaw{ { - Name: "FM", - ID: 3136952023, - }}, + Name: "这里是歌单名称,id为网易云歌单ID", + ID: 123456, + }, + }, + Defaultlist: []dlist{ + { + GroupID: 123456, + Name: "这里是歌单名称,gid是群号", + }, + }, + API: true, + Local: true, } err = saveConfig(cfgFile) if err != nil { - panic(serviceErr + "ERROR:" + err.Error()) + panic(serviceErr + err.Error()) } } - filelist, err = getlist(cfg.MusicPath) - if err != nil { - logrus.Errorln(serviceErr + "ERROR:" + err.Error()) - } // 用户配置 - engine.OnRegex(`^设置猜歌(歌库路径|默认歌单)\s*(.*)$`).SetBlock(true). + engine.OnPrefix("设置猜歌歌库路径", zero.SuperUserPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - option := ctx.State["regex_matched"].([]string)[1] - value := ctx.State["regex_matched"].([]string)[2] - var err error - switch option { - case "歌库路径": - if !zero.SuperUserPermission(ctx) { - ctx.SendChain(message.Text("只有bot主人可以设置!")) - return - } - musicPath := strings.ReplaceAll(value, "\\", "/") - if !strings.HasSuffix(musicPath, "/") { - musicPath += "/" - } - err = os.MkdirAll(musicPath, 0777) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "生成文件夹ERROR:\n", err)) - return - } - cfg.MusicPath = musicPath - case "默认歌单": - gid := ctx.Event.GroupID - if gid == 0 || !zero.AdminPermission(ctx) { - ctx.SendChain(message.Text("无权设置!")) - return - } - index := "" - for _, listinfo := range filelist { - if listinfo.Name == value { - index = value - break - } - } - if index == "" { - ctx.SendChain(message.Text("歌单名称错误,可以发送“歌单列表”获取歌单名称")) - return - } - cfg.Defaultlist = append(cfg.Defaultlist, dlist{ - GroupID: gid, - Name: value, - }) + option := ctx.State["args"].(string) + musicPath := strings.ReplaceAll(option, "\\", "/") + if !strings.HasSuffix(musicPath, "/") { + musicPath += "/" } + err := os.MkdirAll(musicPath, 0755) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + cfg.MusicPath = musicPath err = saveConfig(cfgFile) if err == nil { ctx.SendChain(message.Text("成功!")) } else { - ctx.SendChain(message.Text(serviceErr, "ERROR:\n", err)) + ctx.SendChain(message.Text(serviceErr, err)) } }) - engine.OnRegex(`^猜歌(开启|关闭)(歌单|歌词)自动下载`, zero.SuperUserPermission).SetBlock(true). + engine.OnPrefix("创建歌单", zero.SuperUserPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - swtich := ctx.State["regex_matched"].([]string)[1] - option := ctx.State["regex_matched"].([]string)[1] - chose := true - if swtich == "关闭" { - chose = false - } - if option == "歌单" { - cfg.API = chose + newList := cfg.MusicPath + ctx.State["args"].(string) + if file.IsNotExist(newList) { + err := os.MkdirAll(newList, 0755) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } } else { - cfg.Local = chose - } - err = saveConfig(cfgFile) - if err == nil { - ctx.SendChain(message.Text("成功!")) - } else { - ctx.SendChain(message.Text(serviceErr, "ERROR:\n", err)) + ctx.SendChain(message.Text("歌单已存在!")) } }) - // 本地绑定网易云歌单ID - engine.OnRegex(`^添加歌单\s?(https:.*id=)?(\d+)\s?(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + engine.OnPrefix("删除歌单", zero.SuperUserPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - listID := ctx.State["regex_matched"].([]string)[2] - listName := ctx.State["regex_matched"].([]string)[3] - ctx.SendChain(message.Text("正在校验歌单信息,请稍等")) - // 是否存在该歌单 - apiURL := "https://ovooa.com/API/163_Music_Rand/api.php?id=" + listID - data, err := web.GetData(apiURL) + delList := ctx.State["args"].(string) + err = os.RemoveAll(cfg.MusicPath + delList) if err != nil { - ctx.SendChain(message.Text(serviceErr, "error:", err)) + ctx.SendChain(message.Text("删除失败,可能是歌单名称错误。\n可以发送“歌单列表”获取歌单名称")) return } - var parsed ovooaData - err = json.Unmarshal(data, &parsed) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "无法解析歌单ID内容:", err)) - return - } - if parsed.Code != 1 { - ctx.SendChain(message.Text(serviceErr, "error:", parsed.Text)) - return - } - pathOfMusic := cfg.MusicPath + listName + "/" - err = os.MkdirAll(pathOfMusic, 0777) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "歌单不存在于本地,尝试创建该歌单失败:\n", err)) - return - } - mid, _ := strconv.ParseInt(listID, 10, 64) - cfg.Playlist = append(cfg.Playlist, listRaw{ - Name: listName, - ID: mid, - }) - err = saveConfig(cfgFile) - if err == nil { - ctx.SendChain(message.Text("成功!")) - } else { - ctx.SendChain(message.Text(serviceErr, "error:", err)) - } - }) - engine.OnRegex(`^删除歌单\s?(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). - Handle(func(ctx *zero.Ctx) { - delList := ctx.State["regex_matched"].([]string)[1] - filelist, err = getlist(cfg.MusicPath) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "歌单列表获取error:", err)) - return - } - index := 1024 - for i, listinfo := range filelist { - if delList == listinfo.Name || delList == strconv.FormatInt(listinfo.ID, 10) { - if delList == listinfo.Name { - err = os.RemoveAll(cfg.MusicPath + delList) - if err != nil { - ctx.SendChain(message.Text("歌单文件删除失败:\n", err)) - return - } - } + // 删除绑定的网易云ID + index := -1 + for i, list := range cfg.Playlist { + if delList == list.Name { index = i break } } - if index == 1024 { - ctx.SendChain(message.Text("歌单名称错误,可以发送“歌单列表”获取歌单名称")) + if index == -1 { + ctx.SendChain(message.Text("成功!")) return } - var newCatList []listRaw - for _, list := range cfg.Playlist { - if list.Name == filelist[index].Name { - continue - } - newCatList = append(newCatList, list) - } - cfg.Playlist = newCatList + cfg.Playlist = append(cfg.Playlist[:index], cfg.Playlist[index+1:]...) err = saveConfig(cfgFile) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) - } - filelist, err = getlist(cfg.MusicPath) if err == nil { ctx.SendChain(message.Text("成功!")) } else { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) } }) // 下载歌曲到对应的歌单里面 - engine.OnRegex(`^下载歌曲\s?(\d+|.*[^\s$])\s(.*[^\s$])$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + engine.OnRegex(`^下载歌曲\s*(.*)\s*到\s*(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { keyword := ctx.State["regex_matched"].([]string)[1] listName := ctx.State["regex_matched"].([]string)[2] ctx.SendChain(message.Text("正在校验歌单信息,请稍等")) // 是否存在该歌单 - filelist, err := getlist(cfg.MusicPath) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "获取歌单列表ERROR:", err)) - return - } - ok := true - for _, listinfo := range filelist { - if listName == listinfo.Name { - ok = false - break - } - } - if ok { + if file.IsNotExist(cfg.MusicPath + listName) { ctx.SendChain(message.Text("歌单不存在,是否创建?(是/否)")) - next := zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`(是|否)`), ctx.CheckSession()) + next := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`(是|否)`), ctx.CheckSession()) recv, cancel := next.Repeat() defer cancel() wait := time.NewTimer(120 * time.Second) @@ -316,7 +223,7 @@ func init() { // 插件主体 } err = os.MkdirAll(cfg.MusicPath+listName, 0777) if err != nil { - ctx.SendChain(message.Text(serviceErr, "生成文件夹ERROR:\n", err)) + ctx.SendChain(message.Text(serviceErr, err)) return } } @@ -325,79 +232,166 @@ func init() { // 插件主体 ctx.SendChain(message.Text("查询歌曲失败!\nerr:", err)) return } - listmun := len(searchlist) - if listmun == 0 { + if len(searchlist) == 0 { ctx.SendChain(message.Text("歌曲没有查询到,请确认信息正确")) return } - musicList := make([]string, listmun) - i := 0 + var musicchoose []string for musicName := range searchlist { - musicList[i] = musicName - i++ + musicchoose = append(musicchoose, musicName) } savePath := cfg.MusicPath + listName + "/" - if listmun == 1 { - musicName := musicList[0] - musicID := searchlist[musicName] - // 下载歌曲 - err = wyy.DownloadMusic(musicID, musicName, savePath) - if err == nil { - if cfg.Local { - // 下载歌词 - _ = wyy.DownloadLrc(musicID, musicName, savePath+"歌词/") - } - ctx.SendChain(message.Text("成功!")) - } else { - ctx.SendChain(message.Text(serviceErr, "error:", err)) + index := 0 + if len(musicchoose) > 1 { + var msg []string + msg = append(msg, "搜索到相近的歌曲,请回复对应序号进行下载或回复取消") + for i, musicName := range musicchoose { + msg = append(msg, strconv.Itoa(i)+"."+musicName) } + ctx.SendChain(message.Text(strings.Join(msg, "\n"))) + next := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`[0-4]|取消`), ctx.CheckSession()) + recv, cancel := next.Repeat() + defer cancel() + wait := time.NewTimer(120 * time.Second) + answer := "" + for { + select { + case <-wait.C: + wait.Stop() + ctx.SendChain(message.Text("等待超时,取消下载")) + return + case c := <-recv: + wait.Stop() + answer = c.Event.Message.String() + if answer == "取消" { + ctx.SendChain(message.Text("已取消下载")) + return + } + index, _ = strconv.Atoi(answer) + } + if answer != "" { + break + } + } + } + musicName := musicchoose[index] + // 下载歌曲 + err = wyy.DownloadMusic(searchlist[musicName], musicName, savePath) + if err == nil { + if cfg.Local { + // 下载歌词 + _ = wyy.DownloadLrc(searchlist[musicName], musicName, savePath+"歌词/") + } + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) + } + }) + // 从群文件下载歌曲 + engine.OnRegex(`^上传歌曲\s*(.*)\s*到\s*(.*)$`, zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(ctxext.LimitByUser). + Handle(func(ctx *zero.Ctx) { + fileName := ctx.State["regex_matched"].([]string)[1] + listName := ctx.State["regex_matched"].([]string)[2] + // 判断群文件是否存在 + fileSearchName, fileURL := getFileURLbyFileName(ctx, fileName) + if fileSearchName == "" { + ctx.SendChain(message.Text(serviceErr, "请确认群文件文件名称是否正确或存在")) return } - var msg []string - msg = append(msg, "搜索到相近的歌曲,请回复对应序号进行下载或回复取消") - for j, musicName := range musicList { - msg = append(msg, strconv.Itoa(j)+"."+musicName) + // 解析歌曲信息 + music := strings.Split(fileSearchName, ".") + // 获取音乐后缀 + musictype := music[len(music)-1] + if !strings.Contains(musictypelist, musictype) { + ctx.SendChain(message.Text(fileSearchName, "不是插件支持的后缀,请更改后缀")) + return } - ctx.SendChain(message.Text(strings.Join(msg, "\n"))) - next := zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`[0-4]|取消`), ctx.CheckSession()) + // 获取音乐信息 + musicInfo := strings.Split(strings.ReplaceAll(fileSearchName, "."+musictype, ""), " - ") + infoNum := len(musicInfo) + if infoNum == 1 { + ctx.SendChain(message.Text(fileSearchName, "不符合命名规则,请更改名称")) + return + } + fileName = "歌名:" + musicInfo[0] + "\n歌手:" + musicInfo[1] + musicAlia := "" + if infoNum > 2 { + musicAlia = musicInfo[2] + fileName += "\n其他信息:\n" + strings.ReplaceAll(musicAlia, "&", "\n") + } + // 是否存在该歌单 + if file.IsNotExist(cfg.MusicPath + listName) { + if !zero.SuperUserPermission(ctx) { + ctx.SendChain(message.Text("歌单名称错误。\n可以发送“歌单列表”获取歌单名称")) + return + } + ctx.SendChain(message.Text("歌单不存在,是否创建?(是/否)")) + next := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`(是|否)`), ctx.CheckSession()) + recv, cancel := next.Repeat() + defer cancel() + wait := time.NewTimer(120 * time.Second) + answer := "" + for { + select { + case <-wait.C: + wait.Stop() + ctx.SendChain(message.Text("等待超时,取消下载")) + return + case c := <-recv: + wait.Stop() + answer = c.Event.Message.String() + } + if answer == "否" { + ctx.SendChain(message.Text("下载已经取消")) + return + } + if answer != "" { + break + } + } + err = os.MkdirAll(cfg.MusicPath+listName, 0755) + if err != nil { + ctx.SendChain(message.Text(serviceErr, err)) + return + } + } + // 下载歌曲 + ctx.SendChain(message.Text("在群文件中找到了歌曲,信息如下:\n", fileName, "\n确认正确后回复“是/否”进行上传")) + next := zero.NewFutureEvent("message", 999, false, zero.RegexRule(`(是|否)`), ctx.CheckSession()) recv, cancel := next.Repeat() defer cancel() wait := time.NewTimer(120 * time.Second) + answer := "" for { select { case <-wait.C: wait.Stop() - ctx.SendChain(message.Text("等待超时,取消下载")) + ctx.SendChain(message.Text("等待超时,取消上传")) return case c := <-recv: wait.Stop() - answer := c.Event.Message.String() - if answer == "取消" { - ctx.SendChain(message.Text("已取消下载")) - return - } - index, _ := strconv.Atoi(answer) - // 下载歌曲 - musicName := musicList[index] - err = wyy.DownloadMusic(searchlist[musicName], musicName, savePath) - if err == nil { - if cfg.Local { - // 下载歌词 - _ = wyy.DownloadLrc(searchlist[musicName], musicName, savePath+"歌词/") - } - ctx.SendChain(message.Text("成功!")) - } else { - ctx.SendChain(message.Text(serviceErr, "error:", err)) - } + answer = c.Event.Message.String() + } + if answer == "否" { + ctx.SendChain(message.Text("上传已经取消")) return } + if answer != "" { + break + } + } + err = file.DownloadTo(fileURL, cfg.MusicPath+listName+"/"+fileSearchName, true) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, err)) } }) engine.OnFullMatch("歌单列表").SetBlock(true).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { filelist, err := getlist(cfg.MusicPath) if err != nil { - ctx.SendChain(message.Text(serviceErr, "获取歌单列表ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) return } /***********设置图片的大小和底色***********/ @@ -412,17 +406,17 @@ func init() { // 插件主体 /***********下载字体,可以注销掉***********/ _, err = file.GetLazyData(text.BoldFontFile, true) if err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) } _, err = file.GetLazyData(text.FontFile, true) if err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) } /***********设置字体颜色为黑色***********/ canvas.SetRGB(0, 0, 0) /***********设置字体大小,并获取字体高度用来定位***********/ if err = canvas.LoadFontFace(text.BoldFontFile, fontSize); err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) return } _, h := canvas.MeasureString("序号\t\t歌单名\t\t\t歌曲数量\t\t网易云歌单ID") @@ -431,7 +425,7 @@ func init() { // 插件主体 canvas.DrawString("——————————————————————", 20, 70-h) /***********设置字体大小,并获取字体高度用来定位***********/ if err = canvas.LoadFontFace(text.FontFile, fontSize); err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + ctx.SendChain(message.Text(serviceErr, err)) return } _, h = canvas.MeasureString("焯") @@ -456,203 +450,23 @@ func init() { // 插件主体 } cl() }) - engine.OnRegex(`^(个人|团队)猜歌(-(.*))?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup). + engine.OnPrefix("设置猜歌默认歌单", zero.AdminPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - mode := ctx.State["regex_matched"].([]string)[3] + option := ctx.State["args"].(string) gid := ctx.Event.GroupID - filelist, err := getlist(cfg.MusicPath) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "获取歌单列表ERROR:", err)) + if file.IsNotExist(cfg.MusicPath + option) { + ctx.SendChain(message.Text("歌单名称错误,可以发送“歌单列表”获取歌单名称")) return } - if mode == "" { - for _, dlist := range cfg.Defaultlist { - if dlist.GroupID == gid { - mode = dlist.Name - break - } - } - } - if mode == "" { - mode = filelist[0].Name + cfg.Defaultlist = append(cfg.Defaultlist, dlist{ + GroupID: gid, + Name: option, + }) + err = saveConfig(cfgFile) + if err == nil { + ctx.SendChain(message.Text("成功!")) } else { - ok := true - for _, listinfo := range filelist { - if mode == listinfo.Name { - ok = false - break - } - } - if ok { - ctx.SendChain(message.Text("歌单名称错误,可以发送“歌单列表”获取歌单名称")) - return - } - } - ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲信息(歌名歌手等)|提示|取消]”\n一共3段语音,6次机会")) - // 随机抽歌 - pathOfMusic, musicName, err := musicLottery(cfg.MusicPath, mode) - if err != nil { - ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) - return - } - // 解析歌曲信息 - music := strings.Split(musicName, ".") - // 获取音乐后缀 - musictype := music[len(music)-1] - if !strings.Contains(musictypelist, musictype) { - ctx.SendChain(message.Text("抽取到了歌曲:\n", - musicName, "\n该歌曲不是音乐后缀,请联系bot主人修改")) - return - } - // 获取音乐信息 - musicInfo := strings.Split(strings.ReplaceAll(musicName, "."+musictype, ""), " - ") - infoNum := len(musicInfo) - if infoNum == 1 { - ctx.SendChain(message.Text("抽取到了歌曲:\n", - musicName, "\n该歌曲命名不符合命名规则,请联系bot主人修改")) - return - } - answerString := "歌名:" + musicInfo[0] + "\n歌手:" + musicInfo[1] - musicAlia := "" - if infoNum > 2 { - musicAlia = musicInfo[2] - answerString += "\n其他信息:\n" + strings.ReplaceAll(musicAlia, "&", "\n") - } - // 切割音频,生成3个10秒的音频 - outputPath := cachePath + strconv.FormatInt(gid, 10) + "/" - err = cutMusic(musicName, pathOfMusic, outputPath) - if err != nil { - ctx.SendChain(message.Text(err)) - return - } - // 进行猜歌环节 - ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + "0.wav")) - 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()) - } else { - next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), zero.CheckGroup(ctx.Event.GroupID)) - } - var musicCount = 0 // 音频数量 - var answerCount = 0 // 问答次数 - recv, cancel := next.Repeat() - defer cancel() - wait := time.NewTimer(40 * time.Second) - tick := time.NewTimer(105 * time.Second) - after := time.NewTimer(120 * time.Second) - for { - select { - case <-tick.C: - ctx.SendChain(message.Text("猜歌游戏,你还有15s作答时间")) - case <-after.C: - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, - message.Text("时间超时,猜歌结束,公布答案:\n", answerString))) - return - case <-wait.C: - wait.Reset(40 * time.Second) - musicCount++ - if musicCount > 2 { - wait.Stop() - continue - } - ctx.SendChain( - message.Text("好像有些难度呢,再听这段音频,要仔细听哦"), - ) - ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) - case c := <-recv: - wait.Reset(40 * time.Second) - tick.Reset(105 * time.Second) - after.Reset(120 * time.Second) - answer := strings.Replace(c.Event.Message.String(), "-", "", 1) - switch { - case answer == "取消": - if c.Event.UserID == ctx.Event.UserID { - wait.Stop() - tick.Stop() - after.Stop() - ctx.Send(message.ReplyWithMessage(ctx.Event.MessageID, - message.Text("游戏已取消,猜歌答案是\n", answerString, "\n\n\n下面欣赏猜歌的歌曲"))) - ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) - return - } - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("你无权限取消"), - ), - ) - case answer == "提示": - musicCount++ - if musicCount > 2 { - wait.Stop() - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("已经没有提示了哦"), - ), - ) - continue - } - wait.Reset(40 * time.Second) - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("再听这段音频,要仔细听哦"), - ), - ) - ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) - case strings.Contains(musicInfo[0], answer) || strings.EqualFold(musicInfo[0], answer): - wait.Stop() - tick.Stop() - after.Stop() - ctx.Send(message.ReplyWithMessage(c.Event.MessageID, - message.Text("太棒了,你猜对歌曲名了!答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) - ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) - return - case strings.Contains(musicInfo[1], answer) || strings.EqualFold(musicInfo[1], answer): - wait.Stop() - tick.Stop() - after.Stop() - ctx.Send(message.ReplyWithMessage(c.Event.MessageID, - message.Text("太棒了,你猜对歌手名了!答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) - ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) - 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, "\n\n下面欣赏猜歌的歌曲"))) - ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) - return - default: - musicCount++ - switch { - case musicCount > 2 && answerCount < 6: - wait.Stop() - answerCount++ - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("答案不对哦,加油啊~"), - ), - ) - case musicCount > 2: - wait.Stop() - tick.Stop() - after.Stop() - ctx.Send(message.ReplyWithMessage(c.Event.MessageID, - message.Text("次数到了,没能猜出来。答案是\n", answerString, "\n\n下面欣赏猜歌的歌曲"))) - ctx.SendChain(message.Record("file:///" + pathOfMusic + musicName)) - return - default: - wait.Reset(40 * time.Second) - answerCount++ - ctx.Send( - message.ReplyWithMessage(c.Event.MessageID, - message.Text("答案不对,再听这段音频,要仔细听哦"), - ), - ) - ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + strconv.Itoa(musicCount) + ".wav")) - } - } - } + ctx.SendChain(message.Text(serviceErr, err)) } }) } @@ -674,11 +488,9 @@ func saveConfig(cfgFile string) error { func getlist(pathOfMusic string) (list []listinfo, err error) { wyyID := make(map[string]int64, 100) for _, wyyinfo := range cfg.Playlist { - if wyyinfo.ID != 0 { - wyyID[wyyinfo.Name] = wyyinfo.ID - } + wyyID[wyyinfo.Name] = wyyinfo.ID } - err = os.MkdirAll(pathOfMusic, 0777) + err = os.MkdirAll(pathOfMusic, 0755) if err != nil { return } @@ -687,7 +499,7 @@ func getlist(pathOfMusic string) (list []listinfo, err error) { return } if len(files) == 0 { - err = errors.Errorf("所设置的歌库不存在任何歌单!") + err = errors.New("所设置的歌库不存在任何歌单!") return } for _, name := range files { @@ -708,155 +520,56 @@ func getlist(pathOfMusic string) (list []listinfo, err error) { return } -// 随机抽取音乐 -func musicLottery(musicPath, listName string) (pathOfMusic, musicName string, err error) { - filelist, err := getlist(musicPath) - if err != nil { - err = errors.Errorf("获取列表错误,%s", err) - return - } - var fileList = make(map[string]int64, 100) - for _, listinfo := range filelist { - fileList[listinfo.Name] = listinfo.ID - } - playlistID, ok := fileList[listName] - if !ok { - err = errors.Errorf("指定的歌单不存在与列表当中") - return - } - pathOfMusic = musicPath + listName + "/" - err = os.MkdirAll(pathOfMusic, 0777) - if err != nil { - return - } - files, err := os.ReadDir(pathOfMusic) - if err != nil { - return - } - //如果本地列表为空 - if len(files) == 0 { - if playlistID == 0 || !cfg.API { - err = errors.New("本地歌单数据为0") - return +// 遍历群文件 +func getFileURLbyFileName(ctx *zero.Ctx, fileName string) (fileSearchName, fileURL string) { + filesOfGroup := ctx.GetThisGroupRootFiles(ctx.Event.GroupID) + files := filesOfGroup.Get("files").Array() + folders := filesOfGroup.Get("folders").Array() + // 遍历当前目录的文件名 + if len(files) != 0 { + for _, fileNameOflist := range files { + if strings.Contains(fileNameOflist.Get("file_name").String(), fileName) { + fileSearchName = fileNameOflist.Get("file_name").String() + fileURL = ctx.GetThisGroupFileUrl(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) + return + } } - // 如果绑定了歌单ID - musicName, err = downloadByOvooa(playlistID, pathOfMusic) - err = errors.Errorf("本地歌单数据为0,API下载歌曲失败\n%s", err) - return } - // 进行随机抽取 - if playlistID == 0 || !cfg.API { - musicName = getLocalMusic(files) - } else { - switch rand.Intn(3) { //三分二概率抽取API的 - case 1: - musicName = getLocalMusic(files) - default: - musicName, err = downloadByOvooa(playlistID, pathOfMusic) - if err != nil { - musicName = getLocalMusic(files) - err = nil + // 遍历子文件夹 + if len(folders) != 0 { + for _, folderNameOflist := range folders { + folderID := folderNameOflist.Get("folder_id").String() + fileSearchName, fileURL = getFileURLbyfolderID(ctx, fileName, folderID) + if fileSearchName != "" { return } } } return } - -// 从本地列表中随机抽取一首 -func getLocalMusic(files []fs.DirEntry) (musicName string) { - if len(files) > 1 { - music := files[rand.Intn(len(files))] - // 如果是文件夹就递归 - if music.IsDir() { - musicName = getLocalMusic(files) - } else { - musicName = music.Name() - } - } else { - music := files[0] - if !music.IsDir() { - musicName = files[0].Name() - } - } - return -} - -// 下载从独角兽抽到的歌曲ID(歌单ID, 音乐保存路径, 歌词保存路径) -func downloadByOvooa(playlistID int64, musicPath string) (musicName string, err error) { - // 抽取歌曲 - mid, err := drawByOvooa(playlistID) - if err != nil { - err = errors.Errorf("API ERROR: %s", err) - return - } - // 获取完成的歌名 - musiclist, err := wyy.SearchMusic(strconv.Itoa(mid), 1) - if err != nil { - err = errors.Errorf("API歌曲下载ERROR: %s", err) - return - } - // 歌曲ID理论是唯一的 - mun := len(musiclist) - if mun == 1 { - // 拉取歌名 - musicList := make([]string, mun) - i := 0 - for musicName := range musiclist { - musicList[i] = musicName - } - name := musicList[0] - // 下载歌曲 - err = wyy.DownloadMusic(mid, name, musicPath) - if err == nil { - musicName = name + ".mp3" - if cfg.Local { - // 下载歌词 - _ = wyy.DownloadLrc(mid, name, musicPath+"歌词/") +func getFileURLbyfolderID(ctx *zero.Ctx, fileName, folderid string) (fileSearchName, fileURL string) { + filesOfGroup := ctx.GetThisGroupFilesByFolder(folderid) + files := filesOfGroup.Get("files").Array() + folders := filesOfGroup.Get("folders").Array() + // 遍历当前目录的文件名 + if len(files) != 0 { + for _, fileNameOflist := range files { + if strings.Contains(fileNameOflist.Get("file_name").String(), fileName) { + fileSearchName = fileNameOflist.Get("file_name").String() + fileURL = ctx.GetThisGroupFileUrl(fileNameOflist.Get("busid").Int(), fileNameOflist.Get("file_id").String()) + return + } + } + } + // 遍历子文件夹 + if len(folders) != 0 { + for _, folderNameOflist := range folders { + folderID := folderNameOflist.Get("folder_id").String() + fileSearchName, fileURL = getFileURLbyfolderID(ctx, fileName, folderID) + if fileSearchName != "" { + return } } - } else { - err = errors.Errorf("music ID ERROR: This music ID sreached munber is %d", mun) - } - return -} - -// 通过独角兽API随机抽取歌单歌曲ID(参数:歌单ID) -func drawByOvooa(playlistID int64) (musicID int, err error) { - apiURL := "https://ovooa.com/API/163_Music_Rand/api.php?id=" + strconv.FormatInt(playlistID, 10) - data, err := web.GetData(apiURL) - if err != nil { - return - } - var parsed ovooaData - err = json.Unmarshal(data, &parsed) - if err != nil { - return - } - if parsed.Code != 1 { - return - } - return parsed.Data.ID, nil -} - -// 切割音乐成三个10s音频 -func cutMusic(musicName, pathOfMusic, outputPath string) (err error) { - err = os.MkdirAll(outputPath, 0777) - if err != nil { - err = errors.Errorf("[生成歌曲目录错误]ERROR: %s", err) - return - } - var stderr bytes.Buffer - cmdArguments := []string{"-y", "-i", pathOfMusic + musicName, - "-ss", cuttime[0], "-t", "10", file.BOTPATH + "/" + outputPath + "0.wav", - "-ss", cuttime[1], "-t", "10", file.BOTPATH + "/" + outputPath + "1.wav", - "-ss", cuttime[2], "-t", "10", file.BOTPATH + "/" + outputPath + "2.wav", "-hide_banner"} - cmd := exec.Command("ffmpeg", cmdArguments...) - cmd.Stderr = &stderr - err = cmd.Run() - if err != nil { - err = errors.Errorf("[生成歌曲错误]ERROR: %s", stderr.String()) - return } return } diff --git a/plugin/guessmusic/struct.go b/plugin/guessmusic/struct.go index 052d7395..cd4532ca 100644 --- a/plugin/guessmusic/struct.go +++ b/plugin/guessmusic/struct.go @@ -3,11 +3,12 @@ package guessmusic // config内容 type config struct { MusicPath string `json:"musicPath"` + APIURL string `json:"apiURL"` + Playlist []listRaw `json:"playlist"` + Defaultlist []dlist `json:"defaultlist"` Local bool `json:"local"` API bool `json:"api"` Cookie string `json:"cookie"` - Playlist []listRaw `json:"playlist"` - Defaultlist []dlist `json:"defaultlist"` } // 记录歌单绑定的网易云歌单ID @@ -29,6 +30,408 @@ type listinfo struct { ID int64 // 歌单绑定的歌曲ID } +/*****************************************************************/ +/***************NeteaseCloudMusicApi框架API************************/ +/*****************************************************************/ +// 获取登陆信息 +type keyInfo struct { + Data struct { + Code int `json:"code"` + Unikey string `json:"unikey"` + } `json:"data"` + Code int `json:"code"` +} +type cookyInfo struct { + Code int `json:"code"` + Message string `json:"message"` + Cookie string `json:"cookie"` +} +type qrInfo struct { + Code int `json:"code"` + Data struct { + Qrurl string `json:"qrurl"` + Qrimg string `json:"qrimg"` + } `json:"data"` +} + +// 获取歌单信息 +type listInfoOfAPI 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 float64 `json:"vd"` + Sr int `json:"sr"` + } `json:"h"` + M struct { + Br int `json:"br"` + Fid int `json:"fid"` + Size int `json:"size"` + Vd float64 `json:"vd"` + Sr int `json:"sr"` + } `json:"m"` + L struct { + Br int `json:"br"` + Fid int `json:"fid"` + Size int `json:"size"` + Vd float64 `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 musicListOfApI 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"` +} + +/*****************************************************************/ +/*********************独角兽API随机抽歌信息**************************/ +/*****************************************************************/ // 独角兽API随机抽歌信息 type ovooaData struct { Code int `json:"code"` diff --git a/plugin/qqwife/command.go b/plugin/qqwife/command.go index b0421583..2e24edf8 100644 --- a/plugin/qqwife/command.go +++ b/plugin/qqwife/command.go @@ -470,6 +470,10 @@ func init() { uid := ctx.Event.UserID fiancee := ctx.State["regex_matched"].([]string) gay, _ := strconv.ParseInt(fiancee[2]+fiancee[3], 10, 64) + if gay == uid { + ctx.Send(message.ReplyWithMessage(message.At(uid), message.Text("[qqwife]你想给自己买什么礼物呢?"))) + return + } // 获取CD cdTime, err := 民政局.getCDtime(gid) if err != nil {