Skip to content

Commit

Permalink
Added seek
Browse files Browse the repository at this point in the history
  • Loading branch information
robrotheram committed Feb 24, 2024
1 parent 4c701c2 commit 76c8e14
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 28 deletions.
9 changes: 6 additions & 3 deletions pkg/api/websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type Client struct {
done chan any
progress media.MediaDuration
running bool
exitCode controllers.PlayerExitCode
}

const (
Expand Down Expand Up @@ -84,15 +85,15 @@ 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,
}
wb.running = true
<-wb.done
fmt.Println(WEBPLAYER + "_DONE")
return nil
return wb.exitCode, nil
}

func (wb *Client) Progress() media.MediaDuration {
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
37 changes: 24 additions & 13 deletions pkg/controllers/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +35,8 @@ type Player interface {
}

type Players struct {
players map[string]Player
players map[string]Player
AutoSkip bool
}

func newPlayers() *Players {
Expand All @@ -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)
Expand All @@ -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 {
Expand All @@ -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()
Expand Down
51 changes: 51 additions & 0 deletions pkg/discord/commands/playCmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"net/url"
"w2g/pkg/controllers"
"w2g/pkg/utils"

"github.com/bwmarrin/discordgo"
)
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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:")
Expand Down
39 changes: 27 additions & 12 deletions pkg/discord/players/discord.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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:
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand All @@ -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
}
41 changes: 41 additions & 0 deletions pkg/utils/parseSeek.go
Original file line number Diff line number Diff line change
@@ -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
}
41 changes: 41 additions & 0 deletions pkg/utils/parseSeek_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
})
}
}

0 comments on commit 76c8e14

Please sign in to comment.