Compare commits

...

3 Commits

15 changed files with 459 additions and 324 deletions

View File

@ -1,24 +1,216 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/davecheney/xattr"
"github.com/google/uuid"
)
func fnNoExt(fileName string) string {
return fileName[:len(fileName)-len(filepath.Ext(fileName))]
var httpClient = new(http.Client)
type Ambiance struct {
Id string
Title string
Path string
}
func GetAmbiance() ([]string, error) {
func GetAmbiance(id string) (amb Ambiance, err error) {
fp := filepath.Join("./ambiance", id+".opus")
_, err = os.Stat(fp)
if err != nil {
return
}
title, err := xattr.Getxattr(fp, "title")
if err != nil {
return
}
return Ambiance{
Id: id,
Title: string(title),
Path: fp,
}, nil
}
func GetAmbiances() (amb []Ambiance, err error) {
files, err := os.ReadDir("./ambiance")
if err != nil {
return nil, err
}
var out []string
for _, file := range files {
out = append(out, fnNoExt(file.Name()))
title, err := xattr.Getxattr(filepath.Join("./ambiance", file.Name()), "title")
if err != nil {
return nil, err
}
amb = append(amb, Ambiance{
Id: file.Name()[:len(file.Name())-len(filepath.Ext(file.Name()))],
Title: string(title),
Path: filepath.Join("./ambiance", file.Name()),
})
}
return out, nil
return
}
func AddAmbiance(uri, title string) (Ambiance, error) {
var amb Ambiance
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.opus").Output()
if err != nil {
return amb, err
}
tmpfile = tmpfile[:len(tmpfile)-1]
vid, err := YTUrl(uri)
if err != nil {
return amb, err
}
log.Printf("Start YTdl for %s", uri)
dluri, err := NewYTdl(vid)
if err != nil {
return amb, err
}
log.Printf("Start download.. %s", uri)
resp, err := httpClient.Get(string(dluri))
if err != nil {
return amb, err
}
defer resp.Body.Close()
ff := exec.Command(
"ffmpeg",
"-y",
"-i", "-",
"-vn",
"-acodec", "copy",
"-movflags", "+faststart",
"-t", "01:00:00",
"-v", "error",
// "-stats",
"-progress", "pipe:1",
// "-af", "loudnorm=I=-16:LRA=11:TP=-1.5",
string(tmpfile),
)
ffprogress, err := ff.StdoutPipe()
if err != nil {
return amb, err
}
ff.Stderr = os.Stderr
ff.Stdin = resp.Body
err = ff.Start()
if err != nil {
return amb, err
}
log.Printf("Start ffmpeg to extract audio to %s", string(tmpfile))
msg := make(map[string]interface{})
msg["event"] = "ambiance_encode_start"
data := make(map[string]string)
data["name"] = title
msg["payload"] = data
ws_msg <- msg
msg = make(map[string]interface{})
msg["event"] = "ambiance_encode_progress"
data = make(map[string]string)
data["name"] = title
scanner := bufio.NewScanner(ffprogress)
for scanner.Scan() {
p := strings.Split(scanner.Text(), "=")
if len(p) == 2 {
data[p[0]] = strings.TrimSpace(p[1])
}
prate.Do(func() {
msg["payload"] = data
ws_msg <- msg
})
}
if err := scanner.Err(); err != nil {
return amb, err
}
err = ff.Wait()
if err != nil {
return amb, err
}
msg = make(map[string]interface{})
msg["event"] = "ambiance_encode_complete"
data = make(map[string]string)
data["name"] = title
msg["payload"] = data
ws_msg <- msg
id := uuid.New()
fn := filepath.Join("./ambiance", fmt.Sprintf("%s.opus", id.String()))
log.Printf("Moving to %s", fn)
in, err := os.Open(string(tmpfile))
if err != nil {
return amb, err
}
of, err := os.Create(fn)
if err != nil {
return amb, err
}
_, err = io.Copy(of, in)
if err != nil {
return amb, err
}
err = of.Sync()
if err != nil {
return amb, err
}
err = of.Close()
if err != nil {
return amb, err
}
err = in.Close()
if err != nil {
return amb, err
}
err = os.Remove(string(tmpfile))
if err != nil {
return amb, err
}
log.Println("Setting xattr")
err = xattr.Setxattr(fn, "title", []byte(title))
if err != nil {
return amb, err
}
amb.Id = id.String()
amb.Title = title
log.Println("Return info.")
return amb, nil
}

2
bot.go
View File

@ -52,7 +52,7 @@ type App struct {
youtube *youtube.Service
queue *Queue
ambiance beep.Mixer
curamb string
curamb Ambiance
events events.EventEmmiter
next bool
db *pgx.Conn

View File

@ -1,272 +0,0 @@
package discord
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
"github.com/kataras/go-events"
"github.com/tidwall/gjson"
)
const (
GATEWAYBOTURL = "https://discord.com/api/gateway/bot"
)
const (
PermissionVoiceConnect = 0x0000000000100000
PermissionVoiceSpeak = 0x0000000000200000
PermissionSendMessages = 0x0000000000000800
)
type Connection struct {
events events.EventEmmiter
httpClient *http.Client
dialer *websocket.Dialer
conn *websocket.Conn
token string
seq int
ready chan bool
vstateUpdate chan bool
vserverUpdate chan bool
}
type VoiceConnection struct {
d *Connection
conn *websocket.Conn
sid string
e string
}
type VoiceStateUpdate struct {
Op int `json:"op"`
Data struct {
Guild string `json:"guild_id"`
Channel string `json:"channel_id"`
Mute bool `json:"self_mute"`
Deaf bool `json:"self_dead"`
} `json:"d"`
}
type Identify struct {
Op int `json:"op"`
Data struct {
Token string `json:"token"`
Intents int `json:"intents"`
Properties struct {
OS string `json:"os"`
Browser string `json:"browser"`
Device string `json:"device"`
} `json:"properties"`
} `json:"d"`
}
func NewConnection(token string) (*Connection, error) {
conn := new(Connection)
conn.token = token
conn.ready = make(chan bool)
conn.vserverUpdate = make(chan bool)
conn.vstateUpdate = make(chan bool)
conn.events = events.New()
conn.events.On("READY", conn.eventready)
conn.dialer = &websocket.Dialer{
HandshakeTimeout: 45 * time.Second,
}
conn.httpClient = &http.Client{}
gw, err := conn.GetGateway()
if err != nil {
return nil, err
}
conn.conn, _, err = conn.dialer.Dial(gw, nil)
if err != nil {
return nil, err
}
go func() {
for {
_, b, err := conn.conn.ReadMessage()
if err != nil {
log.Println(err)
break
}
go conn.handleGatewayEvent(b)
}
}()
// Identify
id := new(Identify)
id.Op = 2
id.Data.Token = token
id.Data.Intents = PermissionVoiceConnect | PermissionVoiceSpeak | PermissionSendMessages
id.Data.Properties.OS = "linux"
id.Data.Properties.Browser = "dndmusicbot/0.0.1"
id.Data.Properties.Device = "dndmusicbot"
json.NewEncoder(os.Stdout).Encode(id)
err = conn.conn.WriteJSON(id)
if err != nil {
conn.conn.Close()
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Wait for Ready!
select {
case <-ctx.Done():
return nil, err
case <-conn.ready:
}
return conn, nil
}
func (c *Connection) NewVoiceConnection(guild string, channel string) (*VoiceConnection, error) {
vc := new(VoiceConnection)
vc.d = c
c.events.On("VOICE_STATE_UPDATE", vc.voiceStateUpdate)
c.events.On("VOICE_SERVER_UDPATE", vc.voiceServerUpdate)
msg := new(VoiceStateUpdate)
msg.Op = 4
msg.Data.Guild = guild
msg.Data.Channel = channel
msg.Data.Mute = false
msg.Data.Deaf = true
c.conn.WriteJSON(msg)
var state, server bool
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// We have to wait for both a vserverupdate and a vstateupdate before we continue.
for !state && !server {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-c.vserverUpdate:
server = true
case <-c.vstateUpdate:
state = true
}
}
// We should have gotten all the information by now.. continue..
return vc, nil
}
func (vc *VoiceConnection) voiceStateUpdate(payload ...interface{}) {
vc.d.vstateUpdate <- true
}
func (vc *VoiceConnection) voiceServerUpdate(payload ...interface{}) {
vc.d.vserverUpdate <- true
}
// Ready event. Contains alot of information regarding our session.
func (c *Connection) eventready(payload ...interface{}) {
c.ready <- true
}
func (c *Connection) handleGatewayEvent(js []byte) error {
eventdata := gjson.GetManyBytes(js, "op", "d", "s", "t")
op := eventdata[0]
data := eventdata[1]
seq := eventdata[2]
eventname := eventdata[3]
if seq.Exists() {
c.seq = int(seq.Int())
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c.conn.SetCloseHandler(func(code int, text string) error {
cancel()
return nil
})
switch op.Int() {
case 0:
fmt.Printf("Event: %s Data: %+v\n", eventname.String(), data)
c.events.Emit(events.EventName(eventname.String()), data)
case 10: // Hello
hb_interval, err := time.ParseDuration(fmt.Sprintf("%dms", data.Get("heartbeat_interval").Int()))
if err != nil {
return err
}
log.Printf("We Received a Hello! Starting heartbeat on %.2fs interval\n", hb_interval.Seconds())
hb_ticker := time.NewTicker(hb_interval)
for {
select {
case <-ctx.Done():
break
case <-hb_ticker.C:
msg := make(map[string]interface{})
msg["op"] = 1
if c.seq == 0 {
msg["d"] = nil
} else {
msg["d"] = c.seq
}
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
log.Printf("Sending Heartbeat: %+v\n", msg)
err = c.conn.WriteJSON(msg)
if err != nil {
c.conn.Close()
return err
}
}
}
case 11:
log.Println("Heartbeat ACK!")
default:
log.Printf("%d: %+v\n", op.Int(), data)
}
return nil
}
func (c *Connection) GetGateway() (string, error) {
req, err := http.NewRequest("GET", GATEWAYBOTURL, nil)
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bot "+c.token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
b, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return gjson.GetBytes(b, "url").String(), err
}

View File

@ -1,9 +1,9 @@
package main
import (
"dndmusicbot/opus"
discordspeaker "dndmusicbot/speaker"
"encoding/json"
"fmt"
"log"
"net/url"
"os"
@ -12,7 +12,6 @@ import (
"github.com/faiface/beep"
"github.com/faiface/beep/effects"
"github.com/faiface/beep/mp3"
"github.com/fhs/gompd/v2/mpd"
"github.com/google/uuid"
"github.com/kataras/go-events"
@ -146,11 +145,11 @@ func (app *App) ambiancePlay(payload ...interface{}) {
return
}
var fn string
var id string
switch data := payload[0].(type) {
case json.RawMessage:
var err error
err = json.Unmarshal(data, &fn)
err = json.Unmarshal(data, &id)
if err != nil {
log.Println(err)
return
@ -160,21 +159,30 @@ func (app *App) ambiancePlay(payload ...interface{}) {
return
}
f, err := os.Open(fmt.Sprintf("./ambiance/%s.mp3", fn))
amb, err := GetAmbiance(id)
if err != nil {
log.Fatal(err)
log.Println(err)
return
}
play, _, err := mp3.Decode(f)
f, err := os.Open(amb.Path)
if err != nil {
log.Fatal(err)
log.Println(err)
return
}
play, err := opus.New(f)
if err != nil {
log.Println(err)
return
}
loop := beep.Loop(-1, play)
volume := &effects.Volume{
Streamer: loop,
Base: 2,
Volume: -2,
Volume: -2.5,
Silent: false,
}
@ -187,10 +195,10 @@ func (app *App) ambiancePlay(payload ...interface{}) {
msg := make(map[string]interface{})
out := make(map[string]interface{})
app.curamb = fn
app.curamb = amb
msg["event"] = "ambiance_play"
out["type"] = fn
out["id"] = id
msg["payload"] = out
ws_msg <- msg
}
@ -235,7 +243,7 @@ func (app *App) ambianceAdd(payload ...interface{}) {
return
}
err := DownloadAmbiance(amburl, ambtitle)
amb, err := AddAmbiance(amburl, ambtitle)
if err != nil {
log.Println(err)
return
@ -244,7 +252,8 @@ func (app *App) ambianceAdd(payload ...interface{}) {
msg := make(map[string]interface{})
out := make(map[string]interface{})
msg["event"] = "ambiance_add"
out["type"] = ambtitle
out["title"] = amb.Title
out["id"] = amb.Id
msg["payload"] = out
ws_msg <- msg
}

5
go.mod
View File

@ -6,6 +6,7 @@ go 1.19
require (
github.com/bwmarrin/discordgo v0.26.1
github.com/davecheney/xattr v0.0.0-20151008032638-dc6dbbe49f0b
github.com/faiface/beep v1.1.0
github.com/fhs/gompd/v2 v2.3.0
github.com/gohugoio/hugo v0.106.0
@ -15,13 +16,16 @@ require (
github.com/jackc/pgx/v5 v5.1.0
github.com/julienschmidt/httprouter v1.3.0
github.com/kataras/go-events v0.0.3
github.com/peterhellberg/link v1.2.0
github.com/pkg/errors v0.9.1
github.com/sosodev/duration v1.0.1
github.com/spf13/afero v1.9.3
github.com/spf13/viper v1.14.0
github.com/tidwall/gjson v1.14.3
golang.org/x/net v0.2.0
golang.org/x/time v0.2.0
google.golang.org/api v0.103.0
gopkg.in/hraban/opus.v2 v2.0.0-20220302220929-eeacdbcb92d0
layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32
)
@ -75,7 +79,6 @@ require (
github.com/yuin/goldmark v1.5.3 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.2.0 // indirect
golang.org/x/net v0.2.0 // indirect
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect

13
go.sum
View File

@ -91,11 +91,11 @@ github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozb
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecheney/xattr v0.0.0-20151008032638-dc6dbbe49f0b h1:a/CjIrvEH2NkUUIo4sqWIw+h3E63ttmS8L8Vx3ZaLS0=
github.com/davecheney/xattr v0.0.0-20151008032638-dc6dbbe49f0b/go.mod h1:Gc/R1HBRJIEElnD4PGXGQZQYMb14oPbvTovm6WeuAvk=
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dpup/gohubbub v0.0.0-20140517235056-2dc6969d22d8 h1:t1Ox7k2+GSzIv3fihjV7YFGb40nb/e2oyrTM/ngbzbA=
github.com/dpup/gohubbub v0.0.0-20140517235056-2dc6969d22d8/go.mod h1:QqXVl9BAyVoWIZE4oA9XfkwCjQ3JaajiX4vq7Zh8Vzs=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -277,6 +277,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -284,8 +286,6 @@ github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qR
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/r3labs/sse/v2 v2.8.2 h1:YWZy2i2nLoD5fE3vLLTdTz/8wxIYIFp5XbLNmmrrNts=
github.com/r3labs/sse/v2 v2.8.2/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@ -406,7 +406,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191116160921-f9c825593386/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -663,14 +662,14 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y=
gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UDAkHu8BrjI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fatih/set.v0 v0.2.1 h1:Xvyyp7LXu34P0ROhCyfXkmQCAoOUKb1E2JS9I7SE5CY=
gopkg.in/fatih/set.v0 v0.2.1/go.mod h1:5eLWEndGL4zGGemXWrKuts+wTJR0y+w+auqUJZbmyBg=
gopkg.in/hraban/opus.v2 v2.0.0-20220302220929-eeacdbcb92d0 h1:B8lK1KhYrE4H3urNYBAL/UquYftW65IHPY8JP3gpZ4M=
gopkg.in/hraban/opus.v2 v2.0.0-20220302220929-eeacdbcb92d0/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/neurosnap/sentences.v1 v1.0.6/go.mod h1:YlK+SN+fLQZj+kY3r8DkGDhDr91+S3JmTb5LSxFRQo0=

10
mpd.go
View File

@ -145,7 +145,17 @@ func (m *MPD) Err() error {
func (m *MPD) Stream(samples [][2]float64) (n int, ok bool) {
tmp := make([]byte, m.f.NumChannels+2)
status, err := app.mpd.Status()
if err != nil {
return 0, false
}
for i := range samples {
if status["state"] != "play" {
samples[i] = [2]float64{}
ok = true
continue
}
dn, err := m.file.Read(tmp)
if dn == len(tmp) {
samples[i], _ = m.f.DecodeSigned(tmp)

87
opus/decode.go Normal file
View File

@ -0,0 +1,87 @@
package opus
import (
"io"
"log"
"sync"
"github.com/faiface/beep"
"github.com/pkg/errors"
opusd "gopkg.in/hraban/opus.v2"
)
type decoder struct {
sync.Mutex
rc io.ReadSeekCloser
r *opusd.Stream
err error
}
func New(f io.ReadSeekCloser) (beep.StreamSeeker, error) {
d := new(decoder)
st, err := opusd.NewStream(f)
if err != nil {
return d, err
}
d.rc = f
d.r = st
return d, nil
}
func (s *decoder) Err() error { return s.err }
func (d *decoder) Stream(samples [][2]float64) (n int, ok bool) {
if d.err != nil {
return 0, false
}
var tmp [2]float32
for i := range samples {
dn, err := d.r.ReadFloat32(tmp[:])
if dn == 1 {
samples[i][0], samples[i][1] = float64(tmp[0]), float64(tmp[1])
n++
ok = true
}
if err == io.EOF {
ok = false
break
}
if err != nil {
ok = false
d.err = errors.Wrap(err, "ogg/opus")
break
}
}
return n, ok
}
func (d *decoder) Len() int {
return 0
}
func (d *decoder) Position() int {
return 0
}
func (d *decoder) Seek(p int) error {
_, err := d.rc.Seek(int64(p), io.SeekStart)
if err != nil {
log.Println(err)
return errors.Wrap(err, "ogg/opus")
}
st, err := opusd.NewStream(d.rc)
if err != nil {
log.Println(err)
return errors.Wrap(err, "ogg/opus")
}
d.r = st
return nil
}

79
opus/decode_test.go Normal file
View File

@ -0,0 +1,79 @@
package opus
import (
"context"
"log"
"os"
"sync"
"testing"
"time"
"github.com/faiface/beep"
)
var (
mu sync.Mutex
mixer beep.Mixer
frameSize int = 960
samples = make([][2]float64, frameSize)
buf []byte
maxBytes int = (frameSize * 2) * 2
done chan struct{}
)
func update() {
mu.Lock()
mixer.Stream(samples)
mu.Unlock()
for i := range samples {
for c := range samples[i] {
val := samples[i][c]
if val < -1 {
val = -1
}
if val > +1 {
val = +1
}
valInt16 := int16(val * (1<<15 - 1))
low := byte(valInt16)
high := byte(valInt16 >> 8)
buf[i*4+c*2+0] = low
buf[i*4+c*2+1] = high
}
}
//log.Printf("%+v", buf)
}
func TestMain(t *testing.T) {
buf = make([]byte, maxBytes)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*9)
defer cancel()
file, err := os.Open("test.opus")
if err != nil {
log.Fatal(err)
}
d, err := New(file)
if err != nil {
log.Fatal(err)
}
loop := beep.Loop(-1, d)
mu.Lock()
mixer.Add(loop)
mu.Unlock()
for {
select {
default:
update()
case <-ctx.Done():
return
}
}
}

View File

@ -13,11 +13,6 @@ import (
discordspeaker "dndmusicbot/speaker"
)
type Ambiance struct {
Type string
URL string
}
func init() {
log.Println("queue.go loading..")

View File

@ -48,7 +48,7 @@ func init() {
type IndexData struct {
Playlists []Playlist
Ambiance []string
Ambiance []Ambiance
}
func (app App) ServeFiles(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -79,7 +79,7 @@ func (app App) Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params
http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError)
}
amblist, err := GetAmbiance()
amblist, err := GetAmbiances()
if err != nil {
log.Println(err)
return

View File

@ -107,9 +107,27 @@ window.onload = function () {
ws.onmessage = (e) => {
data = JSON.parse(e.data)
switch (data.event) {
case "ambiance_add":
const container = document.querySelector("#ambiance")
var newdiv = document.createElement('div');
newdiv.className = "item"
newdiv.dataset.id = data.payload.id
newdiv.innerText = data.payload.title
addInteractHandler(newdiv, (e, isTouch) => {
isTouch && e.preventDefault()
var id = e.target.dataset.id
ws.send(JSON.stringify({
"event": ((id === "reset") ? "ambiance_stop" : "ambiance_play"),
"payload": id
}))
})
container.insertBefore(newdiv, document.querySelector("#ambiance div:last-child"))
break
case "ambiance_play":
document.querySelectorAll("#ambiance > div").forEach((e) => {e.style.removeProperty("background-color")})
document.querySelector(`#ambiance > div[data-id='${data.payload.type}']`).style.backgroundColor = "burlywood"
document.querySelector(`#ambiance > div[data-id='${data.payload.id}']`).style.backgroundColor = "burlywood"
ambiance.style.pointerEvents = 'auto'
break
case "ambiance_stop":
@ -172,6 +190,9 @@ window.onload = function () {
"url": url.value
}
}))
title.value = ""
url.value = ""
})
addInteractHandler(submit, (e, isTouch) => {

View File

@ -7,6 +7,7 @@
<title>D&D Music Bot!</title>
<link rel="stylesheet" href="/css/solarized-dark.css">
<link rel="stylesheet" href="/css/style.css">
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACNklEQVQ4jaWSy0vUcRTFP/f7G4cUUqzAZtQsgzYWiDMjjYgotbOtYLSIFlKBix7YH9DWXQaRFRREi8hN9MCFPZDUzB4IUkRo5Oho1CLTcEbne1o0UWCI0FndC+cezr33wH/C/m5mkvFOw04JIsBrmXVVDI0OryfgfhepZH0bWA/wFaMLqDXp/nQyuWVDAhi1wIz36k5n/BXgMVAamI+vJxD6o7TS61Uw7QLbEXH+OGIOQzmtTG7IgVfBPjOdE6oXdtrQd8mOVQ6/+vCbM9XcvGmipia8xsFsLLZN6DBQZWIRKJPZISd5gLFYrCBaYF3KLB0IFxdVzyQTZYZukNXZEEA2CDaH8SUS5zHbBVZo+AE5OzrXEL+eyxHxZo2IyzL/FqywcnhsFCCktrYgnZrqWc35nsCCqHM2mJMWkXuD+RVPUIvzRxB3K0Ze3F5zg8/pj1XCEkEQ9OHoZnH5uaFAYfXjrcmhUkPvyit39v7zC9lcKO1Y3QoEwOD28fEl4CRAqiFxJyddAHuUnpwsnW9MFIdW3JeMW41Ghl++N/AOMuX5YTCqlU/nbCxWZLDXQRfQRNiNr+Z0L+OUFHZwNhm/CRBaWMh8Ki0u+iYoQeyZ3Z+4lcKeytSBqPOyZ2a+X1grWESoE6wFWAZwNRMTWaEzBk/yLtrNdAmow7gaHRm96HEpYCC/divGjGEdv+h5qLk5lM4utUvWAvxw8g8iI2MPAeYbE9VeFMrbNZl2GzoRHRrrWy+hG8ZPg13xb+XS+CIAAAAASUVORK5CYII=">
</head>
<body>
<div class="container">
@ -52,7 +53,7 @@
<section>
<div id="ambiance" class="item-container">
{{ range .Ambiance }}
<div class="item drag" data-id="{{ . }}">{{ . }}</div>
<div class="item drag" data-id="{{ .Id }}">{{ .Title }}</div>
{{ end}}
<div class="item locked stop" data-id="reset">Stop</div>
</div>

2
ws.go
View File

@ -73,7 +73,7 @@ func handleWS(c *websocket.Conn) error {
msg := make(map[string]interface{})
out := make(map[string]interface{})
msg["event"] = "ambiance_play"
out["type"] = app.curamb
out["id"] = app.curamb.Id
msg["payload"] = out
c.WriteJSON(msg)
} else {

39
ytdl.go
View File

@ -1,16 +1,12 @@
package main
import (
"bufio"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/tidwall/gjson"
"golang.org/x/time/rate"
)
@ -19,7 +15,7 @@ var yturl = "https://youtu.be/%s"
func NewYTdl(vid string) ([]byte, error) {
ytdl := config.GetString("youtube.ytdl")
uri, err := exec.Command(
yt := exec.Command(
ytdl,
fmt.Sprintf(yturl, vid),
"--cookies", "./cookies.txt",
@ -30,7 +26,11 @@ func NewYTdl(vid string) ([]byte, error) {
"--restrict-filenames",
"-f", "251",
"--get-url",
).Output()
)
yt.Stderr = os.Stderr
uri, err := yt.Output()
if err != nil {
return nil, err
}
@ -38,15 +38,25 @@ func NewYTdl(vid string) ([]byte, error) {
return uri[:len(uri)-1], nil
}
func DownloadAmbiance(uri string, name string) error {
ytdl := config.GetString("youtube.ytdl")
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.aac").Output()
func YTUrl(uri string) (vid string, err error) {
u, err := url.Parse(uri)
if err != nil {
return err
return "", err
}
tmpfile = tmpfile[:len(tmpfile)-1]
switch u.Host {
case "youtu.be":
vid = u.Path[1:]
case "youtube.com":
vid = u.Query().Get("v")
}
return
}
/*
func DownloadAmbiance(uri string, name string) error {
ytdl := config.GetString("youtube.ytdl")
cmd := exec.Command(
ytdl,
@ -55,7 +65,7 @@ func DownloadAmbiance(uri string, name string) error {
"--no-cache-dir",
"-f", "140",
"--cookies", "../cookies.txt",
"-o", string(tmpfile),
"-o", "-",
"--force-overwrites",
"-q",
"--progress",
@ -180,3 +190,4 @@ func DownloadAmbiance(uri string, name string) error {
return nil
}
*/