feat: add scan status and cover art endpoints

Track scanner status for the web API and Subsonic-compatible scan endpoints, add authenticated cover art serving, and wire album artwork into the web UI. Keep Subsonic auth limited to legacy password mode for now so behavior stays honest with the current bcrypt-based user storage.
This commit is contained in:
2026-04-02 22:37:10 +03:00
parent 46c2c3fb28
commit f8880aa9a4
9 changed files with 394 additions and 23 deletions

View File

@@ -3,9 +3,12 @@ package library
import (
"context"
"database/sql"
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type Artist struct {
ID string `json:"id"`
Name string `json:"name"`
@@ -19,6 +22,7 @@ type Album struct {
Title string `json:"title"`
Year int `json:"year"`
TrackCount int `json:"trackCount"`
CoverArtID string `json:"coverArtId"`
}
type Track struct {
@@ -32,6 +36,7 @@ type Track struct {
DurationSecs int `json:"durationSeconds"`
FilePath string `json:"filePath"`
ContentType string `json:"contentType"`
CoverArtID string `json:"coverArtId"`
}
type HomePayload struct {
@@ -96,6 +101,7 @@ func (s *Service) RecentAlbums(ctx context.Context, limit int) ([]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) AS track_count
, COALESCE(al.cover_art_id, '')
FROM albums al
JOIN artists a ON a.id = al.artist_id
LEFT JOIN tracks t ON t.album_id = al.id
@@ -112,7 +118,7 @@ func (s *Service) RecentAlbums(ctx context.Context, limit int) ([]Album, error)
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); err != nil {
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 album: %w", err)
}
albums = append(albums, album)
@@ -125,7 +131,7 @@ func (s *Service) Tracks(ctx context.Context, limit int) ([]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(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
@@ -152,6 +158,7 @@ func (s *Service) Tracks(ctx context.Context, limit int) ([]Track, error) {
&track.DurationSecs,
&track.FilePath,
&track.ContentType,
&track.CoverArtID,
); err != nil {
return nil, fmt.Errorf("scan track: %w", err)
}
@@ -167,7 +174,7 @@ func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
err := s.db.QueryRowContext(
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(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
@@ -184,6 +191,7 @@ func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
&track.DurationSecs,
&track.FilePath,
&track.ContentType,
&track.CoverArtID,
)
if err != nil {
return Track{}, fmt.Errorf("query track by id: %w", err)
@@ -191,3 +199,35 @@ func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
return track, nil
}
func (s *Service) CoverArtPathByEntityID(ctx context.Context, id string) (string, error) {
var path string
err := s.db.QueryRowContext(
ctx,
`SELECT cover_art_id FROM albums WHERE id = ? AND cover_art_id <> ''`,
id,
).Scan(&path)
if err == nil {
return path, nil
}
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", fmt.Errorf("query album cover art: %w", err)
}
err = s.db.QueryRowContext(
ctx,
`SELECT al.cover_art_id
FROM tracks t
JOIN albums al ON al.id = t.album_id
WHERE t.id = ? AND al.cover_art_id <> ''`,
id,
).Scan(&path)
if err == nil {
return path, nil
}
if errors.Is(err, sql.ErrNoRows) {
return "", ErrNotFound
}
return "", fmt.Errorf("query track cover art: %w", err)
}