package database import ( "database/sql" "errors" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/idgen" ) type Playlist struct { ID string OwnerID string Name string Description string IsPublic bool CreatedAt time.Time UpdatedAt time.Time } func CreatePlaylist(db *sql.DB, ownerID, name, description string, isPublic bool) (*Playlist, error) { id := idgen.New("pl") _, err := db.Exec(`INSERT INTO playlists (id, owner_id, name, description, is_public) VALUES (?, ?, ?, ?, ?)`, id, ownerID, name, description, boolInt(isPublic)) if err != nil { return nil, err } return GetPlaylist(db, id) } const playlistSelect = `SELECT id, owner_id, name, description, is_public, created_at, updated_at FROM playlists` func GetPlaylist(db *sql.DB, id string) (*Playlist, error) { return scanPlaylist(db.QueryRow(playlistSelect+` WHERE id = ?`, id)) } func scanPlaylist(row *sql.Row) (*Playlist, error) { var p Playlist var isPublic int var createdAt, updatedAt string err := row.Scan(&p.ID, &p.OwnerID, &p.Name, &p.Description, &isPublic, &createdAt, &updatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } p.IsPublic = isPublic != 0 p.CreatedAt, _ = parseTime(createdAt) p.UpdatedAt, _ = parseTime(updatedAt) return &p, nil } // ListPlaylistsVisibleToUser returns playlists owned by the user plus any // public playlists owned by others. func ListPlaylistsVisibleToUser(db *sql.DB, userID string) ([]*Playlist, error) { rows, err := db.Query(playlistSelect+` WHERE owner_id = ? OR is_public = 1 ORDER BY name COLLATE NOCASE`, userID) if err != nil { return nil, err } defer rows.Close() var out []*Playlist for rows.Next() { var p Playlist var isPublic int var createdAt, updatedAt string if err := rows.Scan(&p.ID, &p.OwnerID, &p.Name, &p.Description, &isPublic, &createdAt, &updatedAt); err != nil { return nil, err } p.IsPublic = isPublic != 0 p.CreatedAt, _ = parseTime(createdAt) p.UpdatedAt, _ = parseTime(updatedAt) out = append(out, &p) } return out, rows.Err() } func DeletePlaylist(db *sql.DB, id string) error { _, err := db.Exec(`DELETE FROM playlists WHERE id = ?`, id) return err } func RenamePlaylist(db *sql.DB, id, name, description string, isPublic bool) error { _, err := db.Exec(`UPDATE playlists SET name=?, description=?, is_public=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`, name, description, boolInt(isPublic), id) return err } type PlaylistEntry struct { ID string SongID string Position int } func ListPlaylistEntries(db *sql.DB, playlistID string) ([]*PlaylistEntry, error) { rows, err := db.Query(`SELECT id, song_id, position FROM playlist_entries WHERE playlist_id = ? ORDER BY position`, playlistID) if err != nil { return nil, err } defer rows.Close() var out []*PlaylistEntry for rows.Next() { var e PlaylistEntry if err := rows.Scan(&e.ID, &e.SongID, &e.Position); err != nil { return nil, err } out = append(out, &e) } return out, rows.Err() } // AppendPlaylistEntry adds a song at the end of the playlist. func AppendPlaylistEntry(db *sql.DB, playlistID, songID string) error { var next int if err := db.QueryRow(`SELECT COALESCE(MAX(position), -1) + 1 FROM playlist_entries WHERE playlist_id = ?`, playlistID).Scan(&next); err != nil { return err } _, err := db.Exec(`INSERT INTO playlist_entries (id, playlist_id, song_id, position) VALUES (?, ?, ?, ?)`, idgen.New("pe"), playlistID, songID, next) return err } func RemovePlaylistEntry(db *sql.DB, entryID string) error { _, err := db.Exec(`DELETE FROM playlist_entries WHERE id = ?`, entryID) return err } // ReplacePlaylistSongs atomically replaces a playlist's entire song list // with songIDs, in order. func ReplacePlaylistSongs(db *sql.DB, playlistID string, songIDs []string) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() if _, err := tx.Exec(`DELETE FROM playlist_entries WHERE playlist_id = ?`, playlistID); err != nil { return err } for i, songID := range songIDs { if _, err := tx.Exec(`INSERT INTO playlist_entries (id, playlist_id, song_id, position) VALUES (?, ?, ?, ?)`, idgen.New("pe"), playlistID, songID, i); err != nil { return err } } if _, err := tx.Exec(`UPDATE playlists SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, playlistID); err != nil { return err } return tx.Commit() } // ListPlaylistsByOwner returns only playlists owned by userID (used when // an admin requests another user's playlists by username). func ListPlaylistsByOwner(db *sql.DB, ownerID string) ([]*Playlist, error) { rows, err := db.Query(playlistSelect+` WHERE owner_id = ? ORDER BY name COLLATE NOCASE`, ownerID) if err != nil { return nil, err } defer rows.Close() var out []*Playlist for rows.Next() { var p Playlist var isPublic int var createdAt, updatedAt string if err := rows.Scan(&p.ID, &p.OwnerID, &p.Name, &p.Description, &isPublic, &createdAt, &updatedAt); err != nil { return nil, err } p.IsPublic = isPublic != 0 p.CreatedAt, _ = parseTime(createdAt) p.UpdatedAt, _ = parseTime(updatedAt) out = append(out, &p) } return out, rows.Err() }