feat: add subsonic token auth and starred endpoints
This commit is contained in:
@@ -61,6 +61,12 @@ type SearchResults struct {
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
type StarredResults struct {
|
||||
Artists []Artist `json:"artists"`
|
||||
Albums []Album `json:"albums"`
|
||||
Tracks []Track `json:"tracks"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
db *sql.DB
|
||||
}
|
||||
@@ -340,6 +346,34 @@ func (s *Service) Search(ctx context.Context, query string, limit int) (SearchRe
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Starred(ctx context.Context, userID string) (StarredResults, error) {
|
||||
artists, err := s.starredArtists(ctx, userID)
|
||||
if err != nil {
|
||||
return StarredResults{}, err
|
||||
}
|
||||
albums, err := s.starredAlbums(ctx, userID)
|
||||
if err != nil {
|
||||
return StarredResults{}, err
|
||||
}
|
||||
tracks, err := s.starredTracks(ctx, userID)
|
||||
if err != nil {
|
||||
return StarredResults{}, err
|
||||
}
|
||||
return StarredResults{
|
||||
Artists: artists,
|
||||
Albums: albums,
|
||||
Tracks: tracks,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Star(ctx context.Context, userID string, trackIDs, albumIDs, artistIDs []string) error {
|
||||
return s.updateFavorites(ctx, userID, trackIDs, albumIDs, artistIDs, true)
|
||||
}
|
||||
|
||||
func (s *Service) Unstar(ctx context.Context, userID string, trackIDs, albumIDs, artistIDs []string) error {
|
||||
return s.updateFavorites(ctx, userID, trackIDs, albumIDs, artistIDs, false)
|
||||
}
|
||||
|
||||
func (s *Service) CoverArtPathByEntityID(ctx context.Context, id string) (string, error) {
|
||||
var path string
|
||||
|
||||
@@ -545,3 +579,140 @@ func (s *Service) searchTracks(ctx context.Context, pattern string, limit int) (
|
||||
}
|
||||
return tracks, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) updateFavorites(ctx context.Context, userID string, trackIDs, albumIDs, artistIDs []string, star bool) error {
|
||||
tx, err := s.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin favorites transaction: %w", err)
|
||||
}
|
||||
|
||||
ops := []struct {
|
||||
ids []string
|
||||
entityType string
|
||||
}{
|
||||
{ids: trackIDs, entityType: "track"},
|
||||
{ids: albumIDs, entityType: "album"},
|
||||
{ids: artistIDs, entityType: "artist"},
|
||||
}
|
||||
|
||||
for _, op := range ops {
|
||||
for _, id := range op.ids {
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if star {
|
||||
if _, err := tx.ExecContext(ctx, `INSERT OR REPLACE INTO favorites (user_id, entity_id, entity_type, created_at) VALUES (?, ?, ?, datetime('now'))`, userID, id, op.entityType); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("insert favorite: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `DELETE FROM favorites WHERE user_id = ? AND entity_id = ? AND entity_type = ?`, userID, id, op.entityType); err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("delete favorite: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("commit favorites transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) starredArtists(ctx context.Context, userID string) ([]Artist, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT a.id, a.name, COUNT(al.id) AS album_count, COALESCE(a.cover_art_id, '')
|
||||
FROM favorites f
|
||||
JOIN artists a ON a.id = f.entity_id
|
||||
LEFT JOIN albums al ON al.artist_id = a.id
|
||||
WHERE f.user_id = ? AND f.entity_type = 'artist'
|
||||
GROUP BY a.id, a.name, a.cover_art_id
|
||||
ORDER BY f.created_at DESC, a.name ASC`,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query starred artists: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var artists []Artist
|
||||
for rows.Next() {
|
||||
var artist Artist
|
||||
if err := rows.Scan(&artist.ID, &artist.Name, &artist.AlbumCount, &artist.CoverArtID); err != nil {
|
||||
return nil, fmt.Errorf("scan starred artist: %w", err)
|
||||
}
|
||||
artists = append(artists, artist)
|
||||
}
|
||||
return artists, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) starredAlbums(ctx context.Context, userID string) ([]Album, error) {
|
||||
rows, err := s.db.QueryContext(
|
||||
ctx,
|
||||
`SELECT al.id, al.artist_id, a.name, al.title, COALESCE(al.year, 0), COUNT(t.id), COALESCE(al.cover_art_id, '')
|
||||
FROM favorites f
|
||||
JOIN albums al ON al.id = f.entity_id
|
||||
JOIN artists a ON a.id = al.artist_id
|
||||
LEFT JOIN tracks t ON t.album_id = al.id
|
||||
WHERE f.user_id = ? AND f.entity_type = 'album'
|
||||
GROUP BY al.id, al.artist_id, a.name, al.title, al.year, al.cover_art_id
|
||||
ORDER BY f.created_at DESC, al.title ASC`,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query starred albums: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var albums []Album
|
||||
for rows.Next() {
|
||||
var album Album
|
||||
if err := rows.Scan(&album.ID, &album.ArtistID, &album.ArtistName, &album.Title, &album.Year, &album.TrackCount, &album.CoverArtID); err != nil {
|
||||
return nil, fmt.Errorf("scan starred album: %w", err)
|
||||
}
|
||||
albums = append(albums, album)
|
||||
}
|
||||
return albums, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Service) starredTracks(ctx context.Context, userID string) ([]Track, error) {
|
||||
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 favorites f
|
||||
JOIN tracks t ON t.id = f.entity_id
|
||||
JOIN artists a ON a.id = t.artist_id
|
||||
JOIN albums al ON al.id = t.album_id
|
||||
WHERE f.user_id = ? AND f.entity_type = 'track'
|
||||
ORDER BY f.created_at DESC, a.name ASC, al.title ASC, t.track_number ASC`,
|
||||
userID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query starred tracks: %w", err)
|
||||
}
|
||||
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 starred track: %w", err)
|
||||
}
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
return tracks, rows.Err()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user