From ad8cbbd57d209c3feefe52a0c1332af13ed90c6f Mon Sep 17 00:00:00 2001 From: Stein Ivar Berghei Date: Thu, 2 Nov 2023 21:30:29 +0100 Subject: [PATCH] Big audiostuff changes --- Dockerfile.bot | 6 ++- bot.go | 15 +++++++ discord.go | 2 +- events.go | 57 +++++++++++++------------ go.mod | 6 ++- go.sum | 23 +++++++--- mpd.go | 74 +++++++++++++++++++++++++++----- queue.go | 24 ++++++++--- routes.go | 27 +++++++++++- snapcast/client.go | 79 ++++++++++++++++++++++++++++++++++ speaker/discord.go | 103 +++++++++++++++++++++++---------------------- tmpl/mpd.tmpl | 34 +++++++++++++-- youtube.go | 68 ++++++++++++++++++++++++++++++ youtube/youtube.go | 8 ++++ ytdl.go | 25 +++++++++++ 15 files changed, 440 insertions(+), 111 deletions(-) create mode 100644 snapcast/client.go diff --git a/Dockerfile.bot b/Dockerfile.bot index 59a544a..ae2cf71 100644 --- a/Dockerfile.bot +++ b/Dockerfile.bot @@ -8,8 +8,10 @@ FROM debian:bookworm-slim RUN apt-get update && apt-get -y install \ ca-certificates \ libopus-dev libopusfile-dev \ - mpd ffmpeg - + mpd ffmpeg curl && \ + curl -L https://github.com/badaix/snapcast/releases/download/v0.27.0/snapclient_0.27.0-1_without-pulse_amd64.deb -o /tmp/snapcast.deb && \ + curl -L http://ftp.no.debian.org/debian/pool/main/f/flac/libflac8_1.3.3-2+deb11u2_amd64.deb -o /tmp/libflac8.deb && \ + apt -y install /tmp/snapcast.deb /tmp/libflac8.deb COPY --from=builder /src/dndmusicbot /app/ ADD tmpl /app/tmpl WORKDIR /app diff --git a/bot.go b/bot.go index a6d89e5..9913ff1 100644 --- a/bot.go +++ b/bot.go @@ -5,6 +5,7 @@ import ( "log" "os" "os/signal" + "path/filepath" "sync" "syscall" "time" @@ -69,10 +70,21 @@ type App struct { plcancel context.CancelFunc } +var cwd string + +func init() { + ex, err := os.Executable() + if err != nil { + panic(err) + } + cwd = filepath.Dir(ex) +} + func main() { bfs := afero.NewBasePathFs(afero.NewOsFs(), "cache") app.cache = filecache.NewCache(bfs, 1*time.Hour, "") + prune := time.NewTicker(15 * time.Minute) ticker := time.NewTicker(300 * time.Millisecond) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, os.Interrupt) @@ -85,11 +97,14 @@ func main() { app.mpdw.Close() app.mpdc() app.voice.Leave(ctx) + app.cache.Prune(false) dgvc() app.discord.Close() return case <-ticker.C: app.events.Emit("tick") + case <-prune.C: + app.cache.Prune(false) } } } diff --git a/discord.go b/discord.go index 42e3966..fa83588 100644 --- a/discord.go +++ b/discord.go @@ -50,7 +50,7 @@ func init() { app.discord = s app.voice = v - discordspeaker.Init(v) + discordspeaker.Init(v, config.GetInt("discord.bitrate")) log.Println("discord.go done.") } diff --git a/events.go b/events.go index 97660ff..5d83398 100644 --- a/events.go +++ b/events.go @@ -2,19 +2,16 @@ package main import ( "context" - "dndmusicbot/opus" - discordspeaker "dndmusicbot/speaker" "encoding/json" + "fmt" "log" "math/rand" "net/url" - "os" + "path/filepath" "strconv" "time" "github.com/google/uuid" - "github.com/gopxl/beep" - "github.com/gopxl/beep/effects" "github.com/kataras/go-events" "github.com/steino/gompd/v2/mpd" "golang.org/x/time/rate" @@ -271,31 +268,29 @@ func (app *App) ambiancePlay(payload ...interface{}) { return } - f, err := os.Open(amb.Path) + err = app.mpd.Partition("ambiance") if err != nil { log.Println(err) return } - play, err := opus.New(f) + fmt.Println(filepath.Join(cwd, amb.Path)) + err = app.mpd.Add(filepath.Join(cwd, amb.Path)) if err != nil { log.Println(err) return } - loop := beep.Loop(-1, play) - - volume := &effects.Volume{ - Streamer: loop, - Base: 2, - Volume: -2.5, - Silent: false, + err = app.mpd.Play(0) + if err != nil { + log.Println(err) + return } - discordspeaker.Lock() - app.ambiance.Clear() - app.ambiance.Add(volume) - discordspeaker.Unlock() + err = app.mpd.Partition("default") + if err != nil { + log.Fatal(err) + } msg := make(map[string]interface{}) out := make(map[string]interface{}) @@ -310,9 +305,19 @@ func (app *App) ambiancePlay(payload ...interface{}) { func (app *App) ambianceStop(payload ...interface{}) { log.Println("Stopping ambiance") - discordspeaker.Lock() - app.ambiance.Clear() - discordspeaker.Unlock() + + err := app.mpd.Partition("ambiance") + if err != nil { + log.Fatal(err) + } + + app.mpd.Stop() + app.mpd.Clear() + + err = app.mpd.Partition("default") + if err != nil { + log.Fatal(err) + } msg := make(map[string]interface{}) msg["event"] = "ambiance_stop" @@ -554,12 +559,6 @@ func (app *App) loadPlaylist(payload ...interface{}) { for _, ytinfo := range list.Videos { log.Printf("Adding %s (%s - %s)", ytinfo.ID, ytinfo.Author, ytinfo.Title) - vinfo, err := app.youtube.GetVideoFromID(ytinfo.ID) - if err != nil { - log.Println(err) - continue - } - // Run as a local function so we can defer the mutex unlock incase one of these errors. ok := func() (ok bool) { app.plmutex.Lock() @@ -572,7 +571,7 @@ func (app *App) loadPlaylist(payload ...interface{}) { ok = true // state:stop - songid, err := app.mpd.AddID(vinfo.GetHLSPlaylist("234"), 0) + songid, err := app.mpd.AddID("http://localhost:"+config.GetString("web.port")+"/youtube/"+ytinfo.ID, 0) if err != nil { log.Println(err) return @@ -608,6 +607,8 @@ func (app *App) loadPlaylist(payload ...interface{}) { app.mpd.Play(-1) + time.Sleep(time.Second) + return }() diff --git a/go.mod b/go.mod index ac2ca4d..f5f6a67 100644 --- a/go.mod +++ b/go.mod @@ -24,10 +24,10 @@ require ( github.com/markbates/goth v1.76.1 github.com/peterhellberg/link v1.2.0 github.com/pkg/errors v0.9.1 + github.com/romantomjak/shoutcast v1.2.0 github.com/spf13/afero v1.9.5 github.com/spf13/viper v1.16.0 github.com/steino/gompd/v2 v2.3.1 - github.com/tejzpr/ordered-concurrently/v3 v3.0.1 golang.org/x/exp v0.0.0-20230321023759-10a507213a29 golang.org/x/net v0.15.0 golang.org/x/time v0.3.0 @@ -62,6 +62,7 @@ require ( github.com/gorilla/schema v1.2.0 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/icza/bitio v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jdkato/prose v1.2.1 // indirect @@ -69,11 +70,12 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mewkiz/flac v1.0.9 // indirect + github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/niklasfasching/go-org v1.6.5 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect diff --git a/go.sum b/go.sum index 299196d..cf04168 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE= +github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -264,6 +265,9 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/bitio v1.1.0 h1:ysX4vtldjdi3Ygai5m1cWy4oLkhWTAi+SyO6HC8L9T0= +github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A= +github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= @@ -282,6 +286,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jszwec/csvutil v1.5.1/go.mod h1:Rpu7Uu9giO9subDyMCIQfHVDuLrcaC36UA4YcJjGBkg= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kataras/go-events v0.0.3 h1:o5YK53uURXtrlg7qE/vovxd/yKOJcLuFtPQbf1rYMC4= @@ -324,7 +329,11 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= +github.com/mewkiz/flac v1.0.9 h1:/+wxe/2fA8YbD1kjJNhlVAyc6pWaX0XXZC4xCF5OtVY= +github.com/mewkiz/flac v1.0.9/go.mod h1:l7dt5uFY724eKVkHQtAJAQSkhpC3helU3RDxN0ESAqo= github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14 h1:tnAPMExbRERsyEYkmR1YjhTgDM0iqyiBYf8ojRXxdbA= +github.com/mewkiz/pkg v0.0.0-20230226050401-4010bf0fec14/go.mod h1:QYCFBiH5q6XTHEbWhR0uhR3M9qNPoD2CSQzr0g75kE4= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= @@ -340,8 +349,6 @@ github.com/niklasfasching/go-org v1.6.5 h1:5YAIqNTdl6lAOb7lD2AyQ1RuFGPVrAKvUexph github.com/niklasfasching/go-org v1.6.5/go.mod h1:ybv0eGDnxylFUfFE+ySaQc734j/L3+/ChKZ/h63a2wM= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38 h1:4Upfs5rLQdx7KwBct3bmPYAhWsDDJdx660gYb7Lv9TQ= -github.com/oov/audio v0.0.0-20171004131523-88a2be6dbe38/go.mod h1:Xj06yMta9R1RSKiHmxL0Bo2TB8wiKVnMgA0KVopHHkk= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= @@ -360,6 +367,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/romantomjak/shoutcast v1.2.0 h1:MrksaqAd2De3rwvhlkzY8eUEaRYmkvhy9OH0sqM/w74= +github.com/romantomjak/shoutcast v1.2.0/go.mod h1:U4Gem8Jbwbz/A2ml2DYOGK0tVpnfQ5sQpSY3UWXcZM4= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= @@ -398,8 +407,6 @@ github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZy github.com/tdewolff/parse/v2 v2.6.4/go.mod h1:woz0cgbLwFdtbjJu8PIKxhW05KplTFQkOdX78o+Jgrs= github.com/tdewolff/test v1.0.7 h1:8Vs0142DmPFW/bQeHRP3MV19m1gvndjUb1sn8yy74LM= github.com/tdewolff/test v1.0.7/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= -github.com/tejzpr/ordered-concurrently/v3 v3.0.1 h1:TLHtzlQEDshbmGveS8S+hxLw4s5u67aoJw5LLf+X2xY= -github.com/tejzpr/ordered-concurrently/v3 v3.0.1/go.mod h1:mu/neZ6AGXm5jdPc7PEgViYK3rkYNPvVCEm15Cx/iRI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -443,8 +450,8 @@ golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZ golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -507,6 +514,7 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -579,11 +587,13 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -594,6 +604,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/mpd.go b/mpd.go index d4b5cbd..9cb373d 100644 --- a/mpd.go +++ b/mpd.go @@ -6,8 +6,10 @@ import ( discordspeaker "dndmusicbot/speaker" "io" "log" + "net/http" "os" "os/exec" + "path/filepath" "strconv" "syscall" "text/template" @@ -19,8 +21,10 @@ import ( ) type MPD struct { - file int - f beep.Format + file int + f beep.Format + httpClient *http.Client + pcm *io.PipeReader } func init() { @@ -36,7 +40,10 @@ func init() { log.Fatal(err) } - err = t.Execute(f, config.GetStringMapString("mpd")) + mpd_conf := config.GetStringMapString("mpd") + mpd_conf["proxy_port"] = strconv.Itoa(proxy_port) + + err = t.Execute(f, mpd_conf) if err != nil { log.Fatal(err) } @@ -90,6 +97,11 @@ func init() { log.Fatal(err) } + err = app.mpd.NewPartition("ambiance") + if err != nil { + log.Fatal(err) + } + app.mpd.Repeat(true) app.mpd.Random(true) @@ -98,6 +110,46 @@ func init() { log.Fatal(err) } + // To force the httpd stream to start. + err = app.mpd.Add(filepath.Join(cwd, "mp3/silence.mp3")) + if err != nil { + log.Println(err) + return + } + + app.mpd.Play(0) + time.Sleep(1 * time.Second) + app.mpd.Stop() + app.mpd.Clear() + + err = app.mpd.Partition("ambiance") + if err != nil { + log.Fatal(err) + } + + app.mpd.Repeat(true) + + err = app.mpd.MoveOutput("ambiance") + if err != nil { + log.Fatal(err) + } + + // To force the httpd stream to start. + err = app.mpd.Add(filepath.Join(cwd, "mp3/silence.mp3")) + if err != nil { + log.Println(err) + return + } + + app.mpd.Play(0) + time.Sleep(1 * time.Second) + app.mpd.Stop() + app.mpd.Clear() + err = app.mpd.Partition("default") + if err != nil { + log.Fatal(err) + } + app.mpdw, err = mpd.NewWatcher("unix", config.GetString("mpd.sock"), "") if err != nil { log.Fatal(err) @@ -118,15 +170,15 @@ func init() { log.Println("mpd.go done.") } -var MPD_PCM *io.PipeReader - -func NewMPD() (*MPD, error) { +func NewMPD(name string) (*MPD, error) { out := new(MPD) - var pcm *io.PipeWriter - MPD_PCM, pcm = io.Pipe() + out.httpClient = &http.Client{} - f, err := syscall.Open(config.GetString("mpd.fifo"), syscall.O_CREAT|syscall.O_RDONLY|syscall.O_CLOEXEC|syscall.O_NONBLOCK, 0644) + var pcm *io.PipeWriter + out.pcm, pcm = io.Pipe() + + f, err := syscall.Open(name, syscall.O_CREAT|syscall.O_RDONLY|syscall.O_CLOEXEC|syscall.O_NONBLOCK, 0644) if err != nil { return nil, err } @@ -159,11 +211,11 @@ func (m *MPD) Err() error { } func (m *MPD) Stream(samples [][2]float64) (n int, ok bool) { - tmp := make([]byte, m.f.NumChannels+2) + tmp := make([]byte, m.f.Width()) for i := range samples { //dn, err := syscall.Read(m.file, tmp) - dn, err := MPD_PCM.Read(tmp) + dn, err := m.pcm.Read(tmp) if dn == len(tmp) { samples[i], _ = m.f.DecodeSigned(tmp) ok = true diff --git a/queue.go b/queue.go index 9a1bbc1..60fe44b 100644 --- a/queue.go +++ b/queue.go @@ -3,6 +3,8 @@ package main import ( "log" + "dndmusicbot/snapcast" + "github.com/gopxl/beep" "github.com/gopxl/beep/effects" @@ -14,24 +16,27 @@ var amb_volume *effects.Volume func init() { log.Println("beep.go loading..") - - app.ambiance = beep.Mixer{} + amb := beep.Mixer{} amb_volume = &effects.Volume{ - Streamer: &app.ambiance, + Streamer: &amb, Base: 2, - Volume: 2, + Volume: -2, Silent: false, } + discordspeaker.Play(amb_volume) - mpdstream, err := NewMPD() + amb_stream, err := snapcast.New("127.0.0.1", config.GetInt("mpd.ambiance")) if err != nil { log.Fatal(err) } + amb.Add(amb_stream) + + pl := beep.Mixer{} pl_volume = &effects.Volume{ - Streamer: mpdstream, + Streamer: &pl, Base: 2, Volume: -2, Silent: false, @@ -39,5 +44,12 @@ func init() { discordspeaker.Play(pl_volume) + pl_stream, err := snapcast.New("127.0.0.1", config.GetInt("mpd.playlist")) + if err != nil { + log.Fatal(err) + } + + pl.Add(pl_stream) + log.Println("beep.go done.") } diff --git a/routes.go b/routes.go index 98e1fe4..58cf005 100644 --- a/routes.go +++ b/routes.go @@ -6,6 +6,7 @@ import ( "io" "log" "net/http" + "net/netip" "net/url" "os" "path" @@ -58,6 +59,7 @@ func init() { app.router.GET("/public/*js", auth(app.ServeFiles)) app.router.GET("/css/*css", auth(app.ServeFiles)) app.router.GET("/auth/callback", app.AuthHandler) + app.router.GET("/youtube/:id", local(ProxyTube)) app.router.HandlerFunc("GET", "/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("WS connection from %v\n", r.RemoteAddr) @@ -74,7 +76,7 @@ func init() { })) go func() { - log.Fatal(http.ListenAndServe(":8824", app.router)) + log.Fatal(http.ListenAndServe(":"+config.GetString("web.port"), app.router)) }() } @@ -156,8 +158,31 @@ func (app App) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter. } } +// Middleware to check that the connection is local. +func local(n httprouter.Handle) httprouter.Handle { + return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + addr, err := netip.ParseAddrPort(r.RemoteAddr) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + switch { + case addr.Addr().IsLoopback(): + fallthrough + case addr.Addr().IsPrivate(): + n(w, r, ps) + default: + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + } + } +} + func auth(n httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { + if os.Getenv("APP_ENV") == "test" { + n(w, r, ps) + return + } values := r.URL.Query() values.Add("provider", "discord") r.URL.RawQuery = values.Encode() diff --git a/snapcast/client.go b/snapcast/client.go new file mode 100644 index 0000000..49fc2ce --- /dev/null +++ b/snapcast/client.go @@ -0,0 +1,79 @@ +package snapcast + +import ( + "context" + "io" + "log" + "os" + "os/exec" + "strconv" + "sync" + + "github.com/gopxl/beep" +) + +type Stream struct { + sync.Mutex + + Out io.ReadCloser + Cancel context.CancelFunc + Cmd *exec.Cmd + + f beep.Format + buf []byte +} + +func New(host string, port int) (ff beep.Streamer, err error) { + st := new(Stream) + ctx, cancel := context.WithCancel(context.Background()) + st.Cancel = cancel + + st.Cmd = exec.CommandContext( + ctx, + "/usr/bin/snapclient", + "-h", host, + "-p", strconv.Itoa(port), + "--player", "file:filename=stderr", + "--logfilter", "*:error", + ) + + st.Cmd.Stdout = os.Stdin + + st.Out, err = st.Cmd.StderrPipe() + if err != nil { + return nil, err + } + + err = st.Cmd.Start() + if err != nil { + return nil, err + } + + st.buf = make([]byte, 4) + st.f = beep.Format{ + SampleRate: beep.SampleRate(48000), + NumChannels: 2, + Precision: 2, + } + + return st, nil +} + +func (d *Stream) Stream(samples [][2]float64) (n int, ok bool) { + for i := range samples { + _, err := d.Out.Read(d.buf) + if err != nil { + log.Println(err) + ok = false + break + } + samples[i], _ = d.f.DecodeSigned(d.buf) + ok = true + } + + return len(samples), ok +} + +func (d *Stream) Err() error { + return nil +} diff --git a/speaker/discord.go b/speaker/discord.go index 5451844..d50859e 100644 --- a/speaker/discord.go +++ b/speaker/discord.go @@ -2,7 +2,6 @@ package discordspeaker import ( "context" - "fmt" "io" "log" "sync" @@ -14,8 +13,6 @@ import ( "github.com/pkg/errors" "golang.org/x/time/rate" "gopkg.in/hraban/opus.v2" - - cc "github.com/tejzpr/ordered-concurrently/v3" ) var ( @@ -30,16 +27,20 @@ var ( maxBytes int = (frameSize * 2) * 2 buf []byte session *voice.Session - spk bool - input chan cc.WorkFunction pw *io.PipeWriter pr *io.PipeReader - spklimit = rate.NewLimiter(rate.Every(2*time.Second), 1) + + spklimit = rate.NewLimiter(rate.Every(5*time.Second), 1) + spk = true ) +var start time.Time + var Silence = [2]float64{} -func Init(dgv *voice.Session) error { +var dmutex = sync.Mutex{} + +func Init(dgv *voice.Session, bitrate int) error { var err error mu.Lock() @@ -53,51 +54,37 @@ func Init(dgv *voice.Session) error { session = dgv - encoder, err = opus.NewEncoder(sampleRate, channels, opus.AppVoIP) - encoder.SetBitrateToMax() + encoder, err = opus.NewEncoder(sampleRate, channels, opus.AppAudio) + encoder.SetBitrate(bitrate) if err != nil { return errors.Wrap(err, "failed to initialize speaker") } - input = make(chan cc.WorkFunction) - ctx := context.Background() - output := cc.Process(ctx, input, &cc.Options{PoolSize: 4, OutChannelBuffer: 4}) - go func() { for { select { default: update() - case out := <-output: - fmt.Println(pw.Write(out.Value.([]byte))) case <-done: return } } }() - go ReadSend() + go func() { + dmutex.Lock() + _, err := io.Copy(session, pr) + dmutex.Unlock() + if err != nil { + log.Println(err) + return + } + }() return nil } -func ReadSend() { - for { - buf := make([]byte, maxBytes) - n, err := pr.Read(buf) - if err != nil { - log.Println(err) - return - } - _, err = session.Write(buf[:n]) - if err != nil { - log.Println(err) - return - } - } -} - func Close() { } @@ -132,41 +119,57 @@ func Speak(s bool) { } -func update() { - mu.Lock() - mixer.Stream(samples) - mu.Unlock() - +func CheckSilence(samples [][2]float64) { if IsSilent(samples) { if spk && spklimit.Allow() { log.Println("Notspeaking") session.Speaking(context.Background(), voicegateway.NotSpeaking) spk = false } + } else { + if !spk { + log.Println("Speaking") + session.Speaking(context.Background(), voicegateway.Microphone) + spk = true + spklimit.Reserve() + } + } +} +func update() { + start = time.Now() + mu.Lock() + mixer.Stream(samples) + mu.Unlock() + + go CheckSilence(samples) + + if !spk { return } - if !spk && spklimit.Allow() { - log.Println("Speaking") - session.Speaking(context.Background(), voicegateway.Microphone) - spk = true + f32 := make([]float32, len(samples)*2) + var idx int + + for i := 0; i < len(samples); i++ { + f32[idx] = float32(samples[i][0]) + idx++ + f32[idx] = float32(samples[i][1]) + idx++ } - - var f32 []float32 - - for _, sample := range samples { - f32 = append(f32, float32(sample[0])) - f32 = append(f32, float32(sample[1])) - } - n, err := encoder.EncodeFloat32(f32, buf) if err != nil { log.Println(err) return } - pw.Write(buf[:n]) + _, err = pw.Write(buf[:n]) + if err != nil { + log.Println(err) + return + } + //fmt.Println(time.Since(start), len(samples), n) + } func IsSilent(in [][2]float64) bool { diff --git a/tmpl/mpd.tmpl b/tmpl/mpd.tmpl index b6a7dfb..f5f15a2 100644 --- a/tmpl/mpd.tmpl +++ b/tmpl/mpd.tmpl @@ -1,7 +1,33 @@ +# audio_output { +# type "fifo" +# name "fifo-output" +# path "{{ .fifo }}" +# format "48000:16:2" +# enabled "yes" +#} audio_output { - type "fifo" - name "fifo-output" - path "{{ .fifo }}" + type "snapcast" + name "playlist" + bind_to_address "0.0.0.0" + port "{{ .playlist }}" + zeroconf "no" + format "48000:16:2" + enabled "yes" + always_on "yes" +} +audio_output { + type "snapcast" + name "ambiance" + bind_to_address "0.0.0.0" + port "{{ .ambiance }}" + zeroconf "no" + format "48000:16:2" + enabled "yes" + always_on "yes" +} +input { + plugin "curl" + verbose "yes" } decoder { plugin "wildmidi" @@ -21,6 +47,6 @@ resampler { } volume_normalization "yes" -audio_output_format "48000:16:2" +# audio_output_format "48000:16:2" bind_to_address "{{ .sock }}" pid_file "{{ .pid }}" diff --git a/youtube.go b/youtube.go index 8c1fe18..151a6f4 100644 --- a/youtube.go +++ b/youtube.go @@ -1,8 +1,16 @@ package main import ( + "bytes" "dndmusicbot/youtube" + "io" "log" + "net/http" + "os" + "time" + + "github.com/grafov/m3u8" + "github.com/julienschmidt/httprouter" ) func init() { @@ -12,3 +20,63 @@ func init() { log.Println("youtube.go done.") } + +func ProxyTube(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + os.MkdirAll("cache", 0755) + _, data, err := app.cache.GetOrCreateBytes("youtube."+p.ByName("id"), func() (out []byte, err error) { + vinfo, err := app.youtube.GetVideoFromID(p.ByName("id")) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + uri := vinfo.GetHLSPlaylist("234") + + resp, err := http.Get(uri) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + defer resp.Body.Close() + pl, _, err := m3u8.DecodeFrom(resp.Body, true) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + var segresp *http.Response + mediapl := pl.(*m3u8.MediaPlaylist) + + buf := new(bytes.Buffer) + for _, segment := range mediapl.GetAllSegments() { + segresp, err = http.Get(segment.URI) + if err != nil { + segresp.Body.Close() + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, err = io.Copy(buf, segresp.Body) + if err != nil { + segresp.Body.Close() + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + return buf.Bytes(), nil + }) + if err != nil { + log.Println(err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + seeker := bytes.NewReader(data) + w.Header().Add("Content-Type", "audio/mp4") + http.ServeContent(w, r, "youtube."+p.ByName("id"), time.Now(), seeker) +} diff --git a/youtube/youtube.go b/youtube/youtube.go index 7f0c1a4..fa3fb06 100644 --- a/youtube/youtube.go +++ b/youtube/youtube.go @@ -113,6 +113,14 @@ func (c Client) GetVideoFromID(id string) (out Video, err error) { return } +func (v Video) GetUrlByItag(itagNo int64) string { + for _, format := range v.StreamingData.AdaptiveFormats { + if format.Itag == itagNo { + return format.URL + } + } + return "" +} func (v Video) GetHLSPlaylist(itag string) (out string) { resp, err := http.Get(v.StreamingData.HlsManifestURL) diff --git a/ytdl.go b/ytdl.go index 895fde4..93b1c6d 100644 --- a/ytdl.go +++ b/ytdl.go @@ -18,6 +18,31 @@ import ( var prate = rate.Sometimes{Interval: 1 * time.Second} var yturl = "https://youtu.be/%s" +func NewYTdlUrl(vid string) ([]byte, error) { + ytdl := config.GetString("youtube.ytdl") + yt := exec.Command( + ytdl, + fmt.Sprintf(yturl, vid), + "--cookies", "./cookies.txt", + "--no-call-home", + "--no-cache-dir", + "--ignore-errors", + "--newline", + "--restrict-filenames", + "-f", "234", + "--get-url", + ) + + yt.Stderr = os.Stderr + + uri, err := yt.Output() + if err != nil { + return nil, err + } + + return uri[:len(uri)-1], nil +} + func NewYTdl(vid string) ([]byte, error) { tmpfile, err := exec.Command("mktemp", "/tmp/dnd_ytdlp_XXXXXXXXXXXX.m4a").Output() if err != nil {