feat(server): add Role filters to albums (#3829)
* navidrome artist filtering * address discord feedback * perPage min 36 * various artist artist_id -> albumartist_id * artist_id, role_id separate * remove all ui changes I guess * Add tests, check for possible SQL injection Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org> Co-authored-by: Kendall Garner <17521368+kgarner7@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -119,11 +120,17 @@ var albumFilters = sync.OnceValue(func() map[string]filterFunc {
|
|||||||
"has_rating": hasRatingFilter,
|
"has_rating": hasRatingFilter,
|
||||||
"missing": booleanFilter,
|
"missing": booleanFilter,
|
||||||
"genre_id": tagIDFilter,
|
"genre_id": tagIDFilter,
|
||||||
|
"role_total_id": allRolesFilter,
|
||||||
}
|
}
|
||||||
// Add all album tags as filters
|
// Add all album tags as filters
|
||||||
for tag := range model.AlbumLevelTags() {
|
for tag := range model.AlbumLevelTags() {
|
||||||
filters[string(tag)] = tagIDFilter
|
filters[string(tag)] = tagIDFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for role := range model.AllRoles {
|
||||||
|
filters["role_"+role+"_id"] = artistRoleFilter
|
||||||
|
}
|
||||||
|
|
||||||
return filters
|
return filters
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -153,14 +160,25 @@ func yearFilter(_ string, value interface{}) Sqlizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BFR: Support other roles
|
|
||||||
func artistFilter(_ string, value interface{}) Sqlizer {
|
func artistFilter(_ string, value interface{}) Sqlizer {
|
||||||
return Or{
|
return Or{
|
||||||
Exists("json_tree(Participants, '$.albumartist')", Eq{"value": value}),
|
Exists("json_tree(participants, '$.albumartist')", Eq{"value": value}),
|
||||||
Exists("json_tree(Participants, '$.artist')", Eq{"value": value}),
|
Exists("json_tree(participants, '$.artist')", Eq{"value": value}),
|
||||||
}
|
}
|
||||||
// For any role:
|
}
|
||||||
//return Like{"Participants": fmt.Sprintf(`%%"%s"%%`, value)}
|
|
||||||
|
func artistRoleFilter(name string, value interface{}) Sqlizer {
|
||||||
|
roleName := strings.TrimSuffix(strings.TrimPrefix(name, "role_"), "_id")
|
||||||
|
|
||||||
|
// Check if the role name is valid. If not, return an invalid filter
|
||||||
|
if _, ok := model.AllRoles[roleName]; !ok {
|
||||||
|
return Gt{"": nil}
|
||||||
|
}
|
||||||
|
return Exists(fmt.Sprintf("json_tree(participants, '$.%s')", roleName), Eq{"value": value})
|
||||||
|
}
|
||||||
|
|
||||||
|
func allRolesFilter(_ string, value interface{}) Sqlizer {
|
||||||
|
return Like{"participants": fmt.Sprintf(`%%"%s"%%`, value)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
func (r *albumRepository) CountAll(options ...model.QueryOptions) (int64, error) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package persistence
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
@@ -236,6 +237,52 @@ var _ = Describe("AlbumRepository", func() {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("artistRoleFilter", func() {
|
||||||
|
DescribeTable("creates correct SQL expressions for artist roles",
|
||||||
|
func(filterName, artistID, expectedSQL string) {
|
||||||
|
sqlizer := artistRoleFilter(filterName, artistID)
|
||||||
|
sql, args, err := sqlizer.ToSql()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(sql).To(Equal(expectedSQL))
|
||||||
|
Expect(args).To(Equal([]interface{}{artistID}))
|
||||||
|
},
|
||||||
|
Entry("artist role", "role_artist_id", "123",
|
||||||
|
"exists (select 1 from json_tree(participants, '$.artist') where value = ?)"),
|
||||||
|
Entry("albumartist role", "role_albumartist_id", "456",
|
||||||
|
"exists (select 1 from json_tree(participants, '$.albumartist') where value = ?)"),
|
||||||
|
Entry("composer role", "role_composer_id", "789",
|
||||||
|
"exists (select 1 from json_tree(participants, '$.composer') where value = ?)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
It("works with the actual filter map", func() {
|
||||||
|
filters := albumFilters()
|
||||||
|
|
||||||
|
for roleName := range model.AllRoles {
|
||||||
|
filterName := "role_" + roleName + "_id"
|
||||||
|
filterFunc, exists := filters[filterName]
|
||||||
|
Expect(exists).To(BeTrue(), fmt.Sprintf("Filter %s should exist", filterName))
|
||||||
|
|
||||||
|
sqlizer := filterFunc(filterName, "test-id")
|
||||||
|
sql, args, err := sqlizer.ToSql()
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(sql).To(Equal(fmt.Sprintf("exists (select 1 from json_tree(participants, '$.%s') where value = ?)", roleName)))
|
||||||
|
Expect(args).To(Equal([]interface{}{"test-id"}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects invalid roles", func() {
|
||||||
|
sqlizer := artistRoleFilter("role_invalid_id", "123")
|
||||||
|
_, _, err := sqlizer.ToSql()
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("rejects invalid filter names", func() {
|
||||||
|
sqlizer := artistRoleFilter("invalid_name", "123")
|
||||||
|
_, _, err := sqlizer.ToSql()
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
func _p(id, name string, sortName ...string) model.Participant {
|
func _p(id, name string, sortName ...string) model.Participant {
|
||||||
|
|||||||
Reference in New Issue
Block a user