package music import ( "context" "errors" "fmt" "sync" "github.com/disgoorg/disgolink/v4/disgolink" "github.com/disgoorg/disgolink/v4/lavalink" ) var ( ErrNoTrackPlaying = errors.New("no track is currently playing") ErrStreamNotSeekable = errors.New("cannot seek within a live stream") ) type LoopMode int const ( LoopOff LoopMode = iota LoopTrack LoopQueue ) func (m LoopMode) String() string { switch m { case LoopTrack: return "track" case LoopQueue: return "queue" default: return "off" } } type GuildPlayer struct { mu sync.Mutex player *disgolink.Player queue *Queue loopMode LoopMode } func newGuildPlayer(player *disgolink.Player) *GuildPlayer { return &GuildPlayer{ player: player, queue: NewQueue(), } } type EnqueueResult struct { Started lavalink.Track PlayingNow bool Queued int } func (g *GuildPlayer) EnqueueMany(ctx context.Context, tracks []lavalink.Track) (EnqueueResult, error) { g.mu.Lock() defer g.mu.Unlock() if len(tracks) == 0 { return EnqueueResult{}, nil } if g.player.Track != nil { for _, track := range tracks { g.queue.Push(track) } return EnqueueResult{Queued: len(tracks)}, nil } first := tracks[0] if err := g.player.Update(ctx, disgolink.WithTrack(first)); err != nil { return EnqueueResult{}, fmt.Errorf("starting track: %w", err) } for _, track := range tracks[1:] { g.queue.Push(track) } return EnqueueResult{Started: first, PlayingNow: true, Queued: len(tracks) - 1}, nil } func (g *GuildPlayer) Skip(ctx context.Context) (next lavalink.Track, hasNext bool, err error) { g.mu.Lock() defer g.mu.Unlock() next, hasNext = g.queue.Pop() if !hasNext { if err := g.player.Update(ctx, disgolink.WithNullTrack()); err != nil { return lavalink.Track{}, false, fmt.Errorf("stopping player: %w", err) } return lavalink.Track{}, false, nil } if err := g.player.Update(ctx, disgolink.WithTrack(next)); err != nil { return lavalink.Track{}, false, fmt.Errorf("starting next track: %w", err) } return next, true, nil } func (g *GuildPlayer) PlayNext(ctx context.Context, ended lavalink.Track) error { g.mu.Lock() defer g.mu.Unlock() if g.loopMode == LoopTrack { if err := g.player.Update(ctx, disgolink.WithTrack(ended)); err != nil { return fmt.Errorf("repeating track: %w", err) } return nil } if g.loopMode == LoopQueue { g.queue.Push(ended) } next, ok := g.queue.Pop() if !ok { return nil } if err := g.player.Update(ctx, disgolink.WithTrack(next)); err != nil { return fmt.Errorf("starting queued track: %w", err) } return nil } func (g *GuildPlayer) Seek(ctx context.Context, position lavalink.Duration) error { g.mu.Lock() defer g.mu.Unlock() if g.player.Track == nil { return ErrNoTrackPlaying } if g.player.Track.Info.IsStream { return ErrStreamNotSeekable } if position < 0 { position = 0 } else if position > g.player.Track.Info.Length { position = g.player.Track.Info.Length } if err := g.player.Update(ctx, disgolink.WithPosition(position)); err != nil { return fmt.Errorf("seeking: %w", err) } return nil } func (g *GuildPlayer) SetVolume(ctx context.Context, volume int) error { g.mu.Lock() defer g.mu.Unlock() if err := g.player.Update(ctx, disgolink.WithVolume(volume)); err != nil { return fmt.Errorf("updating volume: %w", err) } return nil } func (g *GuildPlayer) SetLoopMode(mode LoopMode) { g.mu.Lock() defer g.mu.Unlock() g.loopMode = mode } func (g *GuildPlayer) Stop(ctx context.Context) error { g.mu.Lock() defer g.mu.Unlock() g.queue.Clear() if err := g.player.Update(ctx, disgolink.WithNullTrack()); err != nil { return fmt.Errorf("stopping player: %w", err) } return nil } func (g *GuildPlayer) SetPaused(ctx context.Context, paused bool) error { g.mu.Lock() defer g.mu.Unlock() if err := g.player.Update(ctx, disgolink.WithPaused(paused)); err != nil { return fmt.Errorf("updating pause state: %w", err) } return nil } func (g *GuildPlayer) NowPlaying() (lavalink.Track, bool) { g.mu.Lock() defer g.mu.Unlock() if g.player.Track == nil { return lavalink.Track{}, false } return *g.player.Track, true } func (g *GuildPlayer) Queued() []lavalink.Track { return g.queue.List() } func (g *GuildPlayer) Shuffle() int { return g.queue.Shuffle() } func (g *GuildPlayer) RemoveQueued(index int) (lavalink.Track, bool) { return g.queue.RemoveAt(index) }