mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
Finamp's connection test GETs /System/Endpoint and treats anything other than a 200 carrying an IsInNetwork key as "not a Jellyfin server", so the test failed against Navidrome and dual-connection setups could never switch to the local address. IsInNetwork mirrors Jellyfin's default LAN set (NetworkManager with no LocalNetworkSubnets configured): loopback, the RFC 1918 ranges, fc00::/7 and fe80::/10. Notably that set omits 169.254.0.0/16, so Go's IsLinkLocalUnicast is deliberately restricted to its IPv6 half. IsLocal mirrors HttpContext.IsLocal(): the caller shares the connection's local address, not merely "is loopback". It falls back to the loopback check when the local address is unavailable.
310 lines
16 KiB
Go
310 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"`
|
|
}
|
|
|
|
// EndPointInfo describes the caller's network location (GET /System/Endpoint).
|
|
type EndPointInfo struct {
|
|
IsLocal bool `json:"IsLocal"`
|
|
IsInNetwork bool `json:"IsInNetwork"`
|
|
}
|
|
|
|
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"`
|
|
// PrimaryImageAspectRatio is width/height of the Primary image, attached only when the request's
|
|
// Fields asks for it; omitted rather than guessed, since a wrong ratio mis-shapes a placeholder.
|
|
PrimaryImageAspectRatio *float64 `json:"PrimaryImageAspectRatio,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"`
|
|
}
|