187 lines
4.7 KiB
Go
187 lines
4.7 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
|
|
)
|
|
|
|
type Share struct {
|
|
ID string
|
|
OwnerID string
|
|
Token string
|
|
Description string
|
|
ExpiresAt sql.NullString
|
|
CreatedAt time.Time
|
|
LastVisitedAt sql.NullString
|
|
VisitCount int64
|
|
}
|
|
|
|
const shareSelect = `SELECT id, owner_id, token, description, expires_at, created_at, last_visited_at, visit_count FROM shares`
|
|
|
|
func CreateShare(db *sql.DB, ownerID, token, description string, expiresAt sql.NullString) (*Share, error) {
|
|
id := idgen.New("sh")
|
|
var exp any
|
|
if expiresAt.Valid {
|
|
exp = expiresAt.String
|
|
}
|
|
_, err := db.Exec(`INSERT INTO shares (id, owner_id, token, description, expires_at) VALUES (?, ?, ?, ?, ?)`, id, ownerID, token, description, exp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return GetShare(db, id)
|
|
}
|
|
|
|
func GetShare(db *sql.DB, id string) (*Share, error) {
|
|
return scanShare(db.QueryRow(shareSelect+` WHERE id = ?`, id))
|
|
}
|
|
|
|
func GetShareByToken(db *sql.DB, token string) (*Share, error) {
|
|
return scanShare(db.QueryRow(shareSelect+` WHERE token = ?`, token))
|
|
}
|
|
|
|
func scanShare(row *sql.Row) (*Share, error) {
|
|
var s Share
|
|
var createdAt string
|
|
err := row.Scan(&s.ID, &s.OwnerID, &s.Token, &s.Description, &s.ExpiresAt, &createdAt, &s.LastVisitedAt, &s.VisitCount)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.CreatedAt, _ = parseTime(createdAt)
|
|
return &s, nil
|
|
}
|
|
|
|
func ListSharesByOwner(db *sql.DB, ownerID string) ([]*Share, error) {
|
|
rows, err := db.Query(shareSelect+` WHERE owner_id = ? ORDER BY created_at DESC`, ownerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanShares(rows)
|
|
}
|
|
|
|
func ListAllShares(db *sql.DB) ([]*Share, error) {
|
|
rows, err := db.Query(shareSelect + ` ORDER BY created_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanShares(rows)
|
|
}
|
|
|
|
func scanShares(rows *sql.Rows) ([]*Share, error) {
|
|
var out []*Share
|
|
for rows.Next() {
|
|
var s Share
|
|
var createdAt string
|
|
if err := rows.Scan(&s.ID, &s.OwnerID, &s.Token, &s.Description, &s.ExpiresAt, &createdAt, &s.LastVisitedAt, &s.VisitCount); err != nil {
|
|
return nil, err
|
|
}
|
|
s.CreatedAt, _ = parseTime(createdAt)
|
|
out = append(out, &s)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func UpdateShare(db *sql.DB, id, description string, expiresAt sql.NullString) error {
|
|
var exp any
|
|
if expiresAt.Valid {
|
|
exp = expiresAt.String
|
|
}
|
|
_, err := db.Exec(`UPDATE shares SET description = ?, expires_at = ? WHERE id = ?`, description, exp, id)
|
|
return err
|
|
}
|
|
|
|
func DeleteShare(db *sql.DB, id string) error {
|
|
_, err := db.Exec(`DELETE FROM shares WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
func RecordShareVisit(db *sql.DB, id string) error {
|
|
_, err := db.Exec(`UPDATE shares SET last_visited_at = strftime('%Y-%m-%dT%H:%M:%fZ','now'), visit_count = visit_count + 1 WHERE id = ?`, id)
|
|
return err
|
|
}
|
|
|
|
// IsExpired reports whether a share's expiry has passed.
|
|
func (s *Share) IsExpired() bool {
|
|
if !s.ExpiresAt.Valid {
|
|
return false
|
|
}
|
|
t, err := parseTime(s.ExpiresAt.String)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return time.Now().After(t)
|
|
}
|
|
|
|
// --- Share items ---
|
|
|
|
type ShareItem struct {
|
|
ID string
|
|
ShareID string
|
|
SongID sql.NullString
|
|
AlbumID sql.NullString
|
|
Position int
|
|
}
|
|
|
|
func AddShareItem(db *sql.DB, shareID string, songID, albumID sql.NullString, position int) error {
|
|
var song, album any
|
|
if songID.Valid {
|
|
song = songID.String
|
|
}
|
|
if albumID.Valid {
|
|
album = albumID.String
|
|
}
|
|
_, err := db.Exec(`INSERT INTO share_items (id, share_id, song_id, album_id, position) VALUES (?, ?, ?, ?, ?)`,
|
|
idgen.New("shi"), shareID, song, album, position)
|
|
return err
|
|
}
|
|
|
|
func ListShareItems(db *sql.DB, shareID string) ([]*ShareItem, error) {
|
|
rows, err := db.Query(`SELECT id, share_id, song_id, album_id, position FROM share_items WHERE share_id = ? ORDER BY position`, shareID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []*ShareItem
|
|
for rows.Next() {
|
|
var si ShareItem
|
|
if err := rows.Scan(&si.ID, &si.ShareID, &si.SongID, &si.AlbumID, &si.Position); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, &si)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// ShareSongIDs resolves a share's items to a flat list of song IDs,
|
|
// expanding any shared albums into their songs.
|
|
func ShareSongIDs(db *sql.DB, shareID string) ([]string, error) {
|
|
items, err := ListShareItems(db, shareID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []string
|
|
for _, item := range items {
|
|
if item.SongID.Valid {
|
|
out = append(out, item.SongID.String)
|
|
continue
|
|
}
|
|
if item.AlbumID.Valid {
|
|
songs, err := ListSongsByAlbum(db, item.AlbumID.String)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range songs {
|
|
out = append(out, s.ID)
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|