mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
* feat(persistence): add AlbumRepository.GetYears for distinct album years * test(persistence): verify GetYears de-duplicates repeated years Regression test that adds two albums with the same non-zero max_year (2005) and verifies that GetYears() returns that year exactly once, ensuring the SQL DISTINCT clause is applied correctly. Catches any future removal of DISTINCT from the GetYears query. * feat(jellyfin): add legacy /Items/Filters endpoint (genres + years) * feat(jellyfin): add /Studios endpoint from record label tags * refactor(persistence): drop duplicate columns in tagRepository.GetAll * fix(jellyfin): exclude missing albums from filter years GetYears only filtered max_year > 0, so albums whose files were all removed (missing=true, kept when Scanner.PurgeMissing=never) contributed stale years to /Items/Filters. Filter them out like the normal album listings do. Also return an empty slice from the MockAlbumRepo to match the real repository. * feat(jellyfin): filter /Items by Years= * feat(jellyfin): filter /Items by StudioIds= (record labels) * feat(jellyfin): scope filter and studio lists to ParentId library * refactor(jellyfin): extract parentIDScope and libraryScopeFilter helpers Collapse the three inline resolveLibraryScope(dto.DecodeID(parentid)) call sites and the duplicated empty-scope guard into two small helpers, so the empty-scope=unrestricted contract lives in one place. Reuse the existing names() helper in the Years= e2e test. * feat(jellyfin): expose record labels as album Studios Add a Studios field to the album BaseItemDto, populated from the record-label tags and gated behind Fields=Studios (matching Jellyfin's ItemFields convention). Studio ids reuse the record-label tag identity, so they round-trip with the /Studios list and the StudioIds= filter. Real Jellyfin leaves Studios empty for music; Feishin reads it as the album's record label.
62 lines
2.2 KiB
Go
62 lines
2.2 KiB
Go
package jellyfin
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/server/jellyfin/dto"
|
|
"github.com/navidrome/navidrome/utils/req"
|
|
)
|
|
|
|
// accessibleLibraryIDs returns the ids of the libraries the current user can access. An empty
|
|
// slice (non-admin with no libraries) is treated as a no-op/unrestricted by the library filters.
|
|
func accessibleLibraryIDs(ctx context.Context) []int {
|
|
u, _ := request.UserFrom(ctx)
|
|
return u.Libraries.IDs()
|
|
}
|
|
|
|
// resolveLibraryScope handles ParentId's ambiguity: a library id (browsing a UserView) or an
|
|
// entity id (artist/album). It's treated as a library only when the user has access; otherwise
|
|
// isLibraryParent is false and callers fall through to entity-id handling.
|
|
func resolveLibraryScope(ctx context.Context, parentId string) (scopeIDs []int, isLibraryParent bool) {
|
|
if parentId != "" {
|
|
if id, err := strconv.Atoi(parentId); err == nil {
|
|
if u, _ := request.UserFrom(ctx); u.HasLibraryAccess(id) {
|
|
return []int{id}, true
|
|
}
|
|
}
|
|
}
|
|
return accessibleLibraryIDs(ctx), false
|
|
}
|
|
|
|
// parentIDScope resolves the request's ParentId param to a library scope (see resolveLibraryScope).
|
|
func parentIDScope(ctx context.Context, r *http.Request) (scopeIDs []int, isLibraryParent bool) {
|
|
return resolveLibraryScope(ctx, dto.DecodeID(req.Params(r).StringOr("parentid", "")))
|
|
}
|
|
|
|
// libraryScopeFilter restricts a tag query to the given library scope. Empty scope means
|
|
// unrestricted (see accessibleLibraryIDs), so it returns nil rather than an impossible IN ().
|
|
func libraryScopeFilter(scope []int) squirrel.Sqlizer {
|
|
if len(scope) == 0 {
|
|
return nil
|
|
}
|
|
return squirrel.Eq{"library_tag.library_id": scope}
|
|
}
|
|
|
|
// libraryView builds the CollectionFolder BaseItemDto representing a library as a top-level node.
|
|
// Shared by getUserViews and getItem, since Finamp fetches a UserView's id as a plain item.
|
|
func libraryView(lib model.Library) dto.BaseItemDto {
|
|
return dto.BaseItemDto{
|
|
Id: dto.EncodeID(strconv.Itoa(lib.ID)),
|
|
Name: lib.Name,
|
|
Type: "CollectionFolder",
|
|
CollectionType: "music",
|
|
IsFolder: true,
|
|
BackdropImageTags: []string{},
|
|
}
|
|
}
|