diff --git a/README.md b/README.md index 3a80cbdb..10ad0c47 100644 --- a/README.md +++ b/README.md @@ -677,31 +677,33 @@ print("run[CQ:image,file="+j["img"]+"]") - 猜歌插件(该插件依赖ffmpeg) - - [x] 设置猜歌缓存歌库路径 [绝对路径] + - 因为API不可抗因素,更改为了本地猜歌,但仍支持歌曲下载(VIP歌曲无法下载,黑胶可以) - - [x] 设置猜歌[本地/Api] [true/false] + - [x] 设置猜歌歌库路径 [绝对路径] - - [x] 登录网易云 + - [x] 猜歌[开启/关闭][歌单/歌词]自动下载 - - 注:不登陆也能用,API有几率返回400 + - [x] 添加歌单 [网易云歌单链接/ID] [歌单名称] - - [x] 添加歌单 [网易云歌单ID] [歌单名称] + - [x] 下载歌曲 [歌曲名称/网易云歌曲ID] [歌单名称] - - 注:[歌单名称]可为空,默认原标题 + - [x] 删除歌单 [网易云歌单ID/歌单名称] - - [x] 删除歌单 [网易云歌单ID/API歌单名称] + - 注:删除网易云歌单ID仅只是解除绑定,删除歌单名称是将本地数据全部删除! - - [x] 获取歌单列表 + - [x] 设置猜歌默认歌单 [歌单名称] - - [x] [网易云歌单ID/API歌单名称]歌单信息 + - [x] 歌单列表 - [x] [个人/团队]猜歌 - - 注:默认歌库为网易云ACG动画榜 + - 注:默认歌库为歌单列表第一个,如果设置了默认歌单变为指定的歌单 - - 可在后面添加[-歌单名称]进行指定歌单猜歌 + - 可在“[个人/团队]猜歌指令”后面添加[-歌单名称]进行指定歌单猜歌 - - 歌单的歌曲命名规则为:歌名 - 歌手 - 其他(歌曲出处之类) + - 猜歌内容必须以[-]开头才会识别 + + - 本地歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)
diff --git a/plugin/guessmusic/main.go b/plugin/guessmusic/main.go index 11cdcfe1..b9cc0957 100644 --- a/plugin/guessmusic/main.go +++ b/plugin/guessmusic/main.go @@ -6,63 +6,63 @@ import ( "encoding/json" "io/fs" "math/rand" - "net/http" - "net/url" "os" "os/exec" "strconv" "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" - - "github.com/FloatTech/floatbox/file" "github.com/FloatTech/floatbox/web" ctrl "github.com/FloatTech/zbpctrl" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/ctxext" + "github.com/pkg/errors" + 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" + "github.com/FloatTech/floatbox/img/writer" "github.com/FloatTech/zbputils/img/text" ) -const ( - ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 Edg/87.0.664.66" -) +const servicename = "guessmusic" var ( - catlist = make(map[string]int64, 100) - filelist []string + 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 ) func init() { // 插件主体 - engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{ + engine := control.Register(servicename, &ctrl.Options[*zero.Ctx]{ DisableOnDefault: false, Help: "猜歌插件(该插件依赖ffmpeg)\n" + + "由于不可抗因素无法获取网易云歌单内容,\n插件改为本地猜歌了,但保留了下歌功能\n" + + "当前的API是独角兽的网易云API,只能从歌单随机抽取歌曲信息\n(也不知道这个API能活到什么时候x)\n" + + "修改了猜歌逻辑:\n可为本地目录添加网易云歌单,\n猜歌时会优先从API歌单中随机抽取歌曲并下载\n" + "------bot主人指令------\n" + - "- 设置猜歌缓存歌库路径 [绝对路径]\n" + - "- 设置猜歌[本地/Api] [true/false]\n" + - "- 登录网易云\n" + - "- 添加歌单 [网易云歌单ID] [歌单名称]\n" + - "- 删除歌单 [网易云歌单ID/API歌单名称]\n" + - "注:\n1.不登陆也能用,API有几率返回400\n" + - "2.[歌单名称]可为空,默认原标题\n" + + "- 设置猜歌歌库路径 [绝对路径]\n" + + "- 猜歌[开启/关闭][歌单/歌词]自动下载\n" + + "- 添加歌单 [网易云歌单链接/ID] [歌单名称]\n" + + "- 下载歌曲 [歌曲名称/网易云歌曲ID] [歌单名称]\n" + + "- 删除歌单 [网易云歌单ID/歌单名称]\n" + + "注:\n删除网易云歌单ID仅只是解除绑定\n删除歌单名称是将本地数据全部删除,慎用\n" + + "------管 理 员 指 令------\n" + + "- 设置猜歌默认歌单 [歌单名称]\n" + "------公 用 指 令------\n" + - "- 获取歌单列表\n" + - "- [网易云歌单ID/API歌单名称]歌单信息\n" + + "- 歌单列表\n" + "- [个人/团队]猜歌\n" + - "注:默认歌库为网易云ACG动画榜\n" + - "可在后面添加[-歌单名称]进行指定歌单猜歌\n" + - "歌单的歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)", + "注:默认歌库为歌单列表第一个\n如果设置了默认歌单变为指定的歌单\n" + + "可在“[个人/团队]猜歌指令”后面添加[-歌单名称]进行指定歌单猜歌\n" + + "猜歌内容必须以[-]开头才会识别\n" + + "本地歌曲命名规则为:\n歌名 - 歌手 - 其他(歌曲出处之类)", PrivateDataFolder: "guessmusic", }).ApplySingle(single.New( single.WithKeyFn(func(ctx *zero.Ctx) int64 { return ctx.Event.GroupID }), @@ -74,378 +74,427 @@ func init() { // 插件主体 ) }), )) + serviceErr := "[" + servicename + "]" + // 用于存放歌曲三个片段的文件夹 cachePath := engine.DataFolder() + "cache/" - err := os.MkdirAll(cachePath, 0755) + err := os.MkdirAll(cachePath, 0777) if err != nil { - panic(err) + panic(serviceErr + "ERROR:" + 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(err) - } - } else { - panic(err) + } + if err != nil { + panic(serviceErr + "ERROR:" + err.Error()) } err = reader.Close() if err != nil { - panic(err) + panic(serviceErr + "ERROR:" + err.Error()) } } else { - var plist = []listRaw{ - { - Name: "动画榜", - ID: 3001835560, - }, - } - cfg = config{ // 默认 config + cfg = config{ // 配置默认 config MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改 - Local: true, // 是否使用本地音乐库 - API: true, // 是否使用 Api - Cookie: "", - Playlist: plist, + API: true, + Local: true, + Playlist: []listRaw{ + { + Name: "FM", + ID: 3136952023, + }}, } err = saveConfig(cfgFile) if err != nil { - panic(err) + panic(serviceErr + "ERROR:" + err.Error()) } } - err = getcatlist(cfg.MusicPath) + filelist, err = getlist(cfg.MusicPath) if err != nil { - logrus.Infof("[guessmusic2]无法获取歌单列表,[error]:%s", err) + panic(serviceErr + "ERROR:" + err.Error()) } - engine.OnRegex(`^设置猜歌(缓存歌库路径|本地|Api)\s*(.*)$`, func(ctx *zero.Ctx) bool { - if !zero.SuperUserPermission(ctx) { - ctx.SendChain(message.Text("只有bot主人可以设置!")) - return false - } - return true - }).SetBlock(true). + // 用户配置 + engine.OnRegex(`^设置猜歌(歌库路径|默认歌单)\s*(.*)$`).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 value == "" { - ctx.SendChain(message.Text("请输入正确的路径!")) + case "歌库路径": + if !zero.SuperUserPermission(ctx) { + ctx.SendChain(message.Text("只有bot主人可以设置!")) return } musicPath := strings.ReplaceAll(value, "\\", "/") if !strings.HasSuffix(musicPath, "/") { musicPath += "/" } - err = os.MkdirAll(cfg.MusicPath, 0755) + err = os.MkdirAll(musicPath, 0777) if err != nil { - ctx.SendChain(message.Text("[生成文件夹错误]ERROR: ", err)) + ctx.SendChain(message.Text(serviceErr, "生成文件夹ERROR:\n", err)) return } cfg.MusicPath = musicPath - case "本地": - choice, err := strconv.ParseBool(value) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) + case "默认歌单": + gid := ctx.Event.GroupID + if gid == 0 || !zero.AdminPermission(ctx) { + ctx.SendChain(message.Text("无权设置!")) return } - cfg.Local = choice - case "Api": - choice, err := strconv.ParseBool(value) - if err != nil { - ctx.SendChain(message.Text("ERROR: ", err)) + index := "" + for _, listinfo := range filelist { + if listinfo.Name == value { + index = value + break + } + } + if index == "" { + ctx.SendChain(message.Text("歌单名称错误,可以发送“歌单列表”获取歌单名称")) return } - cfg.API = choice + cfg.Defaultlist = append(cfg.Defaultlist, dlist{ + GroupID: gid, + Name: value, + }) } err = saveConfig(cfgFile) if err == nil { ctx.SendChain(message.Text("成功!")) } else { - ctx.SendChain(message.Text("ERROR: ", err)) + ctx.SendChain(message.Text(serviceErr, "ERROR:\n", 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). + engine.OnRegex(`^猜歌(开启|关闭)(歌单|歌词)自动下载`).SetBlock(true). Handle(func(ctx *zero.Ctx) { - keyURL := "https://music.cyrilstudio.top/login/qr/key" - data, err := web.GetData(keyURL) - if err != nil { - ctx.SendChain(message.Text("获取网易云key失败, ERROR: ", err)) - return + swtich := ctx.State["regex_matched"].([]string)[1] + option := ctx.State["regex_matched"].([]string)[1] + chose := true + if swtich == "关闭" { + chose = false } - var keyInfo keyInfo - err = json.Unmarshal(data, &keyInfo) - if err != nil { - ctx.SendChain(message.Text("解析网易云key失败, ERROR: ", err)) - return + if option == "歌单" { + cfg.API = chose + } else { + cfg.Local = chose } - 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分钟,登陆后请耐心等待结果,获取cookie过程有些漫长。")) - 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 - } + err = saveConfig(cfgFile) + if err == nil { + ctx.SendChain(message.Text("成功!")) + } else { + ctx.SendChain(message.Text(serviceErr, "ERROR:\n", err)) } }) - engine.OnRegex(`^添加歌单\s?(\d+)(\s(.*))?$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). + // 本地绑定网易云歌单ID + engine.OnRegex(`^添加歌单\s?(https:.*id=)?(\d+)\s?(.*)$`, zero.SuperUserPermission).SetBlock(true).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { - listID := ctx.State["regex_matched"].([]string)[1] + listID := ctx.State["regex_matched"].([]string)[2] listName := ctx.State["regex_matched"].([]string)[3] ctx.SendChain(message.Text("正在校验歌单信息,请稍等")) // 是否存在该歌单 - apiURL := "https://music.cyrilstudio.top/playlist/detail?id=" + listID + "&cookie=" + cfg.Cookie - referer := "https://music.cyrilstudio.top" - data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua) + apiURL := "https://ovooa.com/API/163_Music_Rand/api.php?id=" + listID + data, err := web.GetData(apiURL) if err != nil { - ctx.SendChain(message.Text("无法连接歌单,[error]", err)) + ctx.SendChain(message.Text(serviceErr, "error:", err)) return } - var parsed topList + var parsed ovooaData err = json.Unmarshal(data, &parsed) if err != nil { - ctx.SendChain(message.Text("无法解析歌单ID内容,[error]", err)) + ctx.SendChain(message.Text(serviceErr, "无法解析歌单ID内容:", err)) return } - // 是否有权限访问歌单列表内容 - apiURL = "https://music.cyrilstudio.top/playlist/track/all?id=" + listID + "&cookie=" + cfg.Cookie - referer = "https://music.163.com/" - data, err = web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua) + 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("无法获取歌单列表\n ERROR: ", err)) + ctx.SendChain(message.Text(serviceErr, "歌单不存在于本地,尝试创建该歌单失败:\n", err)) return } - var musiclist topMusicInfo - err = json.Unmarshal(data, &musiclist) - if err != nil { - ctx.SendChain(message.Text("你的cookie在API中无权访问该歌单\n该歌单有可能是用户私人歌单")) - return - } - // 获取列表名字 - if listName == "" { - listName = parsed.Playlist.Name - } - playID, _ := strconv.ParseInt(listID, 10, 64) - catlist[listName] = playID + mid, _ := strconv.ParseInt(listID, 10, 64) cfg.Playlist = append(cfg.Playlist, listRaw{ Name: listName, - ID: playID, + ID: mid, }) err = saveConfig(cfgFile) if err == nil { ctx.SendChain(message.Text("成功!")) } else { - ctx.SendChain(message.Text("ERROR: ", err)) + 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] - 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, "当前添加的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 := os.ReadDir(cfg.MusicPath) - if err == nil { - if len(files) == 0 { - ctx.SendChain(message.Text("缓存目录没有读取到任何歌单")) - filelist = nil - } else { - 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) + filelist, err = getlist(cfg.MusicPath) if err != nil { - ctx.SendChain(message.Text("生成列表图片失败,请重试")) + ctx.SendChain(message.Text(serviceErr, "歌单列表获取error:", err)) 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) - if list == "" { - ctx.SendChain(message.Text("请输入歌单ID或者API歌单名称\n歌单ID为(网页/分享)链接的“playlist”后面的第一串数字")) - return - } - var listIDStr string - for listName, listID := range catlist { - if list == listName || list == strconv.FormatInt(listID, 10) { - listIDStr = strconv.FormatInt(listID, 10) + 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 + } + } + index = i break } } - if listIDStr == "" { - _, err := strconv.ParseInt(list, 10, 64) + if index == 1024 { + 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 + 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)) + } + }) + // 下载歌曲到对应的歌单里面 + engine.OnRegex(`^下载歌曲\s?(\d+|.*[^\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 { + 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, 0777) if err != nil { - ctx.SendChain(message.Text("仅支持歌单ID查询")) + ctx.SendChain(message.Text(serviceErr, "生成文件夹ERROR:\n", err)) return } - listIDStr = list } - apiURL := "https://music.cyrilstudio.top/playlist/detail?id=" + listIDStr + "&cookie=" + cfg.Cookie - referer := "https://music.cyrilstudio.top" - data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua) + searchlist, err := wyy.SearchMusic(keyword, 5) if err != nil { - ctx.SendChain(message.Text("无法连接歌单,[error]", err)) + ctx.SendChain(message.Text("查询歌曲失败!\nerr:", err)) return } - var parsed topList - err = json.Unmarshal(data, &parsed) - if err != nil { - ctx.SendChain(message.Text("无法解析歌单ID内容,[error]", err)) + listmun := len(searchlist) + if listmun == 0 { + ctx.SendChain(message.Text("歌曲没有查询到,请确认信息正确")) 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"), - )) + musicList := make([]string, listmun) + i := 0 + for musicName := range searchlist { + musicList[i] = musicName + i++ + } + 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)) + } + return + } + var msg []string + msg = append(msg, "搜索到相近的歌曲,请回复对应序号进行下载或回复取消") + for j, musicName := range musicList { + msg = append(msg, strconv.Itoa(j)+"."+musicName) + } + ctx.SendChain(message.Text(strings.Join(msg, "\n"))) + next := zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`[0-4]|取消`), ctx.CheckSession()) + recv, cancel := next.Repeat() + defer cancel() + wait := time.NewTimer(120 * time.Second) + 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) + // 下载歌曲 + 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)) + } + return + } + } + }) + 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)) + return + } + /***********设置图片的大小和底色***********/ + number := len(filelist) + fontSize := 20.0 + if number < 10 { + number = 10 + } + canvas := gg.NewContext(480, int(80+fontSize*float64(number))) + canvas.SetRGB(1, 1, 1) // 白色 + canvas.Clear() + /***********下载字体,可以注销掉***********/ + _, err = file.GetLazyData(text.BoldFontFile, true) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + } + _, err = file.GetLazyData(text.FontFile, true) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + } + /***********设置字体颜色为黑色***********/ + canvas.SetRGB(0, 0, 0) + /***********设置字体大小,并获取字体高度用来定位***********/ + if err = canvas.LoadFontFace(text.BoldFontFile, fontSize); err != nil { + ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + return + } + _, h := canvas.MeasureString("序号\t\t歌单名\t\t\t歌曲数量\t\t网易云歌单ID") + /***********绘制标题***********/ + canvas.DrawString("序号\t\t歌单名\t\t歌曲数量\t\t网易云歌单ID", 20, 50-h) // 放置在中间位置 + canvas.DrawString("——————————————————————", 20, 70-h) + /***********设置字体大小,并获取字体高度用来定位***********/ + if err = canvas.LoadFontFace(text.FontFile, fontSize); err != nil { + ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) + return + } + _, h = canvas.MeasureString("焯") + j := 0 + for i, listinfo := range filelist { + canvas.DrawString(strconv.Itoa(i), 15, float64(85+20*i)-h) + canvas.DrawString(listinfo.Name, 85, float64(85+20*i)-h) + canvas.DrawString(strconv.Itoa(listinfo.Number), 220, float64(85+20*i)-h) + if listinfo.ID != 0 { + canvas.DrawString(strconv.FormatInt(listinfo.ID, 10), 320, float64(85+20*i)-h) + } + j = i + 2 + } + for _, dlist := range cfg.Defaultlist { + if dlist.GroupID == ctx.Event.GroupID { + canvas.DrawString("当前设置的默认歌单为: "+dlist.Name, 80, float64(85+20*j)-h) + } + } + data, cl := writer.ToBytes(canvas.Image()) + if id := ctx.SendChain(message.ImageBytes(data)); id.ID() == 0 { + ctx.SendChain(message.Text("ERROR: 可能被风控了")) + } + cl() }) engine.OnRegex(`^(个人|团队)猜歌(-(.*))?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup). Handle(func(ctx *zero.Ctx) { mode := ctx.State["regex_matched"].([]string)[3] - if mode == "" { - mode = "动画榜" - catlist[mode] = 3001835560 - } - _, ok := catlist[mode] - // 如果本地和API不存在该歌单 - if !strings.Contains(strings.Join(filelist, " "), mode) && !ok { - ctx.SendChain(message.Text("歌单名称错误,可以发送“获取歌单列表”获取歌单名称")) + gid := ctx.Event.GroupID + filelist, err := getlist(cfg.MusicPath) + if err != nil { + ctx.SendChain(message.Text(serviceErr, "获取歌单列表ERROR:", err)) return } - gid := strconv.FormatInt(ctx.Event.GroupID, 10) + if mode == "" { + for _, dlist := range cfg.Defaultlist { + if dlist.GroupID == gid { + mode = dlist.Name + break + } + } + } + if mode == "" { + mode = filelist[0].Name + } 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次机会")) // 随机抽歌 - musicName, pathOfMusic, err := musicLottery(mode, cfg.MusicPath) + pathOfMusic, musicName, err := musicLottery(cfg.MusicPath, mode) if err != nil { - ctx.SendChain(message.Text(err)) + ctx.SendChain(message.Text(serviceErr, "ERROR:", err)) return } // 解析歌曲信息 @@ -453,7 +502,7 @@ func init() { // 插件主体 // 获取音乐后缀 musictype := music[len(music)-1] if !strings.Contains(musictypelist, musictype) { - ctx.SendChain(message.Text("抽取到了本地歌曲:\n", + ctx.SendChain(message.Text("抽取到了歌曲:\n", musicName, "\n该歌曲不是音乐后缀,请联系bot主人修改")) return } @@ -461,7 +510,7 @@ func init() { // 插件主体 musicInfo := strings.Split(strings.ReplaceAll(musicName, "."+musictype, ""), " - ") infoNum := len(musicInfo) if infoNum == 1 { - ctx.SendChain(message.Text("抽取到了本地歌曲:\n", + ctx.SendChain(message.Text("抽取到了歌曲:\n", musicName, "\n该歌曲命名不符合命名规则,请联系bot主人修改")) return } @@ -472,7 +521,7 @@ func init() { // 插件主体 answerString += "\n其他信息:\n" + strings.ReplaceAll(musicAlia, "&", "\n") } // 切割音频,生成3个10秒的音频 - outputPath := cachePath + gid + "/" + outputPath := cachePath + strconv.FormatInt(gid, 10) + "/" err = cutMusic(musicName, pathOfMusic, outputPath) if err != nil { ctx.SendChain(message.Text(err)) @@ -610,7 +659,8 @@ func init() { // 插件主体 }) } -func saveConfig(cfgFile string) (err error) { +// 保存用户配置 +func saveConfig(cfgFile string) error { if reader, err := os.Create(cfgFile); err == nil { err = json.NewEncoder(reader).Encode(&cfg) if err != nil { @@ -622,173 +672,178 @@ 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 +// 获取本地歌单列表 +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 + } } - err := os.MkdirAll(pathOfMusic, 0755) + err = os.MkdirAll(pathOfMusic, 0777) if err != nil { - err = errors.Errorf("[生成文件夹错误]ERROR: %s", err) - return err + return } files, err := os.ReadDir(pathOfMusic) if err != nil { - err = errors.Errorf("[读取本地列表错误]ERROR: %s", err) - return err + return } - for i, name := range files { - filelist = append(filelist, strconv.Itoa(i)+":"+name.Name()) + if len(files) == 0 { + err = errors.Errorf("所设置的歌库不存在任何歌单!") + return } - return nil + for _, name := range files { + if !name.IsDir() { + continue + } + listName := name.Name() + listfiles, err := os.ReadDir(pathOfMusic + listName) + if err != nil { + continue + } + list = append(list, listinfo{ + Name: listName, + Number: len(listfiles), + ID: wyyID[listName], + }) + } + return } // 随机抽取音乐 -func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err error) { - pathOfMusic = musicPath + mode + "/" - err = os.MkdirAll(pathOfMusic, 0755) +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 { - err = errors.Errorf("[生成文件夹错误]ERROR: %s", err) return } files, err := os.ReadDir(pathOfMusic) if err != nil { - 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 = getListMusic(listIDstr, pathOfMusic) - if err != nil { - err = errors.Errorf("[本地数据为0,歌曲下载错误]ERROR: %s", err) - return - } - case rand.Intn(2) == 0 || !ok: - // 1/2概率抽本地或者歌单只有本地有时 + //如果本地列表为空 + if len(files) == 0 { + if playlistID == 0 || !cfg.API { + err = errors.New("本地歌单数据为0") + 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 = getListMusic(listIDstr, pathOfMusic) + musicName, err = downloadByOvooa(playlistID, pathOfMusic) if err != nil { - // 如果下载失败就从本地抽一个歌曲 musicName = getLocalMusic(files) err = nil + return } } - return } - if cfg.Local { - if len(files) == 0 { - err = errors.New("[本地数据为0,未开启API数据]") - return - } - musicName = getLocalMusic(files) - return - } - 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 getLocalMusic(files []fs.DirEntry) (musicName string) { if len(files) > 1 { - musicName = files[rand.Intn(len(files))].Name() + music := files[rand.Intn(len(files))] + // 如果是文件夹就递归 + if music.IsDir() { + musicName = getLocalMusic(files) + } else { + musicName = music.Name() + } } else { - musicName = files[0].Name() + music := files[0] + if !music.IsDir() { + musicName = files[0].Name() + } } return } -// 下载网易云歌单音乐 -func getListMusic(listID, pathOfMusic string) (musicName string, err error) { - apiURL := "https://music.cyrilstudio.top/playlist/track/all?id=" + listID + "&cookie=" + cfg.Cookie - referer := "https://music.163.com/" - data, err := web.RequestDataWith(web.NewDefaultClient(), apiURL, "GET", referer, ua) +// 下载从独角兽抽到的歌曲ID(歌单ID, 音乐保存路径, 歌词保存路径) +func downloadByOvooa(playlistID int64, musicPath string) (musicName string, err error) { + // 抽取歌曲 + mid, err := drawByOvooa(playlistID) if err != nil { - err = errors.Errorf("无法获取歌单列表\n ERROR: %s", err) + err = errors.Errorf("API ERROR: %s", err) return } - var parsed topMusicInfo - err = json.Unmarshal(data, &parsed) + // 获取完成的歌名 + musiclist, err := wyy.SearchMusic(strconv.Itoa(mid), 1) if err != nil { - err = errors.Errorf("无法读取歌单列表\n ERROR: %s", err) + err = errors.Errorf("API歌曲下载ERROR: %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 + // 歌曲ID理论是唯一的 + mun := len(musiclist) + if mun == 1 { + // 拉取歌名 + musicList := make([]string, mun) + i := 0 + for musicName := range musiclist { + musicList[i] = musicName } - } - 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 - } - musicURL := "http://music.163.com/song/media/outer/url?id=" + strconv.Itoa(musicID) - response, err := http.Head(musicURL) - if err != nil { - if strings.Contains(err.Error(), "404") { - err = errors.Errorf("歌曲丢失, 可能歌曲已下架或者登录状态已过期。\n可尝试重新登录排除后者问题。") - } else { - err = errors.Errorf("下载音乐失败, ERROR: %s", err) + name := musicList[0] + // 下载歌曲 + err = wyy.DownloadMusic(mid, name, musicPath) + if err == nil { + musicName = name + ".mp3" + if cfg.Local { + // 下载歌词 + _ = wyy.DownloadLrc(mid, name, musicPath+"歌词/") + } } - return - } - _ = response.Body.Close() - if response.StatusCode != 200 { - err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode) - return - } - if cource != "" { - musicName = name + " - " + artistName + " - " + cource + ".mp3" } else { - musicName = name + " - " + artistName + ".mp3" - } - downMusic := pathOfMusic + musicName - if file.IsNotExist(downMusic) { - data, err = web.GetData(musicURL) - if err != nil { - return - } - err = os.WriteFile(downMusic, data, 0666) - if err != nil { - return - } + 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, 0755) + err = os.MkdirAll(outputPath, 0777) if err != nil { err = errors.Errorf("[生成歌曲目录错误]ERROR: %s", err) return diff --git a/plugin/guessmusic/struct.go b/plugin/guessmusic/struct.go index 71e42c14..052d7395 100644 --- a/plugin/guessmusic/struct.go +++ b/plugin/guessmusic/struct.go @@ -1,406 +1,43 @@ package guessmusic -type listRaw struct { - Name string `json:"name"` - ID int64 `json:"id"` -} - +// config内容 type config struct { - MusicPath string `json:"musicPath"` - Local bool `json:"local"` - API bool `json:"api"` - Cookie string `json:"cookie"` - Playlist []listRaw `json:"playlist"` + MusicPath string `json:"musicPath"` + Local bool `json:"local"` + API bool `json:"api"` + Cookie string `json:"cookie"` + Playlist []listRaw `json:"playlist"` + Defaultlist []dlist `json:"defaultlist"` } -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 topList struct { - Code int `json:"code"` - RelatedVideos any `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 any `json:"updateFrequency"` - BackgroundCoverID int `json:"backgroundCoverId"` - BackgroundCoverURL any `json:"backgroundCoverUrl"` - TitleImage int `json:"titleImage"` - TitleImageURL any `json:"titleImageUrl"` - EnglishTitle any `json:"englishTitle"` - OfficialPlaylistType any `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 any `json:"expertTags"` - Experts any `json:"experts"` - DjStatus int `json:"djStatus"` - VipType int `json:"vipType"` - RemarkName any `json:"remarkName"` - AuthenticationTypes int `json:"authenticationTypes"` - AvatarDetail any `json:"avatarDetail"` - Anchor bool `json:"anchor"` - BackgroundImgIDStr string `json:"backgroundImgIdStr"` - AvatarImgIDStr string `json:"avatarImgIdStr"` - AvatarImgIDString string `json:"AvatarImgIDString"` - } `json:"subscribers"` - Subscribed any `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 any `json:"expertTags"` - Experts any `json:"experts"` - DjStatus int `json:"djStatus"` - VipType int `json:"vipType"` - RemarkName any `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 []any `json:"tns"` - Alias []any `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 any `json:"crbt"` - Cf string `json:"cf"` - Al struct { - ID int `json:"id"` - Name string `json:"name"` - PicURL string `json:"picUrl"` - Tns []any `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 any `json:"sq"` - Hr any `json:"hr"` - A any `json:"a"` - Cd string `json:"cd"` - No int `json:"no"` - RtURL any `json:"rtUrl"` - Ftype int `json:"ftype"` - RtUrls []any `json:"rtUrls"` - DjID int `json:"djId"` - Copyright int `json:"copyright"` - SID int `json:"s_id"` - Mark int `json:"mark"` - OriginCoverType int `json:"originCoverType"` - OriginSongSimpleData any `json:"originSongSimpleData"` - TagPicList any `json:"tagPicList"` - ResourceState bool `json:"resourceState"` - Version int `json:"version"` - SongJumpInfo any `json:"songJumpInfo"` - EntertainmentTags any `json:"entertainmentTags"` - Single int `json:"single"` - NoCopyrightRcmd any `json:"noCopyrightRcmd"` - Alg any `json:"alg"` - Rtype int `json:"rtype"` - Rurl any `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 any `json:"videoIds"` - Videos any `json:"videos"` - TrackIds []struct { - ID int `json:"id"` - V int `json:"v"` - T int `json:"t"` - At int64 `json:"at"` - Alg any `json:"alg"` - UID int `json:"uid"` - RcmdReason string `json:"rcmdReason"` - Sc any `json:"sc"` - Lr int `json:"lr,omitempty"` - } `json:"trackIds"` - ShareCount int `json:"shareCount"` - CommentCount int `json:"commentCount"` - RemixVideo any `json:"remixVideo"` - SharedUsers any `json:"sharedUsers"` - HistorySharedUsers any `json:"historySharedUsers"` - GradeStatus string `json:"gradeStatus"` - Score any `json:"score"` - AlgTags any `json:"algTags"` - } `json:"playlist"` - Urls any `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 any `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 any `json:"listenType"` - } `json:"freeTrialPrivilege"` - ChargeInfoList []struct { - Rate int `json:"rate"` - ChargeURL any `json:"chargeUrl"` - ChargeMessage any `json:"chargeMessage"` - ChargeType int `json:"chargeType"` - } `json:"chargeInfoList"` - } `json:"privileges"` - SharedPrivilege any `json:"sharedPrivilege"` - ResEntrance any `json:"resEntrance"` +// 记录歌单绑定的网易云歌单ID +type listRaw struct { + Name string `json:"name"` // 歌单名称 + ID int64 `json:"id"` // 歌单绑定的网易云ID } -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 []any `json:"tns"` - Alias []any `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 any `json:"crbt"` - Cf string `json:"cf"` - Al struct { - ID int `json:"id"` - Name string `json:"name"` - PicURL string `json:"picUrl"` - Tns []any `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 any `json:"sq"` - Hr any `json:"hr"` - A any `json:"a"` - Cd string `json:"cd"` - No int `json:"no"` - RtURL any `json:"rtUrl"` - Ftype int `json:"ftype"` - RtUrls []any `json:"rtUrls"` - DjID int `json:"djId"` - Copyright int `json:"copyright"` - SID int `json:"s_id"` - Mark int `json:"mark"` - OriginCoverType int `json:"originCoverType"` - OriginSongSimpleData any `json:"originSongSimpleData"` - TagPicList any `json:"tagPicList"` - ResourceState bool `json:"resourceState"` - Version int `json:"version"` - SongJumpInfo any `json:"songJumpInfo"` - EntertainmentTags any `json:"entertainmentTags"` - AwardTags any `json:"awardTags"` - Single int `json:"single"` - NoCopyrightRcmd any `json:"noCopyrightRcmd"` - Rtype int `json:"rtype"` - Rurl any `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 any `json:"listenType"` - } `json:"freeTrialPrivilege"` - ChargeInfoList []struct { - Rate int `json:"rate"` - ChargeURL any `json:"chargeUrl"` - ChargeMessage any `json:"chargeMessage"` - ChargeType int `json:"chargeType"` - } `json:"chargeInfoList"` - } `json:"privileges"` - Code int `json:"code"` +// 记录群默认猜歌 +type dlist struct { + GroupID int64 `json:"gid"` // 群号 + Name string `json:"name"` // 歌单名称 +} + +// 本地歌单列表信息 +type listinfo struct { + Name string `json:"name"` // 歌单名称 + Number int // 歌曲数量 + ID int64 // 歌单绑定的歌曲ID +} + +// 独角兽API随机抽歌信息 +type ovooaData struct { + Code int `json:"code"` + Text string `json:"text"` + Data struct { + Song string `json:"song"` + Singer string `json:"singer"` + Cover string `json:"cover"` + Music string `json:"Music"` + ID int `json:"id"` + } `json:"data"` }