diff --git a/model/criteria/criteria.go b/model/criteria/criteria.go index fa92c5ac..54ac5969 100644 --- a/model/criteria/criteria.go +++ b/model/criteria/criteria.go @@ -61,7 +61,12 @@ func (c Criteria) OrderBy() string { if f.order != "" { mapped = f.order } else if f.isTag { - mapped = "COALESCE(json_extract(media_file.tags, '$." + sortField + "[0].value'), '')" + // Use the actual field name (handles aliases like albumtype -> releasetype) + tagName := sortField + if f.field != "" { + tagName = f.field + } + mapped = "COALESCE(json_extract(media_file.tags, '$." + tagName + "[0].value'), '')" } else if f.isRole { mapped = "COALESCE(json_extract(media_file.participants, '$." + sortField + "[0].name'), '')" } else { diff --git a/model/criteria/criteria_test.go b/model/criteria/criteria_test.go index 3792264a..032ead5c 100644 --- a/model/criteria/criteria_test.go +++ b/model/criteria/criteria_test.go @@ -118,6 +118,16 @@ var _ = Describe("Criteria", func() { ) }) + It("sorts by albumtype alias (resolves to releasetype)", func() { + AddTagNames([]string{"releasetype"}) + goObj.Sort = "albumtype" + gomega.Expect(goObj.OrderBy()).To( + gomega.Equal( + "COALESCE(json_extract(media_file.tags, '$.releasetype[0].value'), '') asc", + ), + ) + }) + It("sorts by random", func() { newObj := goObj newObj.Sort = "random" diff --git a/model/criteria/fields.go b/model/criteria/fields.go index 3699eb14..70719cd6 100644 --- a/model/criteria/fields.go +++ b/model/criteria/fields.go @@ -32,7 +32,6 @@ var fieldMap = map[string]*mappedField{ "sortalbum": {field: "media_file.sort_album_name"}, "sortartist": {field: "media_file.sort_artist_name"}, "sortalbumartist": {field: "media_file.sort_album_artist_name"}, - "albumtype": {field: "media_file.mbz_album_type", alias: "releasetype"}, "albumcomment": {field: "media_file.mbz_album_comment"}, "catalognumber": {field: "media_file.catalog_num"}, "filepath": {field: "media_file.path"}, @@ -55,6 +54,9 @@ var fieldMap = map[string]*mappedField{ "mbz_release_group_id": {field: "media_file.mbz_release_group_id"}, "library_id": {field: "media_file.library_id", numeric: true}, + // Backward compatibility: albumtype is an alias for releasetype tag + "albumtype": {field: "releasetype", isTag: true}, + // special fields "random": {field: "", order: "random()"}, // pseudo-field for random sorting "value": {field: "value"}, // pseudo-field for tag and roles values @@ -154,13 +156,19 @@ type tagCond struct { func (e tagCond) ToSql() (string, []any, error) { cond, args, err := e.cond.ToSql() - // Check if this tag is marked as numeric in the fieldMap - if fm, ok := fieldMap[e.tag]; ok && fm.numeric { - cond = strings.ReplaceAll(cond, "value", "CAST(value AS REAL)") + // Resolve the actual tag name (handles aliases like albumtype -> releasetype) + tagName := e.tag + if fm, ok := fieldMap[e.tag]; ok { + if fm.field != "" { + tagName = fm.field + } + if fm.numeric { + cond = strings.ReplaceAll(cond, "value", "CAST(value AS REAL)") + } } cond = fmt.Sprintf("exists (select 1 from json_tree(tags, '$.%s') where key='value' and %s)", - e.tag, cond) + tagName, cond) if e.not { cond = "not " + cond } diff --git a/model/criteria/operators_test.go b/model/criteria/operators_test.go index ee716a9c..4c1db130 100644 --- a/model/criteria/operators_test.go +++ b/model/criteria/operators_test.go @@ -105,6 +105,40 @@ var _ = Describe("Operators", func() { gomega.Expect(sql).To(gomega.BeEmpty()) gomega.Expect(args).To(gomega.BeEmpty()) }) + It("supports releasetype as multi-valued tag", func() { + AddTagNames([]string{"releasetype"}) + op := Contains{"releasetype": "soundtrack"} + sql, args, err := op.ToSql() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Expect(sql).To(gomega.Equal("exists (select 1 from json_tree(tags, '$.releasetype') where key='value' and value LIKE ?)")) + gomega.Expect(args).To(gomega.HaveExactElements("%soundtrack%")) + }) + It("supports albumtype as alias for releasetype", func() { + AddTagNames([]string{"releasetype"}) + op := Contains{"albumtype": "live"} + sql, args, err := op.ToSql() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + gomega.Expect(sql).To(gomega.Equal("exists (select 1 from json_tree(tags, '$.releasetype') where key='value' and value LIKE ?)")) + gomega.Expect(args).To(gomega.HaveExactElements("%live%")) + }) + It("supports albumtype alias with Is operator", func() { + AddTagNames([]string{"releasetype"}) + op := Is{"albumtype": "album"} + sql, args, err := op.ToSql() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + // Should query $.releasetype, not $.albumtype + gomega.Expect(sql).To(gomega.Equal("exists (select 1 from json_tree(tags, '$.releasetype') where key='value' and value = ?)")) + gomega.Expect(args).To(gomega.HaveExactElements("album")) + }) + It("supports albumtype alias with IsNot operator", func() { + AddTagNames([]string{"releasetype"}) + op := IsNot{"albumtype": "compilation"} + sql, args, err := op.ToSql() + gomega.Expect(err).ToNot(gomega.HaveOccurred()) + // Should query $.releasetype, not $.albumtype + gomega.Expect(sql).To(gomega.Equal("not exists (select 1 from json_tree(tags, '$.releasetype') where key='value' and value = ?)")) + gomega.Expect(args).To(gomega.HaveExactElements("compilation")) + }) }) Describe("Custom Roles", func() { diff --git a/ui/src/album/AlbumList.jsx b/ui/src/album/AlbumList.jsx index 40b927a8..f10f8dbd 100644 --- a/ui/src/album/AlbumList.jsx +++ b/ui/src/album/AlbumList.jsx @@ -42,6 +42,9 @@ const useStyles = makeStyles({ }, }) +const formatReleaseType = (record) => + record?.tagValue ? humanize(record?.tagValue) : '-- None --' + const AlbumFilter = (props) => { const classes = useStyles() const translate = useTranslate() @@ -142,9 +145,7 @@ const AlbumFilter = (props) => { > - record?.tagValue ? humanize(record?.tagValue) : '-- None --' - } + optionText={formatReleaseType} />