package database import ( "database/sql" "errors" "time" "git.noroutes.net/NoRoutes/vibetunes/internal/idgen" ) // --- Stars (favorites) --- func Star(db *sql.DB, userID, itemType, itemID string) error { _, err := db.Exec(`INSERT OR IGNORE INTO stars (id, user_id, item_type, item_id) VALUES (?, ?, ?, ?)`, idgen.New("st"), userID, itemType, itemID) return err } func Unstar(db *sql.DB, userID, itemType, itemID string) error { _, err := db.Exec(`DELETE FROM stars WHERE user_id = ? AND item_type = ? AND item_id = ?`, userID, itemType, itemID) return err } func IsStarred(db *sql.DB, userID, itemType, itemID string) (bool, error) { var n int err := db.QueryRow(`SELECT COUNT(*) FROM stars WHERE user_id = ? AND item_type = ? AND item_id = ?`, userID, itemType, itemID).Scan(&n) return n > 0, err } func ListStarredIDs(db *sql.DB, userID, itemType string) ([]string, error) { rows, err := db.Query(`SELECT item_id FROM stars WHERE user_id = ? AND item_type = ? ORDER BY created_at DESC`, userID, itemType) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() } // --- Ratings --- func SetRating(db *sql.DB, userID, itemType, itemID string, rating int) error { if rating <= 0 { _, err := db.Exec(`DELETE FROM ratings WHERE user_id = ? AND item_type = ? AND item_id = ?`, userID, itemType, itemID) return err } _, err := db.Exec(`INSERT INTO ratings (id, user_id, item_type, item_id, rating) VALUES (?, ?, ?, ?, ?) ON CONFLICT (user_id, item_type, item_id) DO UPDATE SET rating = excluded.rating`, idgen.New("rt"), userID, itemType, itemID, rating) return err } // --- Scrobbles & playback history --- func RecordScrobble(db *sql.DB, userID, songID, client string, submittedToLastfm bool) error { _, err := db.Exec(`INSERT INTO scrobbles (id, user_id, song_id, client, submitted_to_lastfm) VALUES (?, ?, ?, ?, ?)`, idgen.New("sb"), userID, songID, client, boolInt(submittedToLastfm)) return err } func RecordPlayback(db *sql.DB, userID, songID string, playDurationSeconds float64) error { _, err := db.Exec(`INSERT INTO playback_history (id, user_id, song_id, play_duration_seconds) VALUES (?, ?, ?, ?)`, idgen.New("ph"), userID, songID, playDurationSeconds) return err } type PlaybackHistoryEntry struct { SongID string PlayedAt time.Time } func ListRecentlyPlayed(db *sql.DB, userID string, limit int) ([]PlaybackHistoryEntry, error) { rows, err := db.Query(`SELECT song_id, played_at FROM playback_history WHERE user_id = ? ORDER BY played_at DESC LIMIT ?`, userID, limit) if err != nil { return nil, err } defer rows.Close() var out []PlaybackHistoryEntry for rows.Next() { var e PlaybackHistoryEntry var playedAt string if err := rows.Scan(&e.SongID, &playedAt); err != nil { return nil, err } e.PlayedAt, _ = parseTime(playedAt) out = append(out, e) } return out, rows.Err() } // --- Now playing --- func SetNowPlaying(db *sql.DB, userID, songID, client string, positionSeconds float64) error { _, err := db.Exec(`INSERT INTO now_playing (user_id, song_id, client, position_seconds) VALUES (?, ?, ?, ?) ON CONFLICT (user_id) DO UPDATE SET song_id=excluded.song_id, started_at=strftime('%Y-%m-%dT%H:%M:%fZ','now'), client=excluded.client, position_seconds=excluded.position_seconds`, userID, songID, client, positionSeconds) return err } func ClearNowPlaying(db *sql.DB, userID string) error { _, err := db.Exec(`DELETE FROM now_playing WHERE user_id = ?`, userID) return err } type NowPlayingEntry struct { UserID string Username string SongID string StartedAt time.Time Client string PositionSeconds float64 } func ListNowPlaying(db *sql.DB) ([]NowPlayingEntry, error) { rows, err := db.Query(`SELECT np.user_id, u.username, np.song_id, np.started_at, np.client, np.position_seconds FROM now_playing np JOIN users u ON u.id = np.user_id ORDER BY np.started_at DESC`) if err != nil { return nil, err } defer rows.Close() var out []NowPlayingEntry for rows.Next() { var e NowPlayingEntry var startedAt string if err := rows.Scan(&e.UserID, &e.Username, &e.SongID, &startedAt, &e.Client, &e.PositionSeconds); err != nil { return nil, err } e.StartedAt, _ = parseTime(startedAt) out = append(out, e) } return out, rows.Err() } // --- Bookmarks --- type Bookmark struct { ID string SongID string PositionMs int Comment string } func SetBookmark(db *sql.DB, userID, songID string, positionMs int, comment string) error { _, err := db.Exec(`INSERT INTO bookmarks (id, user_id, song_id, position_ms, comment) VALUES (?, ?, ?, ?, ?) ON CONFLICT (user_id, song_id) DO UPDATE SET position_ms=excluded.position_ms, comment=excluded.comment, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')`, idgen.New("bk"), userID, songID, positionMs, comment) return err } func DeleteBookmark(db *sql.DB, userID, songID string) error { _, err := db.Exec(`DELETE FROM bookmarks WHERE user_id = ? AND song_id = ?`, userID, songID) return err } func ListBookmarks(db *sql.DB, userID string) ([]*Bookmark, error) { rows, err := db.Query(`SELECT id, song_id, position_ms, comment FROM bookmarks WHERE user_id = ? ORDER BY updated_at DESC`, userID) if err != nil { return nil, err } defer rows.Close() var out []*Bookmark for rows.Next() { var b Bookmark if err := rows.Scan(&b.ID, &b.SongID, &b.PositionMs, &b.Comment); err != nil { return nil, err } out = append(out, &b) } return out, rows.Err() } // --- Play queue --- func SavePlayQueue(db *sql.DB, userID, songIDsJSON string, currentIndex, positionMs int, client string) error { _, err := db.Exec(`INSERT INTO play_queues (user_id, song_ids_json, current_index, position_ms, client) VALUES (?, ?, ?, ?, ?) ON CONFLICT (user_id) DO UPDATE SET song_ids_json=excluded.song_ids_json, current_index=excluded.current_index, position_ms=excluded.position_ms, client=excluded.client, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now')`, userID, songIDsJSON, currentIndex, positionMs, client) return err } type PlayQueue struct { SongIDsJSON string CurrentIndex int PositionMs int Client string UpdatedAt time.Time } func GetPlayQueue(db *sql.DB, userID string) (*PlayQueue, error) { var q PlayQueue var updatedAt string err := db.QueryRow(`SELECT song_ids_json, current_index, position_ms, client, updated_at FROM play_queues WHERE user_id = ?`, userID). Scan(&q.SongIDsJSON, &q.CurrentIndex, &q.PositionMs, &q.Client, &updatedAt) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } q.UpdatedAt, _ = parseTime(updatedAt) return &q, nil }