mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-28 03:19:38 +00:00
Some checks are pending
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
* fix(playlists): allow toggling auto-import (sync) via REST API The updatePlaylistEntity handler was not applying the sync field from incoming requests, causing the auto-import toggle in the UI to have no effect. Apply the sync value for file-backed playlists only. * fix(playlists): enhance update logic for playlist metadata and sync toggle Signed-off-by: Deluan <deluan@navidrome.org> * fix(playlists): address code review feedback - Add pointer equality short-circuit in rulesEqual before reflect.DeepEqual - Guard against empty ID in Put's partial-update path - Only apply Sync when it actually differs from current value, preventing zero-value overwrites from partial payloads * fix(playlists): remove unused parameters from Update method Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
package model
|
|
|
|
import (
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/model/criteria"
|
|
)
|
|
|
|
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"`
|
|
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"`
|
|
EvaluatedAt *time.Time `structs:"evaluated_at" json:"evaluatedAt"`
|
|
}
|
|
|
|
func (pls Playlist) IsSmartPlaylist() bool {
|
|
return pls.Rules != nil && pls.Rules.Expression != nil
|
|
}
|
|
|
|
func (pls Playlist) MediaFiles() MediaFiles {
|
|
if len(pls.Tracks) == 0 {
|
|
return nil
|
|
}
|
|
return pls.Tracks.MediaFiles()
|
|
}
|
|
|
|
func (pls *Playlist) refreshStats() {
|
|
pls.SongCount = len(pls.Tracks)
|
|
pls.Duration = 0
|
|
pls.Size = 0
|
|
for _, t := range pls.Tracks {
|
|
pls.Duration += t.MediaFile.Duration
|
|
pls.Size += t.MediaFile.Size
|
|
}
|
|
}
|
|
|
|
func (pls *Playlist) SetTracks(tracks PlaylistTracks) {
|
|
pls.Tracks = tracks
|
|
pls.refreshStats()
|
|
}
|
|
|
|
func (pls *Playlist) RemoveTracks(idxToRemove []int) {
|
|
var newTracks PlaylistTracks
|
|
for i, t := range pls.Tracks {
|
|
if slices.Contains(idxToRemove, i) {
|
|
continue
|
|
}
|
|
newTracks = append(newTracks, t)
|
|
}
|
|
pls.Tracks = newTracks
|
|
pls.refreshStats()
|
|
}
|
|
|
|
// ToM3U8 exports the playlist to the Extended M3U8 format
|
|
func (pls *Playlist) ToM3U8() string {
|
|
return pls.MediaFiles().ToM3U8(pls.Name, true)
|
|
}
|
|
|
|
func (pls *Playlist) AddMediaFilesByID(mediaFileIds []string) {
|
|
pos := len(pls.Tracks)
|
|
for _, mfId := range mediaFileIds {
|
|
pos++
|
|
t := PlaylistTrack{
|
|
ID: strconv.Itoa(pos),
|
|
MediaFileID: mfId,
|
|
MediaFile: MediaFile{ID: mfId},
|
|
PlaylistID: pls.ID,
|
|
}
|
|
pls.Tracks = append(pls.Tracks, t)
|
|
}
|
|
pls.refreshStats()
|
|
}
|
|
|
|
func (pls *Playlist) AddMediaFiles(mfs MediaFiles) {
|
|
pos := len(pls.Tracks)
|
|
for _, mf := range mfs {
|
|
pos++
|
|
t := PlaylistTrack{
|
|
ID: strconv.Itoa(pos),
|
|
MediaFileID: mf.ID,
|
|
MediaFile: mf,
|
|
PlaylistID: pls.ID,
|
|
}
|
|
pls.Tracks = append(pls.Tracks, t)
|
|
}
|
|
pls.refreshStats()
|
|
}
|
|
|
|
func (pls Playlist) CoverArtID() ArtworkID {
|
|
return artworkIDFromPlaylist(pls)
|
|
}
|
|
|
|
// 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 {
|
|
return UploadedImagePath(consts.EntityPlaylist, pls.UploadedImage)
|
|
}
|
|
|
|
type Playlists []Playlist
|
|
|
|
type PlaylistRepository interface {
|
|
ResourceRepository
|
|
CountAll(options ...QueryOptions) (int64, error)
|
|
Exists(id string) (bool, error)
|
|
Put(pls *Playlist, cols ...string) error
|
|
Get(id string) (*Playlist, error)
|
|
GetWithTracks(id string, refreshSmartPlaylist, includeMissing bool) (*Playlist, error)
|
|
GetAll(options ...QueryOptions) (Playlists, error)
|
|
FindByPath(path string) (*Playlist, error)
|
|
Delete(id string) error
|
|
Tracks(playlistId string, refreshSmartPlaylist bool) PlaylistTrackRepository
|
|
GetPlaylists(mediaFileId string) (Playlists, error)
|
|
}
|
|
|
|
type PlaylistTrack struct {
|
|
ID string `json:"id"`
|
|
MediaFileID string `json:"mediaFileId"`
|
|
PlaylistID string `json:"playlistId"`
|
|
MediaFile
|
|
}
|
|
|
|
type PlaylistTracks []PlaylistTrack
|
|
|
|
func (plt PlaylistTracks) MediaFiles() MediaFiles {
|
|
mfs := make(MediaFiles, len(plt))
|
|
for i, t := range plt {
|
|
mfs[i] = t.MediaFile
|
|
}
|
|
return mfs
|
|
}
|
|
|
|
type PlaylistTrackRepository interface {
|
|
ResourceRepository
|
|
GetAll(options ...QueryOptions) (PlaylistTracks, error)
|
|
GetAlbumIDs(options ...QueryOptions) ([]string, error)
|
|
Add(mediaFileIds []string) (int, error)
|
|
AddAlbums(albumIds []string) (int, error)
|
|
AddArtists(artistIds []string) (int, error)
|
|
AddDiscs(discs []DiscID) (int, error)
|
|
Delete(id ...string) error
|
|
DeleteAll() error
|
|
Reorder(pos int, newPos int) error
|
|
}
|