package musicrepo import ( "context" "database/sql" "time" ) type QueueEntry struct { Title string URL string RequestedBy int64 // ClientID is the track's stable identity (set once by the manager // when a track is first queued), used to upsert this row across // syncs instead of assigning it a fresh DB id every single time. ClientID string } type Repo struct { db *sql.DB } func NewRepo(db *sql.DB) *Repo { return &Repo{db: db} } func (r *Repo) SetNowPlaying(ctx context.Context, guildID int64, title, url string, durationSeconds int, startedAt time.Time, paused bool, volume int, repeatSong, repeatQueue bool) error { const q = ` INSERT INTO music_now_playing (guild_id, track_title, track_url, duration_seconds, started_at, paused, volume, repeat_song, repeat_queue) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (guild_id) DO UPDATE SET track_title = $2, track_url = $3, duration_seconds = $4, started_at = $5, paused = $6, volume = $7, repeat_song = $8, repeat_queue = $9 ` _, err := r.db.ExecContext(ctx, q, guildID, title, url, durationSeconds, startedAt, paused, volume, repeatSong, repeatQueue) return err } func (r *Repo) ClearNowPlaying(ctx context.Context, guildID int64) error { const q = `DELETE FROM music_now_playing WHERE guild_id = $1` _, err := r.db.ExecContext(ctx, q, guildID) return err } // ReplaceQueue syncs music_queue with the current in-memory queue by // upserting each entry on its stable ClientID, rather than deleting and // recreating every row on every call. That matters because music_queue.id // is only an internal DB identity, remove/reorder commands target a // track by ClientID, and that has to survive a track just sitting in the // queue unchanged across syncs. func (r *Repo) ReplaceQueue(ctx context.Context, guildID int64, entries []QueueEntry) error { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() existing := make(map[string]bool) rows, err := tx.QueryContext(ctx, `SELECT client_id FROM music_queue WHERE guild_id = $1`, guildID) if err != nil { return err } for rows.Next() { var clientID string if err := rows.Scan(&clientID); err != nil { rows.Close() return err } existing[clientID] = true } if err := rows.Err(); err != nil { rows.Close() return err } rows.Close() keep := make(map[string]bool, len(entries)) for i, entry := range entries { keep[entry.ClientID] = true const q = ` INSERT INTO music_queue (guild_id, position, track_title, track_url, requested_by, client_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (guild_id, client_id) DO UPDATE SET position = EXCLUDED.position, track_title = EXCLUDED.track_title, track_url = EXCLUDED.track_url, requested_by = EXCLUDED.requested_by ` if _, err := tx.ExecContext(ctx, q, guildID, i, entry.Title, entry.URL, entry.RequestedBy, entry.ClientID); err != nil { return err } } for clientID := range existing { if keep[clientID] { continue } const q = `DELETE FROM music_queue WHERE guild_id = $1 AND client_id = $2` if _, err := tx.ExecContext(ctx, q, guildID, clientID); err != nil { return err } } return tx.Commit() } type MusicCommand struct { ID int64 GuildID int64 CommandType string Payload []byte RequestedBy int64 } func (r *Repo) ListUnprocessedCommands(ctx context.Context) ([]MusicCommand, error) { const q = `SELECT id, guild_id, command_type, payload, requested_by FROM music_commands WHERE processed_at IS NULL ORDER BY id ASC` rows, err := r.db.QueryContext(ctx, q) if err != nil { return nil, err } defer rows.Close() var commands []MusicCommand for rows.Next() { var cmd MusicCommand if err := rows.Scan(&cmd.ID, &cmd.GuildID, &cmd.CommandType, &cmd.Payload, &cmd.RequestedBy); err != nil { return nil, err } commands = append(commands, cmd) } if err := rows.Err(); err != nil { return nil, err } return commands, nil } func (r *Repo) MarkCommandProcessed(ctx context.Context, id int64) error { const q = `UPDATE music_commands SET processed_at = NOW() WHERE id = $1` _, err := r.db.ExecContext(ctx, q, id) return err }