diff --git a/core/agents/local_agent.go b/core/agents/local_agent.go index 67f988ac..e2ae4cb7 100644 --- a/core/agents/local_agent.go +++ b/core/agents/local_agent.go @@ -37,9 +37,13 @@ func (p *localAgent) GetImages(_ context.Context, id, name, mbid string) ([]Arti }, nil } +func (p *localAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]Song, error) { + return nil, nil // TODO return 5-stars and liked songs sorted by playCount +} + func (p *localAgent) artistImage(id string, size int) ArtistImage { return ArtistImage{ - filepath.Join(consts.URLPathPublicImages, artwork.Public(model.NewArtworkID(model.KindArtistArtwork, id), size)), + filepath.Join(consts.URLPathPublicImages, artwork.PublicLink(model.NewArtworkID(model.KindArtistArtwork, id), size)), size, } } diff --git a/core/artwork/artwork.go b/core/artwork/artwork.go index 16e3bf01..c1c6ac2b 100644 --- a/core/artwork/artwork.go +++ b/core/artwork/artwork.go @@ -36,9 +36,6 @@ type artworkReader interface { } func (a *artwork) Get(ctx context.Context, id string, size int) (reader io.ReadCloser, lastUpdate time.Time, err error) { - ctx, cancel := context.WithTimeout(ctx, 10*time.Second) - defer cancel() - artID, err := a.getArtworkId(ctx, id) if err != nil { return nil, time.Time{}, err @@ -112,7 +109,7 @@ func (a *artwork) getArtworkReader(ctx context.Context, artID model.ArtworkID, s return artReader, err } -func Public(artID model.ArtworkID, size int) string { +func PublicLink(artID model.ArtworkID, size int) string { token, _ := auth.CreatePublicToken(map[string]any{ "id": artID.String(), "size": size, diff --git a/core/artwork/cache_warmer.go b/core/artwork/cache_warmer.go index 677f01ce..df6a85e4 100644 --- a/core/artwork/cache_warmer.go +++ b/core/artwork/cache_warmer.go @@ -103,6 +103,9 @@ func (a *cacheWarmer) processBatch(ctx context.Context, batch []string) { } func (a *cacheWarmer) doCacheImage(ctx context.Context, id string) error { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + r, _, err := a.artwork.Get(ctx, id, 0) if err != nil { return fmt.Errorf("error cacheing id='%s': %w", id, err) diff --git a/core/artwork/reader_artist.go b/core/artwork/reader_artist.go index 38137e0f..da54b77e 100644 --- a/core/artwork/reader_artist.go +++ b/core/artwork/reader_artist.go @@ -4,32 +4,73 @@ import ( "context" "fmt" "io" + "net/http" + "strings" "time" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/consts" + "github.com/Masterminds/squirrel" "github.com/navidrome/navidrome/model" ) type artistReader struct { - artID model.ArtworkID + cacheKey + a *artwork + artist model.Artist + files []string } -func newArtistReader(_ context.Context, _ *artwork, artID model.ArtworkID) (*artistReader, error) { - a := &artistReader{ - artID: artID, +func newArtistReader(ctx context.Context, artwork *artwork, artID model.ArtworkID) (*artistReader, error) { + ar, err := artwork.ds.Artist(ctx).Get(artID.ID) + if err != nil { + return nil, err } + als, err := artwork.ds.Album(ctx).GetAll(model.QueryOptions{Filters: squirrel.Eq{"album_artist_id": artID.ID}}) + if err != nil { + return nil, err + } + a := &artistReader{ + a: artwork, + artist: *ar, + } + a.cacheKey.lastUpdate = ar.ExternalInfoUpdatedAt + for _, al := range als { + a.files = append(a.files, al.ImageFiles) + if a.cacheKey.lastUpdate.Before(al.UpdatedAt) { + a.cacheKey.lastUpdate = al.UpdatedAt + } + } + a.cacheKey.artID = artID return a, nil } func (a *artistReader) LastUpdated() time.Time { - return consts.ServerStart // Invalidate cached placeholder every server start -} - -func (a *artistReader) Key() string { - return fmt.Sprintf("placeholder.%d.0.%d", a.LastUpdated().UnixMilli(), conf.Server.CoverJpegQuality) + return a.lastUpdate } func (a *artistReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { - return selectImageReader(ctx, a.artID, fromArtistPlaceholder()) + return selectImageReader(ctx, a.artID, + //fromExternalFile() + fromExternalSource(ctx, a.artist), + fromArtistPlaceholder(), + ) +} + +func fromExternalSource(ctx context.Context, ar model.Artist) sourceFunc { + return func() (io.ReadCloser, string, error) { + imageUrl := ar.ArtistImageUrl() + if !strings.HasPrefix(imageUrl, "http") { + return nil, "", nil + } + hc := http.Client{Timeout: 5 * time.Second} + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, imageUrl, nil) + resp, err := hc.Do(req) + if err != nil { + return nil, "", err + } + if resp.StatusCode != http.StatusOK { + resp.Body.Close() + return nil, "", fmt.Errorf("error retrieveing cover from %s: %s", imageUrl, resp.Status) + } + return resp.Body, imageUrl, nil + } } diff --git a/core/external_metadata.go b/core/external_metadata.go index 37825166..d51dc64c 100644 --- a/core/external_metadata.go +++ b/core/external_metadata.go @@ -2,6 +2,7 @@ package core import ( "context" + "errors" "sort" "strings" "sync" @@ -224,6 +225,9 @@ func (e *externalMetadata) TopSongs(ctx context.Context, artistName string, coun func (e *externalMetadata) getMatchingTopSongs(ctx context.Context, agent agents.ArtistTopSongsRetriever, artist *auxArtist, count int) (model.MediaFiles, error) { songs, err := agent.GetTopSongs(ctx, artist.ID, artist.Name, artist.MbzArtistID, count) + if errors.Is(err, agents.ErrNotFound) { + return nil, nil + } if err != nil { return nil, err } diff --git a/model/artist.go b/model/artist.go index f58450f4..5159e6dc 100644 --- a/model/artist.go +++ b/model/artist.go @@ -34,6 +34,10 @@ func (a Artist) ArtistImageUrl() string { return a.SmallImageUrl } +func (a Artist) CoverArtID() ArtworkID { + return artworkIDFromArtist(a) +} + type Artists []Artist type ArtistIndex struct { diff --git a/model/artwork_id.go b/model/artwork_id.go index 113a907e..915b5186 100644 --- a/model/artwork_id.go +++ b/model/artwork_id.go @@ -82,3 +82,10 @@ func artworkIDFromPlaylist(pls Playlist) ArtworkID { ID: pls.ID, } } + +func artworkIDFromArtist(ar Artist) ArtworkID { + return ArtworkID{ + Kind: KindArtistArtwork, + ID: ar.ID, + } +} diff --git a/server/public/public_endpoints.go b/server/public/public_endpoints.go index 77f9f9a9..31d04e5e 100644 --- a/server/public/public_endpoints.go +++ b/server/public/public_endpoints.go @@ -42,7 +42,10 @@ func (p *Router) routes() http.Handler { } func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) { - _, claims, _ := jwtauth.FromContext(r.Context()) + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + + _, claims, _ := jwtauth.FromContext(ctx) id, ok := claims["id"].(string) if !ok { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -53,7 +56,8 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - imgReader, lastUpdate, err := p.artwork.Get(r.Context(), id, int(size)) + + imgReader, lastUpdate, err := p.artwork.Get(ctx, id, int(size)) w.Header().Set("cache-control", "public, max-age=315360000") w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123)) @@ -71,7 +75,10 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) { } defer imgReader.Close() - _, err = io.Copy(w, imgReader) + cnt, err := io.Copy(w, imgReader) + if err != nil { + log.Warn(ctx, "Error sending image", "count", cnt, err) + } } func jwtVerifier(next http.Handler) http.Handler { diff --git a/server/subsonic/helpers.go b/server/subsonic/helpers.go index c0f55327..4ea5447d 100644 --- a/server/subsonic/helpers.go +++ b/server/subsonic/helpers.go @@ -86,6 +86,7 @@ func toArtist(_ context.Context, a model.Artist) responses.Artist { Name: a.Name, AlbumCount: a.AlbumCount, UserRating: a.Rating, + CoverArt: a.CoverArtID().String(), ArtistImageUrl: a.ArtistImageUrl(), } if a.Starred { @@ -94,11 +95,12 @@ func toArtist(_ context.Context, a model.Artist) responses.Artist { return artist } -func toArtistID3(ctx context.Context, a model.Artist) responses.ArtistID3 { +func toArtistID3(_ context.Context, a model.Artist) responses.ArtistID3 { artist := responses.ArtistID3{ Id: a.ID, Name: a.Name, AlbumCount: a.AlbumCount, + CoverArt: a.CoverArtID().String(), ArtistImageUrl: a.ArtistImageUrl(), UserRating: a.Rating, } diff --git a/server/subsonic/media_retrieval.go b/server/subsonic/media_retrieval.go index 51a2ef59..1e397dc4 100644 --- a/server/subsonic/media_retrieval.go +++ b/server/subsonic/media_retrieval.go @@ -53,10 +53,13 @@ func (api *Router) getPlaceHolderAvatar(w http.ResponseWriter, r *http.Request) } func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { + ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) + defer cancel() + id := utils.ParamString(r, "id") size := utils.ParamInt(r, "size", 0) - imgReader, lastUpdate, err := api.artwork.Get(r.Context(), id, size) + imgReader, lastUpdate, err := api.artwork.Get(ctx, id, size) w.Header().Set("cache-control", "public, max-age=315360000") w.Header().Set("last-modified", lastUpdate.Format(time.RFC1123)) @@ -72,7 +75,10 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons } defer imgReader.Close() - _, err = io.Copy(w, imgReader) + cnt, err := io.Copy(w, imgReader) + if err != nil { + log.Warn(ctx, "Error sending image", "count", cnt, err) + } return nil, err } diff --git a/server/subsonic/responses/responses.go b/server/subsonic/responses/responses.go index 085925a6..79875123 100644 --- a/server/subsonic/responses/responses.go +++ b/server/subsonic/responses/responses.go @@ -77,9 +77,9 @@ type Artist struct { AlbumCount int `xml:"albumCount,attr,omitempty" json:"albumCount,omitempty"` Starred *time.Time `xml:"starred,attr,omitempty" json:"starred,omitempty"` UserRating int `xml:"userRating,attr,omitempty" json:"userRating,omitempty"` + CoverArt string `xml:"coverArt,attr,omitempty" json:"coverArt,omitempty"` ArtistImageUrl string `xml:"artistImageUrl,attr,omitempty" json:"artistImageUrl,omitempty"` /* TODO: - */ }