diff --git a/bot.go b/bot.go index 53398e8..5ad19dd 100644 --- a/bot.go +++ b/bot.go @@ -10,6 +10,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/faiface/beep" + "github.com/fhs/gompd/v2/mpd" "github.com/gohugoio/hugo/cache/filecache" "github.com/jackc/pgx/v5" "github.com/julienschmidt/httprouter" @@ -32,6 +33,9 @@ var ( ) func init() { + log.SetOutput(os.Stdout) + log.SetFlags(log.Lshortfile) + log.Println("bot.go loading..") config.SetConfigName("config") config.SetConfigType("yaml") @@ -57,6 +61,9 @@ type App struct { active []string plidx int cache *filecache.Cache + mpdc context.CancelFunc + mpdw *mpd.Watcher + mpd *mpd.Client } func main() { @@ -72,7 +79,8 @@ func main() { select { case <-sc: app.db.Close(context.Background()) - app.queue.Reset() + app.mpdw.Close() + app.mpdc() app.voice.Close() app.discord.Close() return diff --git a/events.go b/events.go index 47e4b74..fc149d7 100644 --- a/events.go +++ b/events.go @@ -1,17 +1,18 @@ package main import ( - "dndmusicbot/ffmpeg" discordspeaker "dndmusicbot/speaker" "encoding/json" "fmt" "log" "net/url" "os" + "strconv" "time" "github.com/faiface/beep" "github.com/faiface/beep/mp3" + "github.com/fhs/gompd/v2/mpd" "github.com/google/uuid" "github.com/kataras/go-events" "golang.org/x/time/rate" @@ -22,24 +23,26 @@ type SongInfo struct { PlaylistName string `json:"playlistname,omitempty"` Title string `json:"current,omitempty"` Channel string `json:"channel,omitempty"` - Position int `json:"position"` - Length int `json:"len,omitempty"` + Position int64 `json:"position"` + Length int64 `json:"len,omitempty"` Pause bool `json:"pause"` Song string `json:"song,omitempty"` } -var l = rate.Sometimes{Interval: 1 * time.Second} +var l = rate.Sometimes{Interval: 800 * time.Millisecond} func init() { log.Println("events.go loading...") + app.events = events.New() app.events.On("load_playlist", app.loadPlaylist) app.events.On("add_playlist", app.addPlaylist) //app.events.On("preload_song", app.preloadSong) - app.events.On("song_over", app.songInfo) - app.events.On("song_start", app.songInfo) + //app.events.On("song_over", app.songInfo) + //app.events.On("song_start", app.songInfo) + app.events.On("player", app.songInfo) //app.events.On("song_position", app.songPosition) app.events.On("ambiance_play", app.ambiancePlay) @@ -52,24 +55,87 @@ func init() { //app.events.On("tick", app.checkQueue) app.events.On("tick", app.songPosition) - app.events.On("tick", app.checkTimeleft) + //app.events.On("tick", app.checkTimeleft) } func (app *App) songInfoEvent(event string) map[string]interface{} { msg := make(map[string]interface{}) msg["event"] = event - - msg["payload"] = SongInfo{ - Playlist: app.queue.Current().Playlist.Id, - PlaylistName: app.queue.Current().Playlist.Title, - Title: app.queue.Current().Title, - Channel: app.queue.Current().Channel, - Position: 0, - Length: app.queue.Len(), - Pause: !app.queue.IsPlaying(), - Song: app.queue.Current().VideoID, + status, err := app.mpd.Status() + if err != nil { + log.Println(err) + return nil } + cur, err := app.mpd.CurrentSong() + if err != nil { + log.Println(err) + return nil + } + + info := new(SongInfo) + + if status["state"] != "play" { + info.Pause = true + msg["payload"] = info + return msg + } + + duration, ok := status["duration"] + if ok && duration != "" { + slen, err := strconv.ParseFloat(duration, 64) + if err != nil { + log.Println(err) + return nil + } + info.Length = time.Duration(slen * float64(time.Second)).Milliseconds() + } + + elapsed, ok := status["elapsed"] + if ok && elapsed != "" { + spos, err := strconv.ParseFloat(elapsed, 64) + if err != nil { + log.Println(err) + return nil + } + info.Position = time.Duration(spos * float64(time.Second)).Milliseconds() + } + + album, ok := cur["Album"] + if ok { + plid, err := uuid.ParseBytes([]byte(album)) + if err != nil { + log.Println(err) + return nil + } + + pl, err := app.GetPlaylist(plid) + if err != nil { + log.Println(err) + return nil + } + + info.Playlist = pl.Id + info.PlaylistName = pl.Title + } + + title, ok := cur["Title"] + if ok { + info.Title = title + } + + artist, ok := cur["Artist"] + if ok { + info.Channel = artist + } + + location, ok := cur["Location"] + if ok { + info.Song = location + } + + msg["payload"] = *info + return msg } @@ -176,7 +242,13 @@ func (app *App) ambianceAdd(payload ...interface{}) { } func (app *App) songPosition(payload ...interface{}) { - if !app.queue.IsPlaying() { + status, err := app.mpd.Status() + if err != nil { + log.Println(err) + return + } + + if status["state"] == "stop" { return } @@ -184,9 +256,20 @@ func (app *App) songPosition(payload ...interface{}) { msg := make(map[string]interface{}) out := make(map[string]interface{}) + slen, err := strconv.ParseFloat(status["duration"], 64) + if err != nil { + log.Println(err) + return + } + + spos, err := strconv.ParseFloat(status["elapsed"], 64) + if err != nil { + return + } + msg["event"] = "song_position" - out["len"] = app.queue.Len() - out["position"] = app.queue.Position() + out["len"] = time.Duration(slen * float64(time.Second)).Milliseconds() + out["position"] = time.Duration(spos * float64(time.Second)).Milliseconds() msg["payload"] = out ws_msg <- msg @@ -194,54 +277,40 @@ func (app *App) songPosition(payload ...interface{}) { } -func (app *App) checkTimeleft(payload ...interface{}) { - if !app.queue.IsPlaying() { - return - } - timeleft := app.queue.Len() - app.queue.Position() - if timeleft <= 10000 && timeleft > 0 && !app.next { - app.events.Emit("preload_song") - } -} - func (app *App) songInfo(payload ...interface{}) { - log.Println("song_over event received") + log.Println("song_info event received") msg := app.songInfoEvent("song_info") - ws_msg <- msg - - app.next = false + if msg != nil { + ws_msg <- msg + } } func (app *App) stop(payload ...interface{}) { log.Println("stop event received") - discordspeaker.Lock() - app.queue.Reset() - discordspeaker.Unlock() + + app.mpd.Stop() msg := make(map[string]interface{}) msg["event"] = "stop" ws_msg <- msg - } func (app *App) prevSong(payload ...interface{}) { log.Println("prev_song event received") - - app.queue.Prev() - - msg := app.songInfoEvent("song_info") - ws_msg <- msg + err := app.mpd.Previous() + if err != nil { + log.Println(err) + } } func (app *App) nextSong(payload ...interface{}) { log.Println("next_song event received") - - app.queue.Next() - - msg := app.songInfoEvent("song_info") - ws_msg <- msg + err := app.mpd.Next() + if err != nil { + log.Println(err) + } } func (app *App) addPlaylist(payload ...interface{}) { @@ -348,7 +417,8 @@ func (app *App) loadPlaylist(payload ...interface{}) { return } - app.queue.Reset() + app.mpd.Stop() + app.mpd.Clear() go func() { for _, vid := range list { @@ -370,22 +440,43 @@ func (app *App) loadPlaylist(payload ...interface{}) { continue } - ff, err := ffmpeg.NewPCM(string(yt), sampleRate, channels) + // state:stop + songid, err := app.mpd.AddID(string(yt), 0) if err != nil { log.Println(err) continue } - song := &Song{ - Title: ytinfo.Title, - Channel: ytinfo.Channel, - VideoID: vid, - Length: ytinfo.Len, - PCM: ff, - Playlist: *pl, - DLuri: string(yt), + mpdcmd := app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("artist"), ytinfo.Channel) + err = mpdcmd.OK() + if err != nil { + log.Println(err) + continue } - app.queue.Add(song) + + mpdcmd = app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("title"), ytinfo.Title) + err = mpdcmd.OK() + if err != nil { + log.Println(err) + continue + } + + mpdcmd = app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("location"), vid) + err = mpdcmd.OK() + if err != nil { + log.Println(err) + continue + } + + mpdcmd = app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("album"), pl.Id.String()) + err = mpdcmd.OK() + if err != nil { + log.Println(err) + continue + } + + app.mpd.Play(-1) + } }() } diff --git a/go.mod b/go.mod index e45cb39..57d2cc9 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,13 @@ module dndmusicbot +replace github.com/fhs/gompd/v2 => /home/steino/dev/go/gompd/ + go 1.19 require ( github.com/bwmarrin/discordgo v0.26.1 - github.com/dpup/gohubbub v0.0.0-20140517235056-2dc6969d22d8 github.com/faiface/beep v1.1.0 + github.com/fhs/gompd/v2 v2.3.0 github.com/gohugoio/hugo v0.106.0 github.com/google/uuid v1.3.0 github.com/gorilla/websocket v1.5.0 @@ -14,7 +16,6 @@ require ( github.com/julienschmidt/httprouter v1.3.0 github.com/kataras/go-events v0.0.3 github.com/pkg/errors v0.9.1 - github.com/r3labs/sse/v2 v2.8.2 github.com/sosodev/duration v1.0.1 github.com/spf13/afero v1.9.3 github.com/spf13/viper v1.14.0 @@ -82,7 +83,6 @@ require ( google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect google.golang.org/grpc v1.50.1 // indirect google.golang.org/protobuf v1.28.1 // indirect - gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect gopkg.in/fatih/set.v0 v0.2.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/mpd.go b/mpd.go new file mode 100644 index 0000000..bb66bbb --- /dev/null +++ b/mpd.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "context" + "io" + "log" + "os" + "os/exec" + "strconv" + "text/template" + "time" + + "github.com/faiface/beep" + "github.com/fhs/gompd/v2/mpd" + "github.com/kataras/go-events" +) + +type MPD struct { + file *os.File + f beep.Format +} + +func init() { + log.Println("mpd.go loading..") + + f, err := os.OpenFile(config.GetString("mpd.config"), os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + log.Fatal(err) + } + + t, err := template.New("mpd.tmpl").ParseFiles("tmpl/mpd.tmpl") + if err != nil { + log.Fatal(err) + } + + err = t.Execute(f, config.GetStringMapString("mpd")) + if err != nil { + log.Fatal(err) + } + + f.Close() + + pidstr, err := os.ReadFile(config.GetString("mpd.pid")) + switch err { + case os.ErrNotExist: + log.Println("Pidfile not found, doing nothing") + case nil: + pid, _ := strconv.Atoi(string(bytes.TrimSpace(pidstr))) + proc, err := os.FindProcess(pid) + switch err { + case nil: + log.Println(proc.Kill()) + case os.ErrProcessDone: + log.Println("Pid alreadt finished, doing nothing.") + default: + log.Fatal(err) + } + default: + log.Fatal(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + + app.mpdc = cancel + + cmd := exec.CommandContext( + ctx, + config.GetString("mpd.cmd"), + "--no-daemon", + config.GetString("mpd.config"), + "-v", + ) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + go func() { + err := cmd.Run() + if err != nil { + log.Println(err) + } + cancel() + }() + + // wait for mpd to start. + time.Sleep(2 * time.Second) + + app.mpd, err = mpd.Dial("unix", config.GetString("mpd.sock")) + if err != nil { + log.Fatal(err) + } + + app.mpd.Repeat(true) + app.mpd.Random(true) + + err = app.mpd.EnableOutput(0) + if err != nil { + log.Fatal(err) + } + + app.mpdw, err = mpd.NewWatcher("unix", config.GetString("mpd.sock"), "") + if err != nil { + log.Fatal(err) + } + + go func() { + for { + select { + case ev := <-app.mpdw.Event: + app.events.Emit(events.EventName(ev)) + case err := <-app.mpdw.Error: + log.Println(err) + return + } + } + }() + + log.Println("mpd.go done.") +} + +func NewMPD() (*MPD, error) { + out := new(MPD) + + f, err := os.Open(config.GetString("mpd.fifo")) + if err != nil { + return nil, err + } + + out.f = beep.Format{ + SampleRate: beep.SampleRate(sampleRate), + NumChannels: channels, + Precision: 2, + } + + out.file = f + + return out, nil +} + +func (m *MPD) Err() error { + return nil +} + +func (m *MPD) Stream(samples [][2]float64) (n int, ok bool) { + tmp := make([]byte, m.f.NumChannels+2) + + for i := range samples { + dn, err := m.file.Read(tmp) + if dn == len(tmp) { + samples[i], _ = m.f.DecodeSigned(tmp) + ok = true + } + if err == io.EOF { + ok = false + break + } + if err != nil { + log.Println(err) + ok = false + break + } + } + + return len(samples), ok +} diff --git a/queue.go b/queue.go index f238c57..fb3082a 100644 --- a/queue.go +++ b/queue.go @@ -18,13 +18,24 @@ type Ambiance struct { } func init() { + log.Println("queue.go loading..") + app.ambiance = beep.Mixer{} discordspeaker.Play(&app.ambiance) - app.queue = new(Queue) - app.queue.list = list.New() - app.queue.Events = app.events - discordspeaker.Play(app.queue) + mpdstream, err := NewMPD() + if err != nil { + log.Fatal(err) + } + + discordspeaker.Play(mpdstream) + + /* + app.queue = new(Queue) + app.queue.list = list.New() + app.queue.Events = app.events + discordspeaker.Play(app.queue) + */ log.Println("queue.go done.") } @@ -83,10 +94,10 @@ func (q Queue) Current() *Song { } func (q *Queue) Reset() { - q.playing = false - - q.current = nil - q.list = q.list.Init() + err := app.mpd.Clear() + if err != nil { + log.Println(err) + } } func (q *Queue) Next() { diff --git a/tmpl/mpd.tmpl b/tmpl/mpd.tmpl new file mode 100644 index 0000000..b6a7dfb --- /dev/null +++ b/tmpl/mpd.tmpl @@ -0,0 +1,26 @@ +audio_output { + type "fifo" + name "fifo-output" + path "{{ .fifo }}" +} +decoder { + plugin "wildmidi" + enabled "no" +} +decoder { + plugin "hybrid_dsd" + enabled "no" +} +input { + plugin "qobuz" + enabled "no" +} +resampler { + plugin "soxr" + quality "very high" +} + +volume_normalization "yes" +audio_output_format "48000:16:2" +bind_to_address "{{ .sock }}" +pid_file "{{ .pid }}"