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.
145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
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
|
|
}
|