From 28387d07e2aae3881d5c705993c6bfd699713a8f Mon Sep 17 00:00:00 2001 From: Elliot <140675814+ShadowsDieThrice@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:31:00 +0800 Subject: [PATCH 1/6] feat(core/artwork/read_playlist.go): Custom Playlist Art - #406 Adds ability customise playlist art by adding image file with matching name to playlists folder Signed-off-by: ShadowsDieThrice <140675814+ShadowsDieThrice@users.noreply.github.com> --- core/artwork/reader_playlist.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index a2c7c182b..23fb7f0c1 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -8,8 +8,11 @@ import ( "image/draw" "image/png" "io" + "os" + "path/filepath" "time" + "github.com/navidrome/navidrome/conf" "github.com/disintegration/imaging" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" @@ -44,12 +47,40 @@ func (a *playlistArtworkReader) LastUpdated() time.Time { func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { ff := []sourceFunc{ + a.fromPlaylistNamedCover(ctx), a.fromGeneratedTiledCover(ctx), fromAlbumPlaceholder(), } return selectImageReader(ctx, a.artID, ff...) } +func (a *playlistArtworkReader) fromPlaylistNamedCover(ctx context.Context) sourceFunc { + return func() (io.ReadCloser, string, error) { + playlistName := a.pl.Name + imagePath, err := findMatchingImage(playlistName) + if err != nil { + return nil, "", err + } + file, err := os.Open(imagePath) + if err != nil { + return nil, "", err + } + return file, filepath.Ext(imagePath), nil + } +} + +func findMatchingImage(playlistName string) (string, error) { + extensions := []string{".png", ".jpg", ".jpeg"} + for _, ext := range extensions { + mediaFolder := conf.Server.MusicFolder + path := filepath.Join(mediaFolder, "/zPlaylists", playlistName+ext) + if _, err := os.Stat(path); err == nil { + return path, nil + } + } + return "", errors.New("no matching image found") +} + func (a *playlistArtworkReader) fromGeneratedTiledCover(ctx context.Context) sourceFunc { return func() (io.ReadCloser, string, error) { tiles, err := a.loadTiles(ctx) From 807354250977639fa93b17b3456a97fac28f3f63 Mon Sep 17 00:00:00 2001 From: Elliot <140675814+ShadowsDieThrice@users.noreply.github.com> Date: Fri, 21 Feb 2025 13:14:03 +0800 Subject: [PATCH 2/6] feat(core/artwork/read_playlist.go): Custom Playlist Art - #406 Adds ability to customise playlist art by adding image file with matching name to playlists folder Signed-off-by: ShadowsDieThrice <140675814+ShadowsDieThrice@users.noreply.github.com> --- core/artwork/reader_playlist.go | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 8bcea16d5..bb5392aff 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -12,8 +12,8 @@ import ( "path/filepath" "time" - "github.com/navidrome/navidrome/conf" "github.com/disintegration/imaging" + "github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" @@ -73,7 +73,7 @@ func findMatchingImage(playlistName string) (string, error) { extensions := []string{".png", ".jpg", ".jpeg"} for _, ext := range extensions { mediaFolder := conf.Server.MusicFolder - path := filepath.Join(mediaFolder, "/zPlaylists", playlistName+ext) + path := filepath.Join(mediaFolder, "/custom_playlists", playlistName+ext) if _, err := os.Stat(path); err == nil { return path, nil } @@ -92,7 +92,7 @@ func (a *playlistArtworkReader) fromGeneratedTiledCover(ctx context.Context) sou } } -func toAlbumArtworkIDs(albumIDs []string) []model.ArtworkID { +func toArtworkIDs(albumIDs []string) []model.ArtworkID { return slice.Map(albumIDs, func(id string) model.ArtworkID { al := model.Album{ID: id} return al.CoverArtID() @@ -106,21 +106,24 @@ func (a *playlistArtworkReader) loadTiles(ctx context.Context) ([]image.Image, e log.Error(ctx, "Error getting album IDs for playlist", "id", a.pl.ID, "name", a.pl.Name, err) return nil, err } - ids := toAlbumArtworkIDs(albumIds) + ids := toArtworkIDs(albumIds) var tiles []image.Image - for _, id := range ids { - r, _, err := fromAlbum(ctx, a.a, id)() - if err == nil { - tile, err := a.createTile(ctx, r) - if err == nil { - tiles = append(tiles, tile) - } - _ = r.Close() - } - if len(tiles) == 4 { + for len(tiles) < 4 { + if len(ids) == 0 { break } + id := ids[len(ids)-1] + ids = ids[0 : len(ids)-1] + r, _, err := fromAlbum(ctx, a.a, id)() + if err != nil { + continue + } + tile, err := a.createTile(ctx, r) + if err == nil { + tiles = append(tiles, tile) + } + _ = r.Close() } switch len(tiles) { case 0: From ae1a02a670c545865f3b738469a09ab838b460de Mon Sep 17 00:00:00 2001 From: Elliot <140675814+ShadowsDieThrice@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:03:47 +0800 Subject: [PATCH 3/6] feat(core/artwork/read_playlist.go): Custom Playlist Art - #406 Adds ability to customise playlist art by adding image file with matching name to playlists folder Signed-off-by: ShadowsDieThrice <140675814+ShadowsDieThrice@users.noreply.github.com> --- core/artwork/reader_playlist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index bb5392aff..899e6d6cd 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -73,7 +73,7 @@ func findMatchingImage(playlistName string) (string, error) { extensions := []string{".png", ".jpg", ".jpeg"} for _, ext := range extensions { mediaFolder := conf.Server.MusicFolder - path := filepath.Join(mediaFolder, "/custom_playlists", playlistName+ext) + path := filepath.Join(mediaFolder, "/zPlaylists", playlistName+ext) if _, err := os.Stat(path); err == nil { return path, nil } From 9cbea79b117a924f9907b53743ca4d1a8b512941 Mon Sep 17 00:00:00 2001 From: Elliot <140675814+ShadowsDieThrice@users.noreply.github.com> Date: Fri, 21 Feb 2025 14:09:30 +0800 Subject: [PATCH 4/6] feat(core/artwork/read_playlist.go): Custom Playlist Art - #406 Adds ability to customise playlist art by adding image file with matching name to playlists folder Signed-off-by: ShadowsDieThrice <140675814+ShadowsDieThrice@users.noreply.github.com> --- core/artwork/reader_playlist.go | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 899e6d6cd..9aafd5a9d 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -73,7 +73,7 @@ func findMatchingImage(playlistName string) (string, error) { extensions := []string{".png", ".jpg", ".jpeg"} for _, ext := range extensions { mediaFolder := conf.Server.MusicFolder - path := filepath.Join(mediaFolder, "/zPlaylists", playlistName+ext) + path := filepath.Join(mediaFolder, "/customArtwork", playlistName+ext) if _, err := os.Stat(path); err == nil { return path, nil } @@ -92,7 +92,7 @@ func (a *playlistArtworkReader) fromGeneratedTiledCover(ctx context.Context) sou } } -func toArtworkIDs(albumIDs []string) []model.ArtworkID { +func toAlbumArtworkIDs(albumIDs []string) []model.ArtworkID { return slice.Map(albumIDs, func(id string) model.ArtworkID { al := model.Album{ID: id} return al.CoverArtID() @@ -106,24 +106,21 @@ func (a *playlistArtworkReader) loadTiles(ctx context.Context) ([]image.Image, e log.Error(ctx, "Error getting album IDs for playlist", "id", a.pl.ID, "name", a.pl.Name, err) return nil, err } - ids := toArtworkIDs(albumIds) + ids := toAlbumArtworkIDs(albumIds) var tiles []image.Image - for len(tiles) < 4 { - if len(ids) == 0 { + for _, id := range ids { + r, _, err := fromAlbum(ctx, a.a, id)() + if err == nil { + tile, err := a.createTile(ctx, r) + if err == nil { + tiles = append(tiles, tile) + } + _ = r.Close() + } + if len(tiles) == 4 { break } - id := ids[len(ids)-1] - ids = ids[0 : len(ids)-1] - r, _, err := fromAlbum(ctx, a.a, id)() - if err != nil { - continue - } - tile, err := a.createTile(ctx, r) - if err == nil { - tiles = append(tiles, tile) - } - _ = r.Close() } switch len(tiles) { case 0: From 71543cba27b54d71712954ffdd3e7be0a133b658 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Feb 2025 21:28:54 -0500 Subject: [PATCH 5/6] externalize ArtworkFolder configuration, load any supported image format Signed-off-by: Deluan --- conf/configuration.go | 6 ++++++ conf/mime/mime_types.go | 8 +++++++- core/artwork/reader_playlist.go | 16 ++++++++-------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/conf/configuration.go b/conf/configuration.go index 93388ee8c..821fb6d3e 100644 --- a/conf/configuration.go +++ b/conf/configuration.go @@ -66,6 +66,7 @@ type configOptions struct { FFmpegPath string MPVPath string MPVCmdTemplate string + ArtworkFolder string CoverArtPriority string CoverJpegQuality int ArtistArtPriority string @@ -236,6 +237,10 @@ func Load(noConfigDump bool) { os.Exit(1) } + if Server.ArtworkFolder == "" { + Server.ArtworkFolder = filepath.Join(Server.DataFolder, "artwork") + } + Server.ConfigFile = viper.GetViper().ConfigFileUsed() if Server.DbPath == "" { Server.DbPath = filepath.Join(Server.DataFolder, consts.DefaultDbPath) @@ -452,6 +457,7 @@ func init() { viper.SetDefault("ffmpegpath", "") viper.SetDefault("mpvcmdtemplate", "mpv --audio-device=%d --no-audio-display --pause %f --input-ipc-server=%s") + viper.SetDefault("ArtworkFolder", "") viper.SetDefault("coverartpriority", "cover.*, folder.*, front.*, embedded, external") viper.SetDefault("coverjpegquality", 75) viper.SetDefault("artistartpriority", "artist.*, album/artist.*, external") diff --git a/conf/mime/mime_types.go b/conf/mime/mime_types.go index 33542cb8e..2c65cd41f 100644 --- a/conf/mime/mime_types.go +++ b/conf/mime/mime_types.go @@ -15,7 +15,10 @@ type mimeConf struct { Lossless []string `yaml:"lossless"` } -var LosslessFormats []string +var ( + LosslessFormats []string + ValidImageExtensions []string +) func initMimeTypes() { // In some circumstances, Windows sets JS mime-type to `text/plain`! @@ -36,6 +39,9 @@ func initMimeTypes() { } for ext, typ := range mimeConf.Types { _ = mime.AddExtensionType(ext, typ) + if strings.HasPrefix(typ, "image/") { + ValidImageExtensions = append(ValidImageExtensions, ext) + } } for _, ext := range mimeConf.Lossless { diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 9aafd5a9d..ef480cf93 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -14,6 +14,7 @@ import ( "github.com/disintegration/imaging" "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/mime" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" @@ -57,7 +58,7 @@ func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, stri func (a *playlistArtworkReader) fromPlaylistNamedCover(ctx context.Context) sourceFunc { return func() (io.ReadCloser, string, error) { playlistName := a.pl.Name - imagePath, err := findMatchingImage(playlistName) + imagePath, err := findMatchingImage(ctx, "playlist", playlistName) if err != nil { return nil, "", err } @@ -69,13 +70,12 @@ func (a *playlistArtworkReader) fromPlaylistNamedCover(ctx context.Context) sour } } -func findMatchingImage(playlistName string) (string, error) { - extensions := []string{".png", ".jpg", ".jpeg"} - for _, ext := range extensions { - mediaFolder := conf.Server.MusicFolder - path := filepath.Join(mediaFolder, "/customArtwork", playlistName+ext) - if _, err := os.Stat(path); err == nil { - return path, nil +func findMatchingImage(_ context.Context, resource string, name string) (string, error) { + path := filepath.Join(conf.Server.ArtworkFolder, resource, name) + for _, ext := range mime.ValidImageExtensions { + filename := path + ext + if _, err := os.Stat(filename); err == nil { + return filename, nil } } return "", errors.New("no matching image found") From e32ec071269f280440113d238629b9e15b563101 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 22 Feb 2025 22:12:07 -0500 Subject: [PATCH 6/6] generalize `fromNamedArtwork` so it can be used by other resources Signed-off-by: Deluan --- core/artwork/reader_playlist.go | 32 +------------------------------- core/artwork/sources.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 31 deletions(-) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index ef480cf93..a73959d3a 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -8,13 +8,9 @@ import ( "image/draw" "image/png" "io" - "os" - "path/filepath" "time" "github.com/disintegration/imaging" - "github.com/navidrome/navidrome/conf" - "github.com/navidrome/navidrome/conf/mime" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/utils/slice" @@ -48,39 +44,13 @@ func (a *playlistArtworkReader) LastUpdated() time.Time { func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { ff := []sourceFunc{ - a.fromPlaylistNamedCover(ctx), + fromNamedArtwork(ctx, "playlist", a.pl.ID, a.pl.Name), a.fromGeneratedTiledCover(ctx), fromAlbumPlaceholder(), } return selectImageReader(ctx, a.artID, ff...) } -func (a *playlistArtworkReader) fromPlaylistNamedCover(ctx context.Context) sourceFunc { - return func() (io.ReadCloser, string, error) { - playlistName := a.pl.Name - imagePath, err := findMatchingImage(ctx, "playlist", playlistName) - if err != nil { - return nil, "", err - } - file, err := os.Open(imagePath) - if err != nil { - return nil, "", err - } - return file, filepath.Ext(imagePath), nil - } -} - -func findMatchingImage(_ context.Context, resource string, name string) (string, error) { - path := filepath.Join(conf.Server.ArtworkFolder, resource, name) - for _, ext := range mime.ValidImageExtensions { - filename := path + ext - if _, err := os.Stat(filename); err == nil { - return filename, nil - } - } - return "", errors.New("no matching image found") -} - func (a *playlistArtworkReader) fromGeneratedTiledCover(ctx context.Context) sourceFunc { return func() (io.ReadCloser, string, error) { tiles, err := a.loadTiles(ctx) diff --git a/core/artwork/sources.go b/core/artwork/sources.go index f89708255..a77f051cc 100644 --- a/core/artwork/sources.go +++ b/core/artwork/sources.go @@ -3,6 +3,7 @@ package artwork import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -16,6 +17,8 @@ import ( "time" "github.com/dhowden/tag" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/conf/mime" "github.com/navidrome/navidrome/consts" "github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core/ffmpeg" @@ -192,3 +195,32 @@ func fromURL(ctx context.Context, imageUrl *url.URL) (io.ReadCloser, string, err } return resp.Body, imageUrl.String(), nil } + +func fromNamedArtwork(ctx context.Context, resource string, names ...string) sourceFunc { + return func() (io.ReadCloser, string, error) { + for _, name := range names { + playlistName := name + imagePath, err := findMatchingImage(ctx, resource, playlistName) + if err != nil { + continue + } + file, err := os.Open(imagePath) + if err != nil { + return nil, "", err + } + return file, imagePath, nil + } + return nil, "", errors.New("no matching image found") + } +} + +func findMatchingImage(_ context.Context, resource string, name string) (string, error) { + path := filepath.Join(conf.Server.ArtworkFolder, resource, name) + for _, ext := range mime.ValidImageExtensions { + filename := path + ext + if _, err := os.Stat(filename); err == nil { + return filename, nil + } + } + return "", errors.New("no matching image found") +}