Compare commits

..

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

45 changed files with 1059 additions and 3401 deletions

View File

@ -1,16 +0,0 @@
ambiance
bin
cache
config
cookies.txt
dndmusicbot
Dockerfile
.git
.gitignore
.dockerignore
.jsimage
k8s
MPD
oauth.cfg
test
src

3
.gitignore vendored
View File

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

View File

@ -6,4 +6,4 @@ RUN mkdir /public && chmod 777 /public
WORKDIR /app
ENTRYPOINT ["yarn"]
ENTRYPOINT ["yarn"]

View File

@ -1,61 +0,0 @@
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
COPY . .
RUN go build
FROM debian:bookworm-slim
RUN apt-get update && apt-get -y install \
ca-certificates \
libopus-dev libopusfile-dev \
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 \
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 && rm -rf /tmp/*.deb
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
WORKDIR /app
ENTRYPOINT [ "/app/dndmusicbot" ]

View File

@ -1,16 +0,0 @@
ambiance
bin
cache
config
cookies.txt
dndmusicbot
Dockerfile
.git
.gitignore
.dockerignore
.jsimage
k8s
MPD
oauth.cfg
test
src

View File

@ -1,2 +0,0 @@
ambiance
*.mp3

View File

@ -10,9 +10,8 @@ js: image public/script.js
image: .jsimage
.jsimage: export DOCKER_BUILDKIT=1
.jsimage: Dockerfile.js
@docker build -t dndmusicbot-js-build . -f Dockerfile.js
.jsimage: Dockerfile
@docker build -t dndmusicbot-js-build .
@touch .jsimage
src/node_modules:
@ -28,7 +27,7 @@ src/yarn.lock: public src/node_modules src/package.json
public/script.js: src/script.js src/yarn.lock
$(YARN) build
$(APPLICATION_NAME): *.go ffmpeg/*.go speaker/*.go go.mod go.sum
$(APPLICATION_NAME): *.go ffmpeg/*.go speaker/*.go
@go build
build: image js $(APPLICATION_NAME)

View File

@ -1,229 +1,24 @@
package main
import (
"bufio"
"fmt"
"io"
"log"
"math"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/davecheney/xattr"
"github.com/google/uuid"
)
//var httpClient = new(http.Client)
type Ambiance struct {
Id string
Title string
Path string
func fnNoExt(fileName string) string {
return fileName[:len(fileName)-len(filepath.Ext(fileName))]
}
func GetAmbiance(id string) (amb Ambiance, err error) {
fp := filepath.Join(config.GetString("ambiance.path"), 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(config.GetString("ambiance.path"))
func GetAmbiance() ([]string, error) {
files, err := os.ReadDir("./ambiance")
if err != nil {
return nil, err
}
var out []string
for _, file := range files {
title, err := xattr.Getxattr(filepath.Join(config.GetString("ambiance.path"), 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(config.GetString("ambiance.path"), file.Name()),
})
out = append(out, fnNoExt(file.Name()))
}
return
}
func AddAmbiance(uri, title string) (Ambiance, error) {
var amb Ambiance
ev := Event{"ambiance_add_start", map[string]string{
"name": title,
}}
go ws.SendEvent(ev)
defer func() {
ev = Event{"ambiance_add_finish", map[string]string{
"name": title,
}}
go ws.SendEvent(ev)
}()
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.opus").Output()
if err != nil {
return amb, err
}
tmpfile = tmpfile[:len(tmpfile)-1]
log.Printf("Parsing vid from %s", uri)
vid, err := YTUrl(uri)
if err != nil {
return amb, err
}
vinfo, err := app.youtube.GetVideoFromID(vid)
if err != nil {
return amb, err
}
log.Printf("Start ffmpeg for %s (%s)", vid, vinfo.VideoDetails.Title)
ff := exec.Command(
"ffmpeg",
"-y",
"-i", vinfo.GetHLSPlaylist("234"),
"-vn",
//"-acodec", "copy",
"-movflags", "+faststart",
"-t", "01:00:00",
"-ar", "48000",
"-v", "quiet",
// "-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
err = ff.Start()
if err != nil {
return amb, err
}
log.Printf("Start ffmpeg to extract audio to %s", string(tmpfile))
ev = Event{"ambiance_encode_start", map[string]string{
"name": title,
}}
go ws.SendEvent(ev)
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() {
out_time, ok := data["out_time_ms"]
if ok {
out_time_ms, _ := strconv.Atoi(out_time)
percent := fmt.Sprintf("%.0f", math.Floor((float64(out_time_ms)/float64(time.Hour.Microseconds()))*100))
data["percent"] = percent
}
go ws.SendEvent(Event{"ambiance_encode_progress", data})
})
}
if err := scanner.Err(); err != nil {
return amb, err
}
err = ff.Wait()
if err != nil {
return amb, err
}
ev = Event{"ambiance_encode_complete", map[string]string{
"name": title,
}}
go ws.SendEvent(ev)
id := uuid.New()
fn := filepath.Join(config.GetString("ambiance.path"), fmt.Sprintf("%s.aac", 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
return out, nil
}

61
bot.go
View File

@ -5,23 +5,18 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"time"
"dndmusicbot/youtube"
"github.com/diamondburned/arikawa/v3/state"
"github.com/diamondburned/arikawa/v3/voice"
"github.com/bwmarrin/discordgo"
"github.com/faiface/beep"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gopxl/beep"
"github.com/jackc/pgx/v5"
"github.com/julienschmidt/httprouter"
"github.com/kataras/go-events"
"github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/steino/gompd/v2/mpd"
"google.golang.org/api/youtube/v3"
)
const (
@ -37,8 +32,6 @@ var (
)
func init() {
log.SetFlags(log.Ltime | log.Lshortfile)
log.Println("bot.go loading..")
config.SetConfigName("config")
config.SetConfigType("yaml")
@ -47,64 +40,44 @@ func init() {
if err != nil {
log.Fatal(err)
}
app.plmutex = &sync.Mutex{}
log.Println("bot.go done.")
}
type App struct {
discord *state.State
voice *voice.Session
youtube *youtube.Client
discord *discordgo.Session
voice *discordgo.VoiceConnection
youtube *youtube.Service
queue *Queue
ambiance beep.Mixer
curamb Ambiance
curamb string
events events.EventEmmiter
next bool
db *pgx.Conn
router *httprouter.Router
active []string
plidx int
cache *filecache.Cache
mpdc context.CancelFunc
mpdw *mpd.Watcher
mpd *mpd.Client
plmutex *sync.Mutex
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)
defer cancel()
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
for {
select {
case <-ctx.Done():
app.db.Close(ctx)
app.mpdw.Close()
app.mpdc()
app.voice.Leave(ctx)
app.cache.Prune(false)
dgvc()
case <-sc:
app.db.Close(context.Background())
app.queue.Reset()
app.voice.Close()
app.discord.Close()
return
case <-ticker.C:
app.events.Emit("tick")
case <-prune.C:
app.cache.Prune(false)
}
}
}

1
db.go
View File

@ -26,6 +26,7 @@ type Playlist struct {
}
func (app App) GetPlaylists() (playlists []Playlist, err error) {
log.Println(app.db.Ping(context.Background()))
rows, err := app.db.Query(context.Background(), "SELECT id, url, title FROM playlists")
if err != nil {
return nil, err

View File

@ -1,56 +1,39 @@
package main
import (
"context"
discordspeaker "dndmusicbot/speaker"
"log"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/state"
"github.com/diamondburned/arikawa/v3/voice"
"github.com/diamondburned/arikawa/v3/voice/voicegateway"
"github.com/bwmarrin/discordgo"
)
var dgvc context.CancelFunc
func init() {
log.Println("discord.go loading..")
var err error
token := config.GetString("discord.token")
guild := config.GetString("discord.guild")
channel := config.GetString("discord.channel")
ctx, cancel := context.WithCancel(context.Background())
dgvc = cancel
s := state.New("Bot " + token)
// This is required for bots.
voice.AddIntents(s)
if err := s.Open(ctx); err != nil {
log.Fatalln("failed to open gateway:", err)
}
v, err := voice.NewSession(s)
app.discord, err = discordgo.New("Bot " + token)
if err != nil {
log.Fatalln("failed to create voice session:", err)
return
}
chsf, err := discord.ParseSnowflake(channel)
// app.discord.LogLevel = discordgo.LogDebug
err = app.discord.Open()
if err != nil {
log.Fatalln("failed to create snowflake:", err)
return
}
if err := v.JoinChannelAndSpeak(ctx, discord.ChannelID(chsf), false, true); err != nil {
log.Fatalln("failed to join voice channel:", err)
app.voice, err = app.discord.ChannelVoiceJoin(guild, channel, false, true)
if err != nil {
return
}
v.Speaking(ctx, voicegateway.NotSpeaking)
app.discord = s
app.voice = v
discordspeaker.Init(v, config.GetInt("discord.bitrate"))
// app.voice.LogLevel = discordgo.LogDebug
discordspeaker.Init(app.voice)
log.Println("discord.go done.")
}

272
discord/discord.go Normal file
View File

@ -0,0 +1,272 @@
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
}

508
events.go
View File

@ -1,22 +1,20 @@
package main
import (
"context"
"dndmusicbot/ffmpeg"
discordspeaker "dndmusicbot/speaker"
"encoding/json"
"fmt"
"log"
"math/rand"
"net/url"
"path/filepath"
"strconv"
"os"
"time"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/google/uuid"
"github.com/kataras/go-events"
"github.com/steino/gompd/v2/mpd"
"golang.org/x/time/rate"
ytdl "github.com/kkdai/youtube/v2"
)
type SongInfo struct {
@ -24,27 +22,24 @@ type SongInfo struct {
PlaylistName string `json:"playlistname,omitempty"`
Title string `json:"current,omitempty"`
Channel string `json:"channel,omitempty"`
Position int64 `json:"position"`
Length int64 `json:"len,omitempty"`
Position int `json:"position"`
Length int `json:"len,omitempty"`
Pause bool `json:"pause"`
Song string `json:"song,omitempty"`
}
var l = rate.Sometimes{Interval: 800 * time.Millisecond}
var ytdl_client = ytdl.Client{}
var l = rate.Sometimes{Interval: 1 * time.Second}
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("player", app.songInfo)
app.events.On("song_over", app.songInfo)
app.events.On("song_start", app.songInfo)
//app.events.On("song_position", app.songPosition)
app.events.On("ambiance_play", app.ambiancePlay)
@ -55,187 +50,27 @@ func init() {
app.events.On("next", app.nextSong)
app.events.On("prev", app.prevSong)
app.events.On("vol_up", app.volup)
app.events.On("vol_down", app.voldown)
app.events.On("vol_set", app.volset)
//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) volup(payload ...interface{}) {
if !(len(payload) > 0) {
log.Println("volup called without a payload.")
return
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,
}
var t string
switch data := payload[0].(type) {
case json.RawMessage:
var err error
err = json.Unmarshal(data, &t)
if err != nil {
log.Println(err)
return
}
default:
log.Println("volup called with invalid payload.")
return
}
switch t {
case "playlist":
pl_volume.Volume = pl_volume.Volume + 0.1
case "ambiance":
amb_volume.Volume = amb_volume.Volume + 0.1
}
}
func (app *App) voldown(payload ...interface{}) {
if !(len(payload) > 0) {
log.Println("voldown called without a payload.")
return
}
var t string
switch data := payload[0].(type) {
case json.RawMessage:
var err error
err = json.Unmarshal(data, &t)
if err != nil {
log.Println(err)
return
}
default:
log.Println("voldown called with invalid payload.")
return
}
switch t {
case "playlist":
pl_volume.Volume = pl_volume.Volume - 0.1
case "ambiance":
amb_volume.Volume = amb_volume.Volume - 0.1
}
}
func (app *App) volset(payload ...interface{}) {
if !(len(payload) > 0) {
log.Println("volset called without a payload.")
return
}
var data map[string]string
switch js := payload[0].(type) {
case json.RawMessage:
json.Unmarshal(js, &data)
default:
log.Println("volset called with invalid payload.")
return
}
vol, err := strconv.ParseFloat(data["vol"], 64)
if err != nil {
log.Println(err)
return
}
switch data["type"] {
case "playlist":
pl_volume.Volume = vol
case "ambiance":
amb_volume.Volume = vol
}
ev := Event{"volume", map[string]float64{
"playlist": pl_volume.Volume,
"ambiance": amb_volume.Volume,
}}
go ws.SendEvent(ev)
}
func (app *App) songInfoEvent(event string) (ev Event, err error) {
ev.Event = event
status, err := app.mpd.Status()
if err != nil {
return
}
cur, err := app.mpd.CurrentSong()
if err != nil {
return
}
info := new(SongInfo)
if status["state"] != "play" {
info.Pause = true
ev.Payload = info
return
}
duration, ok := status["duration"]
if ok && duration != "" {
var slen float64
slen, err = strconv.ParseFloat(duration, 64)
if err != nil {
log.Println(err)
return
}
info.Length = time.Duration(slen * float64(time.Second)).Milliseconds()
}
elapsed, ok := status["elapsed"]
if ok && elapsed != "" {
var spos float64
spos, err = strconv.ParseFloat(elapsed, 64)
if err != nil {
log.Println(err)
return
}
info.Position = time.Duration(spos * float64(time.Second)).Milliseconds()
}
album, ok := cur["Album"]
if ok {
var plid uuid.UUID
plid, err = uuid.ParseBytes([]byte(album))
if err != nil {
log.Println(err)
return
}
var pl *Playlist
pl, err = app.GetPlaylist(plid)
if err != nil {
log.Println(err)
return
}
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
}
ev.Payload = *info
return ev, nil
return msg
}
func (app *App) ambiancePlay(payload ...interface{}) {
@ -244,11 +79,11 @@ func (app *App) ambiancePlay(payload ...interface{}) {
return
}
var id string
var fn string
switch data := payload[0].(type) {
case json.RawMessage:
var err error
err = json.Unmarshal(data, &id)
err = json.Unmarshal(data, &fn)
if err != nil {
log.Println(err)
return
@ -258,62 +93,44 @@ func (app *App) ambiancePlay(payload ...interface{}) {
return
}
amb, err := GetAmbiance(id)
if err != nil {
log.Println(err)
return
}
err = app.mpd.Partition("ambiance")
if err != nil {
log.Println(err)
return
}
fmt.Println(filepath.Join(cwd, amb.Path))
err = app.mpd.Add(filepath.Join(cwd, amb.Path))
if err != nil {
log.Println(err)
return
}
err = app.mpd.Play(0)
if err != nil {
log.Println(err)
return
}
err = app.mpd.Partition("default")
f, err := os.Open(fmt.Sprintf("./ambiance/%s.mp3", fn))
if err != nil {
log.Fatal(err)
}
app.curamb = amb
play, _, err := mp3.Decode(f)
if err != nil {
log.Fatal(err)
}
ev := Event{"ambiance_play", map[string]string{
"id": id,
}}
discordspeaker.Pause(false)
discordspeaker.Lock()
app.ambiance.Clear()
loop := beep.Loop(-1, play)
app.ambiance.Add(loop)
discordspeaker.Unlock()
go ws.SendEvent(ev)
msg := make(map[string]interface{})
out := make(map[string]interface{})
app.curamb = fn
msg["event"] = "ambiance_play"
out["type"] = fn
msg["payload"] = out
ws_msg <- msg
}
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)
}
msg := make(map[string]interface{})
msg["event"] = "ambiance_stop"
ws_msg <- msg
app.mpd.Stop()
app.mpd.Clear()
err = app.mpd.Partition("default")
if err != nil {
log.Fatal(err)
}
go ws.SendEvent(Event{"ambiance_stop", nil})
}
func (app *App) ambianceAdd(payload ...interface{}) {
@ -344,89 +161,87 @@ func (app *App) ambianceAdd(payload ...interface{}) {
return
}
amb, err := AddAmbiance(amburl, ambtitle)
err := DownloadAmbiance(amburl, ambtitle)
if err != nil {
log.Println(err)
return
}
ev := Event{"ambiance_add", map[string]string{
"title": amb.Title,
"id": amb.Id,
}}
go ws.SendEvent(ev)
msg := make(map[string]interface{})
out := make(map[string]interface{})
msg["event"] = "ambiance_add"
out["type"] = ambtitle
msg["payload"] = out
ws_msg <- msg
}
func (app *App) songPosition(payload ...interface{}) {
status, err := app.mpd.Status()
if err != nil {
log.Println(err)
return
}
if status["state"] == "stop" {
if !app.queue.IsPlaying() {
return
}
l.Do(func() {
msg := make(map[string]interface{})
out := make(map[string]interface{})
slen, _ := strconv.ParseFloat(status["duration"], 64)
spos, _ := strconv.ParseFloat(status["elapsed"], 64)
msg["event"] = "song_position"
out["len"] = app.queue.Len()
out["position"] = app.queue.Position()
ev := Event{"song_position", map[string]int64{
"len": time.Duration(slen * float64(time.Second)).Milliseconds(),
"position": time.Duration(spos * float64(time.Second)).Milliseconds(),
}}
go ws.SendEvent(ev)
msg["payload"] = out
ws_msg <- msg
})
}
func (app *App) songInfo(payload ...interface{}) {
log.Println("song_info event received")
ev, err := app.songInfoEvent("song_info")
if err != nil {
log.Println(err)
func (app *App) checkTimeleft(payload ...interface{}) {
if !app.queue.IsPlaying() {
return
}
go ws.SendEvent(ev)
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")
msg := app.songInfoEvent("song_info")
ws_msg <- msg
app.next = false
}
func (app *App) stop(payload ...interface{}) {
log.Println("stop event received")
discordspeaker.Lock()
app.queue.Reset()
discordspeaker.Unlock()
app.plmutex.Lock()
if app.plcancel != nil {
app.plcancel()
}
app.mpd.Stop()
app.plmutex.Unlock()
msg := make(map[string]interface{})
msg["event"] = "stop"
ws_msg <- msg
go ws.SendEvent(Event{"stop", nil})
}
func (app *App) prevSong(payload ...interface{}) {
log.Println("prev_song event received")
app.plmutex.Lock()
err := app.mpd.Previous()
app.plmutex.Unlock()
if err != nil {
log.Println(err)
}
app.queue.Prev()
msg := app.songInfoEvent("song_info")
ws_msg <- msg
}
func (app *App) nextSong(payload ...interface{}) {
log.Println("next_song event received")
app.plmutex.Lock()
err := app.mpd.Next()
app.plmutex.Unlock()
if err != nil {
log.Println(err)
}
app.queue.Next()
msg := app.songInfoEvent("song_info")
ws_msg <- msg
}
func (app *App) addPlaylist(payload ...interface{}) {
@ -469,7 +284,7 @@ func (app *App) addPlaylist(payload ...interface{}) {
return
}
_, err = ytdl_client.GetPlaylist(plurl)
_, err = app.Playlist(plid)
if err != nil {
log.Println("Error getting youtube playlist info,", plid)
}
@ -479,12 +294,12 @@ func (app *App) addPlaylist(payload ...interface{}) {
log.Println("Error getting youtube playlist info,", plid)
}
ev := Event{"new_playlist", map[string]string{
"url": id.String(),
"title": pltitle,
}}
msg := make(map[string]interface{})
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{}) {
@ -520,88 +335,57 @@ func (app *App) loadPlaylist(payload ...interface{}) {
return
}
list, err := ytdl_client.GetPlaylist(pl.Url)
discordspeaker.Pause(false)
list, err := app.Playlist(pl.Url)
if err != nil {
log.Println("Error getting playlist info,", id)
return
}
rand.Shuffle(len(list.Videos), func(i, j int) { list.Videos[i], list.Videos[j] = list.Videos[j], list.Videos[i] })
app.plmutex.Lock()
if app.plcancel != nil {
app.plcancel()
list, err = ShufflePlaylist(list)
if err != nil {
log.Println("Unable to shuffle playlist")
return
}
app.mpd.Stop()
app.mpd.Clear()
app.plmutex.Unlock()
ctx, cancel := context.WithCancel(context.Background())
app.plcancel = cancel
app.queue.Reset()
go func() {
defer cancel()
for _, ytinfo := range list.Videos {
log.Printf("Adding %s (%s - %s)", ytinfo.ID, ytinfo.Author, ytinfo.Title)
// Run as a local function so we can defer the mutex unlock incase one of these errors.
ok := func() (ok bool) {
app.plmutex.Lock()
defer app.plmutex.Unlock()
if ctx.Err() != nil {
return false
}
ok = true
// state:stop
songid, err := app.mpd.AddID("http://localhost:"+config.GetString("web.port")+"/youtube/"+ytinfo.ID, 0)
if err != nil {
log.Println(err)
return
}
mpdcmd := app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("artist"), ytinfo.Author)
err = mpdcmd.OK()
if err != nil {
log.Println(err)
return
}
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)
return
}
mpdcmd = app.mpd.Command("%s %d %s %s", mpd.Quoted("addtagid"), songid, mpd.Quoted("location"), ytinfo.ID)
err = mpdcmd.OK()
if err != nil {
log.Println(err)
return
}
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)
return
}
app.mpd.Play(-1)
time.Sleep(time.Second)
return
}()
if !ok {
break
for _, vid := range list {
ytinfo, err := app.Video(vid)
if err != nil {
log.Println(err)
continue
}
_, yt, err := app.cache.GetOrCreateBytes(vid+".txt", func() ([]byte, error) {
uri, err := NewYTdl(vid)
if err != nil {
return nil, err
}
return uri, nil
})
if err != nil {
log.Println(err)
continue
}
ff, err := ffmpeg.NewPCM(string(yt), sampleRate, channels)
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),
}
app.queue.Add(song)
}
}()
}

View File

@ -7,7 +7,7 @@ import (
"math"
"time"
"github.com/gopxl/beep"
"github.com/faiface/beep"
)
type PCM struct {

87
go.mod
View File

@ -1,40 +1,32 @@
module dndmusicbot
go 1.21
toolchain go1.21.3
go 1.19
require (
github.com/IzumiSy/go-fdkaac v0.0.0-20220502080852-c56d1bb3e32d
github.com/bitly/go-simplejson v0.5.1
github.com/davecheney/xattr v0.0.0-20151008032638-dc6dbbe49f0b
github.com/diamondburned/arikawa/v3 v3.2.1-0.20230320210521-82c55dffac71
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/gohugoio/hugo v0.106.0
github.com/golang-jwt/jwt/v5 v5.0.0-rc.2
github.com/google/uuid v1.3.0
github.com/gopxl/beep v1.1.0
github.com/gorilla/sessions v1.1.1
github.com/gorilla/websocket v1.5.0
github.com/grafov/bcast v0.0.0-20190217190352-1447f067e08d
github.com/grafov/m3u8 v0.12.0
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/kkdai/youtube/v2 v2.9.0
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
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
golang.org/x/net v0.15.0
golang.org/x/time v0.3.0
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302
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
github.com/tidwall/gjson v1.14.3
golang.org/x/time v0.2.0
google.golang.org/api v0.103.0
layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32
)
require (
cloud.google.com/go/compute v1.12.1 // indirect
cloud.google.com/go/compute/metadata v0.2.1 // indirect
github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
@ -47,51 +39,50 @@ require (
github.com/bep/overlayfs v0.6.0 // indirect
github.com/clbanning/mxj/v2 v2.5.7 // indirect
github.com/cli/safeexec v1.0.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e // indirect
github.com/fhs/gompd/v2 v2.3.0 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gohugoio/locales v0.14.0 // indirect
github.com/gohugoio/localescompressed v1.0.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/mux v1.6.2 // indirect
github.com/gorilla/schema v1.2.0 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/hajimehoshi/go-mp3 v0.3.0 // 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
github.com/kyokomi/emoji/v2 v2.2.10 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/magiconair/properties v1.8.6 // 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/mattn/go-runewidth v0.0.9 // 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/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
github.com/spf13/cast v1.5.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/tdewolff/parse/v2 v2.6.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/yuin/goldmark v1.5.3 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // 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
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
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

277
go.sum
View File

@ -14,18 +14,23 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.67.0/go.mod h1:YNan/mUhNZFrYUor0vqrsQ0Ffl7Xtm/ACOy/vsTS858=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0=
cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48=
cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/longrunning v0.1.1 h1:y50CXG4j0+qvEukslYFBCrzaXX0qpFbBzc3PchSu/LE=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
@ -42,8 +47,6 @@ github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZd
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/IzumiSy/go-fdkaac v0.0.0-20220502080852-c56d1bb3e32d h1:2Fn0vK/lH/pI2/TtUTFgb3iKkbw7TDlxBLglELToDRo=
github.com/IzumiSy/go-fdkaac v0.0.0-20220502080852-c56d1bb3e32d/go.mod h1:jmxJwIPbDUrofy2H3d28lYzz+RL9qt8GltDAAJ/mlb0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
@ -51,7 +54,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/alecthomas/chroma/v2 v2.3.0 h1:83xfxrnjv8eK+Cf8qZDzNo3PPF9IbTWHs7z28GY6D0U=
github.com/alecthomas/chroma/v2 v2.3.0/go.mod h1:mZxeWZlxP2Dy+/8cBob2PYd8O2DwNAzave5AY7A2eQw=
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bep/clock v0.3.0 h1:vfOA6+wVb6pPQEiXow9f/too92vNTLe9MuwO13PfI0M=
@ -59,30 +61,23 @@ github.com/bep/clock v0.3.0/go.mod h1:6Gz2lapnJ9vxpvPxQ2u6FcXFRoj4kkiqQ6pm0ERZlw
github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo=
github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.1.2 h1:zk04w1qc1COTZPPYWDQHvns3y1afOsdRfraFQ3qI840=
github.com/bep/gitmap v1.1.2/go.mod h1:g9VRETxFUXNWzMiuxOwcudo6DfZkW9jOsOW0Ft4kYaY=
github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
github.com/bep/godartsass v0.14.0 h1:pPb6XkpyDEppS+wK0veh7OXDQc4xzOJI9Qcjb743UeQ=
github.com/bep/godartsass v0.14.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8=
github.com/bep/golibsass v1.1.0 h1:pjtXr00IJZZaOdfryNa9wARTB3Q0BmxC3/V1KNcgyTw=
github.com/bep/golibsass v1.1.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
github.com/bep/gowebp v0.2.0 h1:ZVfK8i9PpZqKHEmthQSt3qCnnHycbLzBPEsVtk2ch2Q=
github.com/bep/gowebp v0.2.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo=
github.com/bep/overlayfs v0.6.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM=
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs=
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/mxj/v2 v2.5.7 h1:7q5lvUpaPF/WOkqgIDiwjBJaznaLCCBd78pi8ZyAnE0=
github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI=
@ -96,23 +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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE=
github.com/diamondburned/arikawa/v3 v3.2.1-0.20230320210521-82c55dffac71 h1:1Wqec9jTprYuWfx/XiJVaRkH5RCkyrgnHdFj0O5NFn4=
github.com/diamondburned/arikawa/v3 v3.2.1-0.20230320210521-82c55dffac71/go.mod h1:+ifmDonP/JdBiUOzZmVReEjPTHDUSkyqqRRmjSf9NE8=
github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
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=
@ -120,55 +103,42 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanw/esbuild v0.15.14 h1:J/cqgL3yfj/HDHDo9txKAqyzTBYfAMuqCknkS2jhX24=
github.com/evanw/esbuild v0.15.14/go.mod h1:iINY06rn799hi48UqEnaQvVfZWe6W9bET78LbvN8VWk=
github.com/fhs/gompd/v2 v2.3.0 h1:wuruUjmOODRlJhrYx73rJnzS7vTSXSU7pWmZtM3VPE0=
github.com/fhs/gompd/v2 v2.3.0/go.mod h1:nNdZtcpD5VpmzZbRl5rV6RhxeMmAWTxEsSIMBkmMIy4=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
github.com/frankban/quicktest v1.13.0/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
github.com/getkin/kin-openapi v0.108.0 h1:EYf0GtsKa4hQNIlplGS+Au7NEfGQ1F7MoHD2kcVevPQ=
github.com/getkin/kin-openapi v0.108.0/go.mod h1:QtwUNt0PAAgIIBEvFWYfB7dfngxtAaqCX1zYHMZDeK8=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
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=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/gobuffalo/flect v0.3.0 h1:erfPWM+K1rFNIQeRPdeEXxo8yFr/PO17lhRnS8FUrtk=
github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.9.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20210430103248-4c28c89f8013 h1:Nj29Qbkt0bZ/bJl8eccfxQp3NlU/0IW1v9eyYtQ53XQ=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20210430103248-4c28c89f8013/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/hugo v0.106.0 h1:MDTmX2l1/zTh0HS4CADta4a/b63aiyj6yC2WW4A+UR0=
github.com/gohugoio/hugo v0.106.0/go.mod h1:eBHtMtZZrZweoC65GRsAM+jcYhHGmbzzvSccapxL4ug=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v5 v5.0.0-rc.2 h1:hXPcSazn8wKOfSb9y2m1bdgUMlDxVDarxh3lJVbC6JE=
github.com/golang-jwt/jwt/v5 v5.0.0-rc.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -191,8 +161,8 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -203,11 +173,11 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@ -218,42 +188,27 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb h1:LCMfzVg3sflxTs4UvuP4D8CkoZnfHLe2qzqgDn/4OHs=
github.com/google/pprof v0.0.0-20230907193218-d3ddc7976beb/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs=
github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ=
github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gopxl/beep v1.1.0 h1:YBfaDhZh4bC6IJfDsEi/8wmtUanir0dMIxpRu3F6Yeo=
github.com/gopxl/beep v1.1.0/go.mod h1:N5ClU2N8ESeO6ibbz5UThPRFpdEgbU9G60CLZ6u3v9s=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/pat v0.0.0-20180118222023-199c85a7f6d1/go.mod h1:YeAe0gNeiNT5hoiZRI4yiOky6jVdNvfO2N6Kav/HmxY=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafov/bcast v0.0.0-20190217190352-1447f067e08d h1:Q2+KsA/1GLC9xyLsDun3/EOJ+83rY/IHRsO1DToPrdo=
github.com/grafov/bcast v0.0.0-20190217190352-1447f067e08d/go.mod h1:RInr+B3/Tx70hYm0rpNPMTD7vH0pBG5ny/JsHAs2KcQ=
github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690 h1:XWjCrg/HJRLZCbvsUxS5R/9JhwiiwNctEsRvZ1Vjz5k=
github.com/hairyhenderson/go-codeowners v0.2.3-0.20201026200250-cdc7c0759690/go.mod h1:8Qu9UmnhCRunfRv365Z3w+mT/WfLGKJiK+vugY9qNCU=
github.com/hajimehoshi/go-mp3 v0.3.0 h1:fTM5DXjp/DL2G74HHAs/aBGiS9Tg7wnp+jkU38bHy4g=
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
@ -263,96 +218,65 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
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=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgx/v5 v5.1.0 h1:Z7pLKUb65HK6m18No8GGKT87K34NhIIEHa86rRdjxbU=
github.com/jackc/pgx/v5 v5.1.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
github.com/jarcoal/httpmock v0.0.0-20180424175123-9c70cfe4a1da/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
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=
github.com/kataras/go-events v0.0.3/go.mod h1:bFBgtzwwzrag7kQmGuU1ZaVxhK2qseYPQomXoVEMsj4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/youtube/v2 v2.9.0 h1:J7BvfIsxEyyd1MmB/75LgDvG8BGGsG9bSHpbo/qIb+8=
github.com/kkdai/youtube/v2 v2.9.0/go.mod h1:H0ntZBgaah4F0wxnEUkLa6yUeyTDDg06xFJ3tvA6gOw=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kyokomi/emoji/v2 v2.2.10 h1:1z5eMVcxFifsmEoNpdeq4UahbcicgQ4FEHuzrCVwmiI=
github.com/kyokomi/emoji/v2 v2.2.10/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8=
github.com/marekm4/color-extractor v1.2.0 h1:DCU/FXg3PlAwig7W5PRZshiX5x38k0aNPTxYZ6/fZb0=
github.com/marekm4/color-extractor v1.2.0/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.76.1 h1:Q2adw0e101v+DlBfxwP7OOjLGkU/pwpNMwu/RYym54w=
github.com/markbates/goth v1.76.1/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
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=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/mrjones/oauth v0.0.0-20180629183705-f4e24b6d100c/go.mod h1:skjdDftzkFALcuGzYSklqYd8gvat6F1gZJ4YPVbkZpM=
github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/neurosnap/sentences v1.0.6/go.mod h1:pg1IapvYpWCJJm/Etxeh0+gtMf1rI1STY9S7eUCPbDc=
github.com/niklasfasching/go-org v1.6.5 h1:5YAIqNTdl6lAOb7lD2AyQ1RuFGPVrAKvUexphk8PGbo=
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/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=
github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
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/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=
@ -360,32 +284,27 @@ 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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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.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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
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=
github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
github.com/shogo82148/go-shuffle v0.0.0-20180218125048-27e6095f230d/go.mod h1:2htx6lmL0NGLHlO8ZCf+lQBGBHIbEujyywxJArf+2Yc=
github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM=
github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/sosodev/duration v1.0.1 h1:qovz/BFb6kp30KZ4/AYZvB5Z6zANmeQja5l6W9X1w68=
github.com/sosodev/duration v1.0.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk=
github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc=
github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
github.com/steino/gompd/v2 v2.3.1 h1:hzhzsN18omDnDOJRzSu3KNu/dIyuaJinYE3ICK5bkWs=
github.com/steino/gompd/v2 v2.3.1/go.mod h1:6fLLeC5m6Yq/TUQ8e1Va0m+1iSOAFz++X+FbGvneCE4=
github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU=
github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@ -397,22 +316,27 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs=
github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
github.com/tdewolff/minify/v2 v2.12.4 h1:kejsHQMM17n6/gwdw53qsi6lg0TGddZADVyQOz1KMdE=
github.com/tdewolff/minify/v2 v2.12.4/go.mod h1:h+SRvSIX3kwgwTFOpSckvSxgax3uy8kZTSF1Ojrr3bk=
github.com/tdewolff/parse/v2 v2.6.4 h1:KCkDvNUMof10e3QExio9OPZJT8SbdKojLBumw8YZycQ=
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/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.5.3 h1:3HUJmBFbQW9fhQOzMgseU134xfi6hU+mjWywx5Ty+/M=
github.com/yuin/goldmark v1.5.3/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -421,20 +345,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.2.0 h1:BRXPfhNivWL5Yq0BGQ39a2sW6t44aODpfxkWjYdzewE=
golang.org/x/crypto v0.2.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -445,13 +367,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
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.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
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=
@ -475,9 +394,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -490,6 +406,7 @@ 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=
@ -504,19 +421,15 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200927032502-5d4f70055728/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
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/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -526,8 +439,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk=
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -539,9 +452,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -577,23 +488,14 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.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=
@ -601,18 +503,13 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
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 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
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=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.2.0 h1:52I/1L54xyEQAYdtcSuxtiT84KGYTBGXwayxmIpNJhE=
golang.org/x/time v0.2.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -654,7 +551,6 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20200929161345-d7fc70abf50f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
@ -662,13 +558,12 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@ -685,10 +580,11 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ=
google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -727,13 +623,14 @@ google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200929141702-51c3e5b607fe/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -747,10 +644,11 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -763,17 +661,16 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
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-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM=
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/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=
@ -790,6 +687,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32 h1:/S1gOotFo2sADAIdSGk1sDq1VxetoCWr6f5nxOG0dpY=
layeh.com/gopus v0.0.0-20210501142526-1ee02d434e32/go.mod h1:yDtyzWZDFCVnva8NGtg38eH2Ns4J0D/6hD+MMeUGdF0=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -1,6 +1,6 @@
package loop
import "github.com/gopxl/beep"
import "github.com/faiface/beep"
type loop struct {
s beep.StreamSeekCloser

230
mpd.go
View File

@ -1,230 +0,0 @@
package main
import (
"bytes"
"context"
discordspeaker "dndmusicbot/speaker"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"text/template"
"time"
"github.com/gopxl/beep"
"github.com/kataras/go-events"
"github.com/steino/gompd/v2/mpd"
)
type MPD struct {
file int
f beep.Format
httpClient *http.Client
pcm *io.PipeReader
}
func init() {
log.Println("mpd.go loading..")
f, err := os.Create(config.GetString("mpd.config"))
if err != nil {
log.Fatal(err)
}
t, err := template.New("mpd.tmpl").ParseFiles("tmpl/mpd.tmpl")
if err != nil {
log.Fatal(err)
}
mpd_conf := config.GetStringMapString("mpd")
err = t.Execute(f, mpd_conf)
if err != nil {
log.Fatal(err)
}
f.Close()
pidstr, err := os.ReadFile(config.GetString("mpd.pid"))
if err != nil && os.IsNotExist(err) {
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)
}
} else if err != nil {
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)
}
err = app.mpd.NewPartition("ambiance")
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)
}
// 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)
}
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(name string) (*MPD, error) {
out := new(MPD)
out.httpClient = &http.Client{}
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
}
go func() {
buf := make([]byte, 2048)
for {
_, err := syscall.Read(f, buf)
if err != nil {
pcm.Write(make([]byte, 2048))
continue
}
pcm.Write(buf)
}
}()
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.Width())
for i := range samples {
//dn, err := syscall.Read(m.file, tmp)
dn, err := m.pcm.Read(tmp)
if dn == len(tmp) {
samples[i], _ = m.f.DecodeSigned(tmp)
ok = true
}
if err != nil {
samples[i] = discordspeaker.Silence
ok = true
break
}
}
return len(samples), ok
}

View File

@ -1,87 +0,0 @@
package opus
import (
"io"
"log"
"sync"
"github.com/gopxl/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
}

View File

@ -1,86 +0,0 @@
package opus
import (
"context"
"log"
"os"
"sync"
"testing"
"time"
"github.com/gopxl/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{}
f beep.Format
)
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)
}
f = beep.Format{
SampleRate: beep.SampleRate(48000),
NumChannels: 2,
Precision: 2,
}
loop := beep.Loop(-1, d)
mu.Lock()
mixer.Add(loop)
mu.Unlock()
for {
select {
default:
update()
case <-ctx.Done():
return
}
}
}

Binary file not shown.

206
queue.go
View File

@ -1,55 +1,179 @@
package main
import (
"container/list"
"log"
"time"
"dndmusicbot/snapcast"
"github.com/gopxl/beep"
"github.com/gopxl/beep/effects"
"github.com/faiface/beep"
"github.com/kataras/go-events"
"dndmusicbot/ffmpeg"
discordspeaker "dndmusicbot/speaker"
)
var pl_volume *effects.Volume
var amb_volume *effects.Volume
type Ambiance struct {
Type string
URL string
}
func init() {
log.Println("beep.go loading..")
amb := beep.Mixer{}
app.ambiance = beep.Mixer{}
discordspeaker.Play(&app.ambiance)
amb_volume = &effects.Volume{
Streamer: &amb,
Base: 2,
Volume: -2,
Silent: false,
}
app.queue = new(Queue)
app.queue.list = list.New()
app.queue.Events = app.events
discordspeaker.Play(app.queue)
discordspeaker.Play(amb_volume)
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: &pl,
Base: 2,
Volume: -2,
Silent: false,
}
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.")
log.Println("queue.go done.")
}
type Song struct {
Title string
Channel string
VideoID string
Length time.Duration
PCM *ffmpeg.PCM
Playlist Playlist
DLuri string
}
func (s *Song) NewStream() (err error) {
s.PCM, err = ffmpeg.NewPCM(s.DLuri, sampleRate, channels)
return
}
type Queue struct {
Events events.EventEmmiter
playing bool
list *list.List
current *list.Element
}
func (q Queue) IsPlaying() bool {
return q.playing
}
func (q *Queue) Play() {
q.playing = true
if q.list.Len() > 0 {
play := q.list.Front()
q.current = play
app.events.Emit("song_start")
}
}
func (q *Queue) Add(s *Song) {
el := q.list.PushBack(s)
if el == q.list.Front() {
q.Play()
}
}
func (q Queue) QLen() int {
return q.list.Len()
}
func (q Queue) Current() *Song {
if q.current == nil {
return new(Song)
}
return q.current.Value.(*Song)
}
func (q *Queue) Reset() {
q.playing = false
q.current = nil
q.list = q.list.Init()
}
func (q *Queue) Next() {
if q.current == nil {
return
}
next := q.current.Next()
if next == nil {
next = q.list.Front()
}
pcm := next.Value.(*Song).PCM
if pcm != nil && pcm.Position() != 0 {
err := next.Value.(*Song).NewStream()
log.Println(err)
}
q.current = next
}
func (q *Queue) Prev() {
if q.current == nil {
return
}
prev := q.current.Prev()
if prev == nil {
prev = q.list.Back()
}
err := prev.Value.(*Song).NewStream()
if err != nil {
log.Println(err)
}
q.current = prev
}
func (q *Queue) Preload() {
}
func (q *Queue) Stream(samples [][2]float64) (n int, ok bool) {
// We use the filled variable to track how many samples we've
// successfully filled already. We loop until all samples are filled.
filled := 0
for filled < len(samples) {
// There are no streamers in the queue, so we stream silence.
if q.current == nil || q.list.Len() == 0 {
for i := range samples[filled:] {
samples[i][0] = 0
samples[i][1] = 0
}
break
}
// We stream from the first streamer in the queue.
n, ok := q.current.Value.(*Song).PCM.Stream(samples[filled:])
// If it's drained, we pop it from the queue, thus continuing with
// the next streamer.
if !ok {
q.Next()
q.Events.Emit("song_over", nil)
}
// We update the number of filled samples.
filled += n
}
return len(samples), true
}
func (q *Queue) Err() error {
return nil
}
func (q *Queue) Position() int {
if q.current == nil || q.current.Value.(*Song).PCM == nil {
return 0
}
return q.current.Value.(*Song).PCM.Position()
}
func (q Queue) Len() int {
if q.current == nil {
return 0
}
return int(q.current.Value.(*Song).Length.Milliseconds())
}

208
routes.go
View File

@ -1,65 +1,31 @@
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/netip"
"net/url"
"os"
"path"
"path/filepath"
"text/template"
"time"
"github.com/bitly/go-simplejson"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/gorilla/sessions"
"github.com/gorilla/websocket"
"github.com/julienschmidt/httprouter"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/discord"
"golang.org/x/exp/slices"
)
const COOKIE_NAME = "_dndmusicbot"
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
func init() {
key := []byte(os.Getenv("SESSION_SECRET"))
maxAge := 86400 * 30 // 30 days
isProd := true // Set to true when serving over https
store := sessions.NewCookieStore([]byte(key))
store.MaxAge(maxAge)
store.Options.Path = "/"
store.Options.HttpOnly = true // HttpOnly should always be enabled
store.Options.Secure = isProd
gothic.Store = store
goth.UseProviders(
discord.New(config.GetString("discord.id"), config.GetString("discord.secret"), config.GetString("discord.callback"), discord.ScopeIdentify, discord.ScopeEmail, discord.ScopeGuilds, discord.ScopeReadGuilds),
)
app.router = httprouter.New()
app.router.GET("/", auth(app.Index))
app.router.GET("/playlists", auth(app.Web_Playlists))
app.router.GET("/ambiance", auth(app.Web_Ambiance))
app.router.GET("/play/:playlist", auth(app.Play))
app.router.GET("/reset", auth(app.Reset))
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.GET("/", app.Index)
app.router.GET("/play/:playlist", app.Play)
app.router.GET("/reset", app.Reset)
app.router.GET("/public/*js", app.ServeFiles)
app.router.GET("/css/*css", app.ServeFiles)
app.router.HandlerFunc("GET", "/ws", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("WS connection from %v\n", r.RemoteAddr)
@ -69,152 +35,20 @@ func init() {
return
}
err = ws.join(conn)
err = handleWS(conn)
if err != nil {
log.Printf("WS connection closed, %v\n", r.RemoteAddr)
}
}))
go func() {
log.Fatal(http.ListenAndServe(":"+config.GetString("web.port"), app.router))
log.Fatal(http.ListenAndServe(":8824", app.router))
}()
}
type IndexData struct {
Playlists []Playlist
Ambiance []Ambiance
}
func (app App) AuthHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
fmt.Fprintln(w, err)
return
}
profile_url := &url.URL{Scheme: "https",
Host: "discord.com",
Path: "/api/users/@me",
}
member_url := fmt.Sprintf("%s/guilds/%s/member", profile_url.String(), config.GetString("discord.guild"))
member_req, err := http.NewRequest("GET", member_url, nil)
if err != nil {
fmt.Fprintln(w, err)
return
}
member_req.Header.Add("Authorization", "Bearer "+user.AccessToken)
member_resp, err := http.DefaultClient.Do(member_req)
if err != nil {
fmt.Fprintln(w, err)
return
}
member_body, _ := io.ReadAll(member_resp.Body)
defer member_resp.Body.Close()
member, err := simplejson.NewJson(member_body)
if err != nil {
fmt.Fprintln(w, err)
return
}
groups, err := member.GetPath("roles").StringArray()
if err != nil {
fmt.Fprintln(w, err)
return
}
ok := false
for _, group := range config.GetStringSlice("discord.groups") {
if slices.Contains(groups, group) {
ok = true
}
}
if ok {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"nbf": time.Now().Unix(),
"exp": time.Now().Add(time.Hour * 720).Unix(),
})
tokenString, err := token.SignedString([]byte(os.Getenv("SESSION_SECRET")))
if err != nil {
fmt.Fprintln(w, err)
return
}
cookie := new(http.Cookie)
cookie.Name = COOKIE_NAME
cookie.Value = tokenString
cookie.Path = "/"
cookie.Secure = true
cookie.HttpOnly = true
cookie.MaxAge = 86400 * 30
http.SetCookie(w, cookie)
http.RedirectHandler("/", http.StatusFound).ServeHTTP(w, r)
}
}
// 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()
auth_cookie, err := r.Cookie(COOKIE_NAME)
if err == nil {
token, err := jwt.Parse(auth_cookie.Value, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("SESSION_SECRET")), nil
})
if err != nil {
fmt.Fprintln(w, err)
return
}
if token.Valid {
n(w, r, ps)
} else {
gothic.BeginAuthHandler(w, r)
return
}
} else if err == http.ErrNoCookie {
gothic.BeginAuthHandler(w, r)
return
} else if err != nil {
fmt.Fprintln(w, err)
return
}
}
Ambiance []string
}
func (app App) ServeFiles(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
@ -245,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 := GetAmbiances()
amblist, err := GetAmbiance()
if err != nil {
log.Println(err)
return
@ -260,30 +94,6 @@ func (app App) Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params
}
}
func (app App) Web_Playlists(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
playlists, err := app.GetPlaylists()
if err != nil {
http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError)
}
err = json.NewEncoder(w).Encode(playlists)
if err != nil {
http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError)
}
}
func (app App) Web_Ambiance(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
ambiance, err := GetAmbiances()
if err != nil {
http.Error(w, "Unable to get ambiance. "+err.Error(), http.StatusInternalServerError)
}
err = json.NewEncoder(w).Encode(ambiance)
if err != nil {
http.Error(w, "Unable to get playlists. "+err.Error(), http.StatusInternalServerError)
}
}
func (app *App) Play(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
plname := p.ByName("playlist")

View File

@ -1,79 +0,0 @@
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
}

View File

@ -1,61 +1,52 @@
package discordspeaker
import (
"context"
"io"
"bytes"
"encoding/binary"
"log"
"sync"
"time"
"github.com/diamondburned/arikawa/v3/voice"
"github.com/diamondburned/arikawa/v3/voice/voicegateway"
"github.com/gopxl/beep"
"github.com/bwmarrin/discordgo"
"github.com/faiface/beep"
"github.com/pkg/errors"
"golang.org/x/time/rate"
"gopkg.in/hraban/opus.v2"
"layeh.com/gopus"
)
const bufferSize = 1000
var (
mu sync.Mutex
mixer beep.Mixer
samples [][2]float64
done chan struct{}
encoder *opus.Encoder
encoder *gopus.Encoder
voice *discordgo.VoiceConnection
frameSize int = 960
channels int = 2
sampleRate int = 48000
maxBytes int = (frameSize * 2) * 2
pcm []int16
buf []byte
session *voice.Session
pw *io.PipeWriter
pr *io.PipeReader
spklimit = rate.NewLimiter(rate.Every(5*time.Second), 1)
spk = true
pause bool
)
var start time.Time
var Silence = [2]float64{}
var dmutex = sync.Mutex{}
func Init(dgv *voice.Session, bitrate int) error {
func Init(dgv *discordgo.VoiceConnection) error {
var err error
mu.Lock()
defer mu.Unlock()
Close()
pr, pw = io.Pipe()
buf = make([]byte, maxBytes)
mixer = beep.Mixer{}
buf = make([]byte, maxBytes)
pcm = make([]int16, frameSize*channels)
samples = make([][2]float64, frameSize)
session = dgv
pause = true
encoder, err = opus.NewEncoder(sampleRate, channels, opus.AppAudio)
encoder.SetBitrate(bitrate)
voice = dgv
encoder, err = gopus.NewEncoder(sampleRate, channels, gopus.Audio)
if err != nil {
return errors.Wrap(err, "failed to initialize speaker")
@ -72,16 +63,6 @@ func Init(dgv *voice.Session, bitrate int) error {
}
}()
go func() {
dmutex.Lock()
_, err := io.Copy(session, pr)
dmutex.Unlock()
if err != nil {
log.Println(err)
return
}
}()
return nil
}
@ -109,74 +90,46 @@ func Clear() {
mu.Unlock()
}
func Speak(s bool) {
switch s {
case true:
case false:
}
func Pause(p bool) {
pause = p
}
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 IsPaused() bool {
return pause
}
func update() {
start = time.Now()
mu.Lock()
mixer.Stream(samples)
mu.Unlock()
go CheckSilence(samples)
if !spk {
return
}
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++
}
n, err := encoder.EncodeFloat32(f32, buf)
if err != nil {
log.Println(err)
return
}
_, 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 {
for _, v := range in {
if v != Silence {
return false
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
}
}
return true
binary.Read(bytes.NewReader(buf), binary.LittleEndian, &pcm)
opus, err := encoder.Encode(pcm, frameSize, maxBytes)
if err != nil {
log.Println(err)
return
}
if voice.Ready == false || voice.OpusSend == nil {
return
}
voice.OpusSend <- opus
}

View File

@ -1,188 +0,0 @@
import React, { useEffect, useState } from "react";
import CSS from "csstype";
import { on } from "./events";
import ws from "./ws";
import SortableList, { SortableItem } from "react-easy-sort";
import { arrayMoveImmutable } from "array-move";
export default function Ambiance() {
interface Ambiance {
Id: string;
Title: string;
}
const [ambiance, setAmbiance] = useState<Ambiance[]>([]);
const [active, setActive] = useState("");
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [percent, setPercent] = useState(0);
const [running, setRunning] = useState(false);
const onSortEnd = (oldIndex: number, newIndex: number) => {
setAmbiance((array) => arrayMoveImmutable(array, oldIndex, newIndex));
};
const activeStyle: CSS.Properties = {
backgroundColor: "burlywood",
};
const getOrder = (name: string) => {
var order = localStorage.getItem("dndmusicbot-" + name);
return order ? order.split("|") : [];
};
const setOrder = (name: string) => {
var order = ambiance.map((a) => a.Id);
localStorage.setItem("dndmusicbot-" + name, order.join("|"));
};
const fetchAmbiance = () => {
const order = getOrder("ambiance");
fetch("/ambiance")
.then((response) => {
return response.json();
})
.then((data) => {
data.sort(
(a: Ambiance, b: Ambiance) =>
order.indexOf(a.Id) - order.indexOf(b.Id)
);
setAmbiance(data);
});
};
useEffect(() => {
fetchAmbiance();
}, []);
useEffect(() => {
setOrder("ambiance");
}, [ambiance]);
const Play = (e: any) => {
ws.send(
JSON.stringify({
event: "ambiance_play",
payload: e.target.dataset.id,
})
);
};
const Stop = () => {
ws.send(
JSON.stringify({
event: "ambiance_stop",
})
);
};
const AddAmbiance = () => {
if (title == "" || url == "") {
return;
}
ws.send(
JSON.stringify({
event: "ambiance_add",
payload: {
title: title,
url: url,
},
})
);
};
on("dnd:ambiance_add", () => fetchAmbiance());
on("dnd:ambiance_add_start", () => setRunning(true));
on("dnd:ambiance_add_finish", () => {
setRunning(false);
setPercent(0);
setTitle("");
setUrl("");
});
on("dnd:ambiance_play", (e: any) => setActive(e.detail.id));
on("dnd:ambiance_stop", () => setActive(""));
on("dnd:ambiance_encode_finish", () => setPercent(0));
on("dnd:ambiance_encode_progress", (e: any) => {
setRunning(true);
const p = parseInt(e.detail.percent, 10);
if (!Number.isNaN(p)) {
setPercent(p);
}
});
return (
<>
<h2 className="bot">Ambiance</h2>
<section>
<SortableList
onSortEnd={onSortEnd}
className="item-container"
draggedItemClassName="dragged"
>
{ambiance.map((item) => {
return (
<SortableItem key={item.Id}>
<div
className="item drag"
data-id={item.Id}
style={item.Id == active ? activeStyle : {}}
onClick={Play}
>
{item.Title}
</div>
</SortableItem>
);
})}
<div className="item locked stop" data-id="reset" onClick={Stop}>
Stop
</div>
</SortableList>
</section>
<section>
<div id="progressambiance" className="input-container">
<progress
max="100"
value={percent}
style={{ width: "100%" }}
className={running ? "" : "u-hidden"}
>
{percent}%
</progress>
</div>
</section>
<section className={running ? "u-hidden" : ""}>
<div id="inputambiance" className="input-container">
<input
className="u-full-width"
name="title"
type="text"
value={title}
placeholder="Enter name.."
onChange={(e) => setTitle(e.target.value)}
disabled={running}
/>
<input
className="u-full-width"
name="url"
type="text"
value={url}
placeholder="Enter url..."
onChange={(e) => setUrl(e.target.value)}
disabled={running}
/>
<input
id="addambiance"
name="submit"
value="Add"
type="submit"
onClick={AddAmbiance}
disabled={running}
/>
</div>
</section>
</>
);
}

View File

@ -1,98 +0,0 @@
import React, { useState } from "react";
import CSS from "csstype";
import { on } from "./events";
import ws from "./ws";
export default function Controls() {
const [channel, setChannel] = useState("");
const [title, setTitle] = useState("");
const [pos, setPos] = useState(0);
const [len, setLen] = useState(0);
const [song, setSong] = useState("");
const [pause, setPause] = useState(true);
const linkStyle: CSS.Properties = {
textDecoration: "none",
};
const Next = () => {
ws.send(
JSON.stringify({
event: "next",
})
);
};
const Prev = () => {
ws.send(
JSON.stringify({
event: "next",
})
);
};
const msToTime = (duration: number) => {
var milliseconds = (duration % 1000) / 100,
seconds = Math.floor((duration / 1000) % 60),
minutes = Math.floor((duration / (1000 * 60)) % 60),
minutes = minutes < 10 ? 0 + minutes : minutes;
seconds = seconds < 10 ? 0 + seconds : seconds;
return (
minutes.toString().padStart(2, "0") +
":" +
seconds.toString().padStart(2, "0")
);
};
on("dnd:song_info", (e: any) => {
setChannel(e.detail.channel);
setTitle(e.detail.current);
setPos(e.detail.position);
setLen(e.detail.len);
setSong("https://youtu.be/" + e.detail.song);
setPause(e.detail.pause);
});
on("dnd:song_position", (e: any) => {
setPos(e.detail.position);
setLen(e.detail.len);
});
return (
<section>
<div id="info">
<a
href={song}
id="link"
style={linkStyle}
className={pause ? "u-hidden" : ""}
>
<span id="channel">{channel}</span>
<span> - </span>
<span id="title">{title}</span>
</a>
<div className={pause ? "controls u-hidden" : "controls"}>
<input
id="prev"
name="prev"
type="button"
value="prev"
onClick={Prev}
/>
<span id="time">
{msToTime(pos)} / {msToTime(len)}
</span>
<input
id="next"
name="next"
type="button"
value="next"
onClick={Next}
/>
</div>
</div>
</section>
);
}

View File

@ -1,23 +0,0 @@
function on(eventType: any, listener: { (event: any): void; (this: Document, ev: any): any; }) {
document.addEventListener(eventType, listener);
}
function off(eventType: any, listener: { (event: any): void; (this: Document, ev: any): any; }) {
document.removeEventListener(eventType, listener);
}
function once(eventType: any, listener: (arg0: any) => void) {
on(eventType, handleEventOnce);
function handleEventOnce(event: any) {
listener(event);
off(eventType, handleEventOnce);
}
}
function trigger(eventType: string, data: any) {
const event = new CustomEvent(eventType, { detail: data });
document.dispatchEvent(event);
}
export { on, once, off, trigger };

View File

@ -1,17 +0,0 @@
import { useState, useEffect } from "react";
export const useDebounce = (value: any, milliSeconds: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, milliSeconds);
return () => {
clearTimeout(handler);
};
}, [value, milliSeconds]);
return debouncedValue;
};

View File

@ -1,26 +0,0 @@
import React, { useState } from "react";
import ws from "./ws";
export default function Modal() {
const [waiting, setWaiting] = useState(true);
ws.onopen = () => {
console.log("false");
setWaiting(false);
};
ws.onclose = () => {
console.log("true");
setWaiting(true);
};
return (
<>
<div id="waiting" className={waiting ? "modal" : "modal u-hidden"}>
<div className="modal-content">
<p>Waiting for bot..</p>
</div>
</div>
</>
);
}

View File

@ -1,163 +0,0 @@
import React, { useEffect, useState } from "react";
import CSS from "csstype";
import { on } from "./events";
import ws from "./ws";
import SortableList, { SortableItem } from "react-easy-sort";
import { arrayMoveImmutable } from "array-move";
export default function Playlists() {
interface Playlist {
Id: string;
Title: string;
}
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [active, setActive] = useState("");
const [title, setTitle] = useState("");
const [url, setUrl] = useState("");
const [output, setOutput] = useState("");
const onSortEnd = (oldIndex: number, newIndex: number) => {
setPlaylists((array) => arrayMoveImmutable(array, oldIndex, newIndex));
};
const activeStyle: CSS.Properties = {
backgroundColor: "burlywood",
};
const getOrder = (name: string) => {
var order = localStorage.getItem("dndmusicbot-" + name);
return order ? order.split("|") : [];
};
const setOrder = (name: string) => {
var order = playlists.map((a) => a.Id);
localStorage.setItem("dndmusicbot-" + name, order.join("|"));
};
const fetchPlaylists = () => {
const order = getOrder("playlist");
fetch("/playlists")
.then((response) => {
return response.json();
})
.then((data) => {
data.sort(
(a: Playlist, b: Playlist) =>
order.indexOf(a.Id) - order.indexOf(b.Id)
);
setPlaylists(data);
});
};
useEffect(() => {
fetchPlaylists();
}, []);
useEffect(() => {
setOrder("playlist");
}, [playlists]);
const Play = (e: any) => {
ws.send(
JSON.stringify({
event: "load_playlist",
payload: e.target.dataset.id,
})
);
};
const Stop = () => {
ws.send(
JSON.stringify({
event: "stop",
})
);
};
const AddPlaylist = () => {
if (title == "" || url == "") {
setOutput("Title or Url is empty!");
return;
}
ws.send(
JSON.stringify({
event: "add_playlist",
payload: {
title: title,
url: url,
},
})
);
};
on("dnd:song_info", (e: any) => {
setActive(e.detail.playlist);
});
on("dnd:new_playlist", (e: any) => {
fetchPlaylists();
setOutput("New playlist was added: " + e.details.title);
});
return (
<>
<h2 className="bot">Playlists</h2>
<section>
<SortableList
onSortEnd={onSortEnd}
className="item-container"
draggedItemClassName="dragged"
>
{playlists.map((playlist) => {
return (
<SortableItem key={playlist.Id}>
<div
onClick={Play}
className="item"
data-id={playlist.Id}
style={playlist.Id == active ? activeStyle : {}}
>
{playlist.Title}
</div>
</SortableItem>
);
})}
<div className="item locked stop" onClick={Stop} data-id="reset">
Stop
</div>
</SortableList>
</section>
<section>
<div id="inputplaylist" className="input-container">
<input
className="u-full-width"
name="title"
type="text"
placeholder="Enter name.."
onChange={(e) => setTitle(e.target.value)}
/>
<input
className="u-full-width"
name="url"
type="text"
placeholder="https://youtube.com/playlist?list=..."
onChange={(e) => setUrl(e.target.value)}
/>
<input
id="addplaylist"
name="submit"
value="Add"
type="submit"
onClick={AddPlaylist}
/>
</div>
</section>
<section>
<p>{output}</p>
</section>
</>
);
}

View File

@ -1,30 +0,0 @@
import React, { useState } from "react";
import { createRoot } from "react-dom/client";
import { trigger, on } from "./events";
import Playlists from "./playlist";
import ws from "./ws";
import Controls from "./controls";
import Ambiance from "./ambiance";
import Volumes from "./volume";
import Modal from "./modal";
function Content() {
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
trigger("dnd:" + data.event, data.payload);
};
return (
<>
<Playlists />
<Controls />
<Ambiance />
<Volumes />
<Modal />
</>
);
}
const domNode = document.getElementById("content");
const root = createRoot(domNode!);
root.render(<Content />);

View File

@ -1,111 +0,0 @@
import React, { useState } from "react";
import { on } from "./events";
import ws from "./ws";
import { useEffect, useRef } from "react";
import { useDebounce } from "./hooks/useDebounce";
export default function Volumes() {
const [playlist, setPlaylist] = useState(0);
const [ambiance, setAmbiance] = useState(0);
const plfirstUpdate = useRef(true);
const ambfirstUpdate = useRef(true);
const plsend = useDebounce(playlist, 100);
const ambsend = useDebounce(ambiance, 100);
on("dnd:volume", (e: any) => {
setPlaylist(e.detail.playlist);
setAmbiance(e.detail.ambiance);
});
useEffect(() => {
if (plfirstUpdate.current) {
plfirstUpdate.current = false;
return;
}
ws.send(
JSON.stringify({
event: "vol_set",
payload: {
type: "playlist",
vol: plsend.toString(),
},
})
);
}, [plsend]);
useEffect(() => {
if (ambfirstUpdate.current) {
ambfirstUpdate.current = false;
return;
}
ws.send(
JSON.stringify({
event: "vol_set",
payload: {
type: "ambiance",
vol: ambsend.toString(),
},
})
);
}, [ambsend]);
const PlaylistVol = (e: any) => {
setPlaylist(parseFloat(e.target.value));
};
const AmbianceVol = (e: any) => {
setAmbiance(parseFloat(e.target.value));
};
return (
<>
<section>
<div id="volume_playlist" className="input-container">
<label htmlFor="playlist-volume">Playlist</label>
<input
type="range"
id="playlist-volume"
min="-10"
max="4"
step="0.1"
value={playlist}
onChange={PlaylistVol}
/>
<input
id="playlist-volume-number"
type="number"
min="-10"
max="4"
step="0.1"
style={{ width: "50px" }}
value={playlist}
onChange={PlaylistVol}
/>
</div>
<div id="volume_ambiance" className="input-container">
<label htmlFor="ambiance-volume">Ambiance</label>
<input
type="range"
id="ambiance-volume"
min="-10"
max="4"
step="0.1"
value={ambiance}
onChange={AmbianceVol}
/>
<input
id="ambiance-volume-number"
type="number"
min="-10"
max="4"
step="0.1"
style={{ width: "50px" }}
value={ambiance}
onChange={AmbianceVol}
/>
</div>
</section>
</>
);
}

View File

@ -1,9 +0,0 @@
import ReconnectingWebSocket from "reconnecting-websocket";
const ws = new ReconnectingWebSocket(
(window.location.protocol === "https:" ? "wss://" : "ws://") +
window.location.host +
"/ws"
);
export default ws;

View File

@ -1,20 +1,9 @@
{
"scripts": {
"build": "esbuild app/root.tsx --bundle --outfile=/public/script.js"
"build": "esbuild script.js --bundle --outdir=/public/"
},
"dependencies": {
"@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14",
"array-move": "^4.0.0",
"esbuild": "^0.15.14",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-debounce-input": "^3.3.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-easy-sort": "^1.6.0",
"react-sortable-hoc": "^2.0.0",
"reconnecting-websocket": "^4.4.0",
"sortablejs": "^1.15.0"
}

View File

@ -35,41 +35,7 @@ window.onload = function () {
const link = document.querySelector("#link")
const time = document.querySelector("#time")
const waiting = document.querySelector("#waiting")
const pvol = document.querySelector("#playlist-volume")
const avol = document.querySelector("#ambiance-volume")
pvol.addEventListener("change", (e) => {
e.target.nextElementSibling.value = e.target.value
ws.send(JSON.stringify({
"event": "vol_set",
"payload": {
"type": "playlist",
"vol": e.target.value
}
}))
})
pvol.nextElementSibling.addEventListener("change", (e) => {
e.target.previousElementSibling.value = e.target.value
e.target.previousElementSibling.dispatchEvent(new Event('change'));
})
avol.addEventListener("change", (e) => {
e.target.nextElementSibling.value = e.target.value
ws.send(JSON.stringify({
"event": "vol_set",
"payload": {
"type": "ambiance",
"vol": e.target.value
}
}))
})
avol.nextElementSibling.addEventListener("change", (e) => {
e.target.previousElementSibling.value = e.target.value
e.target.previousElementSibling.dispatchEvent(new Event('change'));
})
addInteractHandler(
document.querySelector("input#next"), (e) => {
ws.send(JSON.stringify({
@ -141,81 +107,9 @@ window.onload = function () {
ws.onmessage = (e) => {
data = JSON.parse(e.data)
switch (data.event) {
case "volume":
pvol.value = data.payload.playlist
pvol.nextElementSibling.value = data.payload.playlist
avol.value = data.payload.ambiance
avol.nextElementSibling.value = data.payload.ambiance
break
case "ambiance_download_start":
var progress = document.querySelector("#progressambiance progress")
progress.style.display = "initial"
progress.style.width = "100%"
progress.value = 0
break
case "ambiance_download_progress":
var progress = document.querySelector("#progressambiance progress")
progress.style.display = "initial"
progress.value = data.payload.percent
console.log(data)
break
case "ambiance_download_complete":
var progress = document.querySelector("#progressambiance progress")
progress.style.display = "initial"
progress.value = 100
break
case "ambiance_add_finish":
var title = document.querySelector("#inputambiance > input[name='title']")
var url = document.querySelector("#inputambiance > input[name='url']")
var submit = document.querySelector("#inputambiance > input[name='submit']")
var progress = document.querySelector("#progressambiance progress")
title.value = ""
url.value = ""
title.disabled = false
url.disabled = false
submit.disabled = false
progress.value = 0
progress.style.display = "none"
break
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"))
var title = document.querySelector("#inputambiance > input[name='title']")
var url = document.querySelector("#inputambiance > input[name='url']")
var submit = document.querySelector("#inputambiance > input[name='submit']")
var progress = document.querySelector("#progressambiance progress")
title.value = ""
url.value = ""
title.disabled = false
url.disabled = false
submit.disabled = false
progress.value = 0
progress.style.display = "none"
break
case "ambiance_play":
document.querySelectorAll("#ambiance > div").forEach((e) => {e.style.removeProperty("background-color")})
document.querySelector(`#ambiance > div[data-id='${data.payload.id}']`).style.backgroundColor = "burlywood"
document.querySelector(`#ambiance > div[data-id='${data.payload.type}']`).style.backgroundColor = "burlywood"
ambiance.style.pointerEvents = 'auto'
break
case "ambiance_stop":
@ -266,7 +160,6 @@ window.onload = function () {
//output.innerText = ""
var title = document.querySelector("#inputambiance > input[name='title']")
var url = document.querySelector("#inputambiance > input[name='url']")
var submit = document.querySelector("#inputambiance > input[name='submit']")
if (title.value == "" || url.value == "") {
console.log("Title or Url is empty!")
return
@ -279,10 +172,6 @@ window.onload = function () {
"url": url.value
}
}))
title.disabled = true
url.disabled = true
submit.disabled = true
})
addInteractHandler(submit, (e, isTouch) => {

View File

@ -2,13 +2,6 @@
# yarn lockfile v1
"@babel/runtime@^7.15.4", "@babel/runtime@^7.2.0", "@babel/runtime@^7.9.2":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
dependencies:
regenerator-runtime "^0.14.0"
"@esbuild/android-arm@0.15.14":
version "0.15.14"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.14.tgz#5d0027f920eeeac313c01fd6ecb8af50c306a466"
@ -19,96 +12,6 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.14.tgz#1221684955c44385f8af34f7240088b7dc08d19d"
integrity sha512-eQi9rosGNVQFJyJWV0HCA5WZae/qWIQME7s8/j8DMvnylfBv62Pbu+zJ2eUDqNf2O4u3WB+OEXyfkpBoe194sg==
"@react-dnd/asap@^5.0.1":
version "5.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
integrity sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==
"@react-dnd/invariant@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/invariant/-/invariant-4.0.2.tgz#b92edffca10a26466643349fac7cdfb8799769df"
integrity sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==
"@react-dnd/shallowequal@^4.0.1":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz#d1b4befa423f692fa4abf1c79209702e7d8ae4b4"
integrity sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.4"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.4.tgz#cc477ce0283bb9d19ea0cbfa2941fe2c8493a1be"
integrity sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==
dependencies:
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/prop-types@*":
version "15.7.9"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.9.tgz#b6f785caa7ea1fe4414d9df42ee0ab67f23d8a6d"
integrity sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==
"@types/react-dom@^18.2.14":
version "18.2.14"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.14.tgz#c01ba40e5bb57fc1dc41569bb3ccdb19eab1c539"
integrity sha512-V835xgdSVmyQmI1KLV2BEIUgqEuinxp9O4G6g3FqO/SqLac049E53aysv0oEFD2kHfejeKU+ZqL2bcFWj9gLAQ==
dependencies:
"@types/react" "*"
"@types/react-redux@^7.1.20":
version "7.1.28"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.28.tgz#30a44303c7daceb6ede9cfb4aaf72e64f1dde4de"
integrity sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw==
dependencies:
"@types/hoist-non-react-statics" "^3.3.0"
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react@*", "@types/react@^18.2.33":
version "18.2.33"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.33.tgz#055356243dc4350a9ee6c6a2c07c5cae12e38877"
integrity sha512-v+I7S+hu3PIBoVkKGpSYYpiBT1ijqEzWpzQD62/jm4K74hPpSP7FF9BnKG6+fg2+62weJYkkBWDJlZt5JO/9hg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.5"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.5.tgz#4751153abbf8d6199babb345a52e1eb4167d64af"
integrity sha512-s/FPdYRmZR8SjLWGMCuax7r3qCWQw9QKHzXVukAuuIJkXkDRwp+Pu5LMIVFi0Fxbav35WURicYr8u1QsoybnQw==
array-move@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/array-move/-/array-move-3.0.1.tgz#179645cc0987b65953a4fc06b6df9045e4ba9618"
integrity sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==
array-move@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/array-move/-/array-move-4.0.0.tgz#2c3730f056cc926f62a59769a5a8cda2fb6d8c55"
integrity sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==
css-box-model@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"
integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==
dependencies:
tiny-invariant "^1.0.6"
csstype@^3.0.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
dnd-core@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-16.0.1.tgz#a1c213ed08961f6bd1959a28bb76f1a868360d19"
integrity sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==
dependencies:
"@react-dnd/asap" "^5.0.1"
"@react-dnd/invariant" "^4.0.1"
redux "^4.2.0"
esbuild-android-64@0.15.14:
version "0.15.14"
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.14.tgz#114e55b0d58fb7b45d7fa3d93516bd13fc8869cc"
@ -237,199 +140,12 @@ esbuild@^0.15.14:
esbuild-windows-64 "0.15.14"
esbuild-windows-arm64 "0.15.14"
fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
lodash.debounce@^4:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
memoize-one@^5.1.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
prop-types@^15.5.7, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"
raf-schd@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
react-beautiful-dnd@^13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"
integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==
dependencies:
"@babel/runtime" "^7.9.2"
css-box-model "^1.2.0"
memoize-one "^5.1.1"
raf-schd "^4.0.2"
react-redux "^7.2.0"
redux "^4.0.4"
use-memo-one "^1.1.1"
react-debounce-input@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-debounce-input/-/react-debounce-input-3.3.0.tgz#85e3ebcaa41f2016e50613134a1ec9fe3cdb422e"
integrity sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==
dependencies:
lodash.debounce "^4"
prop-types "^15.8.1"
react-dnd-html5-backend@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz#87faef15845d512a23b3c08d29ecfd34871688b6"
integrity sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==
dependencies:
dnd-core "^16.0.1"
react-dnd@^16.0.1:
version "16.0.1"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-16.0.1.tgz#2442a3ec67892c60d40a1559eef45498ba26fa37"
integrity sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==
dependencies:
"@react-dnd/invariant" "^4.0.1"
"@react-dnd/shallowequal" "^4.0.1"
dnd-core "^16.0.1"
fast-deep-equal "^3.1.3"
hoist-non-react-statics "^3.3.2"
react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-easy-sort@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/react-easy-sort/-/react-easy-sort-1.6.0.tgz#b40cce827913f0640c1b2e5438dd4d007e26db32"
integrity sha512-zd9Nn90wVlZPEwJrpqElN87sf9GZnFR1StfjgNQVbSpR5QTSzCHjEYK6REuwq49Ip+76KOMSln9tg/ST2KLelg==
dependencies:
array-move "^3.0.1"
tslib "2.0.1"
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-redux@^7.2.0:
version "7.2.9"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d"
integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==
dependencies:
"@babel/runtime" "^7.15.4"
"@types/react-redux" "^7.1.20"
hoist-non-react-statics "^3.3.2"
loose-envify "^1.4.0"
prop-types "^15.7.2"
react-is "^17.0.2"
react-sortable-hoc@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/react-sortable-hoc/-/react-sortable-hoc-2.0.0.tgz#f6780d8aa4b922a21f3e754af542f032677078b7"
integrity sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==
dependencies:
"@babel/runtime" "^7.2.0"
invariant "^2.2.4"
prop-types "^15.5.7"
react@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
reconnecting-websocket@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
redux@^4.0.0, redux@^4.0.4, redux@^4.2.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197"
integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==
dependencies:
"@babel/runtime" "^7.9.2"
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
sortablejs@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
tiny-invariant@^1.0.6:
version "1.3.1"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tslib@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.1.tgz#410eb0d113e5b6356490eec749603725b021b43e"
integrity sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==
use-memo-one@^1.1.1:
version "1.1.3"
resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99"
integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==

View File

@ -7,10 +7,73 @@
<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 id="content" class="container"></div>
<script src="/public/script.js"></script>
<div class="container">
<h2 class="bot">Playlists</h2>
<section>
<div id="items" class="item-container">
{{ range .Playlists }}
<div class="item" data-id="{{ .Id }}">{{ .Title }}</div>
{{ end}}
<div class="item locked stop" data-id="reset">Stop</div>
</div>
</section>
<section>
<div id="inputplaylist" class="input-container">
<input class="u-full-width" name="title" type="text" placeholder="Enter name..">
<input class="u-full-width" name="url" type="text" placeholder="https://youtube.com/playlist?list=...">
<input id="addplaylist" name="submit" value="Add" type="submit">
</div>
</section>
<section>
<p id="output"></p>
<div id="info">
<a id="link" style="text-decoration: none;">
<span id="channel"></span>
<span> - </span>
<span id="title"></span>
</a>
<div class="controls">
<input id="prev" name="prev" type="button" value="prev">
<span id="time"></span>
<input id="next" name="next" type="button" value="next">
</div>
</div>
</section>
<h2 class="bot">Ambiance</h2>
<section>
<div id="ambiance" class="item-container">
{{ range .Ambiance }}
<div class="item drag" data-id="{{ . }}">{{ . }}</div>
{{ end}}
<div class="item locked stop" data-id="reset">Stop</div>
</div>
</section>
<section>
<div id="inputambiance" class="input-container">
<input class="u-full-width" name="title" type="text" placeholder="Enter name..">
<input class="u-full-width" name="url" type="text" placeholder="Enter url...">
<input id="addambiance" name="submit" value="Add" type="submit">
</div>
</section>
<!-- The Modal -->
<div id="waiting" class="modal">
<!-- Modal content -->
<div class="modal-content">
<p>Waiting for bot..</p>
</div>
</div>
</div>
<script src="/public/script.js"></script>
</body>
</html>
</html>

View File

@ -1,52 +0,0 @@
# audio_output {
# type "fifo"
# name "fifo-output"
# path "{{ .fifo }}"
# format "48000:16:2"
# enabled "yes"
#}
audio_output {
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"
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 }}"

62
ws.go
View File

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

View File

@ -2,81 +2,155 @@ package main
import (
"bytes"
"dndmusicbot/youtube"
"context"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"io"
"log"
"net/http"
"os"
"math/big"
"time"
"github.com/grafov/m3u8"
"github.com/julienschmidt/httprouter"
mrand "math/rand"
"github.com/sosodev/duration"
"google.golang.org/api/option"
"google.golang.org/api/youtube/v3"
)
func init() {
log.Println("youtube.go loading..")
app.youtube = youtube.New()
var err error
apikey := config.GetString("youtube.apikey")
app.youtube, err = youtube.NewService(context.Background(), option.WithAPIKey(apikey))
if err != nil {
log.Fatal(err)
}
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
})
func ShufflePlaylist(list []string) ([]string, error) {
seedb := make([]byte, 32)
_, err := rand.Read(seedb)
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
return nil, err
}
seed := binary.BigEndian.Uint64(seedb)
mrand.Seed(int64(seed))
mrand.Shuffle(len(list), func(i, j int) { list[i], list[j] = list[j], list[i] })
return list, nil
}
func (app *App) GetSong(list []string) string {
return list[app.plidx]
}
func (app *App) GetNextSong(list []string) string {
app.plidx++
if app.plidx >= len(app.active) {
app.plidx = 0
}
return list[app.plidx]
}
func (app *App) GetPrevSong(list []string) string {
app.plidx--
if app.plidx < 0 {
app.plidx = len(list)
}
return list[app.plidx]
}
func GetRandomSong(list []string) string {
if !(len(list) > 0) {
return ""
}
seeker := bytes.NewReader(data)
w.Header().Add("Content-Type", "audio/mp4")
http.ServeContent(w, r, "youtube."+p.ByName("id"), time.Now(), seeker)
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(list)-1)))
if err != nil {
log.Println("Failed to get random int, ", err)
return ""
}
return list[idx.Int64()]
}
type VideoInfo struct {
Title string
Channel string
Len time.Duration
Uri string
}
func (app App) Video(vid string) (out VideoInfo, err error) {
_, r, err := app.cache.GetOrCreate(vid+".videoinfo", func() (io.ReadCloser, error) {
call := app.youtube.Videos.List([]string{"snippet", "contentDetails"})
call.MaxResults(1)
call.Id(vid)
resp, err := call.Do()
if err != nil {
return nil, err
}
if len(resp.Items) != 1 {
return nil, errors.New("response contains not 1 item")
}
video := resp.Items[0]
songdur, err := duration.Parse(video.ContentDetails.Duration)
if err != nil {
return nil, err
}
out := new(VideoInfo)
out.Len = songdur.ToTimeDuration()
out.Title = video.Snippet.Title
out.Channel = video.Snippet.ChannelTitle
out.Uri = vid
var buf bytes.Buffer
json.NewEncoder(&buf).Encode(out)
return io.NopCloser(&buf), nil
})
if err != nil {
return
}
json.NewDecoder(r).Decode(&out)
return out, nil
}
func (app App) Playlist(playlist string) ([]string, error) {
call := app.youtube.PlaylistItems.List([]string{"contentDetails"})
pageToken := ""
call = call.MaxResults(50)
call = call.PlaylistId(playlist)
if pageToken != "" {
call = call.PageToken(pageToken)
}
var list []string
for {
response, err := call.Do()
if err != nil {
return nil, err
}
for _, item := range response.Items {
list = append(list, item.ContentDetails.VideoId)
}
pageToken = response.NextPageToken
if pageToken == "" {
break
}
call.PageToken(pageToken)
}
return list, nil
}

View File

@ -1,150 +0,0 @@
package youtube
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/grafov/m3u8"
)
type Client struct {
InnertubeClient *InnertubeClient
HTTPClient *http.Client
}
type InnertubeClient struct {
Key string `json:"-"`
HDRClientName string `json:"-"`
HL string `json:"hl,omitempty"`
GL string `json:"gl,omitempty"`
ClientName string `json:"clientName,omitempty"`
ClientVersion string `json:"clientVersion,omitempty"`
DeviceModel string `json:"deviceModel,omitempty"`
UserAgent string `json:"userAgent,omitempty"`
TimeZone string `json:"timeZone,omitempty"`
UTCOffsetMins int64 `json:"utcOffsetMinutes,omitempty"`
}
type innertubeRequest struct {
VideoID string `json:"videoId,omitempty"`
BrowseID string `json:"browseId,omitempty"`
Continuation string `json:"continuation,omitempty"`
Context struct {
Client InnertubeClient `json:"client"`
} `json:"context"`
PlaybackContext struct {
ContentPlaybackContext struct {
HTML5Preference string `json:"html5Preference"`
} `json:"contentPlaybackContext"`
} `json:"playbackContext,omitempty"`
ContentCheckOK bool `json:"contentCheckOk,omitempty"`
RacyCheckOk bool `json:"racyCheckOk,omitempty"`
Params string `json:"params"`
}
var (
IOSClient = InnertubeClient{
Key: "AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI",
HDRClientName: "5",
HL: "en",
GL: "US",
ClientName: "IOS",
ClientVersion: "17.33.2",
DeviceModel: "iPhone14,3",
UserAgent: "com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)",
TimeZone: "UTC",
UTCOffsetMins: 0,
}
)
var DefaultClient = IOSClient
func New() *Client {
client := new(Client)
client.HTTPClient = &http.Client{}
client.InnertubeClient = &DefaultClient
return client
}
func (c Client) GetVideoFromID(id string) (out Video, err error) {
uri, _ := url.Parse("https://www.youtube.com/youtubei/v1/player")
q := uri.Query()
q.Add("key", c.InnertubeClient.Key)
uri.RawQuery = q.Encode()
reqData := new(innertubeRequest)
reqData.Context.Client = *c.InnertubeClient
reqData.VideoID = id
reqData.ContentCheckOK = true
reqData.RacyCheckOk = true
reqData.Params = "CgIQBg=="
reqData.PlaybackContext.ContentPlaybackContext.HTML5Preference = "HTML5_PREF_WANTS"
reqBody, err := json.Marshal(reqData)
if err != nil {
return
}
payload := bytes.NewReader(reqBody)
req, err := http.NewRequest("POST", uri.String(), payload)
if err != nil {
fmt.Println(err)
return
}
req.Header.Add("User-Agent", c.InnertubeClient.UserAgent)
req.Header.Add("X-YouTube-Client-Name", c.InnertubeClient.HDRClientName)
req.Header.Add("X-YouTube-Client-Version", c.InnertubeClient.ClientVersion)
req.Header.Add("Content-Type", "application/json")
res, err := c.HTTPClient.Do(req)
if err != nil {
fmt.Println(err)
return
}
defer res.Body.Close()
json.NewDecoder(res.Body).Decode(&out)
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)
if err != nil {
return
}
defer resp.Body.Close()
p, _, err := m3u8.DecodeFrom(resp.Body, true)
if err != nil {
return
}
pl := p.(*m3u8.MasterPlaylist)
for _, variant := range pl.Variants {
if len(variant.Alternatives) > 0 {
for _, a := range variant.Alternatives {
if a.GroupId == itag {
return a.URI
}
}
}
}
return
}

View File

@ -1,67 +0,0 @@
package youtube
type Video struct {
StreamingData struct {
AdaptiveFormats []struct {
ApproxDurationMs string `json:"approxDurationMs"`
AudioChannels int64 `json:"audioChannels"`
AudioQuality string `json:"audioQuality"`
AudioSampleRate string `json:"audioSampleRate"`
AverageBitrate int64 `json:"averageBitrate"`
Bitrate int64 `json:"bitrate"`
ColorInfo struct {
MatrixCoefficients string `json:"matrixCoefficients"`
Primaries string `json:"primaries"`
TransferCharacteristics string `json:"transferCharacteristics"`
} `json:"colorInfo"`
ContentLength string `json:"contentLength"`
Fps int64 `json:"fps"`
Height int64 `json:"height"`
HighReplication bool `json:"highReplication"`
IndexRange struct {
End string `json:"end"`
Start string `json:"start"`
} `json:"indexRange"`
InitRange struct {
End string `json:"end"`
Start string `json:"start"`
} `json:"initRange"`
Itag int64 `json:"itag"`
LastModified string `json:"lastModified"`
LoudnessDB float64 `json:"loudnessDb"`
MimeType string `json:"mimeType"`
ProjectionType string `json:"projectionType"`
Quality string `json:"quality"`
QualityLabel string `json:"qualityLabel"`
URL string `json:"url"`
Width int64 `json:"width"`
} `json:"adaptiveFormats"`
AspectRatio float64 `json:"aspectRatio"`
ExpiresInSeconds string `json:"expiresInSeconds"`
HlsManifestURL string `json:"hlsManifestUrl"`
ServerAbrStreamingURL string `json:"serverAbrStreamingUrl"`
} `json:"streamingData"`
VideoDetails struct {
AllowRatings bool `json:"allowRatings"`
Author string `json:"author"`
ChannelID string `json:"channelId"`
IsCrawlable bool `json:"isCrawlable"`
IsLiveContent bool `json:"isLiveContent"`
IsOwnerViewing bool `json:"isOwnerViewing"`
IsPrivate bool `json:"isPrivate"`
IsUnpluggedCorpus bool `json:"isUnpluggedCorpus"`
Keywords []string `json:"keywords"`
LengthSeconds string `json:"lengthSeconds"`
ShortDescription string `json:"shortDescription"`
Thumbnail struct {
Thumbnails []struct {
Height int64 `json:"height"`
URL string `json:"url"`
Width int64 `json:"width"`
} `json:"thumbnails"`
} `json:"thumbnail"`
Title string `json:"title"`
VideoID string `json:"videoId"`
ViewCount string `json:"viewCount"`
} `json:"videoDetails"`
}

View File

@ -1,18 +0,0 @@
package youtube
import (
"fmt"
"log"
"testing"
)
func TestNew(t *testing.T) {
client := New()
video, err := client.GetVideoFromID("LHsGz91Ivwg")
if err != nil {
log.Println(err)
return
}
fmt.Println(video.GetHLSPlaylist("234"))
}

145
ytdl.go
View File

@ -1,45 +1,25 @@
package main
import (
"bufio"
"fmt"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/tidwall/gjson"
"golang.org/x/time/rate"
)
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 NewYTdl(vid string) ([]byte, error) {
ytdl := config.GetString("youtube.ytdl")
yt := exec.Command(
uri, err := exec.Command(
ytdl,
fmt.Sprintf(yturl, vid),
"--cookies", "./cookies.txt",
@ -48,13 +28,9 @@ func NewYTdlUrl(vid string) ([]byte, error) {
"--ignore-errors",
"--newline",
"--restrict-filenames",
"-f", "234",
"-f", "140",
"--get-url",
)
yt.Stderr = os.Stderr
uri, err := yt.Output()
).Output()
if err != nil {
return nil, err
}
@ -62,100 +38,16 @@ func NewYTdlUrl(vid string) ([]byte, error) {
return uri[:len(uri)-1], nil
}
func NewYTdl(vid string) ([]byte, error) {
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_ytdlp_XXXXXXXXXXXX.m4a").Output()
func DownloadAmbiance(uri string, name string) error {
ytdl := config.GetString("youtube.ytdl")
tmpfile, err := exec.Command("mktemp", "/tmp/dnd_XXXXXXXXXXXX.aac").Output()
if err != nil {
return nil, err
return err
}
tmpfile = tmpfile[:len(tmpfile)-1]
ytdl := config.GetString("youtube.ytdl")
yt := exec.Command(
ytdl,
fmt.Sprintf(yturl, vid),
"-q",
"--cookies", "./cookies.txt",
"--no-call-home",
"--no-cache-dir",
"--ignore-errors",
"--newline",
"--restrict-filenames",
"--force-overwrites",
"--progress",
"--progress-template", "download:{ \"dl_bytes\": \"%(progress.downloaded_bytes)s\", \"total_bytes\": \"%(progress.total_bytes)s\" }",
"-f", "251",
"-o", string(tmpfile),
)
yt.Stderr = os.Stderr
ytprogress, err := yt.StdoutPipe()
if err != nil {
return nil, err
}
err = yt.Start()
if err != nil {
return nil, err
}
log.Printf("Start ffmpeg to extract audio to %s", string(tmpfile))
msg := make(map[string]interface{})
msg["event"] = "ambiance_download_start"
data := make(map[string]string)
data["name"] = fmt.Sprintf(yturl, vid)
msg["payload"] = data
ws_msg <- msg
msg = make(map[string]interface{})
msg["event"] = "ambiance_download_progress"
data = make(map[string]string)
data["name"] = fmt.Sprintf(yturl, vid)
scanner := bufio.NewScanner(ytprogress)
for scanner.Scan() {
err := json.Unmarshal(scanner.Bytes(), &data)
if err != nil {
log.Println(err)
continue
}
dl, _ := strconv.ParseFloat(data["dl_bytes"], 64)
total, _ := strconv.ParseFloat(data["total_bytes"], 64)
percent := math.Floor((dl / total) * 100)
data["percent"] = strconv.FormatInt(int64(percent), 10)
prate.Do(func() {
msg["payload"] = data
ws_msg <- msg
})
}
if err := scanner.Err(); err != nil {
return nil, err
}
err = yt.Wait()
if err != nil {
return nil, err
}
msg = make(map[string]interface{})
msg["event"] = "ambiance_download_complete"
data = make(map[string]string)
data["name"] = fmt.Sprintf(yturl, vid)
msg["payload"] = data
ws_msg <- msg
return tmpfile, nil
}
/*
func DownloadAmbiance(uri string, name string) error {
ytdl := config.GetString("youtube.ytdl")
cmd := exec.Command(
ytdl,
uri,
@ -163,7 +55,7 @@ func DownloadAmbiance(uri string, name string) error {
"--no-cache-dir",
"-f", "140",
"--cookies", "../cookies.txt",
"-o", "-",
"-o", string(tmpfile),
"--force-overwrites",
"-q",
"--progress",
@ -288,4 +180,3 @@ func DownloadAmbiance(uri string, name string) error {
return nil
}
*/