Deluan Quintão aa0824e03b
feat(jellyfin): add System/Endpoint so Finamp's connection test passes (#5955)
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.
2026-08-14 09:17:00 -04:00

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"`
}