ZeroBot-Plugin/plugin/guessmusic/main.go
2022-07-03 18:20:04 +08:00

595 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package guessmusic 基于zbp的猜歌插件
package guessmusic
import (
"bytes"
"encoding/json"
"io/fs"
"io/ioutil"
"math/rand"
"net/http"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
zero "github.com/wdvxdr1123/ZeroBot"
"github.com/wdvxdr1123/ZeroBot/message"
ctrl "github.com/FloatTech/zbpctrl"
"github.com/FloatTech/zbputils/control"
"github.com/FloatTech/zbputils/ctxext"
"github.com/FloatTech/zbputils/file"
"github.com/FloatTech/zbputils/web"
"github.com/wdvxdr1123/ZeroBot/extension/single"
)
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"
)
var (
cuttime = [...]string{"00:00:05", "00:00:30", "00:01:00"} // 音乐切割时间点,可自行调节时间(时:分:秒)
cfg = config{ // 默认 config
MusicPath: file.BOTPATH + "/data/guessmusic/music/", // 绝对路径,歌库根目录,通过指令进行更改
Local: true, // 是否使用本地音乐库
API: true, // 是否使用 Api
}
)
func init() { // 插件主体
engine := control.Register("guessmusic", &ctrl.Options[*zero.Ctx]{
DisableOnDefault: false,
Help: "猜歌插件该插件依赖ffmpeg\n" +
"- 个人猜歌\n" +
"- 团队猜歌\n" +
"- 设置猜歌缓存歌库路径 [绝对路径]\n" +
"- 设置猜歌本地 [true/false]\n" +
"- 设置猜歌Api [true/false]\n" +
"注:默认歌库为网易云热歌榜\n" +
"1.可在后面添加“-动漫”进行动漫歌猜歌\n-这个只能猜歌名和歌手\n" +
"2.可在后面添加“-动漫2”进行动漫歌猜歌\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.Send(
message.ReplyWithMessage(ctx.Event.MessageID,
message.Text("已经有正在进行的游戏..."),
),
)
}),
))
cachePath := engine.DataFolder() + "cache/"
err := os.MkdirAll(cachePath, 0755)
if err != nil {
panic(err)
}
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)
}
err = reader.Close()
if err != nil {
panic(err)
}
} else {
err = saveConfig(cfgFile)
if err != nil {
panic(err)
}
}
engine.OnRegex(`^设置猜歌(缓存歌库路径|本地|Api)\s*(.*)$`, func(ctx *zero.Ctx) bool {
if !zero.SuperUserPermission(ctx) {
ctx.SendChain(message.Text("只有bot主人可以设置"))
return false
}
return true
}).SetBlock(true).
Handle(func(ctx *zero.Ctx) {
option := ctx.State["regex_matched"].([]string)[1]
value := ctx.State["regex_matched"].([]string)[2]
switch option {
case "缓存歌库路径":
if value == "" {
ctx.SendChain(message.Text("请输入正确的路径!"))
return
}
musicPath := strings.ReplaceAll(value, "\\", "/")
if !strings.HasSuffix(musicPath, "/") {
musicPath += "/"
}
cfg.MusicPath = musicPath
case "本地":
choice, err := strconv.ParseBool(value)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
cfg.Local = choice
case "Api":
choice, err := strconv.ParseBool(value)
if err != nil {
ctx.SendChain(message.Text("ERROR:", err))
return
}
cfg.API = choice
}
err = saveConfig(cfgFile)
if err == nil {
ctx.SendChain(message.Text("成功!"))
} else {
ctx.SendChain(message.Text("ERROR:", err))
}
})
engine.OnRegex(`^(个人|团队)猜歌(-动漫|-动漫2)?$`, zero.OnlyGroup).SetBlock(true).Limit(ctxext.LimitByGroup).
Handle(func(ctx *zero.Ctx) {
mode := ctx.State["regex_matched"].([]string)[2]
gid := strconv.FormatInt(ctx.Event.GroupID, 10)
if mode == "-动漫2" {
ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|番剧|提示|取消]”\n一共3段语音6次机会"))
} else {
ctx.SendChain(message.Text("正在准备歌曲,请稍等\n回答“-[歌曲名称|歌手|提示|取消]”\n一共3段语音6次机会"))
}
// 随机抽歌
musicName, pathOfMusic, err := musicLottery(mode, cfg.MusicPath)
if err != nil {
ctx.SendChain(message.Text(err))
return
}
// 切割音频生成3个10秒的音频
outputPath := cachePath + gid + "/"
err = cutMusic(musicName, pathOfMusic, outputPath)
if err != nil {
ctx.SendChain(message.Text(err))
return
}
// 进行猜歌环节
ctx.SendChain(message.Record("file:///" + file.BOTPATH + "/" + outputPath + "0.wav"))
answerString := strings.Split(musicName, " - ")
var next *zero.FutureEvent
if ctx.State["regex_matched"].([]string)[1] == "个人" {
next = zero.NewFutureEvent("message", 999, false, zero.OnlyGroup, zero.RegexRule(`^-\S{1,}`), ctx.CheckSession())
} 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:
msg := make(message.Message, 0, 3)
msg = append(msg, message.Reply(ctx.Event.MessageID))
msg = append(msg, message.Text("猜歌超时,游戏结束\n答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
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()
msg := make(message.Message, 0, 3)
msg = append(msg, message.Reply(c.Event.MessageID))
msg = append(msg, message.Text("游戏已取消,猜歌答案是",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
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(answerString[0], answer) || strings.EqualFold(answerString[0], answer):
wait.Stop()
tick.Stop()
after.Stop()
msg := make(message.Message, 0, 3)
msg = append(msg, message.Reply(c.Event.MessageID))
msg = append(msg, message.Text("太棒了,你猜对歌曲名了!答案是",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return
case answerString[1] == "未知" && answer == "未知":
ctx.Send(
message.ReplyWithMessage(c.Event.MessageID,
message.Text("该模式禁止回答“未知”"),
),
)
case strings.Contains(answerString[1], answer) || strings.EqualFold(answerString[1], answer):
wait.Stop()
tick.Stop()
after.Stop()
msg := make(message.Message, 0, 3)
msg = append(msg, message.Reply(c.Event.MessageID))
msg = append(msg, message.Text("太棒了,你猜对歌手名了!答案是",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
return
default:
if mode == "-动漫2" && (strings.Contains(answerString[2], answer) || strings.EqualFold(answerString[2], answer)) {
wait.Stop()
tick.Stop()
after.Stop()
ctx.Send(message.ReplyWithMessage(c.Event.MessageID,
message.Text("太棒了,你猜对番剧名了!答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1],
"\n歌曲出自:", answerString[2]),
))
return
}
musicCount++
switch {
case musicCount > 2 && answerCount < 6:
wait.Stop()
answerCount++
ctx.Send(
message.ReplyWithMessage(c.Event.MessageID,
message.Text("答案不对哦,加油啊~"),
),
)
case musicCount > 2:
wait.Stop()
tick.Stop()
after.Stop()
msg := make(message.Message, 0, 3)
msg = append(msg, message.Reply(c.Event.MessageID))
msg = append(msg, message.Text("次数到了,你没能猜出来。\n答案是:",
"\n歌名:", answerString[0],
"\n歌手:", answerString[1]))
if mode == "-动漫2" {
msg = append(msg, message.Text("\n歌曲出自:", answerString[2]))
}
ctx.Send(msg)
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 saveConfig(cfgFile string) (err error) {
if reader, err := os.Create(cfgFile); err == nil {
err = json.NewEncoder(reader).Encode(&cfg)
if err != nil {
return err
}
} else {
return err
}
return nil
}
// 随机抽取音乐
func musicLottery(mode, musicPath string) (musicName, pathOfMusic string, err error) {
switch mode {
case "-动漫":
pathOfMusic = musicPath + "动漫/"
case "-动漫2":
pathOfMusic = musicPath + "动漫2/"
default:
pathOfMusic = musicPath + "歌榜/"
}
err = os.MkdirAll(pathOfMusic, 0755)
if err != nil {
err = errors.Errorf("[生成文件夹错误]ERROR:%s", err)
return
}
files, err := ioutil.ReadDir(pathOfMusic)
if err != nil {
err = errors.Errorf("[读取本地列表错误]ERROR:%s", err)
return
}
if cfg.Local && cfg.API {
switch {
case len(files) == 0:
// 如果没有任何本地就下载歌曲
musicName, err = getAPIMusic(mode, pathOfMusic)
if err != nil {
err = errors.Errorf("[本地数据为0歌曲下载错误]ERROR:%s", err)
return
}
case rand.Intn(2) == 0:
// [0,1)只会取到0rand不允许的
musicName = getLocalMusic(files)
default:
musicName, err = getAPIMusic(mode, pathOfMusic)
if err != nil {
// 如果下载失败就从本地抽一个歌曲
musicName = getLocalMusic(files)
err = nil
}
}
return
}
if cfg.Local {
if len(files) == 0 {
err = errors.New("[本地数据为0未开启API数据]")
return
}
musicName = getLocalMusic(files)
return
}
if cfg.API {
musicName, err = getAPIMusic(mode, pathOfMusic)
if err != nil {
err = errors.Errorf("[获取API失败未开启本地数据] ERROR:%s", err)
return
}
return
}
err = errors.New("[未开启API以及本地数据]")
return
}
func getAPIMusic(mode string, musicPath string) (musicName string, err error) {
switch mode {
case "-动漫":
musicName, err = getPaugramData(musicPath)
case "-动漫2":
musicName, err = getAnimeData(musicPath)
default:
musicName, err = getNetEaseData(musicPath)
}
return
}
func getLocalMusic(files []fs.FileInfo) (musicName string) {
if len(files) > 1 {
musicName = strings.Replace(files[rand.Intn(len(files))].Name(), ".mp3", "", 1)
} else {
musicName = strings.Replace(files[0].Name(), ".mp3", "", 1)
}
return
}
// 下载保罗API的歌曲
func getPaugramData(musicPath string) (musicName string, err error) {
api := "https://api.paugram.com/acgm/?list=1"
referer := "https://api.paugram.com/"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua)
if err != nil {
return
}
var parsed paugramData
err = json.Unmarshal(data, &parsed)
if err != nil {
return
}
name := parsed.Title
artistsName := parsed.Artist
musicURL := parsed.Link
if name == "" || artistsName == "" {
err = errors.New("无法获API取歌曲信息")
return
}
musicName = name + " - " + artistsName
downMusic := musicPath + "/" + musicName + ".mp3"
response, err := http.Head(musicURL)
if err != nil {
err = errors.Errorf("下载音乐失败, ERROR: %s", err)
return
}
if response.StatusCode != 200 {
err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode)
return
}
if file.IsNotExist(downMusic) {
data, err = web.GetData(musicURL)
if err != nil {
return
}
err = os.WriteFile(downMusic, data, 0666)
if err != nil {
return
}
}
return
}
// 下载animeMusic API的歌曲
func getAnimeData(musicPath string) (musicName string, err error) {
api := "https://anime-music.jijidown.com/api/v2/music"
referer := "https://anime-music.jijidown.com/"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua)
if err != nil {
return
}
var parsed animeData
err = json.Unmarshal(data, &parsed)
if err != nil {
return
}
name := parsed.Res.Title
artistName := parsed.Res.Author
acgName := parsed.Res.AnimeInfo.Title
//musicURL := parsed.Res.PlayURL
if name == "" || artistName == "" {
err = errors.New("无法获API取歌曲信息")
return
}
requestURL := "https://autumnfish.cn/search?keywords=" + url.QueryEscape(name+" "+artistName) + "&limit=1"
if artistName == "未知" {
requestURL = "https://autumnfish.cn/search?keywords=" + url.QueryEscape(acgName+" "+name) + "&limit=1"
}
data, err = web.GetData(requestURL)
if err != nil {
err = errors.Errorf("API歌曲查询失败, ERROR: %s", err)
return
}
var autumnfish autumnfishData
err = json.Unmarshal(data, &autumnfish)
if err != nil {
return
}
if autumnfish.Code != 200 {
err = errors.Errorf("下载音乐失败, Status Code: %d", autumnfish.Code)
return
}
musicID := strconv.Itoa(autumnfish.Result.Songs[0].ID)
if artistName == "未知" {
artistName = strings.ReplaceAll(autumnfish.Result.Songs[0].Artists[0].Name, " - ", "-")
}
musicName = name + " - " + artistName + " - " + acgName
downMusic := musicPath + "/" + musicName + ".mp3"
musicURL := "http://music.163.com/song/media/outer/url?id=" + musicID
response, err := http.Head(musicURL)
if err != nil {
err = errors.Errorf("下载音乐失败, ERROR: %s", err)
return
}
if response.StatusCode != 200 {
err = errors.Errorf("下载音乐失败, Status Code: %d", response.StatusCode)
return
}
if file.IsNotExist(downMusic) {
data, err = web.GetData(musicURL)
if err != nil {
return
}
err = os.WriteFile(downMusic, data, 0666)
if err != nil {
return
}
}
return
}
// 下载网易云热歌榜音乐
func getNetEaseData(musicPath string) (musicName string, err error) {
api := "https://api.uomg.com/api/rand.music?sort=%E7%83%AD%E6%AD%8C%E6%A6%9C&format=json"
referer := "https://api.uomg.com/api/rand.music"
data, err := web.RequestDataWith(web.NewDefaultClient(), api, "GET", referer, ua)
if err != nil {
return
}
var parsed netEaseData
err = json.Unmarshal(data, &parsed)
if err != nil {
return
}
name := parsed.Data.Name
musicURL := parsed.Data.URL
artistsName := parsed.Data.Artistsname
if name == "" || artistsName == "" {
err = errors.New("无法获API取歌曲信息")
return
}
musicName = name + " - " + artistsName
downMusic := musicPath + "/" + musicName + ".mp3"
if file.IsNotExist(downMusic) {
data, err = web.GetData(musicURL)
if err != nil {
return
}
err = os.WriteFile(downMusic, data, 0666)
if err != nil {
return
}
}
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 + ".mp3",
"-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
}