From beb5e852663b353ccb2e1f591ae14f0c13f9660b Mon Sep 17 00:00:00 2001 From: Deluan Date: Sun, 1 Mar 2026 21:25:49 -0500 Subject: [PATCH] refactor(playlist): rename ImageFile to UploadedImage and ArtworkPath to UploadedImagePath Rename playlist model fields and methods for clarity in preparation for adding external image URL and sidecar image support. Add the new ExternalImageURL field to the Playlist model. --- core/artwork/reader_playlist.go | 6 ++--- core/playlists/playlists.go | 12 ++++----- core/playlists/playlists_test.go | 12 ++++----- model/playlist.go | 41 ++++++++++++++++------------- ui/src/playlist/PlaylistDetails.jsx | 2 +- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/core/artwork/reader_playlist.go b/core/artwork/reader_playlist.go index 09bfe221b..10ba2dd89 100644 --- a/core/artwork/reader_playlist.go +++ b/core/artwork/reader_playlist.go @@ -45,15 +45,15 @@ func (a *playlistArtworkReader) LastUpdated() time.Time { func (a *playlistArtworkReader) Reader(ctx context.Context) (io.ReadCloser, string, error) { return selectImageReader(ctx, a.artID, - a.fromPlaylistImage(), + a.fromPlaylistUploadedImage(), a.fromGeneratedTiledCover(ctx), fromAlbumPlaceholder(), ) } -func (a *playlistArtworkReader) fromPlaylistImage() sourceFunc { +func (a *playlistArtworkReader) fromPlaylistUploadedImage() sourceFunc { return func() (io.ReadCloser, string, error) { - absPath := a.pl.ArtworkPath() + absPath := a.pl.UploadedImagePath() if absPath == "" { return nil, "", nil } diff --git a/core/playlists/playlists.go b/core/playlists/playlists.go index 7e881b730..d84d5a088 100644 --- a/core/playlists/playlists.go +++ b/core/playlists/playlists.go @@ -131,7 +131,7 @@ func (s *playlists) Delete(ctx context.Context, id string) error { } // Clean up custom cover image file if one exists - if path := pls.ArtworkPath(); path != "" { + if path := pls.UploadedImagePath(); path != "" { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { log.Warn(ctx, "Failed to remove playlist image on delete", "path", path, err) } @@ -289,9 +289,9 @@ func (s *playlists) SetImage(ctx context.Context, playlistID string, reader io.R } filename := playlistID + ext - oldPath := pls.ArtworkPath() - pls.ImageFile = filename - absPath := pls.ArtworkPath() + oldPath := pls.UploadedImagePath() + pls.UploadedImage = filename + absPath := pls.UploadedImagePath() if err := os.MkdirAll(filepath.Dir(absPath), 0755); err != nil { return fmt.Errorf("creating playlist images directory: %w", err) @@ -324,12 +324,12 @@ func (s *playlists) RemoveImage(ctx context.Context, playlistID string) error { return err } - if path := pls.ArtworkPath(); path != "" { + if path := pls.UploadedImagePath(); path != "" { if err := os.Remove(path); err != nil && !os.IsNotExist(err) { log.Warn(ctx, "Failed to remove playlist image", "path", path, err) } } - pls.ImageFile = "" + pls.UploadedImage = "" return s.ds.Playlist(ctx).Put(pls) } diff --git a/core/playlists/playlists_test.go b/core/playlists/playlists_test.go index 4d42bbeb9..9b835924f 100644 --- a/core/playlists/playlists_test.go +++ b/core/playlists/playlists_test.go @@ -315,13 +315,13 @@ var _ = Describe("Playlists", func() { ps = playlists.NewPlaylists(ds) }) - It("saves image file and updates ImageFile", func() { + It("saves image file and updates UploadedImage", func() { ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) reader := strings.NewReader("fake image data") err := ps.SetImage(ctx, "pls-1", reader, ".jpg") Expect(err).ToNot(HaveOccurred()) - Expect(mockPlsRepo.Last.ImageFile).To(Equal("pls-1.jpg")) + Expect(mockPlsRepo.Last.UploadedImage).To(Equal("pls-1.jpg")) absPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1.jpg") data, err := os.ReadFile(absPath) Expect(err).ToNot(HaveOccurred()) @@ -378,19 +378,19 @@ var _ = Describe("Playlists", func() { Expect(os.WriteFile(filepath.Join(imgDir, "pls-1.jpg"), []byte("img data"), 0600)).To(Succeed()) mockPlsRepo.Data = map[string]*model.Playlist{ - "pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1", ImageFile: "pls-1.jpg"}, + "pls-1": {ID: "pls-1", Name: "My Playlist", OwnerID: "user-1", UploadedImage: "pls-1.jpg"}, "pls-empty": {ID: "pls-empty", Name: "No Cover", OwnerID: "user-1"}, "pls-other": {ID: "pls-other", Name: "Other's", OwnerID: "other-user"}, } ps = playlists.NewPlaylists(ds) }) - It("removes file and clears ImageFile", func() { + It("removes file and clears UploadedImage", func() { ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) err := ps.RemoveImage(ctx, "pls-1") Expect(err).ToNot(HaveOccurred()) - Expect(mockPlsRepo.Last.ImageFile).To(BeEmpty()) + Expect(mockPlsRepo.Last.UploadedImage).To(BeEmpty()) absPath := filepath.Join(tmpDir, "artwork", "playlist", "pls-1.jpg") Expect(absPath).ToNot(BeAnExistingFile()) }) @@ -399,7 +399,7 @@ var _ = Describe("Playlists", func() { ctx = request.WithUser(ctx, model.User{ID: "user-1", IsAdmin: false}) err := ps.RemoveImage(ctx, "pls-empty") Expect(err).ToNot(HaveOccurred()) - Expect(mockPlsRepo.Last.ImageFile).To(BeEmpty()) + Expect(mockPlsRepo.Last.UploadedImage).To(BeEmpty()) }) It("denies non-owner", func() { diff --git a/model/playlist.go b/model/playlist.go index b6a52dcf2..35f6ca5c6 100644 --- a/model/playlist.go +++ b/model/playlist.go @@ -12,21 +12,22 @@ import ( ) type Playlist struct { - ID string `structs:"id" json:"id"` - Name string `structs:"name" json:"name"` - Comment string `structs:"comment" json:"comment"` - Duration float32 `structs:"duration" json:"duration"` - Size int64 `structs:"size" json:"size"` - SongCount int `structs:"song_count" json:"songCount"` - OwnerName string `structs:"-" json:"ownerName"` - OwnerID string `structs:"owner_id" json:"ownerId"` - Public bool `structs:"public" json:"public"` - Tracks PlaylistTracks `structs:"-" json:"tracks,omitempty"` - Path string `structs:"path" json:"path"` - Sync bool `structs:"sync" json:"sync"` - ImageFile string `structs:"image_file" json:"imageFile"` - CreatedAt time.Time `structs:"created_at" json:"createdAt"` - UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` + ID string `structs:"id" json:"id"` + Name string `structs:"name" json:"name"` + Comment string `structs:"comment" json:"comment"` + Duration float32 `structs:"duration" json:"duration"` + Size int64 `structs:"size" json:"size"` + SongCount int `structs:"song_count" json:"songCount"` + OwnerName string `structs:"-" json:"ownerName"` + OwnerID string `structs:"owner_id" json:"ownerId"` + Public bool `structs:"public" json:"public"` + Tracks PlaylistTracks `structs:"-" json:"tracks,omitempty"` + Path string `structs:"path" json:"path"` + Sync bool `structs:"sync" json:"sync"` + UploadedImage string `structs:"uploaded_image" json:"uploadedImage"` + ExternalImageURL string `structs:"external_image_url" json:"externalImageUrl,omitempty"` + CreatedAt time.Time `structs:"created_at" json:"createdAt"` + UpdatedAt time.Time `structs:"updated_at" json:"updatedAt"` // SmartPlaylist attributes Rules *criteria.Criteria `structs:"rules" json:"rules"` @@ -110,11 +111,15 @@ func (pls Playlist) CoverArtID() ArtworkID { return artworkIDFromPlaylist(pls) } -func (pls Playlist) ArtworkPath() string { - if pls.ImageFile == "" { +// UploadedImagePath returns the absolute filesystem path for a manually uploaded +// playlist cover image. Returns empty string if no image has been uploaded. +// This does NOT cover sidecar images or external URLs — those are resolved +// by the artwork reader's fallback chain. +func (pls Playlist) UploadedImagePath() string { + if pls.UploadedImage == "" { return "" } - return filepath.Join(conf.Server.DataFolder, consts.ArtworkFolder, "playlist", pls.ImageFile) + return filepath.Join(conf.Server.DataFolder, consts.ArtworkFolder, "playlist", pls.UploadedImage) } type Playlists []Playlist diff --git a/ui/src/playlist/PlaylistDetails.jsx b/ui/src/playlist/PlaylistDetails.jsx index 4c242dd2a..b24446cb9 100644 --- a/ui/src/playlist/PlaylistDetails.jsx +++ b/ui/src/playlist/PlaylistDetails.jsx @@ -246,7 +246,7 @@ const PlaylistDetails = (props) => { - {record.imageFile && ( + {record.uploadedImage && (