From dc61dfd6d63af187bdcfa2e8e76a4b6caf6ecb40 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 1 Mar 2026 21:48:46 -0500 Subject: [PATCH] feat(artwork): add external image URL source to playlist artwork reader Add fromPlaylistExternalImage source function that resolves playlist cover art from ExternalImageURL, supporting both HTTP(S) URLs (via the existing fromURL helper) and local file paths (via os.Open). Insert it in the Reader() fallback chain between sidecar and tiled cover. --- core/artwork/artwork_internal_test.go | 38 +++++++++++++++++++++++++++ core/artwork/reader_playlist.go | 22 ++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/core/artwork/artwork_internal_test.go b/core/artwork/artwork_internal_test.go index c7e7f25a2..e74a213d0 100644 --- a/core/artwork/artwork_internal_test.go +++ b/core/artwork/artwork_internal_test.go @@ -318,6 +318,44 @@ var _ = Describe("Artwork", func() { Expect(path).To(BeEmpty()) }) }) + + Describe("fromPlaylistExternalImage", func() { + It("opens local path from ExternalImageURL", func() { + tmpDir := GinkgoT().TempDir() + imgPath := filepath.Join(tmpDir, "cover.jpg") + Expect(os.WriteFile(imgPath, []byte("external image data"), 0600)).To(Succeed()) + + reader := &playlistArtworkReader{ + pl: model.Playlist{ExternalImageURL: imgPath}, + } + r, path, err := reader.fromPlaylistExternalImage(ctx)() + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + Expect(path).To(Equal(imgPath)) + data, _ := io.ReadAll(r) + Expect(string(data)).To(Equal("external image data")) + r.Close() + }) + + It("returns nil when ExternalImageURL is empty", func() { + reader := &playlistArtworkReader{ + pl: model.Playlist{ExternalImageURL: ""}, + } + r, path, err := reader.fromPlaylistExternalImage(ctx)() + Expect(err).ToNot(HaveOccurred()) + Expect(r).To(BeNil()) + Expect(path).To(BeEmpty()) + }) + + It("returns error when local file does not exist", func() { + reader := &playlistArtworkReader{ + pl: model.Playlist{ExternalImageURL: "/non/existent/path/cover.jpg"}, + } + r, _, err := reader.fromPlaylistExternalImage(ctx)() + Expect(err).To(HaveOccurred()) + Expect(r).To(BeNil()) + }) + }) }) Describe("resizedArtworkReader", func() { diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index ecc29e6b2..16601b56a 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -8,6 +8,7 @@ import ( "image/draw" "image/png" "io" + "net/url" "os" "path/filepath" "strings" @@ -67,6 +68,7 @@ func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, stri return selectImageReader(ctx, a.artID, a.fromPlaylistUploadedImage(), a.fromPlaylistSidecar(), + a.fromPlaylistExternalImage(ctx), a.fromGeneratedTiledCover(ctx), fromAlbumPlaceholder(), ) @@ -131,6 +133,26 @@ func (a *playlistArtworkReader) fromPlaylistSidecar() sourceFunc { } } +func (a *playlistArtworkReader) fromPlaylistExternalImage(ctx context.Context) sourceFunc { + return func() (io.ReadCloser, string, error) { + if a.pl.ExternalImageURL == "" { + return nil, "", nil + } + if strings.HasPrefix(a.pl.ExternalImageURL, "http://") || strings.HasPrefix(a.pl.ExternalImageURL, "https://") { + parsed, err := url.Parse(a.pl.ExternalImageURL) + if err != nil { + return nil, "", err + } + return fromURL(ctx, parsed) + } + f, err := os.Open(a.pl.ExternalImageURL) + if err != nil { + return nil, "", err + } + return f, a.pl.ExternalImageURL, nil + } +} + func (a *playlistArtworkReader) fromGeneratedTiledCover(ctx context.Context) sourceFunc { return func() (io.ReadCloser, string, error) { tiles, err := a.loadTiles(ctx)