173 lines
5.1 KiB
Go
173 lines
5.1 KiB
Go
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()
|
|
}
|