Subsonic: sharing (getShares, createShare, updateShare, deleteShare) plus real public share web pages and stream route
This commit is contained in:
+158
-13
@@ -3,39 +3,184 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
|
||||
)
|
||||
|
||||
// Shares are a schema foundation for Phase 2's Subsonic share endpoints;
|
||||
// Phase 1 doesn't yet expose a sharing UI.
|
||||
|
||||
type Share struct {
|
||||
ID string
|
||||
OwnerID string
|
||||
Token string
|
||||
Description string
|
||||
ExpiresAt sql.NullString
|
||||
ID string
|
||||
OwnerID string
|
||||
Token string
|
||||
Description string
|
||||
ExpiresAt sql.NullString
|
||||
CreatedAt time.Time
|
||||
LastVisitedAt sql.NullString
|
||||
VisitCount int64
|
||||
}
|
||||
|
||||
func CreateShare(db *sql.DB, ownerID, token, description string) (*Share, error) {
|
||||
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")
|
||||
_, err := db.Exec(`INSERT INTO shares (id, owner_id, token, description) VALUES (?, ?, ?, ?)`, id, ownerID, token, description)
|
||||
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 GetShareByToken(db, token)
|
||||
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
|
||||
err := db.QueryRow(`SELECT id, owner_id, token, description, expires_at FROM shares WHERE token = ?`, token).
|
||||
Scan(&s.ID, &s.OwnerID, &s.Token, &s.Description, &s.ExpiresAt)
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user