mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
* test: add tests for recordingdate alias resolution in smart playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update FieldInfo structure and simplify fieldMap initialization
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: move sort parsing logic from persistence to criteria package
Extracted sort field parsing, validation, and direction handling from
persistence/criteria_sql.go into model/criteria/sort.go. The new
OrderByFields method on Criteria parses the Sort/Order strings into
validated SortField structs (field name + direction), resolving aliases
and handling +/- prefixes and order inversion. The persistence layer now
consumes these parsed fields and only handles SQL expression mapping.
This centralizes sort parsing to enforce consistent implementations.
* refactor: standardize field access in smartPlaylistCriteria structure
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: add ResolveLimit method to Criteria
Moved the percentage-limit resolution logic from playlist_repository
into Criteria.ResolveLimit, replacing the 3-line mutate-after-query
pattern with a single method call. The method preserves LimitPercent
rather than zeroing it, since IsPercentageLimit already returns false
once Limit is set, making the clear redundant and lossy.
* refactor: improve child playlist loading and error handling in refresh logic
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: extract smart playlist logic to dedicated files
Moved refreshSmartPlaylist, addSmartPlaylistAnnotationJoins, and
addCriteria methods from playlist_repository.go to a new
smart_playlist_repository.go file. Extracted all smart playlist tests
to smart_playlist_repository_test.go. Added DeferCleanup to the
"valid rules" test to fix ordering flakiness when Ginkgo randomizes
test execution across files.
* refactor: break refreshSmartPlaylist into smaller focused methods
Split the monolithic refreshSmartPlaylist method into discrete helpers
for readability: shouldRefreshSmartPlaylist for guard checks,
refreshChildPlaylists for recursive dependency refresh,
resolvePercentageLimit for count-based limit resolution,
buildSmartPlaylistQuery for assembling the SELECT with joins, and
addMediaFileAnnotationJoin to DRY up the repeated annotation join clause.
* refactor: deduplicate child playlist IDs in Criteria
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: simplify withSmartPlaylistOwner to accept model.User
Replaced separate ownerID string and ownerIsAdmin bool parameters with a
single model.User struct, reducing the field count in smartPlaylistCriteria
and making the option function signature clearer. Updated all call sites
and tests accordingly.
* fix: handle empty sort fields and propagate child playlist load errors
OrderByFields now falls back to [{title, asc}] when all user-supplied
sort fields are invalid, preventing empty ORDER BY clauses that would
produce invalid SQL in row_number() window functions. Also restored the
original behavior where a DB error loading child playlists aborts the
parent smart playlist refresh, by making refreshChildPlaylists return a
bool.
* refactor: log warning when no valid sort fields are found
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
63 lines
1.4 KiB
Go
63 lines
1.4 KiB
Go
package criteria
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
)
|
|
|
|
type SortField struct {
|
|
Field string
|
|
Desc bool
|
|
}
|
|
|
|
func (c Criteria) OrderByFields() []SortField {
|
|
sortValue := c.Sort
|
|
if sortValue == "" {
|
|
sortValue = "title"
|
|
}
|
|
|
|
order := strings.ToLower(strings.TrimSpace(c.Order))
|
|
if order != "" && order != "asc" && order != "desc" {
|
|
log.Error("Invalid value in 'order' field. Valid values: 'asc', 'desc'", "order", c.Order)
|
|
order = ""
|
|
}
|
|
|
|
parts := strings.Split(sortValue, ",")
|
|
fields := make([]SortField, 0, len(parts))
|
|
for _, part := range parts {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
continue
|
|
}
|
|
desc := false
|
|
if strings.HasPrefix(part, "+") || strings.HasPrefix(part, "-") {
|
|
desc = strings.HasPrefix(part, "-")
|
|
part = strings.TrimSpace(part[1:])
|
|
}
|
|
info, ok := LookupField(part)
|
|
if !ok {
|
|
log.Error("Invalid field in 'sort' field", "sort", part)
|
|
continue
|
|
}
|
|
if order == "desc" {
|
|
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)
|
|
return []SortField{{Field: "title", Desc: false}}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
func (c Criteria) SortFieldNames() []string {
|
|
sortFields := c.OrderByFields()
|
|
names := make([]string, len(sortFields))
|
|
for i, sf := range sortFields {
|
|
names[i] = sf.Field
|
|
}
|
|
return names
|
|
}
|