* fix(smartplaylist): support isMissing/isPresent operators on ReplayGain fields ReplayGain values are stored in dedicated nullable columns (rg_album_gain, rg_album_peak, rg_track_gain, rg_track_peak) rather than in the media_file.tags JSON blob. The isMissing/isPresent operators previously only supported tag and role fields, causing two failure modes: 1. Using the documented alias names (replaygain_album_gain etc.) from PR #5256: these got registered as JSON tags from mappings.yaml, so isMissing queried json_tree(media_file.tags, '$.replaygain_album_gain') which is always empty -> the playlist matched ALL songs. 2. Using the canonical field names (rgalbumgain etc.): not a tag/role, so SQL generation returned an error. Because refreshSmartPlaylist deletes old tracks before regenerating, the abort left the playlist empty. Fix: add Nullable bool to FieldInfo and mark the four ReplayGain fields. Add static alias entries (replaygain_album_gain -> rgalbumgain etc.) with Numeric+Nullable set; because AddTagNames skips names already in the field map, these static entries take precedence over the mappings.yaml tag registration. missingExpr now emits IS NULL / IS NOT NULL for nullable column fields instead of the json_tree lookup. Fixes #5584 * chore(smartplaylist): address code review feedback - Simplify the isMissing/isPresent unsupported-field error message, removing the internal "nullable fields" jargon - Standardize comments on mappings.yaml (the actual filename) - Clarify the alias precedence comment in the LookupField test
173 lines
5.2 KiB
Go
173 lines
5.2 KiB
Go
package criteria
|
|
|
|
import "strings"
|
|
|
|
// FieldInfo contains semantic metadata about a criteria field.
|
|
type FieldInfo struct {
|
|
Alias string // If set, this field is a backward-compat alias for another canonical name
|
|
IsTag bool
|
|
IsRole bool
|
|
Numeric bool
|
|
Boolean bool
|
|
Nullable bool // If set, this column field can be NULL, so isMissing/isPresent are supported on it
|
|
|
|
tagAlias string // If set, a tag name from mappings.yaml that resolves to this field
|
|
name string // Canonical name, populated by LookupField from the map key
|
|
}
|
|
|
|
// Name returns the canonical field name (the map key used to register this field).
|
|
func (f FieldInfo) Name() string {
|
|
return f.name
|
|
}
|
|
|
|
var fieldMap = map[string]FieldInfo{
|
|
"title": {},
|
|
"album": {},
|
|
"hascoverart": {Boolean: true},
|
|
"tracknumber": {},
|
|
"discnumber": {},
|
|
"year": {},
|
|
"date": {tagAlias: "recordingdate"},
|
|
"originalyear": {},
|
|
"originaldate": {},
|
|
"releaseyear": {},
|
|
"releasedate": {},
|
|
"size": {},
|
|
"compilation": {Boolean: true},
|
|
"missing": {Boolean: true},
|
|
"explicitstatus": {},
|
|
"dateadded": {},
|
|
"datemodified": {},
|
|
"discsubtitle": {},
|
|
"comment": {},
|
|
"lyrics": {},
|
|
"sorttitle": {},
|
|
"sortalbum": {},
|
|
"sortartist": {},
|
|
"sortalbumartist": {},
|
|
"albumcomment": {},
|
|
"catalognumber": {},
|
|
"filepath": {},
|
|
"filetype": {},
|
|
"codec": {},
|
|
"duration": {},
|
|
"bitrate": {},
|
|
"bitdepth": {},
|
|
"samplerate": {},
|
|
"bpm": {},
|
|
"channels": {},
|
|
"loved": {Boolean: true},
|
|
"dateloved": {},
|
|
"lastplayed": {},
|
|
"daterated": {},
|
|
"playcount": {},
|
|
"rating": {},
|
|
"averagerating": {Numeric: true},
|
|
"albumrating": {},
|
|
"albumloved": {Boolean: true},
|
|
"albumplaycount": {},
|
|
"albumlastplayed": {},
|
|
"albumdateloved": {},
|
|
"albumdaterated": {},
|
|
"artistrating": {},
|
|
"artistloved": {Boolean: true},
|
|
"artistplaycount": {},
|
|
"artistlastplayed": {},
|
|
"artistdateloved": {},
|
|
"artistdaterated": {},
|
|
"mbz_album_id": {},
|
|
"mbz_album_artist_id": {},
|
|
"mbz_artist_id": {},
|
|
"mbz_recording_id": {},
|
|
"mbz_release_track_id": {},
|
|
"mbz_release_group_id": {},
|
|
"rgalbumgain": {Numeric: true, Nullable: true},
|
|
"rgalbumpeak": {Numeric: true, Nullable: true},
|
|
"rgtrackgain": {Numeric: true, Nullable: true},
|
|
"rgtrackpeak": {Numeric: true, Nullable: true},
|
|
"library_id": {Numeric: true},
|
|
|
|
// Backward compatibility: albumtype is an alias for the releasetype tag.
|
|
"albumtype": {Alias: "releasetype", IsTag: true},
|
|
|
|
// Backward compatibility: the replaygain_* tag names (as written in metadata and in the
|
|
// PR #5256 example) are aliases for the canonical rg* column fields. Without these, the tag
|
|
// names would be registered as empty tags from mappings.yaml and isMissing would always match.
|
|
"replaygain_album_gain": {Alias: "rgalbumgain", Numeric: true, Nullable: true},
|
|
"replaygain_album_peak": {Alias: "rgalbumpeak", Numeric: true, Nullable: true},
|
|
"replaygain_track_gain": {Alias: "rgtrackgain", Numeric: true, Nullable: true},
|
|
"replaygain_track_peak": {Alias: "rgtrackpeak", Numeric: true, Nullable: true},
|
|
|
|
// Pseudo-field for random sorting
|
|
"random": {},
|
|
}
|
|
|
|
// 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) {
|
|
key := strings.ToLower(name)
|
|
f, ok := fieldMap[key]
|
|
if ok {
|
|
if f.Alias != "" {
|
|
f.name = f.Alias
|
|
} else {
|
|
f.name = key
|
|
}
|
|
}
|
|
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{IsRole: true}
|
|
}
|
|
}
|
|
|
|
// AddTagNames adds tag names to the field map. This is used to add all tags mapped in the `mappings.yaml`
|
|
// configuration file.
|
|
func AddTagNames(tagNames []string) {
|
|
for _, tagName := range tagNames {
|
|
name := strings.ToLower(tagName)
|
|
if _, ok := fieldMap[name]; ok {
|
|
continue
|
|
}
|
|
for key, fm := range fieldMap {
|
|
if fm.tagAlias == name {
|
|
fm.Alias = key
|
|
fm.tagAlias = ""
|
|
fieldMap[name] = fm
|
|
break
|
|
}
|
|
}
|
|
if _, ok := fieldMap[name]; !ok {
|
|
fieldMap[name] = FieldInfo{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{IsTag: true, Numeric: true}
|
|
}
|
|
}
|
|
}
|