TrackEntry gets a QueueID generated once when a track is queued and kept for its whole life there, and musicrepo.ReplaceQueue now upserts by that ID instead of deleting and reinserting the whole queue on every sync (which churned every track's database id constantly, even ones that hadn't moved, and would've made remove/reorder commands target the wrong track). RemoveFromQueue and Reorder are new Manager functions built on top of that stable identity. Also adds repeat_song/ repeat_queue to the persisted now-playing state, and makes the two existing toggle functions actually sync it, they never did before.
727 lines
16 KiB
Go
727 lines
16 KiB
Go
package music
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/url"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
"velox-bot/internal/db/repos/musicrepo"
|
|
|
|
"github.com/bwmarrin/discordgo"
|
|
"github.com/disgoorg/disgolink/v3/disgolink"
|
|
"github.com/disgoorg/disgolink/v3/lavalink"
|
|
"github.com/disgoorg/snowflake/v2"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type TrackEntry struct {
|
|
Track lavalink.Track
|
|
RequestedBy string
|
|
RequesterID string
|
|
StartedAt time.Time
|
|
|
|
// QueueID is generated once, when the track is first added to the
|
|
// queue, and never changes for the rest of its life there - it's
|
|
// what lets syncState upsert this track's row across syncs instead
|
|
// of deleting and recreating it (and its DB id) every single time,
|
|
// which is what remove/reorder commands need to reliably target a
|
|
// specific track.
|
|
QueueID string
|
|
}
|
|
|
|
type guildPlayer struct {
|
|
Player disgolink.Player
|
|
Queue []TrackEntry
|
|
RepeatSong bool
|
|
RepeatQueue bool
|
|
Paused bool
|
|
Volume int
|
|
IdleSince time.Time
|
|
TextChannelID string
|
|
NowPlayingMsgID string
|
|
}
|
|
|
|
type Manager struct {
|
|
client disgolink.Client
|
|
session *discordgo.Session
|
|
repo *musicrepo.Repo
|
|
|
|
mu sync.Mutex
|
|
players map[string]*guildPlayer
|
|
}
|
|
|
|
var manager *Manager
|
|
|
|
func Init(session *discordgo.Session, repo *musicrepo.Repo, appID, lavalinkHost, lavalinkPass string) error {
|
|
if lavalinkHost == "" {
|
|
return nil
|
|
}
|
|
|
|
userID, err := snowflake.Parse(appID)
|
|
if err != nil {
|
|
return fmt.Errorf("parse app id for lavalink: %w", err)
|
|
}
|
|
|
|
m := &Manager{
|
|
client: disgolink.New(userID, disgolink.WithListenerFunc(onTrackEnd)),
|
|
session: session,
|
|
repo: repo,
|
|
players: make(map[string]*guildPlayer),
|
|
}
|
|
|
|
u, err := url.Parse(lavalinkHost)
|
|
if err != nil {
|
|
return fmt.Errorf("parse lavalink host: %w", err)
|
|
}
|
|
|
|
secure := u.Scheme == "https" || u.Scheme == "wss"
|
|
password := lavalinkPass
|
|
if password == "" {
|
|
password = "youshallnotpass"
|
|
}
|
|
|
|
// Connecting to the Lavalink node is a blocking network call (and, on
|
|
// failure, disgolink retries indefinitely with backoff) — do it in the
|
|
// background so bot startup isn't held hostage by it. Music commands
|
|
// simply fail with "not initialized" via the manager==nil checks until
|
|
// this completes.
|
|
go func() {
|
|
_, err := m.client.AddNode(context.Background(), disgolink.NodeConfig{
|
|
Name: "main",
|
|
Address: u.Host,
|
|
Password: password,
|
|
Secure: secure,
|
|
})
|
|
if err != nil {
|
|
log.Printf("music: failed to connect to lavalink node: %v", err)
|
|
return
|
|
}
|
|
manager = m
|
|
go idleDisconnectLoop()
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func idleDisconnectLoop() {
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
if manager == nil || manager.session == nil {
|
|
continue
|
|
}
|
|
|
|
now := time.Now()
|
|
var toDisconnect []string
|
|
|
|
manager.mu.Lock()
|
|
for guildID, gp := range manager.players {
|
|
if gp == nil {
|
|
continue
|
|
}
|
|
|
|
// If we're paused or have anything queued/playing, clear idle timer.
|
|
if gp.Paused || len(gp.Queue) > 0 {
|
|
gp.IdleSince = time.Time{}
|
|
continue
|
|
}
|
|
|
|
if gp.IdleSince.IsZero() {
|
|
gp.IdleSince = now
|
|
continue
|
|
}
|
|
|
|
if now.Sub(gp.IdleSince) >= time.Minute {
|
|
toDisconnect = append(toDisconnect, guildID)
|
|
gp.IdleSince = time.Time{}
|
|
}
|
|
}
|
|
manager.mu.Unlock()
|
|
|
|
for _, guildID := range toDisconnect {
|
|
// Best-effort: disconnect. Safe even if we're already disconnected.
|
|
_ = manager.session.ChannelVoiceJoinManual(guildID, "", false, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func OnVoiceStateUpdate(vs *discordgo.VoiceStateUpdate) {
|
|
if manager == nil {
|
|
return
|
|
}
|
|
if vs.UserID != manager.session.State.User.ID {
|
|
return
|
|
}
|
|
|
|
guildID := snowflake.MustParse(vs.GuildID)
|
|
var channelID *snowflake.ID
|
|
if vs.ChannelID != "" {
|
|
id := snowflake.MustParse(vs.ChannelID)
|
|
channelID = &id
|
|
}
|
|
|
|
manager.client.OnVoiceStateUpdate(
|
|
context.Background(),
|
|
guildID,
|
|
channelID,
|
|
vs.SessionID,
|
|
)
|
|
}
|
|
|
|
func OnVoiceServerUpdate(ev *discordgo.VoiceServerUpdate) {
|
|
if manager == nil {
|
|
return
|
|
}
|
|
endpoint := ""
|
|
if ev.Endpoint != "" {
|
|
endpoint = ev.Endpoint
|
|
}
|
|
|
|
manager.client.OnVoiceServerUpdate(
|
|
context.Background(),
|
|
snowflake.MustParse(ev.GuildID),
|
|
ev.Token,
|
|
endpoint,
|
|
)
|
|
}
|
|
|
|
func getOrCreateGuildPlayer(guildID string) *guildPlayer {
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
|
|
if gp, ok := manager.players[guildID]; ok {
|
|
return gp
|
|
}
|
|
|
|
player := manager.client.Player(snowflake.MustParse(guildID))
|
|
gp := &guildPlayer{
|
|
Player: player,
|
|
Queue: make([]TrackEntry, 0),
|
|
Volume: 100,
|
|
}
|
|
manager.players[guildID] = gp
|
|
return gp
|
|
}
|
|
|
|
func syncState(guidID string) {
|
|
if manager == nil || manager.repo == nil {
|
|
return
|
|
}
|
|
manager.mu.Lock()
|
|
gp, ok := manager.players[guidID]
|
|
if !ok {
|
|
manager.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
queue := make([]TrackEntry, len(gp.Queue))
|
|
copy(queue, gp.Queue)
|
|
paused := gp.Paused
|
|
volume := gp.Volume
|
|
repeatSong := gp.RepeatSong
|
|
repeatQueue := gp.RepeatQueue
|
|
manager.mu.Unlock()
|
|
|
|
gID, err := strconv.ParseInt(guidID, 10, 64)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if len(queue) == 0 {
|
|
err := manager.repo.ClearNowPlaying(context.Background(), gID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = manager.repo.ReplaceQueue(context.Background(), gID, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
current := queue[0]
|
|
title := current.Track.Info.Title
|
|
|
|
var uri string
|
|
if current.Track.Info.URI != nil {
|
|
uri = *current.Track.Info.URI
|
|
}
|
|
|
|
length := int(current.Track.Info.Length / 1000)
|
|
startedAt := current.StartedAt
|
|
|
|
err = manager.repo.SetNowPlaying(context.Background(), gID, title, uri, length, startedAt, paused, volume, repeatSong, repeatQueue)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
rest := queue[1:]
|
|
var queueEntries []musicrepo.QueueEntry
|
|
for _, entry := range rest {
|
|
reqID, _ := strconv.ParseInt(entry.RequesterID, 10, 64)
|
|
var uri string
|
|
if entry.Track.Info.URI != nil {
|
|
uri = *entry.Track.Info.URI
|
|
}
|
|
queueEntries = append(queueEntries, musicrepo.QueueEntry{
|
|
Title: entry.Track.Info.Title,
|
|
URL: uri,
|
|
RequestedBy: reqID,
|
|
ClientID: entry.QueueID,
|
|
})
|
|
}
|
|
err = manager.repo.ReplaceQueue(context.Background(), gID, queueEntries)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
func EnqueueAndPlay(guildID, voiceChannelID, textChannelID, query, requester, requesterID string) (*TrackEntry, bool, error) {
|
|
if manager == nil {
|
|
return nil, false, fmt.Errorf("music manager not initialized")
|
|
}
|
|
|
|
// Join voice channel
|
|
if err := manager.session.ChannelVoiceJoinManual(guildID, voiceChannelID, false, false); err != nil {
|
|
return nil, false, fmt.Errorf("join voice channel: %w", err)
|
|
}
|
|
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
if textChannelID != "" {
|
|
manager.mu.Lock()
|
|
gp.TextChannelID = textChannelID
|
|
manager.mu.Unlock()
|
|
}
|
|
|
|
var loadedTracks []lavalink.Track
|
|
|
|
manager.client.BestNode().LoadTracksHandler(
|
|
context.Background(),
|
|
query,
|
|
disgolink.NewResultHandler(
|
|
func(track lavalink.Track) {
|
|
loadedTracks = append(loadedTracks, track)
|
|
},
|
|
func(playlist lavalink.Playlist) {
|
|
if len(playlist.Tracks) == 0 {
|
|
return
|
|
}
|
|
|
|
// Respect Lavalink's selectedTrack when present by rotating the playlist.
|
|
selected := playlist.Info.SelectedTrack
|
|
if selected >= 0 && selected < len(playlist.Tracks) {
|
|
loadedTracks = append(loadedTracks, playlist.Tracks[selected:]...)
|
|
loadedTracks = append(loadedTracks, playlist.Tracks[:selected]...)
|
|
return
|
|
}
|
|
|
|
loadedTracks = append(loadedTracks, playlist.Tracks...)
|
|
},
|
|
func(tracks []lavalink.Track) {
|
|
if len(tracks) > 0 {
|
|
loadedTracks = append(loadedTracks, tracks[0])
|
|
}
|
|
},
|
|
func() {
|
|
},
|
|
func(err error) {
|
|
},
|
|
),
|
|
)
|
|
|
|
if len(loadedTracks) == 0 {
|
|
return nil, false, fmt.Errorf("no tracks found for query")
|
|
}
|
|
|
|
// Some sources may return unplayable entries; pick the first with an encoded track.
|
|
firstPlayableIdx := 0
|
|
for idx := range loadedTracks {
|
|
if loadedTracks[idx].Encoded != "" {
|
|
firstPlayableIdx = idx
|
|
break
|
|
}
|
|
}
|
|
if firstPlayableIdx != 0 && firstPlayableIdx < len(loadedTracks) {
|
|
loadedTracks = append(loadedTracks[firstPlayableIdx:], loadedTracks[:firstPlayableIdx]...)
|
|
}
|
|
|
|
entries := make([]TrackEntry, len(loadedTracks))
|
|
for idx, t := range loadedTracks {
|
|
entries[idx] = TrackEntry{
|
|
Track: t,
|
|
RequestedBy: requester,
|
|
RequesterID: requesterID,
|
|
QueueID: uuid.New().String(),
|
|
}
|
|
}
|
|
firstEntry := entries[0]
|
|
|
|
manager.mu.Lock()
|
|
shouldStart := len(gp.Queue) == 0
|
|
gp.Queue = append(gp.Queue, entries...)
|
|
gp.IdleSince = time.Time{}
|
|
manager.mu.Unlock()
|
|
|
|
if shouldStart {
|
|
gp.Queue[0].StartedAt = time.Now()
|
|
|
|
if err := gp.Player.Update(context.Background(), lavalink.WithTrack(firstEntry.Track)); err != nil {
|
|
return nil, false, fmt.Errorf("start track: %w", err)
|
|
}
|
|
if gp.TextChannelID != "" {
|
|
postNowPlaying(guildID, gp.TextChannelID, firstEntry)
|
|
}
|
|
}
|
|
|
|
syncState(guildID)
|
|
|
|
return &firstEntry, shouldStart, nil
|
|
}
|
|
|
|
func onTrackEnd(player disgolink.Player, event lavalink.TrackEndEvent) {
|
|
if manager == nil {
|
|
return
|
|
}
|
|
if !event.Reason.MayStartNext() {
|
|
return
|
|
}
|
|
|
|
guildID := player.GuildID().String()
|
|
|
|
manager.mu.Lock()
|
|
gp, ok := manager.players[guildID]
|
|
if !ok || len(gp.Queue) == 0 {
|
|
manager.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// repeat current track
|
|
if gp.RepeatSong {
|
|
gp.Queue[0].StartedAt = time.Now()
|
|
next := gp.Queue[0]
|
|
textChannelID := gp.TextChannelID
|
|
manager.mu.Unlock()
|
|
|
|
if err := gp.Player.Update(context.Background(), lavalink.WithTrack(next.Track)); err != nil {
|
|
return
|
|
}
|
|
if textChannelID != "" {
|
|
postNowPlaying(guildID, textChannelID, next)
|
|
}
|
|
syncState(guildID)
|
|
return
|
|
}
|
|
|
|
// pop current and optionally cycle it to end
|
|
finished := gp.Queue[0]
|
|
gp.Queue = gp.Queue[1:]
|
|
|
|
if gp.RepeatQueue {
|
|
gp.Queue = append(gp.Queue, finished)
|
|
}
|
|
|
|
if len(gp.Queue) == 0 {
|
|
textChannelID := gp.TextChannelID
|
|
nowPlayingMsgID := gp.NowPlayingMsgID
|
|
gp.NowPlayingMsgID = ""
|
|
gp.IdleSince = time.Now()
|
|
manager.mu.Unlock()
|
|
if textChannelID != "" && nowPlayingMsgID != "" {
|
|
disableNowPlaying(textChannelID, nowPlayingMsgID)
|
|
}
|
|
_ = gp.Player.Update(context.Background(), lavalink.WithNullTrack())
|
|
syncState(guildID)
|
|
return
|
|
}
|
|
|
|
gp.Queue[0].StartedAt = time.Now()
|
|
next := gp.Queue[0]
|
|
textChannelID := gp.TextChannelID
|
|
manager.mu.Unlock()
|
|
|
|
if err := gp.Player.Update(context.Background(), lavalink.WithTrack(next.Track)); err != nil {
|
|
return
|
|
}
|
|
if textChannelID != "" {
|
|
postNowPlaying(guildID, textChannelID, next)
|
|
}
|
|
syncState(guildID)
|
|
}
|
|
|
|
func Pause(guildID string, pause bool) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
gp.Paused = pause
|
|
if pause {
|
|
gp.IdleSince = time.Time{}
|
|
}
|
|
manager.mu.Unlock()
|
|
syncState(guildID)
|
|
return gp.Player.Update(context.Background(), lavalink.WithPaused(pause))
|
|
}
|
|
|
|
func Skip(guildID string) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
|
|
manager.mu.Lock()
|
|
if len(gp.Queue) == 0 {
|
|
manager.mu.Unlock()
|
|
return nil
|
|
}
|
|
// skip always advances regardless of repeat song
|
|
skipped := gp.Queue[0]
|
|
gp.Queue = gp.Queue[1:]
|
|
if gp.RepeatQueue {
|
|
gp.Queue = append(gp.Queue, skipped)
|
|
}
|
|
|
|
if len(gp.Queue) == 0 {
|
|
textChannelID := gp.TextChannelID
|
|
nowPlayingMsgID := gp.NowPlayingMsgID
|
|
gp.NowPlayingMsgID = ""
|
|
gp.IdleSince = time.Now()
|
|
manager.mu.Unlock()
|
|
if textChannelID != "" && nowPlayingMsgID != "" {
|
|
disableNowPlaying(textChannelID, nowPlayingMsgID)
|
|
}
|
|
syncState(guildID)
|
|
return gp.Player.Update(context.Background(), lavalink.WithNullTrack())
|
|
}
|
|
|
|
gp.Queue[0].StartedAt = time.Now()
|
|
next := gp.Queue[0]
|
|
textChannelID := gp.TextChannelID
|
|
manager.mu.Unlock()
|
|
|
|
if err := gp.Player.Update(context.Background(), lavalink.WithTrack(next.Track)); err != nil {
|
|
return err
|
|
}
|
|
if textChannelID != "" {
|
|
postNowPlaying(guildID, textChannelID, next)
|
|
}
|
|
syncState(guildID)
|
|
return nil
|
|
}
|
|
|
|
func Stop(guildID string) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
|
|
manager.mu.Lock()
|
|
textChannelID := gp.TextChannelID
|
|
nowPlayingMsgID := gp.NowPlayingMsgID
|
|
gp.Queue = nil
|
|
gp.Paused = false
|
|
gp.NowPlayingMsgID = ""
|
|
gp.IdleSince = time.Now()
|
|
manager.mu.Unlock()
|
|
|
|
if textChannelID != "" && nowPlayingMsgID != "" {
|
|
disableNowPlaying(textChannelID, nowPlayingMsgID)
|
|
}
|
|
syncState(guildID)
|
|
|
|
return gp.Player.Update(context.Background(), lavalink.WithNullTrack())
|
|
}
|
|
|
|
func GetQueue(guildID string) (*TrackEntry, []TrackEntry) {
|
|
if manager == nil {
|
|
return nil, nil
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
|
|
if len(gp.Queue) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
current := gp.Queue[0]
|
|
rest := make([]TrackEntry, len(gp.Queue)-1)
|
|
copy(rest, gp.Queue[1:])
|
|
return ¤t, rest
|
|
}
|
|
|
|
func ToggleRepeatSong(guildID string) (bool, bool) {
|
|
if manager == nil {
|
|
return false, false
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
gp.RepeatSong = !gp.RepeatSong
|
|
if gp.RepeatSong {
|
|
gp.RepeatQueue = false
|
|
}
|
|
repeatSong, repeatQueue := gp.RepeatSong, gp.RepeatQueue
|
|
manager.mu.Unlock()
|
|
|
|
syncState(guildID)
|
|
return repeatSong, repeatQueue
|
|
}
|
|
|
|
func ToggleRepeatQueue(guildID string) (bool, bool) {
|
|
if manager == nil {
|
|
return false, false
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
gp.RepeatQueue = !gp.RepeatQueue
|
|
if gp.RepeatQueue {
|
|
gp.RepeatSong = false
|
|
}
|
|
repeatSong, repeatQueue := gp.RepeatSong, gp.RepeatQueue
|
|
manager.mu.Unlock()
|
|
|
|
syncState(guildID)
|
|
return repeatSong, repeatQueue
|
|
}
|
|
|
|
func repeatFlags(guildID string) (bool, bool) {
|
|
if manager == nil {
|
|
return false, false
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
return gp.RepeatSong, gp.RepeatQueue
|
|
}
|
|
|
|
func pausedAndVolume(guildID string) (bool, int) {
|
|
if manager == nil {
|
|
return false, 100
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
vol := gp.Volume
|
|
if vol <= 0 {
|
|
vol = 100
|
|
}
|
|
return gp.Paused, vol
|
|
}
|
|
|
|
func SetVolume(guildID string, volume int) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
if volume < 0 {
|
|
volume = 0
|
|
}
|
|
if volume > 150 {
|
|
volume = 150
|
|
}
|
|
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
gp.Volume = volume
|
|
textChannelID := gp.TextChannelID
|
|
nowPlayingMsgID := gp.NowPlayingMsgID
|
|
manager.mu.Unlock()
|
|
|
|
if err := gp.Player.Update(context.Background(), lavalink.WithVolume(volume)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Refresh the embed so the displayed volume matches.
|
|
if textChannelID != "" && nowPlayingMsgID != "" {
|
|
current, _ := GetQueue(guildID)
|
|
if current != nil {
|
|
postNowPlaying(guildID, textChannelID, *current)
|
|
}
|
|
}
|
|
syncState(guildID)
|
|
return nil
|
|
}
|
|
|
|
func RemoveFromQueue(guildID, queueID string) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
|
|
manager.mu.Lock()
|
|
idx := -1
|
|
for i, q := range gp.Queue {
|
|
if q.QueueID == queueID {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
manager.mu.Unlock()
|
|
return fmt.Errorf("queue not found")
|
|
}
|
|
|
|
if idx == 0 {
|
|
manager.mu.Unlock()
|
|
return Skip(guildID)
|
|
}
|
|
|
|
gp.Queue = append(gp.Queue[:idx], gp.Queue[idx+1:]...)
|
|
manager.mu.Unlock()
|
|
|
|
syncState(guildID)
|
|
return nil
|
|
}
|
|
|
|
func Reorder(guildID, queueID string, newPosition int) error {
|
|
if manager == nil {
|
|
return fmt.Errorf("music manager not initialized")
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
|
|
manager.mu.Lock()
|
|
idx := -1
|
|
for i, q := range gp.Queue {
|
|
if q.QueueID == queueID {
|
|
idx = i
|
|
break
|
|
}
|
|
}
|
|
if idx == -1 {
|
|
manager.mu.Unlock()
|
|
return fmt.Errorf("queue not found")
|
|
}
|
|
|
|
if idx == 0 {
|
|
manager.mu.Unlock()
|
|
return Skip(guildID)
|
|
}
|
|
|
|
entry := gp.Queue[idx]
|
|
gp.Queue = append(gp.Queue[:idx], gp.Queue[idx+1:]...)
|
|
|
|
pos := max(1, min(newPosition, len(gp.Queue)))
|
|
gp.Queue = append(gp.Queue[:pos], append([]TrackEntry{entry}, gp.Queue[pos:]...)...)
|
|
manager.mu.Unlock()
|
|
|
|
syncState(guildID)
|
|
return nil
|
|
}
|
|
|
|
func CurrentNowPlayingMessageID(guildID string) string {
|
|
if manager == nil {
|
|
return ""
|
|
}
|
|
gp := getOrCreateGuildPlayer(guildID)
|
|
manager.mu.Lock()
|
|
defer manager.mu.Unlock()
|
|
return gp.NowPlayingMsgID
|
|
}
|