/// How the alpha-jump bar behaves for libraries on this backend. enum AlphaBarMode { /// No alpha bar — hide entirely. none, /// Plex: server reports per-letter cumulative offsets via `/firstCharacter`, /// taps scroll the grid to the offset. scrollSnap, /// Jellyfin: bar acts as a filter button — taps set `NameStartsWith` query /// param, results re-fetch. nameStartsWithFilter, } /// Static capability flags advertised by a [MediaServerClient]. UI consults /// these to gate feature affordances per server (e.g. hide Live TV when no /// connected server supports it). /// /// These describe what the *backend kind* supports in this app's current /// implementation — not necessarily what the wire protocol can do. As more /// Jellyfin features are wired in over time, the corresponding flags flip /// without changing call sites. class ServerCapabilities { /// Server-side `PlayQueue` resource (Plex `/playQueues`) — enables shared /// queue state across devices and Watch Together coordination. final bool serverSidePlayQueue; /// Server-side editable playlists (Plex `/playlists`, Jellyfin /// `/Playlists`). final bool serverSidePlaylists; /// This backend kind has a Live TV / DVR API the app can talk to. Whether /// a *specific* server has Live TV configured is a runtime concern — /// [MultiServerProvider.checkLiveTvAvailability] probes each server and /// only those with channels surface in [MultiServerProvider.liveTvServers]. final bool liveTv; /// Server has DVR/recording lineups (Plex `/livetv/dvrs`). Channel listing /// is gated by [liveTv]; this flag enables the additional recordings/scheduling /// UI. Jellyfin's DVR API isn't wired in this app yet, so it stays false even /// when [liveTv] is true. final bool liveTvDvr; /// Server proxies subtitle search (e.g. OpenSubtitles). final bool subtitleSearch; /// Server can transcode video. final bool videoTranscoding; /// Server supports server-side downloads / "sync" (the queued-from-server /// model). Both Plex and Jellyfin support client-driven downloads, which /// is a separate concept. final bool serverSideSync; /// Server provides curated recommendation hubs (Plex Discover). Jellyfin /// returns synthesized hubs but with sparser categorisation. final bool richHubs; /// Numeric ratings (Plex 0–10 via [Item.userRating]). Jellyfin has no /// numeric user rating, so star sliders should be hidden. final bool numericUserRating; /// Per-user favorite flag ("heart") on media items. Jellyfin exposes it via /// `/UserFavoriteItems/{itemId}?userId=...`; Plex has no equivalent. final bool userFavorites; /// Hide an item from Continue Watching without changing watch state or /// playback progress. Plex exposes this directly; Jellyfin does not. final bool continueWatchingRemoval; /// External subtitle search/marketplace (Plex `/library/metadata/{id}/subtitles`). /// Hides the "Search subtitles" affordance when false. final bool externalSubtitleSearch; /// Persisting per-track audio/subtitle preferences server-side. Plex uses /// `/library/metadata/{id}/prefs` + `selectStream`; Jellyfin saves selected /// stream indexes from `/Sessions/Playing/Progress` when the user's Jellyfin /// remember-selection settings are enabled. When false, in-player switching /// still works but choices don't follow the user across devices. final bool trackPreferencePersistence; /// Multi-endpoint connection model with endpoint racing/failover. Plex gets /// local/remote/relay candidates from plex.tv; Jellyfin uses user-entered /// URLs for the same server. final bool endpointFailover; /// Watch progress can be queued offline and replayed when reconnected /// ([OfflineWatchSyncService]). Jellyfin reports inline only today. final bool offlineWatchQueue; /// Discord rich-presence integration. Plex-only because the RPC payload /// uses Plex-shaped session/metadata. final bool discordRpc; /// Server exposes metadata edit endpoints. Hides edit affordances when false. final bool richMetadataEdit; /// How the alpha-jump bar should behave for this backend's libraries. final AlphaBarMode alphaBar; /// Server can supply thumbnails for the player's seek-bar scrub preview. /// Plex serves them as a `.bif` asset; Jellyfin uses `/Trickplay` sprite /// sheets. Both backends are wired through [ScrubPreviewSource]; the flag /// gates whether the player attempts the load at all. final bool scrubThumbnails; /// Library section exposes a folder hierarchy. Plex uses /// `/library/sections/{id}/folders`; Jellyfin uses direct-child /// `/Items?ParentId=...&Recursive=false` queries. final bool folderGrouping; /// Server can supply track lyrics. Jellyfin exposes `/Audio/{id}/Lyrics`; /// Plex surfaces sidecar `.lrc`/`.txt` files as track streams /// (`streamType 4`) fetched via `/library/streams/{id}`. Gates the lyrics /// affordance in the music player; per-track absence is the runtime gate. final bool lyrics; /// Server can build an "instant mix" / radio track list from a seed item. /// Jellyfin: `/Items/{id}/InstantMix`; Plex: station play queues /// (`POST /playQueues?type=audio&uri=...station...`). final bool instantMix; /// Server can transcode audio to a capped bitrate. Plex: /// `/music/:/transcode/universal`; Jellyfin: `PlaybackInfo` with an audio /// `TranscodingProfile`. Gates the music quality picker (vs original-only). final bool audioTranscoding; const ServerCapabilities({ this.serverSidePlayQueue = false, this.serverSidePlaylists = false, this.liveTv = false, this.liveTvDvr = false, this.subtitleSearch = false, this.videoTranscoding = true, this.serverSideSync = false, this.richHubs = false, this.numericUserRating = false, this.userFavorites = false, this.continueWatchingRemoval = false, this.externalSubtitleSearch = false, this.trackPreferencePersistence = false, this.endpointFailover = false, this.offlineWatchQueue = false, this.discordRpc = false, this.richMetadataEdit = false, this.alphaBar = AlphaBarMode.none, this.scrubThumbnails = false, this.folderGrouping = false, this.lyrics = false, this.instantMix = false, this.audioTranscoding = false, }); /// Defaults for a fully-featured Plex server. static const ServerCapabilities plex = ServerCapabilities( serverSidePlayQueue: true, serverSidePlaylists: true, liveTv: true, liveTvDvr: true, subtitleSearch: true, videoTranscoding: true, serverSideSync: true, richHubs: true, numericUserRating: true, userFavorites: false, continueWatchingRemoval: true, externalSubtitleSearch: true, trackPreferencePersistence: true, endpointFailover: true, offlineWatchQueue: true, discordRpc: true, richMetadataEdit: true, alphaBar: AlphaBarMode.scrollSnap, scrubThumbnails: true, folderGrouping: true, lyrics: true, instantMix: true, audioTranscoding: true, ); /// Defaults for a Jellyfin server. /// /// `videoTranscoding` is `true` — `JellyfinClient.getPlaybackInitialization` /// negotiates via `POST /Items/{id}/PlaybackInfo` and uses the server's /// `TranscodingUrl` when a non-original quality preset is selected. /// /// `liveTv` is `true` because Jellyfin exposes `/LiveTv/Channels` and /// `/LiveTv/Programs`. Detection + channel listing are wired today; /// EPG and tuning are follow-ups. static const ServerCapabilities jellyfin = ServerCapabilities( serverSidePlayQueue: false, serverSidePlaylists: true, liveTv: true, liveTvDvr: false, subtitleSearch: false, videoTranscoding: true, serverSideSync: false, richHubs: false, numericUserRating: false, userFavorites: true, externalSubtitleSearch: false, trackPreferencePersistence: true, endpointFailover: true, offlineWatchQueue: false, discordRpc: false, richMetadataEdit: true, alphaBar: AlphaBarMode.nameStartsWithFilter, scrubThumbnails: true, folderGrouping: true, lyrics: true, instantMix: true, audioTranscoding: true, ); ServerCapabilities copyWith({ bool? serverSidePlayQueue, bool? serverSidePlaylists, bool? liveTv, bool? liveTvDvr, bool? subtitleSearch, bool? videoTranscoding, bool? serverSideSync, bool? richHubs, bool? numericUserRating, bool? userFavorites, bool? continueWatchingRemoval, bool? externalSubtitleSearch, bool? trackPreferencePersistence, bool? endpointFailover, bool? offlineWatchQueue, bool? discordRpc, bool? richMetadataEdit, AlphaBarMode? alphaBar, bool? scrubThumbnails, bool? folderGrouping, bool? lyrics, bool? instantMix, bool? audioTranscoding, }) { return ServerCapabilities( serverSidePlayQueue: serverSidePlayQueue ?? this.serverSidePlayQueue, serverSidePlaylists: serverSidePlaylists ?? this.serverSidePlaylists, liveTv: liveTv ?? this.liveTv, liveTvDvr: liveTvDvr ?? this.liveTvDvr, subtitleSearch: subtitleSearch ?? this.subtitleSearch, videoTranscoding: videoTranscoding ?? this.videoTranscoding, serverSideSync: serverSideSync ?? this.serverSideSync, richHubs: richHubs ?? this.richHubs, numericUserRating: numericUserRating ?? this.numericUserRating, userFavorites: userFavorites ?? this.userFavorites, continueWatchingRemoval: continueWatchingRemoval ?? this.continueWatchingRemoval, externalSubtitleSearch: externalSubtitleSearch ?? this.externalSubtitleSearch, trackPreferencePersistence: trackPreferencePersistence ?? this.trackPreferencePersistence, endpointFailover: endpointFailover ?? this.endpointFailover, offlineWatchQueue: offlineWatchQueue ?? this.offlineWatchQueue, discordRpc: discordRpc ?? this.discordRpc, richMetadataEdit: richMetadataEdit ?? this.richMetadataEdit, alphaBar: alphaBar ?? this.alphaBar, scrubThumbnails: scrubThumbnails ?? this.scrubThumbnails, folderGrouping: folderGrouping ?? this.folderGrouping, lyrics: lyrics ?? this.lyrics, instantMix: instantMix ?? this.instantMix, audioTranscoding: audioTranscoding ?? this.audioTranscoding, ); } }