mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
* test: add tests for recordingdate alias resolution in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update FieldInfo structure and simplify fieldMap initialization
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: move sort parsing logic from persistence to criteria package
Extracted sort field parsing, validation, and direction handling from
persistence/criteria_sql.go into model/criteria/sort.go. The new
OrderByFields method on Criteria parses the Sort/Order strings into
validated SortField structs (field name + direction), resolving aliases
and handling +/- prefixes and order inversion. The persistence layer now
consumes these parsed fields and only handles SQL expression mapping.
This centralizes sort parsing to enforce consistent implementations.
* refactor: standardize field access in smartPlaylistCriteria structure
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add ResolveLimit method to Criteria
Moved the percentage-limit resolution logic from playlist_repository
into Criteria.ResolveLimit, replacing the 3-line mutate-after-query
pattern with a single method call. The method preserves LimitPercent
rather than zeroing it, since IsPercentageLimit already returns false
once Limit is set, making the clear redundant and lossy.
* refactor: improve child playlist loading and error handling in refresh logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: extract smart playlist logic to dedicated files
Moved refreshSmartPlaylist, addSmartPlaylistAnnotationJoins, and
addCriteria methods from playlist_repository.go to a new
smart_playlist_repository.go file. Extracted all smart playlist tests
to smart_playlist_repository_test.go. Added DeferCleanup to the
"valid rules" test to fix ordering flakiness when Ginkgo randomizes
test execution across files.
* refactor: break refreshSmartPlaylist into smaller focused methods
Split the monolithic refreshSmartPlaylist method into discrete helpers
for readability: shouldRefreshSmartPlaylist for guard checks,
refreshChildPlaylists for recursive dependency refresh,
resolvePercentageLimit for count-based limit resolution,
buildSmartPlaylistQuery for assembling the SELECT with joins, and
addMediaFileAnnotationJoin to DRY up the repeated annotation join clause.
* refactor: deduplicate child playlist IDs in Criteria
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify withSmartPlaylistOwner to accept model.User
Replaced separate ownerID string and ownerIsAdmin bool parameters with a
single model.User struct, reducing the field count in smartPlaylistCriteria
and making the option function signature clearer. Updated all call sites
and tests accordingly.
* fix: handle empty sort fields and propagate child playlist load errors
OrderByFields now falls back to [{title, asc}] when all user-supplied
sort fields are invalid, preventing empty ORDER BY clauses that would
produce invalid SQL in row_number() window functions. Also restored the
original behavior where a DB error loading child playlists aborts the
parent smart playlist refresh, by making refreshChildPlaylists return a
bool.
* refactor: log warning when no valid sort fields are found
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
143 lines
4.8 KiB
Go
143 lines
4.8 KiB
Go
package criteria
|
|
|
|
import "strings"
|
|
|
|
// FieldInfo contains semantic metadata about a criteria field
|
|
type FieldInfo struct {
|
|
Name string
|
|
IsTag bool
|
|
IsRole bool
|
|
Numeric bool
|
|
alias string
|
|
}
|
|
|
|
var fieldMap = map[string]FieldInfo{
|
|
"title": {Name: "title"},
|
|
"album": {Name: "album"},
|
|
"hascoverart": {Name: "hascoverart"},
|
|
"tracknumber": {Name: "tracknumber"},
|
|
"discnumber": {Name: "discnumber"},
|
|
"year": {Name: "year"},
|
|
"date": {Name: "date", alias: "recordingdate"},
|
|
"originalyear": {Name: "originalyear"},
|
|
"originaldate": {Name: "originaldate"},
|
|
"releaseyear": {Name: "releaseyear"},
|
|
"releasedate": {Name: "releasedate"},
|
|
"size": {Name: "size"},
|
|
"compilation": {Name: "compilation"},
|
|
"missing": {Name: "missing"},
|
|
"explicitstatus": {Name: "explicitstatus"},
|
|
"dateadded": {Name: "dateadded"},
|
|
"datemodified": {Name: "datemodified"},
|
|
"discsubtitle": {Name: "discsubtitle"},
|
|
"comment": {Name: "comment"},
|
|
"lyrics": {Name: "lyrics"},
|
|
"sorttitle": {Name: "sorttitle"},
|
|
"sortalbum": {Name: "sortalbum"},
|
|
"sortartist": {Name: "sortartist"},
|
|
"sortalbumartist": {Name: "sortalbumartist"},
|
|
"albumcomment": {Name: "albumcomment"},
|
|
"catalognumber": {Name: "catalognumber"},
|
|
"filepath": {Name: "filepath"},
|
|
"filetype": {Name: "filetype"},
|
|
"codec": {Name: "codec"},
|
|
"duration": {Name: "duration"},
|
|
"bitrate": {Name: "bitrate"},
|
|
"bitdepth": {Name: "bitdepth"},
|
|
"samplerate": {Name: "samplerate"},
|
|
"bpm": {Name: "bpm"},
|
|
"channels": {Name: "channels"},
|
|
"loved": {Name: "loved"},
|
|
"dateloved": {Name: "dateloved"},
|
|
"lastplayed": {Name: "lastplayed"},
|
|
"daterated": {Name: "daterated"},
|
|
"playcount": {Name: "playcount"},
|
|
"rating": {Name: "rating"},
|
|
"averagerating": {Name: "averagerating", Numeric: true},
|
|
"albumrating": {Name: "albumrating"},
|
|
"albumloved": {Name: "albumloved"},
|
|
"albumplaycount": {Name: "albumplaycount"},
|
|
"albumlastplayed": {Name: "albumlastplayed"},
|
|
"albumdateloved": {Name: "albumdateloved"},
|
|
"albumdaterated": {Name: "albumdaterated"},
|
|
"artistrating": {Name: "artistrating"},
|
|
"artistloved": {Name: "artistloved"},
|
|
"artistplaycount": {Name: "artistplaycount"},
|
|
"artistlastplayed": {Name: "artistlastplayed"},
|
|
"artistdateloved": {Name: "artistdateloved"},
|
|
"artistdaterated": {Name: "artistdaterated"},
|
|
"mbz_album_id": {Name: "mbz_album_id"},
|
|
"mbz_album_artist_id": {Name: "mbz_album_artist_id"},
|
|
"mbz_artist_id": {Name: "mbz_artist_id"},
|
|
"mbz_recording_id": {Name: "mbz_recording_id"},
|
|
"mbz_release_track_id": {Name: "mbz_release_track_id"},
|
|
"mbz_release_group_id": {Name: "mbz_release_group_id"},
|
|
"library_id": {Name: "library_id", Numeric: true},
|
|
|
|
// Backward compatibility: albumtype is an alias for the releasetype tag.
|
|
"albumtype": {Name: "releasetype", IsTag: true},
|
|
|
|
"random": {Name: "random"},
|
|
"value": {Name: "value"},
|
|
}
|
|
|
|
// AllFieldNames returns the names of all registered criteria fields.
|
|
func AllFieldNames() []string {
|
|
names := make([]string, 0, len(fieldMap))
|
|
for name := range fieldMap {
|
|
names = append(names, name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// LookupField returns semantic metadata for a criteria field name.
|
|
func LookupField(name string) (FieldInfo, bool) {
|
|
f, ok := fieldMap[strings.ToLower(name)]
|
|
return f, ok
|
|
}
|
|
|
|
// AddRoles adds roles to the field map. This is used to add all artist roles to the field map, so they can be used in
|
|
// smart playlists.
|
|
func AddRoles(roles []string) {
|
|
for _, role := range roles {
|
|
name := strings.ToLower(role)
|
|
if _, ok := fieldMap[name]; ok {
|
|
continue
|
|
}
|
|
fieldMap[name] = FieldInfo{Name: name, IsRole: true}
|
|
}
|
|
}
|
|
|
|
// AddTagNames adds tag names to the field map. This is used to add all tags mapped in the `mappings.yml`
|
|
// configuration file.
|
|
func AddTagNames(tagNames []string) {
|
|
for _, tagName := range tagNames {
|
|
name := strings.ToLower(tagName)
|
|
if _, ok := fieldMap[name]; ok {
|
|
continue
|
|
}
|
|
for _, fm := range fieldMap {
|
|
if fm.alias == name {
|
|
fieldMap[name] = fm
|
|
break
|
|
}
|
|
}
|
|
if _, ok := fieldMap[name]; !ok {
|
|
fieldMap[name] = FieldInfo{Name: name, IsTag: true}
|
|
}
|
|
}
|
|
}
|
|
|
|
// AddNumericTags adds tags that should be treated as numbers.
|
|
func AddNumericTags(tagNames []string) {
|
|
for _, tagName := range tagNames {
|
|
name := strings.ToLower(tagName)
|
|
if fm, ok := fieldMap[name]; ok {
|
|
fm.Numeric = true
|
|
fieldMap[name] = fm
|
|
} else {
|
|
fieldMap[name] = FieldInfo{Name: name, IsTag: true, Numeric: true}
|
|
}
|
|
}
|
|
}
|