mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +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.
301 lines
16 KiB
Go
301 lines
16 KiB
Go
package dto
|
|
|
|
// PublicSystemInfo is the unauthenticated handshake payload (GET /System/Info/Public).
|
|
type PublicSystemInfo struct {
|
|
LocalAddress string `json:"LocalAddress,omitempty"`
|
|
ServerName string `json:"ServerName"`
|
|
Version string `json:"Version"`
|
|
ProductName string `json:"ProductName"`
|
|
OperatingSystem string `json:"OperatingSystem,omitempty"`
|
|
Id string `json:"Id"`
|
|
StartupWizardCompleted bool `json:"StartupWizardCompleted"`
|
|
}
|
|
|
|
// SystemInfo is the authenticated variant (GET /System/Info).
|
|
type SystemInfo struct {
|
|
PublicSystemInfo
|
|
HasPendingRestart bool `json:"HasPendingRestart"`
|
|
IsShuttingDown bool `json:"IsShuttingDown"`
|
|
SupportsLibraryMonitor bool `json:"SupportsLibraryMonitor"`
|
|
CachePath string `json:"CachePath,omitempty"`
|
|
}
|
|
|
|
type NameGuidPair struct {
|
|
Name string `json:"Name"`
|
|
Id string `json:"Id"`
|
|
}
|
|
|
|
type UserItemDataDto struct {
|
|
Rating *float64 `json:"Rating,omitempty"`
|
|
PlaybackPositionTicks int64 `json:"PlaybackPositionTicks"`
|
|
PlayCount int `json:"PlayCount"`
|
|
IsFavorite bool `json:"IsFavorite"`
|
|
Played bool `json:"Played"`
|
|
Key string `json:"Key"`
|
|
ItemId string `json:"ItemId,omitempty"`
|
|
LastPlayedDate *string `json:"LastPlayedDate,omitempty"`
|
|
}
|
|
|
|
type BaseItemDto struct {
|
|
Name string `json:"Name"`
|
|
ServerId string `json:"ServerId,omitempty"`
|
|
Id string `json:"Id"`
|
|
// PlaylistItemId identifies an entry within a playlist listing (GET /Playlists/{id}/Items),
|
|
// distinct from Id so a song appearing more than once can be removed by occurrence
|
|
// (DELETE .../Items?EntryIds=...) rather than by song id.
|
|
PlaylistItemId string `json:"PlaylistItemId,omitempty"`
|
|
Type string `json:"Type"`
|
|
IsFolder bool `json:"IsFolder"`
|
|
MediaType string `json:"MediaType,omitempty"`
|
|
CollectionType string `json:"CollectionType,omitempty"`
|
|
LocationType string `json:"LocationType,omitempty"`
|
|
HasLyrics bool `json:"HasLyrics,omitempty"`
|
|
SortName string `json:"SortName,omitempty"`
|
|
Path string `json:"Path,omitempty"`
|
|
ParentId string `json:"ParentId,omitempty"`
|
|
RunTimeTicks int64 `json:"RunTimeTicks,omitempty"`
|
|
IndexNumber *int `json:"IndexNumber,omitempty"`
|
|
ParentIndexNumber *int `json:"ParentIndexNumber,omitempty"`
|
|
ProductionYear *int `json:"ProductionYear,omitempty"`
|
|
// PremiereDate is the ISO 8601 release date; Finamp sorts "Latest Releases" by it client-side.
|
|
PremiereDate *string `json:"PremiereDate,omitempty"`
|
|
// DateCreated is the ISO 8601 date the item was added to the library; clients show it as
|
|
// "Date Added" and sort "Recently Added" by it.
|
|
DateCreated string `json:"DateCreated,omitempty"`
|
|
Album string `json:"Album,omitempty"`
|
|
AlbumId string `json:"AlbumId,omitempty"`
|
|
AlbumArtist string `json:"AlbumArtist,omitempty"`
|
|
AlbumArtists []NameGuidPair `json:"AlbumArtists,omitempty"`
|
|
AlbumPrimaryImageTag string `json:"AlbumPrimaryImageTag,omitempty"`
|
|
Artists []string `json:"Artists,omitempty"`
|
|
ArtistItems []NameGuidPair `json:"ArtistItems,omitempty"`
|
|
Genres []string `json:"Genres,omitempty"`
|
|
GenreItems []NameGuidPair `json:"GenreItems,omitempty"`
|
|
Studios []NameGuidPair `json:"Studios,omitempty"`
|
|
NormalizationGain *float64 `json:"NormalizationGain,omitempty"`
|
|
AlbumNormalizationGain *float64 `json:"AlbumNormalizationGain,omitempty"`
|
|
ChildCount *int `json:"ChildCount,omitempty"`
|
|
SongCount *int `json:"SongCount,omitempty"`
|
|
AlbumCount *int `json:"AlbumCount,omitempty"`
|
|
ImageTags map[string]string `json:"ImageTags,omitempty"`
|
|
// ImageBlurHashes is keyed by image type (e.g. "Primary") then image tag. Finamp uses it as a
|
|
// de-dup key for image downloads (and a placeholder); absent, it warns the server isn't
|
|
// calculating blurhashes.
|
|
ImageBlurHashes map[string]map[string]string `json:"ImageBlurHashes,omitempty"`
|
|
BackdropImageTags []string `json:"BackdropImageTags"`
|
|
UserData *UserItemDataDto `json:"UserData,omitempty"`
|
|
MediaSources []MediaSourceInfo `json:"MediaSources,omitempty"`
|
|
Container string `json:"Container,omitempty"`
|
|
CanDownload bool `json:"CanDownload"`
|
|
}
|
|
|
|
// PlaylistUserPermissions is the response shape for GET /Playlists/{id}/Users(/{userId}), which
|
|
// Finamp probes before allowing playlist edits.
|
|
type PlaylistUserPermissions struct {
|
|
UserId string `json:"UserId"`
|
|
CanEdit bool `json:"CanEdit"`
|
|
}
|
|
|
|
// PlaylistInfo is the response shape for GET /Playlists/{id}. ItemIds are media item ids, not
|
|
// playlist-entry ids (matching real Jellyfin); Finamp reads OpenAccess for the public-visibility toggle.
|
|
type PlaylistInfo struct {
|
|
OpenAccess bool `json:"OpenAccess"`
|
|
Shares []PlaylistUserPermissions `json:"Shares"`
|
|
ItemIds []string `json:"ItemIds"`
|
|
}
|
|
|
|
type QueryResult struct {
|
|
Items []BaseItemDto `json:"Items"`
|
|
TotalRecordCount int `json:"TotalRecordCount"`
|
|
StartIndex int `json:"StartIndex"`
|
|
}
|
|
|
|
type UserDto struct {
|
|
Name string `json:"Name"`
|
|
ServerId string `json:"ServerId,omitempty"`
|
|
ServerName string `json:"ServerName,omitempty"`
|
|
Id string `json:"Id"`
|
|
HasPassword bool `json:"HasPassword"`
|
|
HasConfiguredPassword bool `json:"HasConfiguredPassword"`
|
|
HasConfiguredEasyPassword bool `json:"HasConfiguredEasyPassword"`
|
|
PrimaryImageTag string `json:"PrimaryImageTag,omitempty"`
|
|
Policy *UserPolicy `json:"Policy,omitempty"`
|
|
Configuration *UserConfiguration `json:"Configuration,omitempty"`
|
|
}
|
|
|
|
// UserPolicy mirrors real Jellyfin's User.Policy. Finamp reads it right after login and crashes if
|
|
// it's absent, so every field must be present even though Navidrome lacks most of these concepts.
|
|
type UserPolicy struct {
|
|
IsAdministrator bool `json:"IsAdministrator"`
|
|
IsHidden bool `json:"IsHidden"`
|
|
EnableCollectionManagement bool `json:"EnableCollectionManagement"`
|
|
EnableSubtitleManagement bool `json:"EnableSubtitleManagement"`
|
|
EnableLyricManagement bool `json:"EnableLyricManagement"`
|
|
IsDisabled bool `json:"IsDisabled"`
|
|
BlockedTags []string `json:"BlockedTags"`
|
|
AllowedTags []string `json:"AllowedTags"`
|
|
EnableUserPreferenceAccess bool `json:"EnableUserPreferenceAccess"`
|
|
AccessSchedules []string `json:"AccessSchedules"`
|
|
BlockUnratedItems []string `json:"BlockUnratedItems"`
|
|
EnableRemoteControlOfOtherUsers bool `json:"EnableRemoteControlOfOtherUsers"`
|
|
EnableSharedDeviceControl bool `json:"EnableSharedDeviceControl"`
|
|
EnableRemoteAccess bool `json:"EnableRemoteAccess"`
|
|
EnableLiveTvManagement bool `json:"EnableLiveTvManagement"`
|
|
EnableLiveTvAccess bool `json:"EnableLiveTvAccess"`
|
|
EnableMediaPlayback bool `json:"EnableMediaPlayback"`
|
|
EnableAudioPlaybackTranscoding bool `json:"EnableAudioPlaybackTranscoding"`
|
|
EnableVideoPlaybackTranscoding bool `json:"EnableVideoPlaybackTranscoding"`
|
|
EnablePlaybackRemuxing bool `json:"EnablePlaybackRemuxing"`
|
|
ForceRemoteSourceTranscoding bool `json:"ForceRemoteSourceTranscoding"`
|
|
EnableContentDeletion bool `json:"EnableContentDeletion"`
|
|
EnableContentDeletionFromFolders []string `json:"EnableContentDeletionFromFolders"`
|
|
EnableContentDownloading bool `json:"EnableContentDownloading"`
|
|
EnableSyncTranscoding bool `json:"EnableSyncTranscoding"`
|
|
EnableMediaConversion bool `json:"EnableMediaConversion"`
|
|
EnabledDevices []string `json:"EnabledDevices"`
|
|
EnableAllDevices bool `json:"EnableAllDevices"`
|
|
EnabledChannels []string `json:"EnabledChannels"`
|
|
EnableAllChannels bool `json:"EnableAllChannels"`
|
|
EnabledFolders []string `json:"EnabledFolders"`
|
|
EnableAllFolders bool `json:"EnableAllFolders"`
|
|
InvalidLoginAttemptCount int `json:"InvalidLoginAttemptCount"`
|
|
LoginAttemptsBeforeLockout int `json:"LoginAttemptsBeforeLockout"`
|
|
MaxActiveSessions int `json:"MaxActiveSessions"`
|
|
EnablePublicSharing bool `json:"EnablePublicSharing"`
|
|
BlockedMediaFolders []string `json:"BlockedMediaFolders"`
|
|
BlockedChannels []string `json:"BlockedChannels"`
|
|
RemoteClientBitrateLimit int `json:"RemoteClientBitrateLimit"`
|
|
AuthenticationProviderId string `json:"AuthenticationProviderId"`
|
|
PasswordResetProviderId string `json:"PasswordResetProviderId"`
|
|
SyncPlayAccess string `json:"SyncPlayAccess"`
|
|
}
|
|
|
|
// UserConfiguration mirrors real Jellyfin's User.Configuration. Like UserPolicy, clients expect it
|
|
// always present, even though most settings don't apply to Navidrome's audio-only library.
|
|
type UserConfiguration struct {
|
|
PlayDefaultAudioTrack bool `json:"PlayDefaultAudioTrack"`
|
|
SubtitleLanguagePreference string `json:"SubtitleLanguagePreference"`
|
|
DisplayMissingEpisodes bool `json:"DisplayMissingEpisodes"`
|
|
GroupedFolders []string `json:"GroupedFolders"`
|
|
SubtitleMode string `json:"SubtitleMode"`
|
|
DisplayCollectionsView bool `json:"DisplayCollectionsView"`
|
|
EnableLocalPassword bool `json:"EnableLocalPassword"`
|
|
OrderedViews []string `json:"OrderedViews"`
|
|
LatestItemsExcludes []string `json:"LatestItemsExcludes"`
|
|
MyMediaExcludes []string `json:"MyMediaExcludes"`
|
|
HidePlayedInLatest bool `json:"HidePlayedInLatest"`
|
|
RememberAudioSelections bool `json:"RememberAudioSelections"`
|
|
RememberSubtitleSelections bool `json:"RememberSubtitleSelections"`
|
|
EnableNextEpisodeAutoPlay bool `json:"EnableNextEpisodeAutoPlay"`
|
|
CastReceiverId string `json:"CastReceiverId"`
|
|
}
|
|
|
|
type SessionInfo struct {
|
|
Id string `json:"Id"`
|
|
UserId string `json:"UserId"`
|
|
}
|
|
|
|
type AuthenticationResult struct {
|
|
User *UserDto `json:"User"`
|
|
SessionInfo *SessionInfo `json:"SessionInfo,omitempty"`
|
|
AccessToken string `json:"AccessToken"`
|
|
ServerId string `json:"ServerId"`
|
|
}
|
|
|
|
// MediaStream mirrors real Jellyfin's MediaStream. Finamp declares several bools as non-nullable, so
|
|
// they must always be emitted (no omitempty). Finamp also does MediaStreams.firstWhere((s) => s.type
|
|
// == 'Audio'), so MediaSourceInfo must include at least one Audio stream or that lookup throws.
|
|
type MediaStream struct {
|
|
Codec string `json:"Codec,omitempty"`
|
|
Type string `json:"Type"`
|
|
Index int `json:"Index"`
|
|
BitRate int `json:"BitRate,omitempty"`
|
|
Channels int `json:"Channels,omitempty"`
|
|
SampleRate int `json:"SampleRate,omitempty"`
|
|
ChannelLayout string `json:"ChannelLayout,omitempty"`
|
|
IsInterlaced bool `json:"IsInterlaced"`
|
|
IsDefault bool `json:"IsDefault"`
|
|
IsForced bool `json:"IsForced"`
|
|
IsExternal bool `json:"IsExternal"`
|
|
IsTextSubtitleStream bool `json:"IsTextSubtitleStream"`
|
|
SupportsExternalStream bool `json:"SupportsExternalStream"`
|
|
}
|
|
|
|
// MediaSourceInfo mirrors real Jellyfin's MediaSourceInfo. Finamp declares several bools/arrays as
|
|
// non-nullable, so a missing field deserializes to null and throws a cast error that aborts parsing
|
|
// of the whole item list; emit them always (no omitempty on bools).
|
|
type MediaSourceInfo struct {
|
|
Id string `json:"Id"`
|
|
Path string `json:"Path,omitempty"`
|
|
Protocol string `json:"Protocol"`
|
|
Container string `json:"Container,omitempty"`
|
|
TranscodingUrl string `json:"TranscodingUrl,omitempty"`
|
|
TranscodingSubProtocol string `json:"TranscodingSubProtocol,omitempty"`
|
|
Size int64 `json:"Size,omitempty"`
|
|
Name string `json:"Name,omitempty"`
|
|
IsRemote bool `json:"IsRemote"`
|
|
RunTimeTicks int64 `json:"RunTimeTicks,omitempty"`
|
|
Bitrate int `json:"Bitrate,omitempty"`
|
|
SupportsTranscoding bool `json:"SupportsTranscoding"`
|
|
SupportsDirectStream bool `json:"SupportsDirectStream"`
|
|
SupportsDirectPlay bool `json:"SupportsDirectPlay"`
|
|
Type string `json:"Type"`
|
|
ReadAtNativeFramerate bool `json:"ReadAtNativeFramerate"`
|
|
IgnoreDts bool `json:"IgnoreDts"`
|
|
IgnoreIndex bool `json:"IgnoreIndex"`
|
|
GenPtsInput bool `json:"GenPtsInput"`
|
|
IsInfiniteStream bool `json:"IsInfiniteStream"`
|
|
UseMostCompatibleTranscodingProfile bool `json:"UseMostCompatibleTranscodingProfile"`
|
|
RequiresOpening bool `json:"RequiresOpening"`
|
|
RequiresClosing bool `json:"RequiresClosing"`
|
|
RequiresLooping bool `json:"RequiresLooping"`
|
|
SupportsProbing bool `json:"SupportsProbing"`
|
|
HasSegments bool `json:"HasSegments"`
|
|
MediaStreams []MediaStream `json:"MediaStreams"`
|
|
MediaAttachments []any `json:"MediaAttachments"`
|
|
Formats []string `json:"Formats"`
|
|
}
|
|
|
|
type PlaybackInfoResponse struct {
|
|
MediaSources []MediaSourceInfo `json:"MediaSources"`
|
|
PlaySessionId string `json:"PlaySessionId"`
|
|
}
|
|
|
|
// LyricDto mirrors Jellyfin's GET /Audio/{itemId}/Lyrics response. Feishin and Jellify read only
|
|
// Lyrics[].Text and Start (ticks); Metadata is for completeness/Finamp.
|
|
type LyricDto struct {
|
|
Metadata LyricMetadata `json:"Metadata"`
|
|
Lyrics []LyricLine `json:"Lyrics"`
|
|
}
|
|
|
|
type LyricMetadata struct {
|
|
Artist string `json:"Artist,omitempty"`
|
|
Album string `json:"Album,omitempty"`
|
|
Title string `json:"Title,omitempty"`
|
|
Length int64 `json:"Length,omitempty"`
|
|
Offset *int64 `json:"Offset,omitempty"`
|
|
IsSynced bool `json:"IsSynced"`
|
|
}
|
|
|
|
type LyricLine struct {
|
|
Text string `json:"Text"`
|
|
Start *int64 `json:"Start,omitempty"`
|
|
Cues []LyricLineCue `json:"Cues,omitempty"`
|
|
}
|
|
|
|
type LyricLineCue struct {
|
|
Position int `json:"Position"`
|
|
EndPosition int `json:"EndPosition"`
|
|
Start int64 `json:"Start"`
|
|
End *int64 `json:"End,omitempty"`
|
|
}
|
|
|
|
// QueryFiltersLegacy is the response for GET /Items/Filters. All four lists are always present;
|
|
// clients (jellyfin-web) render each unconditionally.
|
|
type QueryFiltersLegacy struct {
|
|
Genres []string `json:"Genres"`
|
|
Tags []string `json:"Tags"`
|
|
OfficialRatings []string `json:"OfficialRatings"`
|
|
Years []int `json:"Years"`
|
|
}
|