feat: add recently played and scrobble flow
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
@@ -42,6 +44,7 @@ type Track struct {
|
||||
|
||||
type HomePayload struct {
|
||||
RecentAlbums []Album `json:"recentAlbums"`
|
||||
RecentTracks []Track `json:"recentTracks"`
|
||||
Artists []Artist `json:"artists"`
|
||||
}
|
||||
|
||||
@@ -86,8 +89,14 @@ func (s *Service) Home(ctx context.Context) (HomePayload, error) {
|
||||
return HomePayload{}, err
|
||||
}
|
||||
|
||||
recentTracks, err := s.RecentTracks(ctx, "", 8)
|
||||
if err != nil {
|
||||
return HomePayload{}, err
|
||||
}
|
||||
|
||||
return HomePayload{
|
||||
RecentAlbums: albums,
|
||||
RecentTracks: recentTracks,
|
||||
Artists: artists,
|
||||
}, nil
|
||||
}
|
||||
@@ -406,6 +415,82 @@ func (s *Service) CoverArtPathByEntityID(ctx context.Context, id string) (string
|
||||
return "", fmt.Errorf("query track cover art: %w", err)
|
||||
}
|
||||
|
||||
func (s *Service) RecentTracks(ctx context.Context, userID string, limit int) ([]Track, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
if userID != "" {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
JOIN (
|
||||
SELECT track_id, MAX(played_at) AS last_played_at
|
||||
FROM play_history
|
||||
WHERE user_id = ?
|
||||
GROUP BY track_id
|
||||
) history ON history.track_id = t.id
|
||||
ORDER BY history.last_played_at DESC
|
||||
LIMIT ?`,
|
||||
userID,
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query recent tracks by user: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
||||
COALESCE(t.duration_seconds, 0), t.file_path, COALESCE(t.content_type, ''), COALESCE(al.cover_art_id, '')
|
||||
FROM tracks t
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
ORDER BY al.updated_at DESC, t.updated_at DESC, t.track_number ASC
|
||||
LIMIT ?`,
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query fallback recent tracks: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
func (s *Service) RecordPlayEvent(ctx context.Context, userID, trackID, eventType, clientName string, playedAt time.Time, submission bool) error {
|
||||
if strings.TrimSpace(userID) == "" || strings.TrimSpace(trackID) == "" {
|
||||
return nil
|
||||
}
|
||||
if eventType == "" {
|
||||
eventType = "play"
|
||||
}
|
||||
if playedAt.IsZero() {
|
||||
playedAt = time.Now().UTC()
|
||||
}
|
||||
_, err := s.db.ExecContext(
|
||||
ctx,
|
||||
`INSERT INTO play_history (id, user_id, track_id, event_type, played_at, client_name, submission)
|
||||
VALUES (lower(hex(randomblob(16))), ?, ?, ?, ?, ?, ?)`,
|
||||
userID,
|
||||
trackID,
|
||||
eventType,
|
||||
playedAt.UTC().Format(time.RFC3339),
|
||||
strings.TrimSpace(clientName),
|
||||
boolToInt(submission),
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("insert play history event: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) albumsByArtistID(ctx context.Context, artistID string) ([]Album, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
@@ -452,28 +537,7 @@ func (s *Service) tracksByAlbumID(ctx context.Context, albumID string) ([]Track,
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tracks []Track
|
||||
for rows.Next() {
|
||||
var track Track
|
||||
if err := rows.Scan(
|
||||
&track.ID,
|
||||
&track.AlbumID,
|
||||
&track.ArtistID,
|
||||
&track.Title,
|
||||
&track.ArtistName,
|
||||
&track.AlbumTitle,
|
||||
&track.TrackNumber,
|
||||
&track.DurationSecs,
|
||||
&track.FilePath,
|
||||
&track.ContentType,
|
||||
&track.CoverArtID,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan tracks by album: %w", err)
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
|
||||
return tracks, rows.Err()
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
func (s *Service) searchArtists(ctx context.Context, pattern string, limit int) ([]Artist, error) {
|
||||
@@ -557,27 +621,7 @@ func (s *Service) searchTracks(ctx context.Context, pattern string, limit int) (
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tracks []Track
|
||||
for rows.Next() {
|
||||
var track Track
|
||||
if err := rows.Scan(
|
||||
&track.ID,
|
||||
&track.AlbumID,
|
||||
&track.ArtistID,
|
||||
&track.Title,
|
||||
&track.ArtistName,
|
||||
&track.AlbumTitle,
|
||||
&track.TrackNumber,
|
||||
&track.DurationSecs,
|
||||
&track.FilePath,
|
||||
&track.ContentType,
|
||||
&track.CoverArtID,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan searched track: %w", err)
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
return tracks, rows.Err()
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
func (s *Service) updateFavorites(ctx context.Context, userID string, trackIDs, albumIDs, artistIDs []string, star bool) error {
|
||||
@@ -694,6 +738,10 @@ func (s *Service) starredTracks(ctx context.Context, userID string) ([]Track, er
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanTracks(rows)
|
||||
}
|
||||
|
||||
func scanTracks(rows *sql.Rows) ([]Track, error) {
|
||||
var tracks []Track
|
||||
for rows.Next() {
|
||||
var track Track
|
||||
@@ -710,9 +758,16 @@ func (s *Service) starredTracks(ctx context.Context, userID string) ([]Track, er
|
||||
&track.ContentType,
|
||||
&track.CoverArtID,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan starred track: %w", err)
|
||||
return nil, fmt.Errorf("scan track row: %w", err)
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
return tracks, rows.Err()
|
||||
}
|
||||
|
||||
func boolToInt(value bool) int {
|
||||
if value {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user