Compare commits

..

No commits in common. "master" and "youtube" have entirely different histories.

9 changed files with 165 additions and 193 deletions

3
.gitignore vendored
View File

@ -16,6 +16,3 @@ ambiance
public/ public/
node_modules/ node_modules/
src/node_modules src/node_modules
*.aac
*.opus
*.mp3

View File

@ -1,61 +1,18 @@
FROM golang:1.21-bookworm as builder FROM golang:1.21-bookworm as builder
ENV FFMPEG_VERSION=5.1.2
ENV MPD_VERSION=0.23.14
RUN apt-get update && apt-get -y install \
libopus-dev libopusfile-dev \
meson g++ nasm \
libfmt-dev \
libpcre2-dev \
libmad0-dev libmpg123-dev libid3tag0-dev \
libflac-dev libvorbis-dev libopus-dev libogg-dev \
libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \
libsamplerate0-dev libsoxr-dev \
libcurl4-gnutls-dev \
libboost-dev \
zlib1g-dev
WORKDIR /tmp
RUN curl -LOs http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz && tar xvf ffmpeg-${FFMPEG_VERSION}.tar.xz && \
cd ffmpeg-${FFMPEG_VERSION} && \
./configure \
--enable-version3 \
--enable-gpl \
--enable-nonfree \
--enable-small \
--enable-libvorbis \
--enable-libopus \
--enable-postproc \
--enable-openssl \
--disable-debug && \
make && \
make install
RUN curl -LOs https://www.musicpd.org/download/mpd/0.23/mpd-${MPD_VERSION}.tar.xz && tar xvf mpd-${MPD_VERSION}.tar.xz && \
cd mpd-${MPD_VERSION} && \
meson . output/release --buildtype=minsize -Db_ndebug=true && \
ninja -C output/release
WORKDIR /src WORKDIR /src
COPY . . COPY . .
RUN go build RUN apt-get update && apt-get -y install libopus-dev libopusfile-dev && \
go build
FROM debian:bookworm-slim FROM debian:bookworm-slim
RUN apt-get update && apt-get -y install \ RUN apt-get update && apt-get -y install \
ca-certificates \ ca-certificates \
libopus-dev libopusfile-dev \ libopus-dev libopusfile-dev \
libfmt-dev \ mpd ffmpeg curl && \
libpcre2-dev \
libmad0-dev libmpg123-dev libid3tag0-dev \
libflac-dev libvorbis-dev libopus-dev libogg-dev \
libadplug-dev libaudiofile-dev libsndfile1-dev libfaad-dev \
libsamplerate0-dev libsoxr-dev \
libcurl4-gnutls-dev \
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 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 && \ 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 && rm -rf /tmp/*.deb apt -y install /tmp/snapcast.deb /tmp/libflac8.deb
COPY --from=builder /src/dndmusicbot /app/ COPY --from=builder /src/dndmusicbot /app/
COPY --from=builder /tmp/mpd-0.23.14/output/release/mpd /usr/local/bin
COPY --from=builder /usr/local/bin/ffmpeg /usr/local/bin
COPY mp3 /app/mp3
ADD tmpl /app/tmpl ADD tmpl /app/tmpl
WORKDIR /app WORKDIR /app
ENTRYPOINT [ "/app/dndmusicbot" ] ENTRYPOINT [ "/app/dndmusicbot" ]

View File

@ -69,17 +69,20 @@ func GetAmbiances() (amb []Ambiance, err error) {
func AddAmbiance(uri, title string) (Ambiance, error) { func AddAmbiance(uri, title string) (Ambiance, error) {
var amb Ambiance var amb Ambiance
ev := Event{"ambiance_add_start", map[string]string{ msg := make(map[string]interface{})
"name": title, msg["event"] = "ambiance_add_start"
}} data := make(map[string]string)
data["name"] = title
go ws.SendEvent(ev) msg["payload"] = data
ws_msg <- msg
defer func() { defer func() {
ev = Event{"ambiance_add_finish", map[string]string{ msg = make(map[string]interface{})
"name": title, msg["event"] = "ambiance_add_finish"
}} data = make(map[string]string)
go ws.SendEvent(ev) data["name"] = title
msg["payload"] = data
ws_msg <- msg
}() }()
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.opus").Output() tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.opus").Output()
@ -131,13 +134,16 @@ func AddAmbiance(uri, title string) (Ambiance, error) {
log.Printf("Start ffmpeg to extract audio to %s", string(tmpfile)) log.Printf("Start ffmpeg to extract audio to %s", string(tmpfile))
ev = Event{"ambiance_encode_start", map[string]string{ msg = make(map[string]interface{})
"name": title, msg["event"] = "ambiance_encode_start"
}} data = make(map[string]string)
data["name"] = title
msg["payload"] = data
ws_msg <- msg
go ws.SendEvent(ev) msg = make(map[string]interface{})
msg["event"] = "ambiance_encode_progress"
data := make(map[string]string) data = make(map[string]string)
data["name"] = title data["name"] = title
scanner := bufio.NewScanner(ffprogress) scanner := bufio.NewScanner(ffprogress)
@ -156,7 +162,8 @@ func AddAmbiance(uri, title string) (Ambiance, error) {
data["percent"] = percent data["percent"] = percent
} }
go ws.SendEvent(Event{"ambiance_encode_progress", data}) msg["payload"] = data
ws_msg <- msg
}) })
} }
@ -169,14 +176,15 @@ func AddAmbiance(uri, title string) (Ambiance, error) {
return amb, err return amb, err
} }
ev = Event{"ambiance_encode_complete", map[string]string{ msg = make(map[string]interface{})
"name": title, msg["event"] = "ambiance_encode_complete"
}} data = make(map[string]string)
data["name"] = title
go ws.SendEvent(ev) msg["payload"] = data
ws_msg <- msg
id := uuid.New() id := uuid.New()
fn := filepath.Join(config.GetString("ambiance.path"), fmt.Sprintf("%s.aac", id.String())) fn := filepath.Join(config.GetString("ambiance.path"), fmt.Sprintf("%s.opus", id.String()))
log.Printf("Moving to %s", fn) log.Printf("Moving to %s", fn)

121
events.go
View File

@ -148,70 +148,74 @@ func (app *App) volset(payload ...interface{}) {
amb_volume.Volume = vol amb_volume.Volume = vol
} }
ev := Event{"volume", map[string]float64{ app.sendVolume()
"playlist": pl_volume.Volume,
"ambiance": amb_volume.Volume,
}}
go ws.SendEvent(ev)
} }
func (app *App) songInfoEvent(event string) (ev Event, err error) { func (app *App) sendVolume() {
ev.Event = event msg := make(map[string]interface{})
out := make(map[string]float64)
msg["event"] = "volume"
out["playlist"] = pl_volume.Volume
out["ambiance"] = amb_volume.Volume
msg["payload"] = out
ws_msg <- msg
}
func (app *App) songInfoEvent(event string) map[string]interface{} {
msg := make(map[string]interface{})
msg["event"] = event
status, err := app.mpd.Status() status, err := app.mpd.Status()
if err != nil { if err != nil {
return log.Println(err)
return nil
} }
cur, err := app.mpd.CurrentSong() cur, err := app.mpd.CurrentSong()
if err != nil { if err != nil {
return log.Println(err)
return nil
} }
info := new(SongInfo) info := new(SongInfo)
if status["state"] != "play" { if status["state"] != "play" {
info.Pause = true info.Pause = true
ev.Payload = info msg["payload"] = info
return return msg
} }
duration, ok := status["duration"] duration, ok := status["duration"]
if ok && duration != "" { if ok && duration != "" {
var slen float64 slen, err := strconv.ParseFloat(duration, 64)
slen, err = strconv.ParseFloat(duration, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return nil
} }
info.Length = time.Duration(slen * float64(time.Second)).Milliseconds() info.Length = time.Duration(slen * float64(time.Second)).Milliseconds()
} }
elapsed, ok := status["elapsed"] elapsed, ok := status["elapsed"]
if ok && elapsed != "" { if ok && elapsed != "" {
var spos float64 spos, err := strconv.ParseFloat(elapsed, 64)
spos, err = strconv.ParseFloat(elapsed, 64)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return nil
} }
info.Position = time.Duration(spos * float64(time.Second)).Milliseconds() info.Position = time.Duration(spos * float64(time.Second)).Milliseconds()
} }
album, ok := cur["Album"] album, ok := cur["Album"]
if ok { if ok {
var plid uuid.UUID plid, err := uuid.ParseBytes([]byte(album))
plid, err = uuid.ParseBytes([]byte(album))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return nil
} }
var pl *Playlist
pl, err = app.GetPlaylist(plid) pl, err := app.GetPlaylist(plid)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return return nil
} }
info.Playlist = pl.Id info.Playlist = pl.Id
@ -233,9 +237,9 @@ func (app *App) songInfoEvent(event string) (ev Event, err error) {
info.Song = location info.Song = location
} }
ev.Payload = *info msg["payload"] = *info
return ev, nil return msg
} }
func (app *App) ambiancePlay(payload ...interface{}) { func (app *App) ambiancePlay(payload ...interface{}) {
@ -288,13 +292,15 @@ func (app *App) ambiancePlay(payload ...interface{}) {
log.Fatal(err) log.Fatal(err)
} }
msg := make(map[string]interface{})
out := make(map[string]interface{})
app.curamb = amb app.curamb = amb
ev := Event{"ambiance_play", map[string]string{ msg["event"] = "ambiance_play"
"id": id, out["id"] = id
}} msg["payload"] = out
ws_msg <- msg
go ws.SendEvent(ev)
} }
func (app *App) ambianceStop(payload ...interface{}) { func (app *App) ambianceStop(payload ...interface{}) {
@ -313,7 +319,10 @@ func (app *App) ambianceStop(payload ...interface{}) {
log.Fatal(err) log.Fatal(err)
} }
go ws.SendEvent(Event{"ambiance_stop", nil}) msg := make(map[string]interface{})
msg["event"] = "ambiance_stop"
ws_msg <- msg
} }
func (app *App) ambianceAdd(payload ...interface{}) { func (app *App) ambianceAdd(payload ...interface{}) {
@ -350,12 +359,13 @@ func (app *App) ambianceAdd(payload ...interface{}) {
return return
} }
ev := Event{"ambiance_add", map[string]string{ msg := make(map[string]interface{})
"title": amb.Title, out := make(map[string]interface{})
"id": amb.Id, msg["event"] = "ambiance_add"
}} out["title"] = amb.Title
out["id"] = amb.Id
go ws.SendEvent(ev) msg["payload"] = out
ws_msg <- msg
} }
func (app *App) songPosition(payload ...interface{}) { func (app *App) songPosition(payload ...interface{}) {
@ -370,16 +380,18 @@ func (app *App) songPosition(payload ...interface{}) {
} }
l.Do(func() { l.Do(func() {
msg := make(map[string]interface{})
out := make(map[string]interface{})
slen, _ := strconv.ParseFloat(status["duration"], 64) slen, _ := strconv.ParseFloat(status["duration"], 64)
spos, _ := strconv.ParseFloat(status["elapsed"], 64) spos, _ := strconv.ParseFloat(status["elapsed"], 64)
ev := Event{"song_position", map[string]int64{ msg["event"] = "song_position"
"len": time.Duration(slen * float64(time.Second)).Milliseconds(), out["len"] = time.Duration(slen * float64(time.Second)).Milliseconds()
"position": time.Duration(spos * float64(time.Second)).Milliseconds(), out["position"] = time.Duration(spos * float64(time.Second)).Milliseconds()
}}
go ws.SendEvent(ev) msg["payload"] = out
ws_msg <- msg
}) })
} }
@ -387,12 +399,10 @@ func (app *App) songPosition(payload ...interface{}) {
func (app *App) songInfo(payload ...interface{}) { func (app *App) songInfo(payload ...interface{}) {
log.Println("song_info event received") log.Println("song_info event received")
ev, err := app.songInfoEvent("song_info") msg := app.songInfoEvent("song_info")
if err != nil { if msg != nil {
log.Println(err) ws_msg <- msg
return
} }
go ws.SendEvent(ev)
} }
func (app *App) stop(payload ...interface{}) { func (app *App) stop(payload ...interface{}) {
@ -405,7 +415,10 @@ func (app *App) stop(payload ...interface{}) {
app.mpd.Stop() app.mpd.Stop()
app.plmutex.Unlock() app.plmutex.Unlock()
go ws.SendEvent(Event{"stop", nil}) msg := make(map[string]interface{})
msg["event"] = "stop"
ws_msg <- msg
} }
func (app *App) prevSong(payload ...interface{}) { func (app *App) prevSong(payload ...interface{}) {
@ -479,12 +492,12 @@ func (app *App) addPlaylist(payload ...interface{}) {
log.Println("Error getting youtube playlist info,", plid) log.Println("Error getting youtube playlist info,", plid)
} }
ev := Event{"new_playlist", map[string]string{ msg := make(map[string]interface{})
"url": id.String(),
"title": pltitle,
}}
go ws.SendEvent(ev) msg["event"] = "new_playlist"
msg["payload"] = map[string]string{"url": id.String(), "title": pltitle}
ws_msg <- msg
} }
func (app *App) loadPlaylist(payload ...interface{}) { func (app *App) loadPlaylist(payload ...interface{}) {

1
mpd.go
View File

@ -41,6 +41,7 @@ func init() {
} }
mpd_conf := config.GetStringMapString("mpd") mpd_conf := config.GetStringMapString("mpd")
mpd_conf["proxy_port"] = strconv.Itoa(proxy_port)
err = t.Execute(f, mpd_conf) err = t.Execute(f, mpd_conf)
if err != nil { if err != nil {

View File

@ -69,7 +69,7 @@ func init() {
return return
} }
err = ws.join(conn) err = handleWS(conn)
if err != nil { if err != nil {
log.Printf("WS connection closed, %v\n", r.RemoteAddr) log.Printf("WS connection closed, %v\n", r.RemoteAddr)
} }

View File

@ -1,6 +1,6 @@
{ {
"scripts": { "scripts": {
"build": "esbuild app/root.tsx --bundle --outfile=/public/script.js" "build": "esbuild app/root.tsx --bundle --outfile=../public_test/script.js"
}, },
"dependencies": { "dependencies": {
"@types/react": "^18.2.33", "@types/react": "^18.2.33",

66
ws.go
View File

@ -12,21 +12,18 @@ import (
"github.com/kataras/go-events" "github.com/kataras/go-events"
) )
type Websocket struct {
sync.Mutex
clients *bcast.Group
}
var ws *Websocket
func init() { func init() {
log.Println("ws.go loading..") log.Println("ws.go loading..")
ws = new(Websocket) go ws_clients.Broadcast(0)
ws_msg = make(chan interface{})
ws.clients = bcast.NewGroup() go func() {
var msg interface{}
go ws.clients.Broadcast(0) for {
msg = <-ws_msg
ws_clients.Send(msg)
}
}()
log.Println("ws.go done.") log.Println("ws.go done.")
} }
@ -36,19 +33,12 @@ type WSmsg struct {
Payload json.RawMessage Payload json.RawMessage
} }
type Event struct { var ws_clients = bcast.NewGroup()
Event string `json:"event"` var ws_msg chan interface{}
Payload any `json:"payload,omitempty"` var WSMutex = &sync.Mutex{}
}
func (ws *Websocket) SendEvent(e Event) { func handleWS(c *websocket.Conn) error {
ws.Lock() memb := ws_clients.Join()
ws.clients.Send(e)
ws.Unlock()
}
func (ws *Websocket) join(c *websocket.Conn) error {
memb := ws.clients.Join()
defer memb.Close() defer memb.Close()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -74,27 +64,29 @@ func (ws *Websocket) join(c *websocket.Conn) error {
return nil return nil
}) })
msg, err := app.songInfoEvent("song_info") msg := app.songInfoEvent("song_info")
if err != nil {
return err
}
vol := Event{"volume", map[string]float64{ vol := make(map[string]interface{})
"playlist": pl_volume.Volume, volout := make(map[string]float64)
"ambiance": amb_volume.Volume, vol["event"] = "volume"
}} volout["playlist"] = pl_volume.Volume
volout["ambiance"] = amb_volume.Volume
vol["payload"] = volout
c.SetWriteDeadline(time.Now().Add(10 * time.Second)) c.SetWriteDeadline(time.Now().Add(10 * time.Second))
c.WriteJSON(msg) c.WriteJSON(msg)
c.WriteJSON(vol) c.WriteJSON(vol)
if app.ambiance.Len() > 0 { if app.ambiance.Len() > 0 {
msg := Event{"ambiance_play", map[string]string{ msg := make(map[string]interface{})
"id": app.curamb.Id, out := make(map[string]interface{})
}} msg["event"] = "ambiance_play"
out["id"] = app.curamb.Id
msg["payload"] = out
c.WriteJSON(msg) c.WriteJSON(msg)
} else { } else {
msg := Event{"ambiance_stop", nil} msg := make(map[string]interface{})
msg["event"] = "ambiance_stop"
c.WriteJSON(msg) c.WriteJSON(msg)
} }
@ -105,6 +97,6 @@ func (ws *Websocket) join(c *websocket.Conn) error {
return err return err
} }
app.events.Emit(events.EventName(msg.Event), msg.Payload, memb) app.events.Emit(events.EventName(msg.Event), msg.Payload)
} }
} }

58
ytdl.go
View File

@ -1,42 +1,23 @@
package main package main
import ( import (
"bufio"
"encoding/json"
"fmt" "fmt"
"log"
"math"
"net/url" "net/url"
"os"
"os/exec"
"strconv"
"time" "time"
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
var prate = rate.Sometimes{Interval: 1 * time.Second} var prate = rate.Sometimes{Interval: 1 * time.Second}
var yturl = "https://youtu.be/%s"
// var yturl = "https://youtu.be/%s"
func YTUrl(uri string) (vid string, err error) {
u, err := url.Parse(uri)
if err != nil {
return "", err
}
switch u.Host {
case "youtu.be":
vid = u.Path[1:]
case "m.youtube.com":
fallthrough
case "youtube.com":
fallthrough
case "www.youtube.com":
vid = u.Query().Get("v")
}
if vid == "" {
return vid, fmt.Errorf("unable to parse vid")
}
return
}
/*
func NewYTdlUrl(vid string) ([]byte, error) { func NewYTdlUrl(vid string) ([]byte, error) {
ytdl := config.GetString("youtube.ytdl") ytdl := config.GetString("youtube.ytdl")
yt := exec.Command( yt := exec.Command(
@ -151,6 +132,29 @@ func NewYTdl(vid string) ([]byte, error) {
return tmpfile, nil return tmpfile, nil
} }
func YTUrl(uri string) (vid string, err error) {
u, err := url.Parse(uri)
if err != nil {
return "", err
}
switch u.Host {
case "youtu.be":
vid = u.Path[1:]
case "m.youtube.com":
fallthrough
case "youtube.com":
fallthrough
case "www.youtube.com":
vid = u.Query().Get("v")
}
if vid == "" {
return vid, fmt.Errorf("unable to parse vid")
}
return
}
/* /*
func DownloadAmbiance(uri string, name string) error { func DownloadAmbiance(uri string, name string) error {