Files
vibesonic/internal/database/activity.go
T

214 lines
6.9 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
CreatedAt time.Time
UpdatedAt time.Time
}
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, created_at, updated_at 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
var createdAt, updatedAt string
if err := rows.Scan(&b.ID, &b.SongID, &b.PositionMs, &b.Comment, &createdAt, &updatedAt); err != nil {
return nil, err
}
b.CreatedAt, _ = parseTime(createdAt)
b.UpdatedAt, _ = parseTime(updatedAt)
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
}