* feat(playlist): add migration for playlist image field rename and external URL
* 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.
* feat(playlist): parse #EXTALBUMARTURL directive in M3U imports
* feat(playlist): always sync ExternalImageURL on re-scan, preserve UploadedImage
* feat(artwork): add sidecar image discovery and cache invalidation for playlists
Add playlist sidecar image support to the artwork reader fallback chain.
A sidecar image (e.g., MyPlaylist.jpg next to MyPlaylist.m3u) is discovered
via case-insensitive base name matching using model.IsImageFile(). Cache
invalidation uses max(playlist.UpdatedAt, imageFile.ModTime()) to bust
stale artwork when sidecar or ExternalImageURL local files change.
* 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.
* refactor(artwork): simplify playlist artwork source functions
Extract shared fromLocalFile helper, use url.Parse for scheme check,
and collapse sidecar directory scan conditions.
* test(artwork): remove redundant fromPlaylistSidecar tests
These tests duplicated scenarios already covered by findPlaylistSidecarPath
tests combined with fromLocalFile (tested via fromPlaylistExternalImage).
After refactoring fromPlaylistSidecar to a one-liner composing those two
functions, the wrapper tests add no value.
* fix(playlist): address security review comments from PR #5131:
- Use url.PathUnescape instead of url.QueryUnescape for file:// URLs so
that '+' in filenames is preserved (not decoded as space).
- Validate all local image paths (file://, absolute, relative) against
known library boundaries via libraryMatcher, rejecting paths outside
any configured library.
- Harden #EXTALBUMARTURL against path traversal and SSRF by adding EnableM3UExternalAlbumArt config flag (default false, also
disabled by EnableExternalServices=false) to gate HTTP(S) URL storage
at parse time and fetching at read time (defense in depth).
- Log a warning when os.ReadDir fails in findPlaylistSidecarPath for
diagnosability.
- Extract resolveLocalPath helper to simplify resolveImageURL.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(playlist): implement human-friendly filename generation for uploaded playlist cover images
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(playlist): add custom playlist cover art upload - #406
Allow users to upload, view, and remove custom cover images for playlists.
Custom images take priority over the auto-generated tiled artwork.
Backend:
- Add `image_path` column to playlist table (migration with proper rollback)
- Add `SetImage`/`RemoveImage` methods to playlist service
- Add `POST/DELETE /api/playlist/{id}/image` endpoints
- Prioritize custom image in artwork reader pipeline
- Clean up image files on playlist deletion
- Use glob-based cleanup to prevent orphaned files across format changes
- Reject uploads with undetermined image type (400)
Frontend:
- Hover overlay on playlist cover with upload (camera) and remove (trash) buttons
- Lightbox for full-size cover art viewing
- Cover art thumbnails in the playlist list view
- Loading/error states and i18n strings
Closes#406
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
* refactor: rename playlist image path migration file
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(playlist): address review feedback for cover art upload - #406
- Use httpClient instead of raw fetch for image upload/remove
- Revert glob cleanup to simple imagePath check
- Add log.Error before all error HTTP responses
- Add backend tests for SetImage and RemoveImage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
* refactor(playlist): use Playlist.ArtworkPath() for image storage
Migrate all playlist image path handling to use the new
Playlist.ArtworkPath() method as the single source of truth. The DB now
stores only the filename (e.g. "pls-1.jpg") instead of a relative path,
and images are stored under {DataFolder}/artwork/playlist/ instead of
{DataFolder}/playlist_images/. The artwork root directory is created at
startup alongside DataFolder and CacheFolder. This also removes the
conf dependency from reader_playlist.go since path resolution is now
fully encapsulated in the model.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(playlist): streamline artwork image selection logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: move translation keys, add pt-BR translations
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor(playlist): rename image_path to image_file
Rename the playlist cover art column and field from image_path/ImagePath
to image_file/ImageFile across the migration, model, service, tests, and
UI. The new name more accurately describes what the field stores (a
filename, not a path) and aligns with the existing ImageFiles/IsImageFile
naming conventions in the codebase.
---------
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: make playlist name sorting case-insensitive
Add collation NOCASE to playlist.name column to ensure case-insensitive sorting, matching the behavior of other tables like radio and user. This fixes the issue where uppercase playlist names would appear before lowercase names regardless of alphabetical order.
The migration recreates the playlist table with the proper collation and recreates all associated indexes. Corresponding collation tests are added to verify the fix persists through future migrations.
* fix: add default sorting to playlist names
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
The bulk action buttons (Make Public, Make Private, Delete) on the playlists list were displaying with poor text contrast when using dark themes like AMusic. The buttons had pinkish text (theme's primary color) on a dark red background, making them difficult to read.
This fix applies the same styling pattern used for song bulk actions by adding a makeStyles hook that sets white text color for dark themes. This ensures proper contrast between the button text and background while maintaining correct styling on light themes.
Tested on AMusic (dark) and Light themes to verify contrast improvement and backward compatibility.
Signed-off-by: Deluan <deluan@navidrome.org>
Added genre as a toggleable column in the playlist songs table. The Genre column
displays genre information for each song in playlists and is available through
the column toggle menu but disabled by default.
Implements feature request from GitHub discussion #4400.
Signed-off-by: Deluan <deluan@navidrome.org>
Closes#1737
* wrapping playlist comment in a <Collapse> element
* Extract common collapsible logic into a component
---------
Co-authored-by: Deluan <deluan@navidrome.org>
* playlist view: optionally show genre and comment columns
* Remove genre from Playlist columns, as it is not a valid attribute of playlist
Co-authored-by: Deluan <deluan@navidrome.org>
* Show playlists in sidebar menu
* Fix menu
* Refresh playlist submenu when adding new playlist
* Group shared playlists below user's playlists
* Fix text overflow in menu options
* Add button in playlist menu to go to Playlists list
* Add config option `DevSidebarPlaylists` to enable this feature (default false)