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"
|
||||
|
||||
// FieldInfo contains semantic metadata about a criteria field
|
||||
// FieldInfo contains semantic metadata about a criteria field.
|
||||
type FieldInfo struct {
|
||||
Name string
|
||||
Alias string // If set, this field is a backward-compat alias for another canonical name
|
||||
IsTag bool
|
||||
IsRole 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{
|
||||
"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"},
|
||||
"rgalbumgain": {Name: "rgalbumgain", Numeric: true},
|
||||
"rgalbumpeak": {Name: "rgalbumpeak", Numeric: true},
|
||||
"rgtrackgain": {Name: "rgtrackgain", Numeric: true},
|
||||
"rgtrackpeak": {Name: "rgtrackpeak", Numeric: true},
|
||||
"library_id": {Name: "library_id", Numeric: true},
|
||||
"title": {},
|
||||
"album": {},
|
||||
"hascoverart": {},
|
||||
"tracknumber": {},
|
||||
"discnumber": {},
|
||||
"year": {},
|
||||
"date": {tagAlias: "recordingdate"},
|
||||
"originalyear": {},
|
||||
"originaldate": {},
|
||||
"releaseyear": {},
|
||||
"releasedate": {},
|
||||
"size": {},
|
||||
"compilation": {},
|
||||
"missing": {},
|
||||
"explicitstatus": {},
|
||||
"dateadded": {},
|
||||
"datemodified": {},
|
||||
"discsubtitle": {},
|
||||
"comment": {},
|
||||
"lyrics": {},
|
||||
"sorttitle": {},
|
||||
"sortalbum": {},
|
||||
"sortartist": {},
|
||||
"sortalbumartist": {},
|
||||
"albumcomment": {},
|
||||
"catalognumber": {},
|
||||
"filepath": {},
|
||||
"filetype": {},
|
||||
"codec": {},
|
||||
"duration": {},
|
||||
"bitrate": {},
|
||||
"bitdepth": {},
|
||||
"samplerate": {},
|
||||
"bpm": {},
|
||||
"channels": {},
|
||||
"loved": {},
|
||||
"dateloved": {},
|
||||
"lastplayed": {},
|
||||
"daterated": {},
|
||||
"playcount": {},
|
||||
"rating": {},
|
||||
"averagerating": {Numeric: true},
|
||||
"albumrating": {},
|
||||
"albumloved": {},
|
||||
"albumplaycount": {},
|
||||
"albumlastplayed": {},
|
||||
"albumdateloved": {},
|
||||
"albumdaterated": {},
|
||||
"artistrating": {},
|
||||
"artistloved": {},
|
||||
"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},
|
||||
"rgalbumpeak": {Numeric: true},
|
||||
"rgtrackgain": {Numeric: true},
|
||||
"rgtrackpeak": {Numeric: true},
|
||||
"library_id": {Numeric: true},
|
||||
|
||||
// Backward compatibility: albumtype is an alias for the releasetype tag.
|
||||
"albumtype": {Name: "releasetype", IsTag: true},
|
||||
"albumtype": {Alias: "releasetype", IsTag: true},
|
||||
|
||||
"random": {Name: "random"},
|
||||
"value": {Name: "value"},
|
||||
"random": {},
|
||||
"value": {},
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
||||
@ -108,7 +123,7 @@ func AddRoles(roles []string) {
|
||||
if _, ok := fieldMap[name]; ok {
|
||||
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 {
|
||||
continue
|
||||
}
|
||||
for _, fm := range fieldMap {
|
||||
if fm.alias == name {
|
||||
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{Name: name, IsTag: true}
|
||||
fieldMap[name] = FieldInfo{IsTag: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -140,7 +157,7 @@ func AddNumericTags(tagNames []string) {
|
||||
fm.Numeric = true
|
||||
fieldMap[name] = fm
|
||||
} 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")
|
||||
|
||||
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")
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
@ -26,7 +26,7 @@ var _ = Describe("fields", func() {
|
||||
field, ok := LookupField("value")
|
||||
|
||||
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() {
|
||||
@ -35,7 +35,7 @@ var _ = Describe("fields", func() {
|
||||
field, ok := LookupField("task3_mood")
|
||||
|
||||
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())
|
||||
})
|
||||
|
||||
@ -56,7 +56,7 @@ var _ = Describe("fields", func() {
|
||||
field, ok := LookupField("task3_producer")
|
||||
|
||||
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())
|
||||
})
|
||||
})
|
||||
|
||||
@ -43,7 +43,7 @@ func (c Criteria) OrderByFields() []SortField {
|
||||
if order == "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 {
|
||||
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 {
|
||||
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 {
|
||||
@ -412,7 +412,7 @@ func sqlFields(values map[string]any) (map[string]any, error) {
|
||||
if info.IsTag || info.IsRole {
|
||||
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 == "" {
|
||||
return nil, fmt.Errorf("invalid field in criteria: %s", field)
|
||||
}
|
||||
@ -431,7 +431,7 @@ func fieldJoinType(name string) smartPlaylistJoinType {
|
||||
if !ok {
|
||||
return smartPlaylistJoinNone
|
||||
}
|
||||
field, ok := smartPlaylistFields[info.Name]
|
||||
field, ok := smartPlaylistFields[info.Name()]
|
||||
if !ok {
|
||||
return smartPlaylistJoinNone
|
||||
}
|
||||
@ -479,17 +479,17 @@ func sortExpr(sortField string) (string, bool) {
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
if field, ok := smartPlaylistFields[info.Name]; ok && field.order != "" {
|
||||
if field, ok := smartPlaylistFields[info.Name()]; ok && field.order != "" {
|
||||
return field.order, true
|
||||
}
|
||||
var mapped string
|
||||
switch {
|
||||
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:
|
||||
mapped = "COALESCE(json_extract(media_file.participants, '$." + info.Name + "[0].name'), '')"
|
||||
mapped = "COALESCE(json_extract(media_file.participants, '$." + info.Name() + "[0].name'), '')"
|
||||
default:
|
||||
field, ok := smartPlaylistFields[info.Name]
|
||||
field, ok := smartPlaylistFields[info.Name()]
|
||||
if !ok || field.expr == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
@ -194,8 +194,8 @@ var _ = Describe("Smart playlist criteria SQL", func() {
|
||||
if info.IsTag || info.IsRole {
|
||||
continue
|
||||
}
|
||||
_, hasSQLField := smartPlaylistFields[info.Name]
|
||||
Expect(hasSQLField).To(BeTrue(), "criteria field %q (name=%q) has no entry in smartPlaylistFields", name, info.Name)
|
||||
_, hasSQLField := smartPlaylistFields[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