mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
refactor(smartplaylist): clarify FieldInfo naming in criteria package
Rename FieldInfo.Name to Alias (only meaningful for backward-compat entries like albumtype→releasetype) and FieldInfo.alias to tagAlias (tag names from mappings.yml that resolve to existing fields). Add a Name() method that derives the canonical name from the map key, eliminating 60+ redundant Name declarations where the value matched the key. LookupField now populates the private name field automatically. Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
d9dac44456
commit
0fd9c6df2e
@ -2,87 +2,94 @@ package criteria
|
|||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// FieldInfo contains semantic metadata about a criteria field
|
// FieldInfo contains semantic metadata about a criteria field.
|
||||||
type FieldInfo struct {
|
type FieldInfo struct {
|
||||||
Name string
|
Alias string // If set, this field is a backward-compat alias for another canonical name
|
||||||
IsTag bool
|
IsTag bool
|
||||||
IsRole bool
|
IsRole bool
|
||||||
Numeric bool
|
Numeric bool
|
||||||
alias string
|
|
||||||
|
tagAlias string // If set, a tag name from mappings.yml 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{
|
var fieldMap = map[string]FieldInfo{
|
||||||
"title": {Name: "title"},
|
"title": {},
|
||||||
"album": {Name: "album"},
|
"album": {},
|
||||||
"hascoverart": {Name: "hascoverart"},
|
"hascoverart": {},
|
||||||
"tracknumber": {Name: "tracknumber"},
|
"tracknumber": {},
|
||||||
"discnumber": {Name: "discnumber"},
|
"discnumber": {},
|
||||||
"year": {Name: "year"},
|
"year": {},
|
||||||
"date": {Name: "date", alias: "recordingdate"},
|
"date": {tagAlias: "recordingdate"},
|
||||||
"originalyear": {Name: "originalyear"},
|
"originalyear": {},
|
||||||
"originaldate": {Name: "originaldate"},
|
"originaldate": {},
|
||||||
"releaseyear": {Name: "releaseyear"},
|
"releaseyear": {},
|
||||||
"releasedate": {Name: "releasedate"},
|
"releasedate": {},
|
||||||
"size": {Name: "size"},
|
"size": {},
|
||||||
"compilation": {Name: "compilation"},
|
"compilation": {},
|
||||||
"missing": {Name: "missing"},
|
"missing": {},
|
||||||
"explicitstatus": {Name: "explicitstatus"},
|
"explicitstatus": {},
|
||||||
"dateadded": {Name: "dateadded"},
|
"dateadded": {},
|
||||||
"datemodified": {Name: "datemodified"},
|
"datemodified": {},
|
||||||
"discsubtitle": {Name: "discsubtitle"},
|
"discsubtitle": {},
|
||||||
"comment": {Name: "comment"},
|
"comment": {},
|
||||||
"lyrics": {Name: "lyrics"},
|
"lyrics": {},
|
||||||
"sorttitle": {Name: "sorttitle"},
|
"sorttitle": {},
|
||||||
"sortalbum": {Name: "sortalbum"},
|
"sortalbum": {},
|
||||||
"sortartist": {Name: "sortartist"},
|
"sortartist": {},
|
||||||
"sortalbumartist": {Name: "sortalbumartist"},
|
"sortalbumartist": {},
|
||||||
"albumcomment": {Name: "albumcomment"},
|
"albumcomment": {},
|
||||||
"catalognumber": {Name: "catalognumber"},
|
"catalognumber": {},
|
||||||
"filepath": {Name: "filepath"},
|
"filepath": {},
|
||||||
"filetype": {Name: "filetype"},
|
"filetype": {},
|
||||||
"codec": {Name: "codec"},
|
"codec": {},
|
||||||
"duration": {Name: "duration"},
|
"duration": {},
|
||||||
"bitrate": {Name: "bitrate"},
|
"bitrate": {},
|
||||||
"bitdepth": {Name: "bitdepth"},
|
"bitdepth": {},
|
||||||
"samplerate": {Name: "samplerate"},
|
"samplerate": {},
|
||||||
"bpm": {Name: "bpm"},
|
"bpm": {},
|
||||||
"channels": {Name: "channels"},
|
"channels": {},
|
||||||
"loved": {Name: "loved"},
|
"loved": {},
|
||||||
"dateloved": {Name: "dateloved"},
|
"dateloved": {},
|
||||||
"lastplayed": {Name: "lastplayed"},
|
"lastplayed": {},
|
||||||
"daterated": {Name: "daterated"},
|
"daterated": {},
|
||||||
"playcount": {Name: "playcount"},
|
"playcount": {},
|
||||||
"rating": {Name: "rating"},
|
"rating": {},
|
||||||
"averagerating": {Name: "averagerating", Numeric: true},
|
"averagerating": {Numeric: true},
|
||||||
"albumrating": {Name: "albumrating"},
|
"albumrating": {},
|
||||||
"albumloved": {Name: "albumloved"},
|
"albumloved": {},
|
||||||
"albumplaycount": {Name: "albumplaycount"},
|
"albumplaycount": {},
|
||||||
"albumlastplayed": {Name: "albumlastplayed"},
|
"albumlastplayed": {},
|
||||||
"albumdateloved": {Name: "albumdateloved"},
|
"albumdateloved": {},
|
||||||
"albumdaterated": {Name: "albumdaterated"},
|
"albumdaterated": {},
|
||||||
"artistrating": {Name: "artistrating"},
|
"artistrating": {},
|
||||||
"artistloved": {Name: "artistloved"},
|
"artistloved": {},
|
||||||
"artistplaycount": {Name: "artistplaycount"},
|
"artistplaycount": {},
|
||||||
"artistlastplayed": {Name: "artistlastplayed"},
|
"artistlastplayed": {},
|
||||||
"artistdateloved": {Name: "artistdateloved"},
|
"artistdateloved": {},
|
||||||
"artistdaterated": {Name: "artistdaterated"},
|
"artistdaterated": {},
|
||||||
"mbz_album_id": {Name: "mbz_album_id"},
|
"mbz_album_id": {},
|
||||||
"mbz_album_artist_id": {Name: "mbz_album_artist_id"},
|
"mbz_album_artist_id": {},
|
||||||
"mbz_artist_id": {Name: "mbz_artist_id"},
|
"mbz_artist_id": {},
|
||||||
"mbz_recording_id": {Name: "mbz_recording_id"},
|
"mbz_recording_id": {},
|
||||||
"mbz_release_track_id": {Name: "mbz_release_track_id"},
|
"mbz_release_track_id": {},
|
||||||
"mbz_release_group_id": {Name: "mbz_release_group_id"},
|
"mbz_release_group_id": {},
|
||||||
"rgalbumgain": {Name: "rgalbumgain", Numeric: true},
|
"rgalbumgain": {Numeric: true},
|
||||||
"rgalbumpeak": {Name: "rgalbumpeak", Numeric: true},
|
"rgalbumpeak": {Numeric: true},
|
||||||
"rgtrackgain": {Name: "rgtrackgain", Numeric: true},
|
"rgtrackgain": {Numeric: true},
|
||||||
"rgtrackpeak": {Name: "rgtrackpeak", Numeric: true},
|
"rgtrackpeak": {Numeric: true},
|
||||||
"library_id": {Name: "library_id", Numeric: true},
|
"library_id": {Numeric: true},
|
||||||
|
|
||||||
// Backward compatibility: albumtype is an alias for the releasetype tag.
|
// Backward compatibility: albumtype is an alias for the releasetype tag.
|
||||||
"albumtype": {Name: "releasetype", IsTag: true},
|
"albumtype": {Alias: "releasetype", IsTag: true},
|
||||||
|
|
||||||
"random": {Name: "random"},
|
"random": {},
|
||||||
"value": {Name: "value"},
|
"value": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllFieldNames returns the names of all registered criteria fields.
|
// AllFieldNames returns the names of all registered criteria fields.
|
||||||
@ -96,7 +103,15 @@ func AllFieldNames() []string {
|
|||||||
|
|
||||||
// LookupField returns semantic metadata for a criteria field name.
|
// LookupField returns semantic metadata for a criteria field name.
|
||||||
func LookupField(name string) (FieldInfo, bool) {
|
func LookupField(name string) (FieldInfo, bool) {
|
||||||
f, ok := fieldMap[strings.ToLower(name)]
|
key := strings.ToLower(name)
|
||||||
|
f, ok := fieldMap[key]
|
||||||
|
if ok {
|
||||||
|
if f.Alias != "" {
|
||||||
|
f.name = f.Alias
|
||||||
|
} else {
|
||||||
|
f.name = key
|
||||||
|
}
|
||||||
|
}
|
||||||
return f, ok
|
return f, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +123,7 @@ func AddRoles(roles []string) {
|
|||||||
if _, ok := fieldMap[name]; ok {
|
if _, ok := fieldMap[name]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fieldMap[name] = FieldInfo{Name: name, IsRole: true}
|
fieldMap[name] = FieldInfo{IsRole: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,14 +135,16 @@ func AddTagNames(tagNames []string) {
|
|||||||
if _, ok := fieldMap[name]; ok {
|
if _, ok := fieldMap[name]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, fm := range fieldMap {
|
for key, fm := range fieldMap {
|
||||||
if fm.alias == name {
|
if fm.tagAlias == name {
|
||||||
|
fm.Alias = key
|
||||||
|
fm.tagAlias = ""
|
||||||
fieldMap[name] = fm
|
fieldMap[name] = fm
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, ok := fieldMap[name]; !ok {
|
if _, ok := fieldMap[name]; !ok {
|
||||||
fieldMap[name] = FieldInfo{Name: name, IsTag: true}
|
fieldMap[name] = FieldInfo{IsTag: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +157,7 @@ func AddNumericTags(tagNames []string) {
|
|||||||
fm.Numeric = true
|
fm.Numeric = true
|
||||||
fieldMap[name] = fm
|
fieldMap[name] = fm
|
||||||
} else {
|
} else {
|
||||||
fieldMap[name] = FieldInfo{Name: name, IsTag: true, Numeric: true}
|
fieldMap[name] = FieldInfo{IsTag: true, Numeric: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,14 +11,14 @@ var _ = Describe("fields", func() {
|
|||||||
field, ok := LookupField("Title")
|
field, ok := LookupField("Title")
|
||||||
|
|
||||||
gomega.Expect(ok).To(gomega.BeTrue())
|
gomega.Expect(ok).To(gomega.BeTrue())
|
||||||
gomega.Expect(field).To(gomega.Equal(FieldInfo{Name: "title"}))
|
gomega.Expect(field.Name()).To(gomega.Equal("title"))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("resolves aliases to their semantic field name", func() {
|
It("resolves aliases to their canonical field name", func() {
|
||||||
field, ok := LookupField("albumtype")
|
field, ok := LookupField("albumtype")
|
||||||
|
|
||||||
gomega.Expect(ok).To(gomega.BeTrue())
|
gomega.Expect(ok).To(gomega.BeTrue())
|
||||||
gomega.Expect(field.Name).To(gomega.Equal("releasetype"))
|
gomega.Expect(field.Name()).To(gomega.Equal("releasetype"))
|
||||||
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ var _ = Describe("fields", func() {
|
|||||||
field, ok := LookupField("value")
|
field, ok := LookupField("value")
|
||||||
|
|
||||||
gomega.Expect(ok).To(gomega.BeTrue())
|
gomega.Expect(ok).To(gomega.BeTrue())
|
||||||
gomega.Expect(field.Name).To(gomega.Equal("value"))
|
gomega.Expect(field.Name()).To(gomega.Equal("value"))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("finds registered tag names", func() {
|
It("finds registered tag names", func() {
|
||||||
@ -35,7 +35,7 @@ var _ = Describe("fields", func() {
|
|||||||
field, ok := LookupField("task3_mood")
|
field, ok := LookupField("task3_mood")
|
||||||
|
|
||||||
gomega.Expect(ok).To(gomega.BeTrue())
|
gomega.Expect(ok).To(gomega.BeTrue())
|
||||||
gomega.Expect(field.Name).To(gomega.Equal("task3_mood"))
|
gomega.Expect(field.Name()).To(gomega.Equal("task3_mood"))
|
||||||
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
gomega.Expect(field.IsTag).To(gomega.BeTrue())
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ var _ = Describe("fields", func() {
|
|||||||
field, ok := LookupField("task3_producer")
|
field, ok := LookupField("task3_producer")
|
||||||
|
|
||||||
gomega.Expect(ok).To(gomega.BeTrue())
|
gomega.Expect(ok).To(gomega.BeTrue())
|
||||||
gomega.Expect(field.Name).To(gomega.Equal("task3_producer"))
|
gomega.Expect(field.Name()).To(gomega.Equal("task3_producer"))
|
||||||
gomega.Expect(field.IsRole).To(gomega.BeTrue())
|
gomega.Expect(field.IsRole).To(gomega.BeTrue())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -43,7 +43,7 @@ func (c Criteria) OrderByFields() []SortField {
|
|||||||
if order == "desc" {
|
if order == "desc" {
|
||||||
desc = !desc
|
desc = !desc
|
||||||
}
|
}
|
||||||
fields = append(fields, SortField{Field: info.Name, Desc: desc})
|
fields = append(fields, SortField{Field: info.Name(), Desc: desc})
|
||||||
}
|
}
|
||||||
if len(fields) == 0 {
|
if len(fields) == 0 {
|
||||||
log.Warn("No valid sort fields found in 'sort', falling back to 'title'", "sort", sortValue)
|
log.Warn("No valid sort fields found in 'sort', falling back to 'title'", "sort", sortValue)
|
||||||
|
|||||||
@ -338,9 +338,9 @@ func (c smartPlaylistCriteria) inList(values map[string]any, negate bool) (squir
|
|||||||
|
|
||||||
func jsonExpr(info criteria.FieldInfo, cond squirrel.Sqlizer, negate bool) squirrel.Sqlizer {
|
func jsonExpr(info criteria.FieldInfo, cond squirrel.Sqlizer, negate bool) squirrel.Sqlizer {
|
||||||
if info.IsRole {
|
if info.IsRole {
|
||||||
return roleCond{role: info.Name, cond: cond, not: negate}
|
return roleCond{role: info.Name(), cond: cond, not: negate}
|
||||||
}
|
}
|
||||||
return tagCond{tag: info.Name, numeric: info.Numeric, cond: cond, not: negate}
|
return tagCond{tag: info.Name(), numeric: info.Numeric, cond: cond, not: negate}
|
||||||
}
|
}
|
||||||
|
|
||||||
type tagCond struct {
|
type tagCond struct {
|
||||||
@ -412,7 +412,7 @@ func sqlFields(values map[string]any) (map[string]any, error) {
|
|||||||
if info.IsTag || info.IsRole {
|
if info.IsTag || info.IsRole {
|
||||||
return nil, fmt.Errorf("tag and role criteria must contain exactly one field: %s", field)
|
return nil, fmt.Errorf("tag and role criteria must contain exactly one field: %s", field)
|
||||||
}
|
}
|
||||||
sqlField, ok := fieldExpr(info.Name)
|
sqlField, ok := fieldExpr(info.Name())
|
||||||
if !ok || sqlField == "" {
|
if !ok || sqlField == "" {
|
||||||
return nil, fmt.Errorf("invalid field in criteria: %s", field)
|
return nil, fmt.Errorf("invalid field in criteria: %s", field)
|
||||||
}
|
}
|
||||||
@ -431,7 +431,7 @@ func fieldJoinType(name string) smartPlaylistJoinType {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return smartPlaylistJoinNone
|
return smartPlaylistJoinNone
|
||||||
}
|
}
|
||||||
field, ok := smartPlaylistFields[info.Name]
|
field, ok := smartPlaylistFields[info.Name()]
|
||||||
if !ok {
|
if !ok {
|
||||||
return smartPlaylistJoinNone
|
return smartPlaylistJoinNone
|
||||||
}
|
}
|
||||||
@ -479,17 +479,17 @@ func sortExpr(sortField string) (string, bool) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if field, ok := smartPlaylistFields[info.Name]; ok && field.order != "" {
|
if field, ok := smartPlaylistFields[info.Name()]; ok && field.order != "" {
|
||||||
return field.order, true
|
return field.order, true
|
||||||
}
|
}
|
||||||
var mapped string
|
var mapped string
|
||||||
switch {
|
switch {
|
||||||
case info.IsTag:
|
case info.IsTag:
|
||||||
mapped = "COALESCE(json_extract(media_file.tags, '$." + info.Name + "[0].value'), '')"
|
mapped = "COALESCE(json_extract(media_file.tags, '$." + info.Name() + "[0].value'), '')"
|
||||||
case info.IsRole:
|
case info.IsRole:
|
||||||
mapped = "COALESCE(json_extract(media_file.participants, '$." + info.Name + "[0].name'), '')"
|
mapped = "COALESCE(json_extract(media_file.participants, '$." + info.Name() + "[0].name'), '')"
|
||||||
default:
|
default:
|
||||||
field, ok := smartPlaylistFields[info.Name]
|
field, ok := smartPlaylistFields[info.Name()]
|
||||||
if !ok || field.expr == "" {
|
if !ok || field.expr == "" {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@ -194,8 +194,8 @@ var _ = Describe("Smart playlist criteria SQL", func() {
|
|||||||
if info.IsTag || info.IsRole {
|
if info.IsTag || info.IsRole {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, hasSQLField := smartPlaylistFields[info.Name]
|
_, hasSQLField := smartPlaylistFields[info.Name()]
|
||||||
Expect(hasSQLField).To(BeTrue(), "criteria field %q (name=%q) has no entry in smartPlaylistFields", name, info.Name)
|
Expect(hasSQLField).To(BeTrue(), "criteria field %q (name=%q) has no entry in smartPlaylistFields", name, info.Name())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user