diff --git a/Makefile b/Makefile index c8d996f0..ada96186 100644 --- a/Makefile +++ b/Makefile @@ -85,6 +85,10 @@ build: warning-noui-build check_go_env ##@Build Build only backend go build -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo .PHONY: build +debug-build: warning-noui-build check_go_env ##@Build Build only backend (with remote debug on) + go build -gcflags="all=-N -l" -ldflags="-X github.com/navidrome/navidrome/consts.gitSha=$(GIT_SHA) -X github.com/navidrome/navidrome/consts.gitTag=$(GIT_TAG)-SNAPSHOT" -tags=netgo +.PHONY: debug-build + buildjs: check_node_env ##@Build Build only frontend @(cd ./ui && npm run build) .PHONY: buildjs diff --git a/cmd/root.go b/cmd/root.go index d9fd9d61..72102fa0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,6 +12,7 @@ import ( "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core" + "github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/db" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/resources" @@ -75,6 +76,10 @@ func runNavidrome() { g.Go(startScheduler(ctx)) g.Go(schedulePeriodicScan(ctx)) + if conf.Server.Jukebox.Enabled { + g.Go(startPlaybackServer(ctx)) + } + if err := g.Wait(); err != nil && !errors.Is(err, interrupted) { log.Error("Fatal error in Navidrome. Aborting", err) } @@ -146,6 +151,16 @@ func startScheduler(ctx context.Context) func() error { } } +func startPlaybackServer(ctx context.Context) func() error { + log.Info(ctx, "Starting playback server") + + playbackInstance := playback.GetInstance() + + return func() error { + return playbackInstance.Run(ctx) + } +} + // TODO: Implement some struct tags to map flags to viper func init() { cobra.OnInitialize(func() { diff --git a/conf/configuration.go b/conf/configuration.go index d5521884..13ea3632 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -55,6 +55,7 @@ type configOptions struct { IndexGroups string SubsonicArtistParticipations bool FFmpegPath string + MPVPath string CoverArtPriority string CoverJpegQuality int ArtistArtPriority string @@ -78,6 +79,7 @@ type configOptions struct { ReverseProxyWhitelist string Prometheus prometheusOptions Scanner scannerOptions + Jukebox jukeboxOptions Agents string LastFM lastfmOptions @@ -129,6 +131,14 @@ type prometheusOptions struct { MetricsPath string } +type AudioDeviceDefinition []string + +type jukeboxOptions struct { + Enabled bool + Devices []AudioDeviceDefinition + Default string +} + var ( Server = &configOptions{} hooks []func() @@ -313,6 +323,10 @@ func init() { viper.SetDefault("prometheus.enabled", false) viper.SetDefault("prometheus.metricspath", "/metrics") + viper.SetDefault("jukebox.enabled", false) + viper.SetDefault("jukebox.devices", []AudioDeviceDefinition{}) + viper.SetDefault("jukebox.default", "") + viper.SetDefault("scanner.extractor", consts.DefaultScannerExtractor) viper.SetDefault("scanner.genreseparators", ";/,") viper.SetDefault("scanner.groupalbumreleases", false) diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index 4085d56c..d3a64068 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -18,6 +18,8 @@ import ( type FFmpeg interface { Transcode(ctx context.Context, command, path string, maxBitRate int) (io.ReadCloser, error) ExtractImage(ctx context.Context, path string) (io.ReadCloser, error) + ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) + ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) Probe(ctx context.Context, files []string) (string, error) CmdPath() (string, error) } @@ -29,6 +31,8 @@ func New() FFmpeg { const ( extractImageCmd = "ffmpeg -i %s -an -vcodec copy -f image2pipe -" probeCmd = "ffmpeg %s -f ffmetadata" + createWavCmd = "ffmpeg -i %s -c:a pcm_s16le -f wav -" + createFLACCmd = "ffmpeg -i %s -f flac -" ) type ffmpeg struct{} @@ -49,6 +53,16 @@ func (e *ffmpeg) ExtractImage(ctx context.Context, path string) (io.ReadCloser, return e.start(ctx, args) } +func (e *ffmpeg) ConvertToWAV(ctx context.Context, path string) (io.ReadCloser, error) { + args := createFFmpegCommand(createWavCmd, path, 0) + return e.start(ctx, args) +} + +func (e *ffmpeg) ConvertToFLAC(ctx context.Context, path string) (io.ReadCloser, error) { + args := createFFmpegCommand(createFLACCmd, path, 0) + return e.start(ctx, args) +} + func (e *ffmpeg) Probe(ctx context.Context, files []string) (string, error) { if _, err := ffmpegCmd(); err != nil { return "", err diff --git a/core/playback/beepaudio/decoder.go b/core/playback/beepaudio/decoder.go new file mode 100644 index 00000000..17ce803f --- /dev/null +++ b/core/playback/beepaudio/decoder.go @@ -0,0 +1,66 @@ +//go:build beep + +package beepaudio + +import ( + "context" + "io" + "os" + + "github.com/faiface/beep" + "github.com/faiface/beep/flac" + "github.com/faiface/beep/mp3" + "github.com/faiface/beep/wav" + "github.com/navidrome/navidrome/core/ffmpeg" + "github.com/navidrome/navidrome/log" +) + +func DecodeMp3(path string) (s beep.StreamSeekCloser, format beep.Format, err error) { + f, err := os.Open(path) + if err != nil { + return nil, beep.Format{}, err + } + return mp3.Decode(f) +} + +func DecodeWAV(path string) (s beep.StreamSeekCloser, format beep.Format, err error) { + f, err := os.Open(path) + if err != nil { + return nil, beep.Format{}, err + } + return wav.Decode(f) +} + +func DecodeFLAC(path string) (s beep.StreamSeekCloser, format beep.Format, fileToCleanup string, err error) { + // TODO: Turn this into a semi-parallel operation: start playing while still transcoding/copying + log.Debug("decode to FLAC", "filename", path) + fFmpeg := ffmpeg.New() + readCloser, err := fFmpeg.ConvertToFLAC(context.TODO(), path) + if err != nil { + log.Error("error converting file to FLAC", path, err) + return nil, beep.Format{}, "", err + } + + tempFile, err := os.CreateTemp("", "*.flac") + + if err != nil { + log.Error("error creating temp file", err) + return nil, beep.Format{}, "", err + } + log.Debug("created tempfile", "filename", tempFile.Name()) + + written, err := io.Copy(tempFile, readCloser) + if err != nil { + log.Error("error coping file", "dest", tempFile.Name()) + } + log.Debug("copy pipe into tempfile", "bytes written", written, "filename", tempFile.Name()) + + f, err := os.Open(tempFile.Name()) + if err != nil { + log.Error("could not re-open tempfile", "filename", tempFile.Name()) + return nil, beep.Format{}, "", err + } + + s, format, err = flac.Decode(f) + return s, format, tempFile.Name(), err +} diff --git a/core/playback/beepaudio/track.go b/core/playback/beepaudio/track.go new file mode 100644 index 00000000..693607e8 --- /dev/null +++ b/core/playback/beepaudio/track.go @@ -0,0 +1,162 @@ +//go:build beep + +package beepaudio + +import ( + "fmt" + "os" + "time" + + "github.com/faiface/beep" + "github.com/faiface/beep/effects" + "github.com/faiface/beep/speaker" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +type BeepTrack struct { + MediaFile model.MediaFile + Ctrl *beep.Ctrl + Volume *effects.Volume + ActiveStream beep.StreamSeekCloser + TempfileToCleanup string + SampleRate beep.SampleRate + PlaybackDone chan bool +} + +func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*BeepTrack, error) { + t := BeepTrack{} + + contentType := mf.ContentType() + log.Debug("loading track", "trackname", mf.Path, "mediatype", contentType) + + var streamer beep.StreamSeekCloser + var format beep.Format + var err error + var tmpfileToCleanup = "" + + switch contentType { + case "audio/mpeg": + streamer, format, err = DecodeMp3(mf.Path) + case "audio/x-wav": + streamer, format, err = DecodeWAV(mf.Path) + case "audio/mp4": + streamer, format, tmpfileToCleanup, err = DecodeFLAC(mf.Path) + default: + return nil, fmt.Errorf("unsupported content type: %s", contentType) + } + + if err != nil { + log.Error(err) + return nil, err + } + + // save running stream for closing when switching tracks + t.ActiveStream = streamer + t.TempfileToCleanup = tmpfileToCleanup + + log.Debug("Setting up audio device") + t.Ctrl = &beep.Ctrl{Streamer: streamer, Paused: true} + t.Volume = &effects.Volume{Streamer: t.Ctrl, Base: 2} + t.SampleRate = format.SampleRate + t.PlaybackDone = playbackDoneChannel + t.MediaFile = mf + + err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/10)) + if err != nil { + log.Error(err) + } + log.Debug("speaker.Init() finished") + + go func() { + speaker.Play(beep.Seq(t.Volume, beep.Callback(func() { + log.Info("Hitting end-of-stream, signalling on channel") + t.PlaybackDone <- true + log.Debug("Signalling finished") + }))) + log.Debug("dropping out of speaker.Play()") + }() + return &t, nil +} + +func (t *BeepTrack) String() string { + return fmt.Sprintf("Name: %s", t.MediaFile.Path) +} + +func (t *BeepTrack) SetVolume(value float64) { + speaker.Lock() + t.Volume.Volume += value + speaker.Unlock() +} + +func (t *BeepTrack) Unpause() { + speaker.Lock() + if t.Ctrl.Paused { + t.Ctrl.Paused = false + } else { + log.Debug("tried to unpause while not paused") + } + speaker.Unlock() +} + +func (t *BeepTrack) Pause() { + speaker.Lock() + if t.Ctrl.Paused { + log.Debug("tried to pause while already paused") + } else { + t.Ctrl.Paused = true + } + speaker.Unlock() +} + +func (t *BeepTrack) Close() { + if t.ActiveStream != nil { + log.Debug("closing activ stream") + t.ActiveStream.Close() + t.ActiveStream = nil + } + + speaker.Close() + + if t.TempfileToCleanup != "" { + log.Debug("Removing tempfile", "tmpfilename", t.TempfileToCleanup) + err := os.Remove(t.TempfileToCleanup) + if err != nil { + log.Error("error cleaning up tempfile: ", t.TempfileToCleanup) + } + } +} + +// Position returns the playback position in seconds +func (t *BeepTrack) Position() int { + if t.Ctrl.Streamer == nil { + log.Debug("streamer is not setup (nil), could not get position") + return 0 + } + + streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker) + if ok { + position := t.SampleRate.D(streamer.Position()) + posSecs := position.Round(time.Second).Seconds() + return int(posSecs) + } else { + log.Debug("streamer is no beep.StreamSeeker, could not get position") + return 0 + } +} + +// offset = pd.PlaybackQueue.Offset +func (t *BeepTrack) SetPosition(offset int) error { + streamer, ok := t.Ctrl.Streamer.(beep.StreamSeeker) + if ok { + sampleRatePerSecond := t.SampleRate.N(time.Second) + nextPosition := sampleRatePerSecond * offset + log.Debug("SetPosition", "samplerate", sampleRatePerSecond, "nextPosition", nextPosition) + return streamer.Seek(nextPosition) + } + return fmt.Errorf("streamer is not seekable") +} + +func (t *BeepTrack) IsPlaying() bool { + return t.Ctrl != nil && !t.Ctrl.Paused +} diff --git a/core/playback/device.go b/core/playback/device.go new file mode 100644 index 00000000..358985ff --- /dev/null +++ b/core/playback/device.go @@ -0,0 +1,287 @@ +package playback + +import ( + "context" + "fmt" + + "github.com/navidrome/navidrome/core/playback/mpv" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +type Track interface { + IsPlaying() bool + SetVolume(value float32) // Used to control the playback volume. A float value between 0.0 and 1.0. + Pause() + Unpause() + Position() int + SetPosition(offset int) error + Close() +} + +type PlaybackDevice struct { + ParentPlaybackServer PlaybackServer + Default bool + User string + Name string + Method string + DeviceName string + PlaybackQueue *Queue + Gain float32 + PlaybackDone chan bool + ActiveTrack Track + TrackSwitcherStarted bool +} + +type DeviceStatus struct { + CurrentIndex int + Playing bool + Gain float32 + Position int +} + +const DefaultGain float32 = 1.0 + +var EmptyStatus = DeviceStatus{CurrentIndex: -1, Playing: false, Gain: DefaultGain, Position: 0} + +func (pd *PlaybackDevice) getStatus() DeviceStatus { + pos := 0 + if pd.ActiveTrack != nil { + pos = pd.ActiveTrack.Position() + } + return DeviceStatus{ + CurrentIndex: pd.PlaybackQueue.Index, + Playing: pd.isPlaying(), + Gain: pd.Gain, + Position: pos, + } +} + +// NewPlaybackDevice creates a new playback device which implements all the basic Jukebox mode commands defined here: +// http://www.subsonic.org/pages/api.jsp#jukeboxControl +// Starts the trackSwitcher goroutine for the device. +func NewPlaybackDevice(playbackServer PlaybackServer, name string, method string, deviceName string) *PlaybackDevice { + return &PlaybackDevice{ + ParentPlaybackServer: playbackServer, + User: "", + Name: name, + Method: method, + DeviceName: deviceName, + Gain: DefaultGain, + PlaybackQueue: NewQueue(), + PlaybackDone: make(chan bool), + TrackSwitcherStarted: false, + } +} + +func (pd *PlaybackDevice) String() string { + return fmt.Sprintf("Name: %s, Gain: %.4f, Loaded track: %s", pd.Name, pd.Gain, pd.ActiveTrack) +} + +func (pd *PlaybackDevice) Get(ctx context.Context) (model.MediaFiles, DeviceStatus, error) { + log.Debug(ctx, "processing Get action") + return pd.PlaybackQueue.Get(), pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Status(ctx context.Context) (DeviceStatus, error) { + log.Debug(ctx, fmt.Sprintf("processing Status action on: %s, queue: %s", pd, pd.PlaybackQueue)) + return pd.getStatus(), nil +} + +// set is similar to a clear followed by a add, but will not change the currently playing track. +func (pd *PlaybackDevice) Set(ctx context.Context, ids []string) (DeviceStatus, error) { + _, err := pd.Clear(ctx) + if err != nil { + log.Error(ctx, "error setting tracks", ids) + return pd.getStatus(), err + } + return pd.Add(ctx, ids) +} + +func (pd *PlaybackDevice) Start(ctx context.Context) (DeviceStatus, error) { + log.Debug(ctx, "processing Start action") + + if !pd.TrackSwitcherStarted { + log.Info(ctx, "Starting trackSwitcher goroutine") + // Start one trackSwitcher goroutine with each device + go func() { + pd.trackSwitcherGoroutine() + }() + pd.TrackSwitcherStarted = true + } + + if pd.ActiveTrack != nil { + if pd.isPlaying() { + log.Debug("trying to start an already playing track") + } else { + pd.ActiveTrack.Unpause() + } + } else { + if !pd.PlaybackQueue.IsEmpty() { + err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index) + if err != nil { + return pd.getStatus(), err + } + pd.ActiveTrack.Unpause() + } + } + + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Stop(ctx context.Context) (DeviceStatus, error) { + log.Debug(ctx, "processing Stop action") + if pd.ActiveTrack != nil { + pd.ActiveTrack.Pause() + } + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Skip(ctx context.Context, index int, offset int) (DeviceStatus, error) { + log.Debug(ctx, "processing Skip action", "index", index, "offset", offset) + + wasPlaying := pd.isPlaying() + + if pd.ActiveTrack != nil && wasPlaying { + pd.ActiveTrack.Pause() + } + + if index != pd.PlaybackQueue.Index { + if pd.ActiveTrack != nil { + pd.ActiveTrack.Close() + pd.ActiveTrack = nil + } + + err := pd.switchActiveTrackByIndex(index) + if err != nil { + return pd.getStatus(), err + } + } + + err := pd.ActiveTrack.SetPosition(offset) + if err != nil { + log.Error(ctx, "error setting position", err) + return pd.getStatus(), err + } + + if wasPlaying { + _, err = pd.Start(ctx) + if err != nil { + log.Error(ctx, "error starting new track after skipping") + return pd.getStatus(), err + } + } + + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Add(ctx context.Context, ids []string) (DeviceStatus, error) { + log.Debug(ctx, "processing Add action") + + items := model.MediaFiles{} + + for _, id := range ids { + mf, err := pd.ParentPlaybackServer.GetMediaFile(id) + if err != nil { + return DeviceStatus{}, err + } + log.Debug(ctx, "Found mediafile: "+mf.Path) + items = append(items, *mf) + } + pd.PlaybackQueue.Add(items) + + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Clear(ctx context.Context) (DeviceStatus, error) { + log.Debug(ctx, fmt.Sprintf("processing Clear action on: %s", pd)) + if pd.ActiveTrack != nil { + pd.ActiveTrack.Pause() + pd.ActiveTrack.Close() + pd.ActiveTrack = nil + } + pd.PlaybackQueue.Clear() + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Remove(ctx context.Context, index int) (DeviceStatus, error) { + log.Debug(ctx, "processing Remove action") + // pausing if attempting to remove running track + if pd.isPlaying() && pd.PlaybackQueue.Index == index { + _, err := pd.Stop(ctx) + if err != nil { + log.Error(ctx, "error stopping running track") + return pd.getStatus(), err + } + } + + if index > -1 && index < pd.PlaybackQueue.Size() { + pd.PlaybackQueue.Remove(index) + } else { + log.Error(ctx, "Index to remove out of range: "+fmt.Sprint(index)) + } + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) Shuffle(ctx context.Context) (DeviceStatus, error) { + log.Debug(ctx, "processing Shuffle action") + if pd.PlaybackQueue.Size() > 1 { + pd.PlaybackQueue.Shuffle() + } + return pd.getStatus(), nil +} + +// Used to control the playback volume. A float value between 0.0 and 1.0. +func (pd *PlaybackDevice) SetGain(ctx context.Context, gain float32) (DeviceStatus, error) { + log.Debug(ctx, fmt.Sprintf("processing SetGain action. Actual gain: %f, gain to set: %f", pd.Gain, gain)) + + if pd.ActiveTrack != nil { + pd.ActiveTrack.SetVolume(gain) + } + pd.Gain = gain + + return pd.getStatus(), nil +} + +func (pd *PlaybackDevice) isPlaying() bool { + return pd.ActiveTrack != nil && pd.ActiveTrack.IsPlaying() +} + +func (pd *PlaybackDevice) trackSwitcherGoroutine() { + log.Info("Starting trackSwitcher goroutine") + for { + <-pd.PlaybackDone + log.Info("track switching detected") + if pd.ActiveTrack != nil { + pd.ActiveTrack.Close() + pd.ActiveTrack = nil + } + + if !pd.PlaybackQueue.IsAtLastElement() { + pd.PlaybackQueue.IncreaseIndex() + log.Debug("Switching to next song", "queue", pd.PlaybackQueue.String()) + err := pd.switchActiveTrackByIndex(pd.PlaybackQueue.Index) + if err != nil { + log.Error("error switching track", "error", err) + } + pd.ActiveTrack.Unpause() + } else { + log.Debug("There is no song left in the playlist. Finish.") + } + } +} + +func (pd *PlaybackDevice) switchActiveTrackByIndex(index int) error { + pd.PlaybackQueue.SetIndex(index) + currentTrack := pd.PlaybackQueue.Current() + if currentTrack == nil { + return fmt.Errorf("could not get current track") + } + + track, err := mpv.NewTrack(pd.PlaybackDone, *currentTrack) + if err != nil { + return err + } + pd.ActiveTrack = track + return nil +} diff --git a/core/playback/mpv/mpv.go b/core/playback/mpv/mpv.go new file mode 100644 index 00000000..a622f037 --- /dev/null +++ b/core/playback/mpv/mpv.go @@ -0,0 +1,141 @@ +package mpv + +import ( + "context" + "crypto/rand" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/log" +) + +// mpv --no-audio-display --pause 'Jack Johnson/On And On/01 Times Like These.m4a' --input-ipc-server=/tmp/gonzo.socket +const ( + mpvComdTemplate = "mpv --no-audio-display --pause %f --input-ipc-server=%s" +) + +func start(args []string) (Executor, error) { + log.Debug("Executing mpv command", "cmd", args) + j := Executor{args: args} + j.PipeReader, j.out = io.Pipe() + err := j.start() + if err != nil { + return Executor{}, err + } + go j.wait() + return j, nil +} + +func (j *Executor) Cancel() error { + if j.cmd != nil { + return j.cmd.Cancel() + } + return fmt.Errorf("there is non command to cancel") +} + +type Executor struct { + *io.PipeReader + out *io.PipeWriter + args []string + cmd *exec.Cmd + ctx context.Context +} + +func (j *Executor) start() error { + ctx := context.Background() + j.ctx = ctx + cmd := exec.CommandContext(ctx, j.args[0], j.args[1:]...) // #nosec + cmd.Stdout = j.out + if log.CurrentLevel() >= log.LevelTrace { + cmd.Stderr = os.Stderr + } else { + cmd.Stderr = io.Discard + } + j.cmd = cmd + + if err := cmd.Start(); err != nil { + return fmt.Errorf("starting cmd: %w", err) + } + return nil +} + +func (j *Executor) wait() { + if err := j.cmd.Wait(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + _ = j.out.CloseWithError(fmt.Errorf("%s exited with non-zero status code: %d", j.args[0], exitErr.ExitCode())) + } else { + _ = j.out.CloseWithError(fmt.Errorf("waiting %s cmd: %w", j.args[0], err)) + } + return + } + _ = j.out.Close() +} + +// Path will always be an absolute path +func createMPVCommand(cmd, filename string, socketName string) []string { + split := strings.Split(fixCmd(cmd), " ") + for i, s := range split { + s = strings.ReplaceAll(s, "%f", filename) + s = strings.ReplaceAll(s, "%s", socketName) + split[i] = s + } + + return split +} + +func fixCmd(cmd string) string { + split := strings.Split(cmd, " ") + var result []string + cmdPath, _ := mpvCommand() + for _, s := range split { + if s == "mpv" || s == "mpv.exe" { + result = append(result, cmdPath) + } else { + result = append(result, s) + } + } + return strings.Join(result, " ") +} + +// This is a 1:1 copy of the stuff in ffmpeg.go, need to be unified. +func mpvCommand() (string, error) { + mpvOnce.Do(func() { + if conf.Server.MPVPath != "" { + mpvPath = conf.Server.FFmpegPath + mpvPath, mpvErr = exec.LookPath(mpvPath) + } else { + mpvPath, mpvErr = exec.LookPath("mpv") + if errors.Is(mpvErr, exec.ErrDot) { + log.Trace("mpv found in current folder '.'") + mpvPath, mpvErr = exec.LookPath("./mpv") + } + } + if mpvErr == nil { + log.Info("Found mpv", "path", mpvPath) + return + } + }) + return mpvPath, mpvErr +} + +var ( + mpvOnce sync.Once + mpvPath string + mpvErr error +) + +func TempFileName(prefix, suffix string) string { + randBytes := make([]byte, 16) + // we can savely ignore the return value since we're loading into a precreated, fixedsized buffer + _, _ = rand.Read(randBytes) + return filepath.Join(os.TempDir(), prefix+hex.EncodeToString(randBytes)+suffix) +} diff --git a/core/playback/mpv/track.go b/core/playback/mpv/track.go new file mode 100644 index 00000000..76da153d --- /dev/null +++ b/core/playback/mpv/track.go @@ -0,0 +1,223 @@ +package mpv + +// Audio-playback using mpv media-server. See mpv.io +// https://github.com/dexterlb/mpvipc +// https://mpv.io/manual/master/#json-ipc +// https://mpv.io/manual/master/#properties + +import ( + "fmt" + "os" + "time" + + "github.com/DexterLB/mpvipc" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +type MpvTrack struct { + MediaFile model.MediaFile + PlaybackDone chan bool + Conn *mpvipc.Connection + IPCSocketName string + Exe *Executor + CloseCalled bool +} + +func NewTrack(playbackDoneChannel chan bool, mf model.MediaFile) (*MpvTrack, error) { + log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType()) + + if _, err := mpvCommand(); err != nil { + return nil, err + } + + tmpSocketName := TempFileName("mpv-ctrl-", ".socket") + + args := createMPVCommand(mpvComdTemplate, mf.Path, tmpSocketName) + exe, err := start(args) + if err != nil { + log.Error("error starting mpv process", "error", err) + return nil, err + } + + // wait for socket to show up + err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond) + if err != nil { + log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err) + return nil, err + } + + conn := mpvipc.NewConnection(tmpSocketName) + err = conn.Open() + + if err != nil { + log.Error("error opening new connection", "error", err) + return nil, err + } + + theTrack := &MpvTrack{MediaFile: mf, PlaybackDone: playbackDoneChannel, Conn: conn, IPCSocketName: tmpSocketName, Exe: &exe, CloseCalled: false} + + go func() { + conn.WaitUntilClosed() + log.Info("Hitting end-of-stream, signalling on channel") + if !theTrack.CloseCalled { + playbackDoneChannel <- true + } + }() + + return theTrack, nil +} + +func (t *MpvTrack) String() string { + return fmt.Sprintf("Name: %s, Socket: %s", t.MediaFile.Path, t.IPCSocketName) +} + +// Used to control the playback volume. A float value between 0.0 and 1.0. +func (t *MpvTrack) SetVolume(value float32) { + // mpv's volume as described in the --volume parameter: + // Set the startup volume. 0 means silence, 100 means no volume reduction or amplification. + // Negative values can be passed for compatibility, but are treated as 0. + log.Debug("request for gain", "gain", value) + vol := int(value * 100) + + err := t.Conn.Set("volume", vol) + if err != nil { + log.Error(err) + } + log.Debug("set volume", "volume", vol) +} + +func (t *MpvTrack) Unpause() { + err := t.Conn.Set("pause", false) + if err != nil { + log.Error(err) + } + log.Info("unpaused track") +} + +func (t *MpvTrack) Pause() { + err := t.Conn.Set("pause", true) + if err != nil { + log.Error(err) + } + log.Info("paused track") +} + +func (t *MpvTrack) Close() { + log.Debug("closing resources") + t.CloseCalled = true + // trying to shutdown mpv process using socket + if t.isSocketfilePresent() { + log.Debug("sending shutdown command") + _, err := t.Conn.Call("quit") + if err != nil { + log.Error("error sending quit command to mpv-ipc socket", "error", err) + + if t.Exe != nil { + log.Debug("cancelling executor") + err = t.Exe.Cancel() + if err != nil { + log.Error("error canceling executor") + } + } + } + } + + if t.isSocketfilePresent() { + log.Debug("Removing socketfile", "socketfile", t.IPCSocketName) + err := os.Remove(t.IPCSocketName) + if err != nil { + log.Error("error cleaning up socketfile: ", t.IPCSocketName) + } + } +} + +func (t *MpvTrack) isSocketfilePresent() bool { + if len(t.IPCSocketName) < 1 { + return false + } + + fileInfo, err := os.Stat(t.IPCSocketName) + return err == nil && fileInfo != nil && !fileInfo.IsDir() +} + +// Position returns the playback position in seconds +// every now and then the mpv IPC interface returns "mpv error: property unavailable" +// in this case we have to retry +func (t *MpvTrack) Position() int { + retryCount := 0 + for { + position, err := t.Conn.Get("time-pos") + if err != nil && err.Error() == "mpv error: property unavailable" { + log.Debug("got the mpv error: property unavailable error, retry ...") + retryCount += 1 + if retryCount > 5 { + return 0 + } + break + } + + if err != nil { + log.Error("error getting position in track", "error", err) + return 0 + } + + pos, ok := position.(float64) + if !ok { + log.Error("could not cast position from mpv into float64") + return 0 + } else { + return int(pos) + } + } + return 0 +} + +func (t *MpvTrack) SetPosition(offset int) error { + pos := t.Position() + if pos == offset { + log.Debug("no position difference, skipping operation") + return nil + } + err := t.Conn.Set("time-pos", float64(offset)) + if err != nil { + log.Error("could not set the position in track", "offset", offset, "error", err) + return err + } + log.Info("set position", "offset", offset) + return nil +} + +func (t *MpvTrack) IsPlaying() bool { + pausing, err := t.Conn.Get("pause") + if err != nil { + log.Error("problem getting paused status", "error", err) + return false + } + + pause, ok := pausing.(bool) + if !ok { + log.Error("could not cast pausing to boolean") + return false + } + return !pause +} + +func waitForFile(path string, timeout time.Duration, pause time.Duration) error { + start := time.Now() + end := start.Add(timeout) + var retries int = 0 + + for { + fileInfo, err := os.Stat(path) + if err == nil && fileInfo != nil && !fileInfo.IsDir() { + log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds()) + return nil + } + if time.Now().After(end) { + return fmt.Errorf("timeout reached: %s", timeout) + } + time.Sleep(pause) + retries += 1 + } +} diff --git a/core/playback/playback_suite_test.go b/core/playback/playback_suite_test.go new file mode 100644 index 00000000..8e1134f2 --- /dev/null +++ b/core/playback/playback_suite_test.go @@ -0,0 +1,17 @@ +package playback + +import ( + "testing" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPlayback(t *testing.T) { + tests.Init(t, false) + log.SetLevel(log.LevelFatal) + RegisterFailHandler(Fail) + RunSpecs(t, "Playback Suite") +} diff --git a/core/playback/playbackserver.go b/core/playback/playbackserver.go new file mode 100644 index 00000000..451dc844 --- /dev/null +++ b/core/playback/playbackserver.go @@ -0,0 +1,110 @@ +// Package playback implements audio playback using PlaybackDevices. It is used to implement the Jukebox mode in turn. +// It makes use of the BEEP library to do the playback. Major parts are: +// - decoder which includes decoding and transcoding of various audio file formats +// - device implementing the basic functions to work with audio devices like set, play, stop, skip, ... +// - queue a simple playlist +package playback + +import ( + "context" + "fmt" + + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/db" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" + "github.com/navidrome/navidrome/persistence" + "github.com/navidrome/navidrome/utils/singleton" +) + +type PlaybackServer interface { + Run(ctx context.Context) error + GetDeviceForUser(user string) (*PlaybackDevice, error) + GetMediaFile(id string) (*model.MediaFile, error) + GetCtx() *context.Context +} + +type playbackServer struct { + ctx *context.Context + datastore model.DataStore + playbackDevices []PlaybackDevice +} + +// GetInstance returns the playback-server singleton +func GetInstance() PlaybackServer { + return singleton.GetInstance(func() *playbackServer { + return &playbackServer{} + }) +} + +// Run starts the playback server which serves request until canceled using the given context +func (ps *playbackServer) Run(ctx context.Context) error { + ps.datastore = persistence.New(db.Db()) + devices, err := ps.initDeviceStatus(conf.Server.Jukebox.Devices, conf.Server.Jukebox.Default) + ps.playbackDevices = devices + + if err != nil { + return err + } + log.Info(ctx, fmt.Sprintf("%d audio devices found", len(conf.Server.Jukebox.Devices))) + log.Info(ctx, "Using default audio device: "+conf.Server.Jukebox.Default) + + ps.ctx = &ctx + + <-ctx.Done() + return nil +} + +// GetCtx produces the context this server was started with. Used for data-retrieval and cancellation +func (ps *playbackServer) GetCtx() *context.Context { + return ps.ctx +} + +func (ps *playbackServer) initDeviceStatus(devices []conf.AudioDeviceDefinition, defaultDevice string) ([]PlaybackDevice, error) { + pbDevices := make([]PlaybackDevice, len(devices)) + defaultDeviceFound := false + + for idx, audioDevice := range devices { + if len(audioDevice) != 3 { + return []PlaybackDevice{}, fmt.Errorf("audio device definition ought to contain 3 fields, found: %d ", len(audioDevice)) + } + + pbDevices[idx] = *NewPlaybackDevice(ps, audioDevice[0], audioDevice[1], audioDevice[2]) + + if audioDevice[0] == defaultDevice { + pbDevices[idx].Default = true + defaultDeviceFound = true + } + } + + if !defaultDeviceFound { + return []PlaybackDevice{}, fmt.Errorf("default device name not found: %s ", defaultDevice) + } + return pbDevices, nil +} + +func (ps *playbackServer) getDefaultDevice() (*PlaybackDevice, error) { + for idx, audioDevice := range ps.playbackDevices { + if audioDevice.Default { + return &ps.playbackDevices[idx], nil + } + } + return &PlaybackDevice{}, fmt.Errorf("no default device found") +} + +// GetMediaFile retrieves the MediaFile given by the id parameter +func (ps *playbackServer) GetMediaFile(id string) (*model.MediaFile, error) { + return ps.datastore.MediaFile(*ps.ctx).Get(id) +} + +// GetDeviceForUser returns the audio playback device for the given user. As of now this is but only the default device. +func (ps *playbackServer) GetDeviceForUser(user string) (*PlaybackDevice, error) { + log.Debug("processing GetDevice") + // README: here we might plug-in the user-device mapping one fine day + device, err := ps.getDefaultDevice() + if err != nil { + return &PlaybackDevice{}, err + } + device.User = user + return device, nil +} diff --git a/core/playback/queue.go b/core/playback/queue.go new file mode 100644 index 00000000..9f4b5069 --- /dev/null +++ b/core/playback/queue.go @@ -0,0 +1,150 @@ +package playback + +import ( + "fmt" + "math/rand" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +type Queue struct { + Index int + Items model.MediaFiles +} + +func NewQueue() *Queue { + return &Queue{ + Index: -1, + Items: model.MediaFiles{}, + } +} + +func (pd *Queue) String() string { + filenames := "" + for idx, item := range pd.Items { + filenames += fmt.Sprint(idx) + ":" + item.Path + " " + } + return fmt.Sprintf("#Items: %d, idx: %d, files: %s", len(pd.Items), pd.Index, filenames) +} + +// returns the current mediafile or nil +func (pd *Queue) Current() *model.MediaFile { + if pd.Index == -1 { + return nil + } + if pd.Index >= len(pd.Items) { + log.Error("internal error: current song index out of bounds", "idx", pd.Index, "length", len(pd.Items)) + return nil + } + + return &pd.Items[pd.Index] +} + +// returns the whole queue +func (pd *Queue) Get() model.MediaFiles { + return pd.Items +} + +func (pd *Queue) Size() int { + return len(pd.Items) +} + +func (pd *Queue) IsEmpty() bool { + return len(pd.Items) < 1 +} + +// set is similar to a clear followed by a add, but will not change the currently playing track. +func (pd *Queue) Set(items model.MediaFiles) { + pd.Clear() + pd.Items = append(pd.Items, items...) +} + +// adding mediafiles to the queue +func (pd *Queue) Add(items model.MediaFiles) { + pd.Items = append(pd.Items, items...) + if pd.Index == -1 && len(pd.Items) > 0 { + pd.Index = 0 + } +} + +// empties whole queue +func (pd *Queue) Clear() { + pd.Index = -1 + pd.Items = nil +} + +// idx Zero-based index of the song to skip to or remove. +func (pd *Queue) Remove(idx int) { + current := pd.Current() + backupID := "" + if current != nil { + backupID = current.ID + } + + pd.Items = append(pd.Items[:idx], pd.Items[idx+1:]...) + + var err error + pd.Index, err = pd.getMediaFileIndexByID(backupID) + if err != nil { + // we seem to have deleted the current id, setting to default: + pd.Index = -1 + } +} + +func (pd *Queue) Shuffle() { + current := pd.Current() + backupID := "" + if current != nil { + backupID = current.ID + } + + rand.Shuffle(len(pd.Items), func(i, j int) { pd.Items[i], pd.Items[j] = pd.Items[j], pd.Items[i] }) + + var err error + pd.Index, err = pd.getMediaFileIndexByID(backupID) + if err != nil { + log.Error("Could not find ID while shuffling: " + backupID) + } +} + +func (pd *Queue) getMediaFileIndexByID(id string) (int, error) { + for idx, item := range pd.Items { + if item.ID == id { + return idx, nil + } + } + return -1, fmt.Errorf("ID not found in playlist: " + id) +} + +// Sets the index to a new, valid value inside the Items. Values lower than zero are going to be zero, +// values above will be limited by number of items. +func (pd *Queue) SetIndex(idx int) { + pd.Index = max(0, min(idx, len(pd.Items)-1)) +} + +// Are we at the last track? +func (pd *Queue) IsAtLastElement() bool { + return (pd.Index + 1) >= len(pd.Items) +} + +// Goto next index +func (pd *Queue) IncreaseIndex() { + if !pd.IsAtLastElement() { + pd.SetIndex(pd.Index + 1) + } +} + +func max(x, y int) int { + if x < y { + return y + } + return x +} + +func min(x, y int) int { + if x > y { + return y + } + return x +} diff --git a/core/playback/queue_test.go b/core/playback/queue_test.go new file mode 100644 index 00000000..00df522f --- /dev/null +++ b/core/playback/queue_test.go @@ -0,0 +1,121 @@ +package playback + +import ( + "github.com/navidrome/navidrome/model" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Queues", func() { + var queue *Queue + + BeforeEach(func() { + queue = NewQueue() + }) + + Describe("use empty queue", func() { + It("is empty", func() { + Expect(queue.Items).To(BeEmpty()) + Expect(queue.Index).To(Equal(-1)) + }) + }) + + Describe("Operate on small queue", func() { + BeforeEach(func() { + mfs := model.MediaFiles{ + { + ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3", + }, + { + ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3", + }, + } + queue.Add(mfs) + }) + + It("contains the preloaded data", func() { + Expect(queue.Get).ToNot(BeNil()) + Expect(queue.Size()).To(Equal(2)) + }) + + It("could read data by ID", func() { + idx, err := queue.getMediaFileIndexByID("1") + Expect(err).ToNot(HaveOccurred()) + Expect(idx).ToNot(BeNil()) + Expect(idx).To(Equal(0)) + + queue.SetIndex(idx) + + mf := queue.Current() + + Expect(mf).ToNot(BeNil()) + Expect(mf.ID).To(Equal("1")) + Expect(mf.Artist).To(Equal("Queen")) + Expect(mf.Path).To(Equal("/music1/hammer.mp3")) + }) + }) + + Describe("Read/Write operations", func() { + BeforeEach(func() { + mfs := model.MediaFiles{ + { + ID: "1", Artist: "Queen", Compilation: false, Path: "/music1/hammer.mp3", + }, + { + ID: "2", Artist: "Vinyard Rose", Compilation: false, Path: "/music1/cassidy.mp3", + }, + { + ID: "3", Artist: "Pink Floyd", Compilation: false, Path: "/music1/time.mp3", + }, + { + ID: "4", Artist: "Mike Oldfield", Compilation: false, Path: "/music1/moonlight-shadow.mp3", + }, + { + ID: "5", Artist: "Red Hot Chili Peppers", Compilation: false, Path: "/music1/californication.mp3", + }, + } + queue.Add(mfs) + }) + + It("contains the preloaded data", func() { + Expect(queue.Get).ToNot(BeNil()) + Expect(queue.Size()).To(Equal(5)) + }) + + It("could read data by ID", func() { + idx, err := queue.getMediaFileIndexByID("5") + Expect(err).ToNot(HaveOccurred()) + Expect(idx).ToNot(BeNil()) + Expect(idx).To(Equal(4)) + + queue.SetIndex(idx) + + mf := queue.Current() + + Expect(mf).ToNot(BeNil()) + Expect(mf.ID).To(Equal("5")) + Expect(mf.Artist).To(Equal("Red Hot Chili Peppers")) + Expect(mf.Path).To(Equal("/music1/californication.mp3")) + }) + + It("could shuffle the data correctly", func() { + queue.Shuffle() + Expect(queue.Size()).To(Equal(5)) + }) + + It("could remove entries correctly", func() { + queue.Remove(0) + Expect(queue.Size()).To(Equal(4)) + + queue.Remove(3) + Expect(queue.Size()).To(Equal(3)) + }) + + It("clear the whole thing on request", func() { + Expect(queue.Size()).To(Equal(5)) + queue.Clear() + Expect(queue.Size()).To(Equal(0)) + }) + }) + +}) diff --git a/go.mod b/go.mod index 161f53b1..707c4b1c 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.20 require ( code.cloudfoundry.org/go-diodes v0.0.0-20190809170250-f77fb823c7ee + github.com/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d github.com/Masterminds/squirrel v1.5.4 github.com/ReneKroon/ttlcache/v2 v2.11.0 - github.com/beego/beego/v2 v2.1.0 + github.com/beego/beego/v2 v2.0.7 github.com/bradleyjkemp/cupaloy/v2 v2.8.0 github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 @@ -16,37 +17,38 @@ require ( github.com/djherbis/fscache v0.10.2-0.20220222230828-2909c950912d github.com/djherbis/stream v1.4.0 github.com/dustin/go-humanize v1.0.1 + github.com/faiface/beep v1.1.0 github.com/fatih/structs v1.1.0 - github.com/go-chi/chi/v5 v5.0.10 + github.com/go-chi/chi/v5 v5.0.8 github.com/go-chi/cors v1.2.1 github.com/go-chi/httprate v0.7.4 - github.com/go-chi/jwtauth/v5 v5.1.1 + github.com/go-chi/jwtauth/v5 v5.1.0 github.com/google/uuid v1.3.0 github.com/google/wire v0.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/kr/pretty v0.3.1 - github.com/lestrrat-go/jwx/v2 v2.0.11 + github.com/lestrrat-go/jwx/v2 v2.0.9 github.com/matoous/go-nanoid/v2 v2.0.0 - github.com/mattn/go-sqlite3 v1.14.17 + github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-zglob v0.0.3 - github.com/microcosm-cc/bluemonday v1.0.25 - github.com/mileusna/useragent v1.3.3 - github.com/onsi/ginkgo/v2 v2.11.0 - github.com/onsi/gomega v1.27.9 - github.com/pressly/goose/v3 v3.13.4 - github.com/prometheus/client_golang v1.16.0 + github.com/microcosm-cc/bluemonday v1.0.23 + github.com/mileusna/useragent v1.3.2 + github.com/onsi/ginkgo/v2 v2.9.5 + github.com/onsi/gomega v1.27.6 + github.com/pressly/goose/v3 v3.11.2 + github.com/prometheus/client_golang v1.15.1 github.com/robfig/cron/v3 v3.0.1 - github.com/sirupsen/logrus v1.9.3 + github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.7.0 - github.com/spf13/viper v1.16.0 - github.com/stretchr/testify v1.8.4 + github.com/spf13/viper v1.15.0 + github.com/stretchr/testify v1.8.2 github.com/unrolled/secure v1.13.0 github.com/xrash/smetrics v0.0.0-20200730060457-89a2a8a1fb0b golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e - golang.org/x/image v0.9.0 - golang.org/x/sync v0.3.0 - golang.org/x/text v0.11.0 - golang.org/x/tools v0.11.0 + golang.org/x/image v0.7.0 + golang.org/x/sync v0.2.0 + golang.org/x/text v0.9.0 + golang.org/x/tools v0.9.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -55,18 +57,21 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect - github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/hajimehoshi/go-mp3 v0.3.4 // indirect + github.com/hajimehoshi/oto v1.0.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/icza/bitio v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect @@ -78,28 +83,32 @@ require ( github.com/lestrrat-go/option v1.0.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mewkiz/flac v1.0.7 // indirect + github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.11.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect - github.com/segmentio/asm v1.2.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/afero v1.9.3 // 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/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect go.uber.org/goleak v1.1.11 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) diff --git a/go.sum b/go.sum index 00475fc1..08aeb432 100644 --- a/go.sum +++ b/go.sum @@ -40,14 +40,17 @@ code.cloudfoundry.org/go-diodes v0.0.0-20190809170250-f77fb823c7ee/go.mod h1:Jzi dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 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/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d h1:UyefntSsjbYaTDUdZF4A1vPZX3Xpnewv6JNBzQPYAzY= +github.com/DexterLB/mpvipc v0.0.0-20221227161445-38b9935eae9d/go.mod h1:nMVB54ifXmC1hpgfq7gTpotbv891pd2wAX/whuUj1q4= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM= github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/beego/beego/v2 v2.1.0 h1:Lk0FtQGvDQCx5V5yEu4XwDsIgt+QOlNjt5emUa3/ZmA= -github.com/beego/beego/v2 v2.1.0/go.mod h1:6h36ISpaxNrrpJ27siTpXBG8d/Icjzsc7pU1bWpp0EE= +github.com/beego/beego/v2 v2.0.7 h1:9KNnUM40tn3pbCOFfe6SJ1oOL0oTi/oBS/C/wCEdAXA= +github.com/beego/beego/v2 v2.0.7/go.mod h1:f0uOEkmJWgAuDTlTxUdgJzwG3PDSIf3UWF3NpMohbFE= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -64,12 +67,13 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= 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/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47 h1:IhGAYGDi212gspq0XkYAI+DN5e9lfAIm8Qgu1wj9yN4= github.com/deluan/rest v0.0.0-20211101235434-380523c4bb47/go.mod h1:tSgDythFsl0QgS/PFWfIZqcJKnkADWneY80jaVRlqK8= github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 h1:mGvOb3zxl4vCLv+dbf7JA6CAaM2UH/AGP1KX4DsJmTI= @@ -92,21 +96,28 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 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/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/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +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/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-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= +github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M= github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= -github.com/go-chi/jwtauth/v5 v5.1.1 h1:Pjixqu5YkjE9sCLpzE01L0Q4sQzJIPdo7uz9r8ftp/c= -github.com/go-chi/jwtauth/v5 v5.1.1/go.mod h1:CYP1WSbzD4MPuKCr537EM3kfFhSQgpUEtMJFuYJjqWU= +github.com/go-chi/jwtauth/v5 v5.1.0 h1:wJyf2YZ/ohPvNJBwPOzZaQbyzwgMZZceE1m8FOzXLeA= +github.com/go-chi/jwtauth/v5 v5.1.0/go.mod h1:MA93hc1au3tAQwCKry+fI4LqJ5MIVN4XSsglOo+lSc8= 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= @@ -173,8 +184,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf 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-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= -github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/pprof v0.0.0-20230323073829-e72429f035bd h1:r8yyd+DJDmsUhGrRBxH5Pj7KeFK5l+Y3FsgT8keqKtk= +github.com/google/pprof v0.0.0-20230323073829-e72429f035bd/go.mod h1:79YE0hCXdHag9sBkw2o+N/YnZtTkXi0UT9Nnixa5eYk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -189,6 +200,14 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= +github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= +github.com/hajimehoshi/go-mp3 v0.3.4/go.mod h1:fRtZraRFcWb0pu7ok0LqyFhCUrPeMsGRSVop0eemFmo= +github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= +github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto v1.0.1 h1:8AMnq0Yr2YmzaiqTg/k1Yzd6IygUGk2we9nmjgbgPn4= +github.com/hajimehoshi/oto v1.0.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos= +github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -202,8 +221,14 @@ github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 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/icza/bitio v1.0.0 h1:squ/m1SHyFeCA6+6Gyol1AxV9nmPPlJFT8c2vKdj3U8= +github.com/icza/bitio v1.0.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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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/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/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -231,28 +256,34 @@ github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJG github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= -github.com/lestrrat-go/jwx/v2 v2.0.11 h1:ViHMnaMeaO0qV16RZWBHM7GTrAnX2aFLVKofc7FuKLQ= -github.com/lestrrat-go/jwx/v2 v2.0.11/go.mod h1:ZtPtMFlrfDrH2Y0iwfa3dRFn8VzwBrB+cyrm3IBWdDg= +github.com/lestrrat-go/jwx/v2 v2.0.9 h1:TRX4Q630UXxPVLvP5vGaqVJO7S+0PE6msRZUsFSBoC8= +github.com/lestrrat-go/jwx/v2 v2.0.9/go.mod h1:K68euYaR95FnL0hIQB8VvzL70vB7pSifbJUydCTPmgM= github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +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/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-zglob v0.0.3 h1:6Ry4EYsScDyt5di4OI6xw1bYhOqfE5S33Z1OPy+d+To= github.com/mattn/go-zglob v0.0.3/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= -github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= -github.com/mileusna/useragent v1.3.3 h1:hrIVmPevJY3ICS1Ob4yjqJToQiv2eD9iHaJBjxMihWY= -github.com/mileusna/useragent v1.3.3/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= +github.com/mewkiz/flac v1.0.7 h1:uIXEjnuXqdRaZttmSFM5v5Ukp4U6orrZsnYGGR3yow8= +github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU= +github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2 h1:EyTNMdePWaoWsRSGQnXiSoQu0r6RS1eA557AwJhlzHU= +github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA= +github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY= +github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4= +github.com/mileusna/useragent v1.3.2 h1:yGBQVNkyrlnSe4l0rlaQoH8XlG9xDkc6a7ygwPxALoU= +github.com/mileusna/useragent v1.3.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= 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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -262,62 +293,60 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.9 h1:qIyVWbOsvQEye2QCqLsNSeH/5L1RS9vS382erEWfT3o= -github.com/onsi/gomega v1.27.9/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/pressly/goose/v3 v3.13.4 h1:9xRcg/hEU9HqeRNeKh69VLtPWCKAYTX6l2VsXWOX86A= -github.com/pressly/goose/v3 v3.13.4/go.mod h1:Fo8rYaf9tYfQiDpo+ymrnZi8vvLkvguRl16nu7QnUT4= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= +github.com/pressly/goose/v3 v3.11.2 h1:QgTP45FhBBHdmf7hWKlbWFHtwPtxo0phSDkwDKGUrYs= +github.com/pressly/goose/v3 v3.11.2/go.mod h1:LWQzSc4vwfHA/3B8getTp8g3J5Z8tFBxgxinmGlMlJk= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk= -github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= -github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18 h1:DAYUYH5869yV94zvCES9F51oYtN5oGlwjxJJz7ZCnik= github.com/shiena/ansicolor v0.0.0-20200904210342-c7312218db18/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -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/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/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 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/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -330,9 +359,9 @@ 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.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk= @@ -361,10 +390,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U 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-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= 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= @@ -377,11 +406,14 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc h1:JMi0oO0NoPZTAzHSdkdUoHbdcLfo9nPtK37kzE6I3Hk= +golang.org/x/exp/shiny v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= +golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= -golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= +golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw= +golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg= 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= @@ -394,7 +426,10 @@ golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41 h1:539vykMVJsmdiucRtMmdeLLZaTVhWhaAHFcPabj2lws= +golang.org/x/mobile v0.0.0-20230427221453-e8d11dd0ba41/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -406,8 +441,8 @@ 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -442,12 +477,11 @@ golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v 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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 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= @@ -470,18 +504,20 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ 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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.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-20180909124046-d0be0721c37e/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= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -511,35 +547,33 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/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-20220412211240-33da011f77ad/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-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 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= 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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.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= @@ -598,8 +632,8 @@ 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/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= -golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= 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= @@ -703,6 +737,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -719,13 +755,13 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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= lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo= -modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= -modernc.org/ccgo/v3 v3.16.14 h1:af6KNtFgsVmnDYrWk3PQCS9XT6BXe7o3ZFJKkIKvXNQ= -modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.22.1 h1:P2+Dhp5FR1RlVRkQ3dDfCiv3Ok8XPxqpe70IjYVA9oE= modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/server/subsonic/api.go b/server/subsonic/api.go index bdad5e2d..ae28e4ec 100644 --- a/server/subsonic/api.go +++ b/server/subsonic/api.go @@ -179,8 +179,15 @@ func (api *Router) routes() http.Handler { h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions) }) + if conf.Server.Jukebox.Enabled { + r.Group(func(r chi.Router) { + h(r, "jukeboxControl", api.JukeboxControl) + }) + } else { + h501(r, "jukeboxControl") + } + // Not Implemented (yet?) - h501(r, "jukeboxControl") h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel", "deletePodcastEpisode", "downloadPodcastEpisode") h501(r, "createUser", "updateUser", "deleteUser", "changePassword") diff --git a/server/subsonic/jukebox.go b/server/subsonic/jukebox.go new file mode 100644 index 00000000..f3d94b59 --- /dev/null +++ b/server/subsonic/jukebox.go @@ -0,0 +1,142 @@ +package subsonic + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/navidrome/navidrome/core/playback" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/server/subsonic/responses" +) + +const ( + ActionGet = "get" + ActionStatus = "status" + ActionSet = "set" + ActionStart = "start" + ActionStop = "stop" + ActionSkip = "skip" + ActionAdd = "add" + ActionClear = "clear" + ActionRemove = "remove" + ActionShuffle = "shuffle" + ActionSetGain = "setGain" +) + +func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) { + ctx := r.Context() + user := getUser(ctx) + + actionString, err := requiredParamString(r, "action") + if err != nil { + return nil, err + } + + pbServer := playback.GetInstance() + pb, err := pbServer.GetDeviceForUser(user.UserName) + if err != nil { + return nil, err + } + log.Debug(fmt.Sprintf("processing action: %s", actionString)) + + switch actionString { + case ActionGet: + mediafiles, status, err := pb.Get(ctx) + if err != nil { + return nil, err + } + + playlist := responses.JukeboxPlaylist{ + JukeboxStatus: *deviceStatusToJukeboxStatus(status), + Entry: childrenFromMediaFiles(ctx, mediafiles), + } + + response := newResponse() + response.JukeboxPlaylist = &playlist + return response, nil + case ActionStatus: + return createResponse(pb.Status(ctx)) + case ActionSet: + ids, err := requiredParamStrings(r, "id") + if err != nil { + return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err) + } + status, err := pb.Set(ctx, ids) + if err != nil { + return nil, err + } + return statusResponse(status), nil + case ActionStart: + return createResponse(pb.Start(ctx)) + case ActionStop: + return createResponse(pb.Stop(ctx)) + case ActionSkip: + index, err := requiredParamInt(r, "index") + if err != nil { + return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err) + } + + offset, err := requiredParamInt(r, "offset") + if err != nil { + offset = 0 + } + + return createResponse(pb.Skip(ctx, index, offset)) + case ActionAdd: + ids, err := requiredParamStrings(r, "id") + if err != nil { + return nil, newError(responses.ErrorMissingParameter, "missing parameter id, err: %s", err) + } + + return createResponse(pb.Add(ctx, ids)) + case ActionClear: + return createResponse(pb.Clear(ctx)) + case ActionRemove: + index, err := requiredParamInt(r, "index") + if err != nil { + return nil, err + } + + return createResponse(pb.Remove(ctx, index)) + case ActionShuffle: + return createResponse(pb.Shuffle(ctx)) + case ActionSetGain: + gainStr, err := requiredParamString(r, "gain") + if err != nil { + return nil, newError(responses.ErrorMissingParameter, "missing parameter gain, err: %s", err) + } + + gain, err := strconv.ParseFloat(gainStr, 32) + if err != nil { + return nil, newError(responses.ErrorMissingParameter, "error parsing gain integer value, err: %s", err) + } + + return createResponse(pb.SetGain(ctx, float32(gain))) + default: + return nil, newError(responses.ErrorMissingParameter, "Unknown action: %s", actionString) + } +} + +// createResponse is to shorten the case-switch in the JukeboxController +func createResponse(status playback.DeviceStatus, err error) (*responses.Subsonic, error) { + if err != nil { + return nil, err + } + return statusResponse(status), nil +} + +func statusResponse(status playback.DeviceStatus) *responses.Subsonic { + response := newResponse() + response.JukeboxStatus = deviceStatusToJukeboxStatus(status) + return response +} + +func deviceStatusToJukeboxStatus(status playback.DeviceStatus) *responses.JukeboxStatus { + return &responses.JukeboxStatus{ + CurrentIndex: int32(status.CurrentIndex), + Playing: status.Playing, + Gain: status.Gain, + Position: int32(status.Position), + } +} diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 0df2c0ad..495b7b2b 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -51,7 +51,11 @@ type Subsonic struct { ScanStatus *ScanStatus `xml:"scanStatus,omitempty" json:"scanStatus,omitempty"` Lyrics *Lyrics `xml:"lyrics,omitempty" json:"lyrics,omitempty"` - InternetRadioStations *InternetRadioStations `xml:"internetRadioStations,omitempty" json:"internetRadioStations,omitempty"` + InternetRadioStations *InternetRadioStations `xml:"internetRadioStations,omitempty" json:"internetRadioStations,omitempty"` + + JukeboxStatus *JukeboxStatus `xml:"jukeboxStatus,omitempty" json:"jukeboxStatus,omitempty"` + JukeboxPlaylist *JukeboxPlaylist `xml:"jukeboxPlaylist,omitempty" json:"jukeboxPlaylist,omitempty"` + OpenSubsonicExtensions *OpenSubsonicExtensions `xml:"openSubsonicExtensions,omitempty" json:"openSubsonicExtensions,omitempty"` } @@ -402,4 +406,15 @@ type Radio struct { HomepageUrl string `xml:"homePageUrl,omitempty,attr" json:"homePageUrl,omitempty"` } +type JukeboxStatus struct { + CurrentIndex int32 `xml:"currentIndex,attr" json:"currentIndex"` + Playing bool `xml:"playing,attr" json:"playing"` + Gain float32 `xml:"gain,attr" json:"gain"` + Position int32 `xml:"position,omitempty,attr" json:"position"` +} + +type JukeboxPlaylist struct { + JukeboxStatus + Entry []Child `xml:"entry,omitempty" json:"entry,omitempty"` +} type OpenSubsonicExtensions struct{} diff --git a/tests/mock_ffmpeg.go b/tests/mock_ffmpeg.go index 09e651d9..4f9cac1d 100644 --- a/tests/mock_ffmpeg.go +++ b/tests/mock_ffmpeg.go @@ -33,6 +33,20 @@ func (ff *MockFFmpeg) ExtractImage(context.Context, string) (io.ReadCloser, erro return ff, nil } +func (ff *MockFFmpeg) ConvertToFLAC(context.Context, string) (io.ReadCloser, error) { + if ff.Error != nil { + return nil, ff.Error + } + return ff, nil +} + +func (ff *MockFFmpeg) ConvertToWAV(context.Context, string) (io.ReadCloser, error) { + if ff.Error != nil { + return nil, ff.Error + } + return ff, nil +} + func (ff *MockFFmpeg) Probe(context.Context, []string) (string, error) { if ff.Error != nil { return "", ff.Error