From 76c8e14428abd022830d6790c3496e3738ac46e0 Mon Sep 17 00:00:00 2001 From: robrotheram Date: Sat, 24 Feb 2024 20:39:53 +0000 Subject: [PATCH] Added seek --- pkg/api/websocket.go | 9 ++++-- pkg/controllers/player.go | 37 +++++++++++++++--------- pkg/discord/commands/playCmd.go | 51 +++++++++++++++++++++++++++++++++ pkg/discord/players/discord.go | 39 +++++++++++++++++-------- pkg/utils/parseSeek.go | 41 ++++++++++++++++++++++++++ pkg/utils/parseSeek_test.go | 41 ++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 28 deletions(-) create mode 100644 pkg/utils/parseSeek.go create mode 100644 pkg/utils/parseSeek_test.go diff --git a/pkg/api/websocket.go b/pkg/api/websocket.go index 3fcf69e..4bafddb 100644 --- a/pkg/api/websocket.go +++ b/pkg/api/websocket.go @@ -33,6 +33,7 @@ type Client struct { done chan any progress media.MediaDuration running bool + exitCode controllers.PlayerExitCode } const ( @@ -84,7 +85,7 @@ func (wb *Client) Id() string { return wb.id } -func (wb *Client) Play(url string, start int) error { +func (wb *Client) Play(url string, start int) (controllers.PlayerExitCode, error) { fmt.Println(WEBPLAYER + "_PLAY") wb.progress = media.MediaDuration{ Progress: 0, @@ -92,7 +93,7 @@ func (wb *Client) Play(url string, start int) error { wb.running = true <-wb.done fmt.Println(WEBPLAYER + "_DONE") - return nil + return wb.exitCode, nil } func (wb *Client) Progress() media.MediaDuration { @@ -108,6 +109,7 @@ func (wb *Client) Unpause() { } func (wb *Client) Stop() { + wb.exitCode = controllers.STOP_EXITCODE fmt.Println(WEBPLAYER + "_STOP") if wb.running { wb.running = false @@ -116,7 +118,7 @@ func (wb *Client) Stop() { } func (wb *Client) Close() { fmt.Println(WEBPLAYER + "_CLOSE") - wb.Stop() + wb.exitCode = controllers.EXIT_EXITCODE } func (wb *Client) Status() bool { @@ -139,6 +141,7 @@ func NewClient(socket *websocket.Conn, contoller *controllers.Controller, user U send: make(chan []byte, MessageBufferSize), done: make(chan any), contoller: contoller, + exitCode: controllers.STOP_EXITCODE, } go client.Read() go client.Write() diff --git a/pkg/controllers/player.go b/pkg/controllers/player.go index 8faafc4..f36e5fa 100644 --- a/pkg/controllers/player.go +++ b/pkg/controllers/player.go @@ -13,8 +13,16 @@ type PlayerMeta struct { Type PlayerType `json:"type"` Running bool `json:"running"` } +type PlayerExitCode uint8 + +const ( + STOP_EXITCODE PlayerExitCode = iota + EXIT_EXITCODE + SKIP_EXITCODE +) + type Player interface { - Play(string, int) error + Play(string, int) (PlayerExitCode, error) Progress() media.MediaDuration Seek(time.Duration) Type() PlayerType @@ -27,7 +35,8 @@ type Player interface { } type Players struct { - players map[string]Player + players map[string]Player + AutoSkip bool } func newPlayers() *Players { @@ -44,6 +53,13 @@ func (p *Players) Add(player Player) { p.players[player.Id()] = player } +func (p *Players) Remvoe(id string) { + if player, ok := p.players[id]; ok { + player.Close() + delete(p.players, id) + } +} + func (p *Players) Seek(seconds time.Duration) { for _, player := range p.players { player.Seek(seconds) @@ -62,13 +78,6 @@ func (p *Players) GetProgress() map[string]PlayerMeta { return data } -func (p *Players) Remvoe(id string) { - if player, ok := p.players[id]; ok { - player.Close() - delete(p.players, id) - } -} - func (p *Players) Progress() media.MediaDuration { progress := media.MediaDuration{} for _, player := range p.players { @@ -82,16 +91,18 @@ func (p *Players) Progress() media.MediaDuration { func (p *Players) Play(url string, start int) { wg := sync.WaitGroup{} + wg.Add(len(p.players)) for _, player := range p.players { - wg.Add(1) + player := player //TODO: Remove in when we upgrade go 1.22 go func(player Player) { - err := player.Play(url, start) + exit, err := player.Play(url, start) if err != nil { fmt.Printf("%s player error: %v", player.Type(), err) } wg.Done() - //Currently Set it the the first "player to finish will override all other players" - p.Stop() + if exit == STOP_EXITCODE { + p.Stop() + } }(player) } wg.Wait() diff --git a/pkg/discord/commands/playCmd.go b/pkg/discord/commands/playCmd.go index afa4e27..40a418a 100644 --- a/pkg/discord/commands/playCmd.go +++ b/pkg/discord/commands/playCmd.go @@ -3,6 +3,7 @@ package commands import ( "net/url" "w2g/pkg/controllers" + "w2g/pkg/utils" "github.com/bwmarrin/discordgo" ) @@ -49,6 +50,32 @@ func init() { }, Function: pauseCmd, }, + Command{ + Name: "seek", + ApplicationCommand: []discordgo.ApplicationCommand{ + { + Description: "Set the position of the track to the given time. ", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "time", + Description: "Position to fast forward e.g 30 for seconds ", + Required: true, + }, + }, + }, + }, + Function: seekCMD, + }, + Command{ + Name: "restart", + ApplicationCommand: []discordgo.ApplicationCommand{ + { + Description: "Restart the currently playing track.", + }, + }, + Function: restartCmd, + }, Command{ Name: "add", ApplicationCommand: []discordgo.ApplicationCommand{ @@ -124,6 +151,30 @@ func playCmd(ctx CommandCtx) *discordgo.InteractionResponse { return ctx.Reply(":play_pause: Now Playing :thumbsup:") } +func seekCMD(ctx CommandCtx) *discordgo.InteractionResponse { + if !ctx.Controller.ContainsPlayer(ctx.Guild.ID) { + if join(ctx) != nil { + return ctx.Reply("User not connected to voice channel") + } + } + seekTime, err := utils.ParseTime(ctx.Args[0]) + if err != nil { + return ctx.Reply("Invalid time format") + } + ctx.Controller.Seek(seekTime, ctx.Member.User.Username) + return ctx.Replyf(":fast_forward: Seeking to %d seconds into the track :thumbsup:", seekTime) +} + +func restartCmd(ctx CommandCtx) *discordgo.InteractionResponse { + if !ctx.Controller.ContainsPlayer(ctx.Guild.ID) { + if join(ctx) != nil { + return ctx.Reply("User not connected to voice channel") + } + } + ctx.Controller.Seek(0, ctx.Member.User.Username) + return ctx.Reply(":leftwards_arrow_with_hook: Restarting Track") +} + func skipCmd(ctx CommandCtx) *discordgo.InteractionResponse { ctx.Controller.Skip(ctx.Member.User.Username) return ctx.Reply(":fast_forward: Now Skipping :thumbsup:") diff --git a/pkg/discord/players/discord.go b/pkg/discord/players/discord.go index fedc2a5..72faa2d 100644 --- a/pkg/discord/players/discord.go +++ b/pkg/discord/players/discord.go @@ -22,14 +22,17 @@ type DiscordPlayer struct { voice *discordgo.VoiceConnection progress media.MediaDuration running bool + seekTo time.Duration startTime int + exitcode controllers.PlayerExitCode } func NewDiscordPlayer(id string, voice *discordgo.VoiceConnection) *DiscordPlayer { audio := &DiscordPlayer{ - id: id, - done: make(chan error), - voice: voice, + id: id, + done: make(chan error), + voice: voice, + exitcode: controllers.STOP_EXITCODE, } return audio } @@ -73,8 +76,9 @@ func (player *DiscordPlayer) playStream() { ticker := time.NewTicker(time.Second) for { select { - case <-player.done: + case msg := <-player.done: // Clean up incase something happened and ffmpeg is still running + fmt.Printf("player msg: %v\n", msg) player.Finish() return case <-ticker.C: @@ -96,12 +100,18 @@ func (player *DiscordPlayer) Status() bool { } func (player *DiscordPlayer) Close() { + player.exitcode = controllers.EXIT_EXITCODE player.Stop() player.voice.Disconnect() } func (player *DiscordPlayer) Seek(seconds time.Duration) { - + player.seekTo = seconds + if player.session == nil { + return + } + player.session.Stop() + player.Finish() } func (player *DiscordPlayer) Finish() { @@ -138,9 +148,10 @@ func (player *DiscordPlayer) Progress() media.MediaDuration { return player.progress } -func (player *DiscordPlayer) Play(url string, startTime int) error { +func (player *DiscordPlayer) Play(url string, startTime int) (controllers.PlayerExitCode, error) { + player.seekTo = -1 if player.running { - return fmt.Errorf("playing already started") + return controllers.STOP_EXITCODE, fmt.Errorf("playing already started") } opts := dca.StdEncodeOptions opts.RawOutput = true @@ -152,13 +163,17 @@ func (player *DiscordPlayer) Play(url string, startTime int) error { player.ParseDuration(url) encodeSession, err := dca.EncodeFile(url, opts) if err != nil { - return fmt.Errorf("failed creating an encoding session: %v", err) + return controllers.STOP_EXITCODE, fmt.Errorf("failed creating an encoding session: %v", err) } player.session = encodeSession - player.voice.Speaking(true) - defer player.voice.Speaking(false) - defer player.Stop() player.playStream() - return nil + + if player.seekTo > -1 { + fmt.Println("SEEKING") + player.Finish() + return player.Play(url, int(player.seekTo.Seconds())) + } + player.Finish() + return player.exitcode, nil } diff --git a/pkg/utils/parseSeek.go b/pkg/utils/parseSeek.go new file mode 100644 index 0000000..c74b42f --- /dev/null +++ b/pkg/utils/parseSeek.go @@ -0,0 +1,41 @@ +package utils + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// parseTime parses a time string in the format "hours:minutes:seconds" +// where each component (hours, minutes, seconds) is optional. +// If hours is not provided, it defaults to 0. +// If only minutes are provided, it is interpreted as minutes and seconds. +// If only seconds are provided, it is interpreted as seconds. +// The function returns a time.Duration representing the parsed time. +// An error is returned if the input string is in an invalid format. + +func ParseTime(input string) (time.Duration, error) { + if len(input) == 0 { + return 0, fmt.Errorf("invalid time format") + } + parts := strings.Split(input, ":") + var hours, minutes, seconds int + + switch len(parts) { + case 1: + seconds, _ = strconv.Atoi(parts[0]) + case 2: + minutes, _ = strconv.Atoi(parts[0]) + seconds, _ = strconv.Atoi(parts[1]) + case 3: + hours, _ = strconv.Atoi(parts[0]) + minutes, _ = strconv.Atoi(parts[1]) + seconds, _ = strconv.Atoi(parts[2]) + default: + return 0, fmt.Errorf("invalid time format") + } + + duration := time.Duration(hours)*time.Hour + time.Duration(minutes)*time.Minute + time.Duration(seconds)*time.Second + return duration, nil +} diff --git a/pkg/utils/parseSeek_test.go b/pkg/utils/parseSeek_test.go new file mode 100644 index 0000000..dc859dd --- /dev/null +++ b/pkg/utils/parseSeek_test.go @@ -0,0 +1,41 @@ +package utils + +import ( + "testing" + "time" +) + +func TestParseTime(t *testing.T) { + testCases := []struct { + input string + expected time.Duration + err bool + }{ + {"1:0:0", 1 * time.Hour, false}, + {"1:61:64", 2*time.Hour + 2*time.Minute + 4*time.Second, false}, + {"0:1:0", 1*time.Minute, false}, + {"1:23:40", 1*time.Hour + 23*time.Minute + 40*time.Second, false}, + {"23:40", 23*time.Minute + 40*time.Second, false}, + {"40", 40 * time.Second, false}, + {"1:23:40:50", 0, true}, // Invalid format + {"", 0, true}, // Empty string + } + + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + duration, err := ParseTime(tc.input) + if tc.err { + if err == nil { + t.Errorf("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if duration != tc.expected { + t.Errorf("expected %v, got %v", tc.expected, duration) + } + } + }) + } +}