209 lines
6.7 KiB
Go
209 lines
6.7 KiB
Go
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
|
|
}
|