From 0fd9c6df2eaf653186228d59a4a9a460b6db69d8 Mon Sep 17 00:00:00 2001 From: Deluan Date: Tue, 28 Apr 2026 20:22:48 -0400 Subject: [PATCH] refactor(smartplaylist): clarify FieldInfo naming in criteria package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- model/criteria/fields.go | 171 +++++++++++++++++-------------- model/criteria/fields_test.go | 12 +-- model/criteria/sort.go | 2 +- persistence/criteria_sql.go | 16 +-- persistence/criteria_sql_test.go | 4 +- 5 files changed, 111 insertions(+), 94 deletions(-) diff --git a/model/criteria/fields.go b/model/criteria/fields.go index 37b4cc017..44d43457b 100644 --- a/model/criteria/fields.go +++ b/model/criteria/fields.go @@ -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} } } } diff --git a/model/criteria/fields_test.go b/model/criteria/fields_test.go index ecbeb5857..f60666788 100644 --- a/model/criteria/fields_test.go +++ b/model/criteria/fields_test.go @@ -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()) }) }) diff --git a/model/criteria/sort.go b/model/criteria/sort.go index 05b108cf9..e38fe0551 100644 --- a/model/criteria/sort.go +++ b/model/criteria/sort.go @@ -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) diff --git a/persistence/criteria_sql.go b/persistence/criteria_sql.go index 123ec29eb..4fd8e62e6 100644 --- a/persistence/criteria_sql.go +++ b/persistence/criteria_sql.go @@ -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 } diff --git a/persistence/criteria_sql_test.go b/persistence/criteria_sql_test.go index c93465735..40d21c9bf 100644 --- a/persistence/criteria_sql_test.go +++ b/persistence/criteria_sql_test.go @@ -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()) } })