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.
This commit is contained in:
Deluan 2026-03-01 21:25:49 -05:00
parent 169d3b8d72
commit beb5e85266
5 changed files with 39 additions and 34 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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() {

View File

@ -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

View File

@ -246,7 +246,7 @@ const PlaylistDetails = (props) => {
<PhotoCameraIcon className={classes.overlayIcon} />
</IconButton>
</Tooltip>
{record.imageFile && (
{record.uploadedImage && (
<Tooltip
title={translate('resources.playlist.actions.removeCover')}
>